backport: 16.0.0-pre1
This commit is contained in:
parent
47cbf3c9b6
commit
c77b50a31d
135 changed files with 813 additions and 13461 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -146,4 +146,5 @@ gradle-app.setting
|
||||||
|
|
||||||
jbr
|
jbr
|
||||||
secrets
|
secrets
|
||||||
.idea/runConfigurations/Sign_Plugin.xml
|
.idea/runConfigurations/*.xml
|
||||||
|
.intellijPlatform
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -17,6 +17,16 @@ Changelog structure reference:
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Zig
|
||||||
|
- Color settings has more accurate color preview text.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- LSP
|
||||||
|
- Migrated to Red Hat's LSP4IJ LSP adapter.
|
||||||
|
|
||||||
## [15.2.0]
|
## [15.2.0]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
12
README.md
12
README.md
|
@ -83,18 +83,6 @@ Adds support for the Zig Language, utilizing the ZLS language server for advance
|
||||||
1. Download the latest version of Zig from https://ziglang.org/download
|
1. Download the latest version of Zig from https://ziglang.org/download
|
||||||
2. Download and compile the ZLS language server, available at https://github.com/zigtools/zls
|
2. Download and compile the ZLS language server, available at https://github.com/zigtools/zls
|
||||||
3. Go to `Settings` -> `Languages & Frameworks` -> `Zig`, and point the `Toolchain Location` and `ZLS path` to the correct places
|
3. Go to `Settings` -> `Languages & Frameworks` -> `Zig`, and point the `Toolchain Location` and `ZLS path` to the correct places
|
||||||
4. Open a .zig file, and wait for the circle in the bottom status bar to turn Green (empty).
|
|
||||||
See below (`LSP status icon explanation`) for an explanation on what the circle means.
|
|
||||||
|
|
||||||
### LSP status icon explanation
|
|
||||||
Red (X symbol):
|
|
||||||
LSP server is stopped. You either don't have a proper ZLS path set, or you don't have a .zig file open.
|
|
||||||
|
|
||||||
Yellow ("refresh arrow" symbol):
|
|
||||||
LSP server is starting, please be patient.
|
|
||||||
|
|
||||||
Green (empty):
|
|
||||||
LSP server is running.
|
|
||||||
|
|
||||||
## Debugging
|
## Debugging
|
||||||
|
|
||||||
|
|
448
build.gradle.kts
448
build.gradle.kts
|
@ -1,22 +1,21 @@
|
||||||
import groovy.xml.XmlParser
|
import groovy.xml.XmlParser
|
||||||
import groovy.xml.XmlSlurper
|
|
||||||
import org.jetbrains.changelog.Changelog
|
import org.jetbrains.changelog.Changelog
|
||||||
import org.jetbrains.changelog.markdownToHTML
|
import org.jetbrains.changelog.markdownToHTML
|
||||||
import org.jetbrains.intellij.tasks.PatchPluginXmlTask
|
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
|
||||||
import org.jetbrains.intellij.tasks.PublishPluginTask
|
import org.jetbrains.intellij.platform.gradle.tasks.PatchPluginXmlTask
|
||||||
|
import org.jetbrains.intellij.platform.gradle.tasks.PublishPluginTask
|
||||||
|
import org.jetbrains.intellij.platform.gradle.utils.extensionProvider
|
||||||
|
|
||||||
fun properties(key: String) = providers.gradleProperty(key)
|
fun properties(key: String) = providers.gradleProperty(key)
|
||||||
fun environment(key: String) = providers.environmentVariable(key)
|
fun environment(key: String) = providers.environmentVariable(key)
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("java") // Java support
|
java
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
id("java-library")
|
`java-library`
|
||||||
id("org.jetbrains.intellij") version("1.17.3")
|
id("org.jetbrains.intellij.platform") version("2.0.0-beta8")
|
||||||
id("org.jetbrains.changelog") version("2.2.0")
|
id("org.jetbrains.changelog") version("2.2.1")
|
||||||
id("org.jetbrains.grammarkit") version("2022.3.2.2")
|
id("org.jetbrains.grammarkit") version("2022.3.2.2")
|
||||||
id("com.palantir.git-version") version("3.0.0")
|
|
||||||
id("org.jetbrains.kotlin.jvm") version("1.9.22") //Only used by backport module
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val publishVersions = listOf("232", "233", "241", "242")
|
val publishVersions = listOf("232", "233", "241", "242")
|
||||||
|
@ -32,44 +31,35 @@ val rootPackagePath = rootPackage.replace('.', '/')
|
||||||
val javaLangVersion: JavaLanguageVersion = JavaLanguageVersion.of(17)
|
val javaLangVersion: JavaLanguageVersion = JavaLanguageVersion.of(17)
|
||||||
val javaVersion = JavaVersion.VERSION_17
|
val javaVersion = JavaVersion.VERSION_17
|
||||||
|
|
||||||
val baseIDE: String = properties("baseIDE").get()
|
val baseIDE = properties("baseIDE").get()
|
||||||
val ideaVersion: String = properties("ideaVersion").get()
|
val ideaVersion = properties("ideaVersion").get()
|
||||||
val clionVersion: String = properties("clionVersion").get()
|
val clionVersion = properties("clionVersion").get()
|
||||||
val baseVersion = when(baseIDE) {
|
|
||||||
"idea" -> ideaVersion
|
|
||||||
"clion" -> clionVersion
|
|
||||||
else -> error("Unexpected IDE name: `$baseIDE")
|
|
||||||
}
|
|
||||||
|
|
||||||
val clionPlugins = listOf("com.intellij.clion", "com.intellij.cidr.lang", "com.intellij.cidr.base", "com.intellij.nativeDebug")
|
val clionPlugins = listOf("com.intellij.clion", "com.intellij.cidr.lang", "com.intellij.cidr.base", "com.intellij.nativeDebug")
|
||||||
|
|
||||||
|
val lsp4jVersion = "0.21.1"
|
||||||
|
val lsp4ijVersion = "0.3.0-20240704-134935"
|
||||||
|
|
||||||
|
val lsp4ijNightly = lsp4ijVersion.contains("-")
|
||||||
|
val lsp4ijDepString = "${if (lsp4ijNightly) "nightly." else ""}com.jetbrains.plugins:com.redhat.devtools.lsp4ij:$lsp4ijVersion"
|
||||||
|
val lsp4ijPluginString = "com.redhat.devtools.lsp4ij:$lsp4ijVersion${if (lsp4ijNightly) "@nightly" else ""}"
|
||||||
|
|
||||||
|
val lsp4ijDep: DependencyHandler.() -> Unit = {
|
||||||
|
intellijPlatformPluginDependency(lsp4ijDepString)
|
||||||
|
compileOnlyApi(lsp4ijDepString)
|
||||||
|
compileOnlyApi("org.eclipse.lsp4j:org.eclipse.lsp4j:$lsp4jVersion")
|
||||||
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
wrapper {
|
wrapper {
|
||||||
gradleVersion = properties("gradleVersion").get()
|
gradleVersion = properties("gradleVersion").get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pluginVersionGit(): Provider<String> {
|
|
||||||
return provider {
|
|
||||||
try {
|
|
||||||
gitVersion()
|
|
||||||
} catch (_: java.lang.Exception) {
|
|
||||||
error("Git version not found and RELEASE_VERSION environment variable is not set!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pluginVersion(): Provider<String> {
|
fun pluginVersion(): Provider<String> {
|
||||||
return provider {
|
return provider {
|
||||||
System.getenv("RELEASE_VERSION")
|
System.getenv("RELEASE_VERSION")
|
||||||
}.orElse(pluginVersionGit().map {
|
}.orElse(properties("pluginVersion"))
|
||||||
val suffix = "-" + properties("pluginSinceBuild").get()
|
|
||||||
if (it.endsWith(suffix)) {
|
|
||||||
it.substring(0, it.length - suffix.length)
|
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pluginVersionFull(): Provider<String> {
|
fun pluginVersionFull(): Provider<String> {
|
||||||
|
@ -78,31 +68,77 @@ fun pluginVersionFull(): Provider<String> {
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
apply {
|
apply {
|
||||||
plugin("org.jetbrains.grammarkit")
|
plugin("org.jetbrains.intellij.platform")
|
||||||
plugin("org.jetbrains.intellij")
|
|
||||||
plugin("org.jetbrains.kotlin.jvm")
|
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://cache-redirector.jetbrains.com/repo.maven.apache.org/maven2")
|
intellijPlatform {
|
||||||
maven("https://cache-redirector.jetbrains.com/intellij-dependencies")
|
localPlatformArtifacts {
|
||||||
|
content {
|
||||||
|
includeGroup("bundledPlugin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
marketplace {
|
||||||
|
content {
|
||||||
|
includeGroup("com.jetbrains.plugins")
|
||||||
|
includeGroup("nightly.com.jetbrains.plugins")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
releases {
|
||||||
|
content {
|
||||||
|
includeModule("com.jetbrains.intellij.clion", "clion")
|
||||||
|
includeModule("com.jetbrains.intellij.idea", "ideaIC")
|
||||||
|
includeModule("com.jetbrains.intellij.idea", "ideaIU")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("org.projectlombok:lombok:1.18.30")
|
compileOnly("org.projectlombok:lombok:1.18.32")
|
||||||
annotationProcessor("org.projectlombok:lombok:1.18.30")
|
annotationProcessor("org.projectlombok:lombok:1.18.32")
|
||||||
|
if (path !in listOf(":", ":plugin", ":debugger")) {
|
||||||
|
intellijPlatform {
|
||||||
|
intellijIdeaCommunity(ideaVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
intellij {
|
|
||||||
version = baseVersion
|
if (path in listOf(":zig", ":zon")) {
|
||||||
updateSinceUntilBuild = true
|
apply {
|
||||||
instrumentCode = false
|
plugin("org.jetbrains.grammarkit")
|
||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
java {
|
java {
|
||||||
srcDirs(
|
srcDirs(
|
||||||
"${grammarKitGenDir}/lexer",
|
"${grammarKitGenDir}/lexer",
|
||||||
"${grammarKitGenDir}/parser"
|
"${grammarKitGenDir}/parser"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tasks {
|
||||||
|
|
||||||
|
generateLexer {
|
||||||
|
enabled = true
|
||||||
|
purgeOldFiles = true
|
||||||
|
}
|
||||||
|
|
||||||
|
generateParser {
|
||||||
|
enabled = true
|
||||||
|
targetRootOutputDir = file("${grammarKitGenDir}/parser")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
register<DefaultTask>("generateGrammars") {
|
||||||
|
description = "Generate source code from parser/lexer definitions"
|
||||||
|
group = "build setup"
|
||||||
|
dependsOn("generateLexer")
|
||||||
|
dependsOn("generateParser")
|
||||||
|
}
|
||||||
|
|
||||||
|
compileJava {
|
||||||
|
dependsOn("generateGrammars")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,148 +163,47 @@ allprojects {
|
||||||
runIde { enabled = false }
|
runIde { enabled = false }
|
||||||
prepareSandbox { enabled = false }
|
prepareSandbox { enabled = false }
|
||||||
buildSearchableOptions { enabled = false }
|
buildSearchableOptions { enabled = false }
|
||||||
|
verifyPlugin { enabled = false }
|
||||||
|
buildPlugin { enabled = false }
|
||||||
|
signPlugin { enabled = false }
|
||||||
|
verifyPluginProjectConfiguration { enabled = false }
|
||||||
|
|
||||||
withType<PatchPluginXmlTask> {
|
withType<PatchPluginXmlTask> {
|
||||||
sinceBuild = properties("pluginSinceBuild")
|
sinceBuild = properties("pluginSinceBuild")
|
||||||
untilBuild = properties("pluginUntilBuild")
|
untilBuild = properties("pluginUntilBuild")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
withType<org.jetbrains.intellij.tasks.RunIdeBase> {
|
intellijPlatform {
|
||||||
rootProject.file("jbr/lib/openjdk/bin/java")
|
instrumentCode = false
|
||||||
.takeIf { it.exists() }
|
|
||||||
?.let { projectExecutable.set(it.toString()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
withType<org.jetbrains.intellij.tasks.RunPluginVerifierTask> {
|
|
||||||
rootProject.file("jbr/lib/openjdk")
|
|
||||||
.takeIf { it.exists() }
|
|
||||||
?.let { runtimeDir.set(it.toString()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
generateLexer {
|
|
||||||
purgeOldFiles = true
|
|
||||||
}
|
|
||||||
generateParser {
|
|
||||||
targetRootOutputDir = file("${grammarKitGenDir}/parser")
|
|
||||||
purgeOldFiles = true
|
|
||||||
}
|
|
||||||
|
|
||||||
register<DefaultTask>("generateGrammars") {
|
|
||||||
description = "Generate source code from parser/lexer definitions"
|
|
||||||
group = "build setup"
|
|
||||||
dependsOn("generateLexer")
|
|
||||||
dependsOn("generateParser")
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyPlugin {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
compileKotlin {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
compileTestKotlin {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
project(":") {
|
project(":common") {
|
||||||
apply {
|
|
||||||
plugin("org.jetbrains.changelog")
|
|
||||||
}
|
|
||||||
task<Exec>("nixos_jbr") {
|
|
||||||
description = "Create a symlink to package jetbrains.jdk"
|
|
||||||
group = "build setup"
|
|
||||||
commandLine("nix-build", "<nixpkgs>", "-A", "jetbrains.jdk", "-o", "jbr")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks {
|
|
||||||
buildPlugin {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changelog {
|
|
||||||
groups.empty()
|
|
||||||
repositoryUrl = properties("pluginRepositoryUrl")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
project(":backports") {
|
|
||||||
tasks {
|
|
||||||
compileKotlin {
|
|
||||||
enabled = true
|
|
||||||
kotlinOptions.jvmTarget = "17"
|
|
||||||
}
|
|
||||||
|
|
||||||
compileTestKotlin {
|
|
||||||
enabled = true
|
|
||||||
kotlinOptions.jvmTarget = "17"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
project(":debugger") {
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":zig"))
|
|
||||||
implementation(project(":project"))
|
|
||||||
implementation(project(":common"))
|
|
||||||
implementation(project(":lsp-common"))
|
|
||||||
implementation(project(":lsp"))
|
|
||||||
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.debug:0.22.0")
|
|
||||||
}
|
|
||||||
intellij {
|
|
||||||
version = clionVersion
|
|
||||||
plugins = clionPlugins
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
project(":lsp-common") {
|
|
||||||
apply {
|
|
||||||
plugin("java-library")
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
api("org.eclipse.lsp4j:org.eclipse.lsp4j:0.22.0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
project(":lsp") {
|
|
||||||
apply {
|
|
||||||
plugin("java-library")
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":common"))
|
|
||||||
implementation(project(":backports"))
|
|
||||||
api(project(":lsp-common"))
|
|
||||||
api("org.apache.commons:commons-lang3:3.14.0")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
project(":zig") {
|
project(":zig") {
|
||||||
|
apply {
|
||||||
|
plugin("java-library")
|
||||||
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":lsp"))
|
|
||||||
implementation(project(":common"))
|
implementation(project(":common"))
|
||||||
|
lsp4ijDep()
|
||||||
|
intellijPlatform {
|
||||||
|
plugin(lsp4ijPluginString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tasks {
|
tasks {
|
||||||
generateLexer {
|
generateLexer {
|
||||||
enabled = true
|
|
||||||
sourceFile = file("src/main/grammar/Zig.flex")
|
sourceFile = file("src/main/grammar/Zig.flex")
|
||||||
targetOutputDir = file("${grammarKitGenDir}/lexer/${rootPackagePath}/zig/lexer")
|
targetOutputDir = file("${grammarKitGenDir}/lexer/${rootPackagePath}/zig/lexer")
|
||||||
}
|
}
|
||||||
|
|
||||||
generateParser {
|
generateParser {
|
||||||
enabled = true
|
|
||||||
sourceFile = file("src/main/grammar/Zig.bnf")
|
sourceFile = file("src/main/grammar/Zig.bnf")
|
||||||
pathToParser = "${rootPackagePath}/zig/psi/ZigParser.java"
|
pathToParser = "${rootPackagePath}/zig/psi/ZigParser.java"
|
||||||
pathToPsiRoot = "${rootPackagePath}/zig/psi"
|
pathToPsiRoot = "${rootPackagePath}/zig/psi"
|
||||||
}
|
}
|
||||||
|
|
||||||
compileJava {
|
|
||||||
dependsOn("generateGrammars")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,48 +214,103 @@ project(":project") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
project(":debugger") {
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":zig"))
|
||||||
|
implementation(project(":project"))
|
||||||
|
implementation(project(":common"))
|
||||||
|
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.debug:$lsp4jVersion") {
|
||||||
|
exclude("org.eclipse.lsp4j", "org.eclipse.lsp4j")
|
||||||
|
exclude("org.eclipse.lsp4j", "org.eclipse.lsp4j.jsonrpc")
|
||||||
|
exclude("com.google.code.gson", "gson")
|
||||||
|
}
|
||||||
|
intellijPlatform {
|
||||||
|
clion(clionVersion)
|
||||||
|
for (p in clionPlugins) {
|
||||||
|
bundledPlugin(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
project(":zon") {
|
project(":zon") {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":common"))
|
implementation(project(":common"))
|
||||||
}
|
}
|
||||||
tasks {
|
tasks {
|
||||||
generateLexer {
|
generateLexer {
|
||||||
enabled = true
|
|
||||||
sourceFile = file("src/main/grammar/Zon.flex")
|
sourceFile = file("src/main/grammar/Zon.flex")
|
||||||
targetOutputDir = file("${grammarKitGenDir}/lexer/${rootPackagePath}/zon/lexer")
|
targetOutputDir = file("${grammarKitGenDir}/lexer/${rootPackagePath}/zon/lexer")
|
||||||
}
|
}
|
||||||
|
|
||||||
generateParser {
|
generateParser {
|
||||||
enabled = true
|
|
||||||
sourceFile = file("src/main/grammar/Zon.bnf")
|
sourceFile = file("src/main/grammar/Zon.bnf")
|
||||||
pathToParser = "${rootPackagePath}/zon/psi/ZonParser.java"
|
pathToParser = "${rootPackagePath}/zon/psi/ZonParser.java"
|
||||||
pathToPsiRoot = "${rootPackagePath}/zon/psi"
|
pathToPsiRoot = "${rootPackagePath}/zon/psi"
|
||||||
}
|
}
|
||||||
|
|
||||||
compileJava {
|
|
||||||
dependsOn("generateGrammars")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
project(":plugin") {
|
project(":plugin") {
|
||||||
apply {
|
|
||||||
plugin("org.jetbrains.changelog")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(project(":common"))
|
||||||
implementation(project(":zig"))
|
implementation(project(":zig"))
|
||||||
implementation(project(":project"))
|
implementation(project(":project"))
|
||||||
implementation(project(":zon"))
|
implementation(project(":zon"))
|
||||||
implementation(project(":debugger"))
|
implementation(project(":debugger"))
|
||||||
implementation(project(":"))
|
intellijPlatform {
|
||||||
|
zipSigner()
|
||||||
|
pluginVerifier()
|
||||||
|
when (baseIDE) {
|
||||||
|
"idea" -> intellijIdeaCommunity(ideaVersion)
|
||||||
|
"clion" -> clion(clionVersion)
|
||||||
|
}
|
||||||
|
plugin(lsp4ijPluginString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
intellij {
|
intellijPlatform {
|
||||||
pluginName = properties("pluginName")
|
projectName = "ZigBrains"
|
||||||
|
pluginConfiguration {
|
||||||
|
name = properties("pluginName")
|
||||||
|
description = providers.fileContents(rootProject.layout.projectDirectory.file("README.md")).asText.map {
|
||||||
|
val start = "<!-- Plugin description -->"
|
||||||
|
val end = "<!-- Plugin description end -->"
|
||||||
|
|
||||||
|
with(it.lines()) {
|
||||||
|
if (!containsAll(listOf(start, end))) {
|
||||||
|
throw GradleException("Plugin description section not found in README.md:\n$start ... $end")
|
||||||
|
}
|
||||||
|
subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changeNotes = pluginVersion().map { pluginVersion ->
|
||||||
|
with(rootProject.changelog) {
|
||||||
|
renderItem(
|
||||||
|
(getOrNull(pluginVersion) ?: getUnreleased())
|
||||||
|
.withHeader(false)
|
||||||
|
.withEmptySections(false),
|
||||||
|
Changelog.OutputType.HTML,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
version = pluginVersionFull()
|
||||||
|
}
|
||||||
|
signing {
|
||||||
|
certificateChainFile = rootProject.file("secrets/chain.crt")
|
||||||
|
privateKeyFile = rootProject.file("secrets/private.pem")
|
||||||
|
password = environment("PRIVATE_KEY_PASSWORD")
|
||||||
|
}
|
||||||
|
verifyPlugin {
|
||||||
|
ides {
|
||||||
|
ide(IntelliJPlatformType.IntellijIdeaCommunity, ideaVersion)
|
||||||
|
ide(IntelliJPlatformType.IntellijIdeaUltimate, ideaVersion)
|
||||||
|
ide(IntelliJPlatformType.CLion, clionVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include the generated files in the source set
|
// Include the generated files in the source set
|
||||||
|
|
||||||
// Collects all jars produced by compilation of project modules and merges them into singe one.
|
// Collects all jars produced by compilation of project modules and merges them into singe one.
|
||||||
// We need to put all plugin manifest files into single jar to make new plugin model work
|
// We need to put all plugin manifest files into single jar to make new plugin model work
|
||||||
|
@ -333,7 +323,7 @@ project(":plugin") {
|
||||||
|
|
||||||
val pluginLibDir by lazy {
|
val pluginLibDir by lazy {
|
||||||
val sandboxTask = tasks.prepareSandbox.get()
|
val sandboxTask = tasks.prepareSandbox.get()
|
||||||
sandboxTask.destinationDir.resolve("${sandboxTask.pluginName.get()}/lib")
|
sandboxTask.destinationDir.resolve("${project.extensionProvider.map { it.projectName }.get()}/lib")
|
||||||
}
|
}
|
||||||
|
|
||||||
val pluginJars by lazy {
|
val pluginJars by lazy {
|
||||||
|
@ -354,11 +344,6 @@ project(":plugin") {
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
|
||||||
buildPlugin {
|
|
||||||
archiveBaseName.set("ZigBrains")
|
|
||||||
}
|
|
||||||
|
|
||||||
runIde {
|
runIde {
|
||||||
dependsOn(mergePluginJarTask)
|
dependsOn(mergePluginJarTask)
|
||||||
enabled = true
|
enabled = true
|
||||||
|
@ -373,65 +358,56 @@ project(":plugin") {
|
||||||
dependsOn(mergePluginJarTask)
|
dependsOn(mergePluginJarTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
patchPluginXml {
|
|
||||||
version = pluginVersionFull()
|
|
||||||
|
|
||||||
// Extract the <!-- Plugin description --> section from README.md and provide for the plugin's manifest
|
|
||||||
pluginDescription = providers.fileContents(rootProject.layout.projectDirectory.file("README.md")).asText.map {
|
|
||||||
val start = "<!-- Plugin description -->"
|
|
||||||
val end = "<!-- Plugin description end -->"
|
|
||||||
|
|
||||||
with (it.lines()) {
|
|
||||||
if (!containsAll(listOf(start, end))) {
|
|
||||||
throw GradleException("Plugin description section not found in README.md:\n$start ... $end")
|
|
||||||
}
|
|
||||||
subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val changelog = rootProject.changelog // local variable for configuration cache compatibility
|
|
||||||
// Get the latest available change notes from the changelog file
|
|
||||||
changeNotes = pluginVersion().map { pluginVersion ->
|
|
||||||
with(changelog) {
|
|
||||||
renderItem(
|
|
||||||
(getOrNull(pluginVersion) ?: getUnreleased())
|
|
||||||
.withHeader(false)
|
|
||||||
.withEmptySections(false),
|
|
||||||
Changelog.OutputType.HTML,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signPlugin {
|
|
||||||
certificateChainFile = rootProject.file("secrets/chain.crt")
|
|
||||||
privateKeyFile = rootProject.file("secrets/private.pem")
|
|
||||||
password = environment("PRIVATE_KEY_PASSWORD")
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyPluginSignature {
|
|
||||||
certificateChainFile = rootProject.file("secrets/chain.crt")
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyPlugin {
|
verifyPlugin {
|
||||||
dependsOn(mergePluginJarTask)
|
dependsOn(mergePluginJarTask)
|
||||||
enabled = true
|
enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
listProductsReleases {
|
verifyPluginProjectConfiguration {
|
||||||
types = listOf("IU", "IC", "CL")
|
enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signPlugin {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyPluginSignature {
|
||||||
|
dependsOn(signPlugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPlugin {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
intellijPlatform {
|
||||||
|
when (baseIDE) {
|
||||||
|
"idea" -> intellijIdeaCommunity(ideaVersion)
|
||||||
|
"clion" -> clion(clionVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
generateLexer {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
generateParser {
|
||||||
|
enabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun distFile(it: String) = layout.buildDirectory.file("dist/ZigBrains-${pluginVersion().get()}-$it-signed.zip")
|
fun distFile(it: String) = layout.buildDirectory.file("dist/ZigBrains-${pluginVersion().get()}-$it-signed.zip")
|
||||||
|
|
||||||
publishVersions.forEach {
|
publishVersions.forEach {
|
||||||
tasks.register<PublishPluginTask>("jbpublish-$it") {
|
tasks.register<PublishPluginTask>("jbpublish-$it").configure {
|
||||||
distributionFile.set(distFile(it))
|
archiveFile = distFile(it)
|
||||||
token = environment("IJ_PUBLISH_TOKEN")
|
token = environment("IJ_PUBLISH_TOKEN")
|
||||||
|
channels = if (pluginVersion().get().contains("-")) listOf("nightly") else listOf("default")
|
||||||
}
|
}
|
||||||
tasks.named("publish") {
|
tasks.named("publish").configure {
|
||||||
dependsOn("jbpublish-$it")
|
dependsOn("jbpublish-$it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -479,4 +455,4 @@ fun File.isManifestFile(): Boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return rootNode.name() == "idea-plugin"
|
return rootNode.name() == "idea-plugin"
|
||||||
}
|
}
|
||||||
|
|
2
build.sh
2
build.sh
|
@ -76,6 +76,6 @@ git checkout "$DEFAULT_BRANCH"
|
||||||
|
|
||||||
mkdir -p build/dist
|
mkdir -p build/dist
|
||||||
|
|
||||||
cp plugin/build/distributions/*-signed.zip build/dist/
|
cp modules/plugin/build/distributions/*-signed.zip build/dist/
|
||||||
|
|
||||||
./gradlew publish
|
./gradlew publish
|
|
@ -1,22 +1,25 @@
|
||||||
pluginGroup = com.falsepattern.zigbrains
|
pluginGroup=com.falsepattern.zigbrains
|
||||||
pluginName = ZigBrains
|
pluginName=ZigBrains
|
||||||
pluginRepositoryUrl = https://github.com/FalsePattern/ZigBrains
|
pluginRepositoryUrl=https://github.com/FalsePattern/ZigBrains
|
||||||
|
|
||||||
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
|
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
|
||||||
pluginSinceBuild = 233
|
pluginSinceBuild=233
|
||||||
pluginUntilBuild = 233.*
|
pluginUntilBuild=233.*
|
||||||
|
|
||||||
baseIDE = clion
|
baseIDE=clion
|
||||||
|
|
||||||
ideaVersion = IC-2023.3.6
|
ideaVersion=2023.3.7
|
||||||
clionVersion = CL-2023.3.4
|
clionVersion=2023.3.5
|
||||||
|
|
||||||
|
pluginVersion=16.0.0-pre1
|
||||||
|
|
||||||
# Gradle Releases -> https://github.com/gradle/gradle/releases
|
# Gradle Releases -> https://github.com/gradle/gradle/releases
|
||||||
gradleVersion = 8.7
|
gradleVersion=8.8
|
||||||
|
|
||||||
# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
|
# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
|
||||||
org.gradle.caching = true
|
org.gradle.caching=true
|
||||||
|
|
||||||
# Enable Gradle Kotlin DSL Lazy Property Assignment -> https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:assignment
|
# Enable Gradle Kotlin DSL Lazy Property Assignment -> https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:assignment
|
||||||
systemProp.org.gradle.unsafe.kotlin.assignment = true
|
systemProp.org.gradle.unsafe.kotlin.assignment=true
|
||||||
kotlin.stdlib.default.dependency = false
|
|
||||||
|
org.jetbrains.intellij.platform.buildFeature.useBinaryReleases=false
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
19
gradlew
vendored
19
gradlew
vendored
|
@ -55,7 +55,7 @@
|
||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
@ -83,7 +83,8 @@ done
|
||||||
# This is normally unused
|
# This is normally unused
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
|
@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
|
@ -201,11 +202,11 @@ fi
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# 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"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
# Collect all arguments for the java command:
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
# and any embedded shellness will be escaped.
|
||||||
# double quotes to make sure that they get re-expanded; and
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
|
20
gradlew.bat
vendored
20
gradlew.bat
vendored
|
@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,10 @@ package com.falsepattern.zigbrains.backports.com.intellij.lang.documentation
|
||||||
import com.intellij.lang.Language
|
import com.intellij.lang.Language
|
||||||
import com.intellij.lang.documentation.DocumentationMarkup.*
|
import com.intellij.lang.documentation.DocumentationMarkup.*
|
||||||
import com.intellij.lang.documentation.DocumentationSettings.InlineCodeHighlightingMode
|
import com.intellij.lang.documentation.DocumentationSettings.InlineCodeHighlightingMode
|
||||||
import com.intellij.openapi.editor.colors.EditorColorsScheme
|
|
||||||
import com.intellij.openapi.editor.colors.TextAttributesKey
|
|
||||||
import com.intellij.openapi.editor.markup.TextAttributes
|
|
||||||
import com.intellij.openapi.editor.richcopy.HtmlSyntaxInfoUtil
|
import com.intellij.openapi.editor.richcopy.HtmlSyntaxInfoUtil
|
||||||
import com.intellij.openapi.fileTypes.FileTypeManager
|
import com.intellij.openapi.fileTypes.FileTypeManager
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.util.NlsSafe
|
import com.intellij.openapi.util.NlsSafe
|
||||||
import com.intellij.psi.PsiElement
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.ui.components.JBHtmlPaneStyleConfiguration
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.ui.components.JBHtmlPaneStyleConfiguration.*
|
import com.falsepattern.zigbrains.backports.com.intellij.ui.components.JBHtmlPaneStyleConfiguration.*
|
||||||
import com.intellij.lang.documentation.DocumentationSettings
|
import com.intellij.lang.documentation.DocumentationSettings
|
||||||
import com.intellij.util.concurrency.annotations.RequiresReadLock
|
import com.intellij.util.concurrency.annotations.RequiresReadLock
|
||||||
|
|
|
@ -1,271 +0,0 @@
|
||||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
|
||||||
package com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc
|
|
||||||
|
|
||||||
|
|
||||||
import com.intellij.lang.Language
|
|
||||||
import com.intellij.lang.documentation.DocumentationSettings
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.lang.documentation.QuickDocHighlightingHelper
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl.DocFlavourDescriptor
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl.DocTagRenderer
|
|
||||||
import com.intellij.openapi.diagnostic.ControlFlowException
|
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
|
||||||
import com.intellij.openapi.project.Project
|
|
||||||
import com.intellij.openapi.util.NlsSafe
|
|
||||||
import com.intellij.openapi.util.TextRange
|
|
||||||
import com.intellij.openapi.util.text.StringUtil
|
|
||||||
import com.intellij.psi.PsiFile
|
|
||||||
import com.intellij.ui.ColorUtil
|
|
||||||
import com.intellij.util.concurrency.annotations.RequiresReadLock
|
|
||||||
import com.intellij.util.containers.CollectionFactory
|
|
||||||
import com.intellij.util.containers.ContainerUtil
|
|
||||||
import com.intellij.util.ui.UIUtil
|
|
||||||
import org.intellij.markdown.IElementType
|
|
||||||
import org.intellij.markdown.html.HtmlGenerator
|
|
||||||
import org.intellij.markdown.parser.MarkdownParser
|
|
||||||
import org.jetbrains.annotations.Contract
|
|
||||||
import org.jetbrains.annotations.Nls
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [DocMarkdownToHtmlConverter] handles conversion of Markdown text to HTML, which is intended
|
|
||||||
* to be displayed in Quick Doc popup, or inline in an editor.
|
|
||||||
*/
|
|
||||||
object DocMarkdownToHtmlConverter {
|
|
||||||
private val LOG = Logger.getInstance(DocMarkdownToHtmlConverter::class.java)
|
|
||||||
private val TAG_START_OR_CLOSE_PATTERN: Pattern = Pattern.compile("(<)/?(\\w+)[> ]")
|
|
||||||
internal val TAG_PATTERN: Pattern = Pattern.compile("^</?([a-z][a-z-_0-9]*)[^>]*>$", Pattern.CASE_INSENSITIVE)
|
|
||||||
private val FENCE_PATTERN = "\\s+```.*".toRegex()
|
|
||||||
private const val FENCED_CODE_BLOCK = "```"
|
|
||||||
|
|
||||||
private val HTML_DOC_SUBSTITUTIONS: Map<String, String> = mapOf(
|
|
||||||
"<em>" to "<i>",
|
|
||||||
"</em>" to "</i>",
|
|
||||||
"<strong>" to "<b>",
|
|
||||||
"</strong>" to "</b>",
|
|
||||||
": //" to "://", // Fix URL
|
|
||||||
"<p></p>" to "",
|
|
||||||
"</p>" to "",
|
|
||||||
"<br />" to "",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
internal val ACCEPTABLE_BLOCK_TAGS: MutableSet<CharSequence> = CollectionFactory.createCharSequenceSet(false)
|
|
||||||
.apply {
|
|
||||||
addAll(listOf( // Text content
|
|
||||||
"blockquote", "dd", "dl", "dt",
|
|
||||||
"hr", "li", "ol", "ul", "pre", "p", // Table,
|
|
||||||
|
|
||||||
"caption", "col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val ACCEPTABLE_TAGS: Set<CharSequence> = CollectionFactory.createCharSequenceSet(false)
|
|
||||||
.apply {
|
|
||||||
addAll(ACCEPTABLE_BLOCK_TAGS)
|
|
||||||
addAll(listOf( // Content sectioning
|
|
||||||
"h1", "h2", "h3", "h4", "h5", "h6",
|
|
||||||
// Inline text semantic
|
|
||||||
"a", "b", "br", "code", "em", "i", "s", "span", "strong", "u", "wbr", "kbd", "samp",
|
|
||||||
// Image and multimedia
|
|
||||||
"img",
|
|
||||||
// Svg and math
|
|
||||||
"svg",
|
|
||||||
// Obsolete
|
|
||||||
"tt",
|
|
||||||
// special IJ tags
|
|
||||||
"shortcut", "icon"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts provided Markdown text to HTML. The results are intended to be used for Quick Documentation.
|
|
||||||
* If [defaultLanguage] is provided, it will be used for syntax coloring of inline code and code blocks, if language specifier is missing.
|
|
||||||
* Block and inline code syntax coloring is being done by [QuickDocHighlightingHelper], which honors [DocumentationSettings].
|
|
||||||
* Conversion must be run within a Read Action as it might require to create intermediate [PsiFile] to highlight block of code,
|
|
||||||
* or an inline code.
|
|
||||||
*/
|
|
||||||
@Contract(pure = true)
|
|
||||||
@JvmStatic
|
|
||||||
@RequiresReadLock
|
|
||||||
@JvmOverloads
|
|
||||||
fun convert(project: Project, @Nls markdownText: String, defaultLanguage: Language? = null): @Nls String {
|
|
||||||
val lines = markdownText.lines()
|
|
||||||
val minCommonIndent =
|
|
||||||
lines
|
|
||||||
.filter(String::isNotBlank)
|
|
||||||
.minOfOrNull { line -> line.indexOfFirst { !it.isWhitespace() }.let { if (it == -1) line.length else it } }
|
|
||||||
?: 0
|
|
||||||
val processedLines = ArrayList<String>(lines.size)
|
|
||||||
var isInCode = false
|
|
||||||
var isInTable = false
|
|
||||||
var tableFormats: List<String>? = null
|
|
||||||
for (i in lines.indices) {
|
|
||||||
var processedLine = lines[i].let { if (it.length <= minCommonIndent) "" else it.substring(minCommonIndent) }
|
|
||||||
processedLine = processedLine.trimEnd()
|
|
||||||
val count = StringUtil.getOccurrenceCount(processedLine, FENCED_CODE_BLOCK)
|
|
||||||
if (count > 0) {
|
|
||||||
isInCode = if (count % 2 == 0) isInCode else !isInCode
|
|
||||||
if (processedLine.matches(FENCE_PATTERN)) {
|
|
||||||
processedLine = processedLine.trim { it <= ' ' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!isInCode) {
|
|
||||||
// TODO merge custom table generation code with Markdown parser
|
|
||||||
val tableDelimiterIndex = processedLine.indexOf('|')
|
|
||||||
if (tableDelimiterIndex != -1) {
|
|
||||||
if (!isInTable) {
|
|
||||||
if (i + 1 < lines.size) {
|
|
||||||
tableFormats = parseTableFormats(splitTableCols(lines[i + 1]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// create table only if we've successfully read the formats line
|
|
||||||
if (!ContainerUtil.isEmpty(tableFormats)) {
|
|
||||||
val parts = splitTableCols(processedLine)
|
|
||||||
if (isTableHeaderSeparator(parts)) continue
|
|
||||||
processedLine = getProcessedRow(project, defaultLanguage, isInTable, parts, tableFormats)
|
|
||||||
if (!isInTable) processedLine = "<table style=\"border: 0px;\" cellspacing=\"0\">$processedLine"
|
|
||||||
isInTable = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (isInTable) processedLine += "</table>"
|
|
||||||
isInTable = false
|
|
||||||
tableFormats = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
processedLines.add(processedLine)
|
|
||||||
}
|
|
||||||
var normalizedMarkdown = StringUtil.join(processedLines, "\n")
|
|
||||||
if (isInTable) normalizedMarkdown += "</table>" //NON-NLS
|
|
||||||
|
|
||||||
return performConversion(project, defaultLanguage, normalizedMarkdown)?.trimEnd()
|
|
||||||
?: adjustHtml(replaceProhibitedTags(convertNewLinePlaceholdersToTags(markdownText), ContainerUtil.emptyList()))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun convertNewLinePlaceholdersToTags(generatedDoc: String): String {
|
|
||||||
return StringUtil.replace(generatedDoc, "\n", "\n<p>")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseTableFormats(cols: List<String>): List<String>? {
|
|
||||||
val formats = ArrayList<String>()
|
|
||||||
for (col in cols) {
|
|
||||||
if (!isHeaderSeparator(col)) return null
|
|
||||||
formats.add(parseFormat(col.trim { it <= ' ' }))
|
|
||||||
}
|
|
||||||
return formats
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isTableHeaderSeparator(parts: List<String>): Boolean =
|
|
||||||
parts.all { isHeaderSeparator(it) }
|
|
||||||
|
|
||||||
private fun isHeaderSeparator(s: String): Boolean =
|
|
||||||
s.trim { it <= ' ' }.removePrefix(":").removeSuffix(":").chars().allMatch { it == '-'.code }
|
|
||||||
|
|
||||||
private fun splitTableCols(processedLine: String): List<String> {
|
|
||||||
val parts = ArrayList(StringUtil.split(processedLine, "|"))
|
|
||||||
if (parts.isEmpty()) return parts
|
|
||||||
if (parts[0].isNullOrBlank())
|
|
||||||
parts.removeAt(0)
|
|
||||||
if (!parts.isEmpty() && parts[parts.size - 1].isNullOrBlank())
|
|
||||||
parts.removeAt(parts.size - 1)
|
|
||||||
return parts
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getProcessedRow(project: Project,
|
|
||||||
defaultLanguage: Language?,
|
|
||||||
isInTable: Boolean,
|
|
||||||
parts: List<String>,
|
|
||||||
tableFormats: List<String>?): String {
|
|
||||||
val openingTagStart = if (isInTable)
|
|
||||||
"<td style=\"$border\" "
|
|
||||||
else
|
|
||||||
"<th style=\"$border\" "
|
|
||||||
val closingTag = if (isInTable) "</td>" else "</th>"
|
|
||||||
val resultBuilder = StringBuilder("<tr style=\"$border\">$openingTagStart")
|
|
||||||
resultBuilder.append("align=\"").append(getAlign(0, tableFormats)).append("\">")
|
|
||||||
for (i in parts.indices) {
|
|
||||||
if (i > 0) {
|
|
||||||
resultBuilder.append(closingTag).append(openingTagStart).append("align=\"").append(getAlign(i, tableFormats)).append("\">")
|
|
||||||
}
|
|
||||||
resultBuilder.append(performConversion(project, defaultLanguage, parts[i].trim { it <= ' ' }))
|
|
||||||
}
|
|
||||||
resultBuilder.append(closingTag).append("</tr>")
|
|
||||||
return resultBuilder.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAlign(index: Int, formats: List<String>?): String {
|
|
||||||
return if (formats == null || index >= formats.size) "left" else formats[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseFormat(format: String): String {
|
|
||||||
if (format.length <= 1) return "left"
|
|
||||||
val c0 = format[0]
|
|
||||||
val cE = format[format.length - 1]
|
|
||||||
return if (c0 == ':' && cE == ':') "center" else if (cE == ':') "right" else "left"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val embeddedHtmlType = IElementType("ROOT")
|
|
||||||
|
|
||||||
private fun performConversion(project: Project, defaultLanguage: Language?, text: @Nls String): @NlsSafe String? {
|
|
||||||
try {
|
|
||||||
val flavour = DocFlavourDescriptor(project, defaultLanguage)
|
|
||||||
val parsedTree = MarkdownParser(flavour).parse(embeddedHtmlType, text, true)
|
|
||||||
return HtmlGenerator(text, parsedTree, flavour, false)
|
|
||||||
.generateHtml(DocTagRenderer(text))
|
|
||||||
}
|
|
||||||
catch (e: Exception) {
|
|
||||||
if (e is ControlFlowException) throw e
|
|
||||||
LOG.warn(e.message, e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun replaceProhibitedTags(line: String, skipRanges: List<TextRange>): @NlsSafe String {
|
|
||||||
val matcher = TAG_START_OR_CLOSE_PATTERN.matcher(line)
|
|
||||||
val builder = StringBuilder(line)
|
|
||||||
|
|
||||||
var diff = 0
|
|
||||||
l@ while (matcher.find()) {
|
|
||||||
val tagName = matcher.group(2)
|
|
||||||
|
|
||||||
if (ACCEPTABLE_TAGS.contains(tagName)) continue
|
|
||||||
|
|
||||||
val startOfTag = matcher.start(2)
|
|
||||||
for (range in skipRanges) {
|
|
||||||
if (range.contains(startOfTag)) {
|
|
||||||
continue@l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val start = matcher.start(1) + diff
|
|
||||||
if (StringUtil.toLowerCase(tagName) == "div") {
|
|
||||||
val isOpenTag = !matcher.group(0).contains("/")
|
|
||||||
val end = start + (if (isOpenTag) 5 else 6)
|
|
||||||
val replacement = if (isOpenTag) "<span>" else "</span>"
|
|
||||||
builder.replace(start, end, replacement)
|
|
||||||
diff += 1
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
builder.replace(start, start + 1, "<")
|
|
||||||
diff += 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Contract(pure = true)
|
|
||||||
private fun adjustHtml(html: String): String {
|
|
||||||
var str = html
|
|
||||||
for ((key, value) in HTML_DOC_SUBSTITUTIONS) {
|
|
||||||
if (str.indexOf(key) > 0) {
|
|
||||||
str = str.replace(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str.trim { it <= ' ' }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val border: String
|
|
||||||
get() = "margin: 0; border: 1px solid; border-color: #" + ColorUtil
|
|
||||||
.toHex(UIUtil.getTooltipSeparatorColor()) + "; border-spacing: 0; border-collapse: collapse;vertical-align: baseline;"
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
|
||||||
package com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl
|
|
||||||
|
|
||||||
import com.intellij.lang.Language
|
|
||||||
import com.intellij.openapi.project.Project
|
|
||||||
import org.intellij.markdown.IElementType
|
|
||||||
import org.intellij.markdown.MarkdownElementTypes
|
|
||||||
import org.intellij.markdown.MarkdownTokenTypes
|
|
||||||
import org.intellij.markdown.flavours.commonmark.CommonMarkMarkerProcessor
|
|
||||||
import org.intellij.markdown.flavours.gfm.GFMConstraints
|
|
||||||
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
|
||||||
import org.intellij.markdown.html.GeneratingProvider
|
|
||||||
import org.intellij.markdown.html.ImageGeneratingProvider
|
|
||||||
import org.intellij.markdown.html.ReferenceLinksGeneratingProvider
|
|
||||||
import org.intellij.markdown.html.makeXssSafe
|
|
||||||
import org.intellij.markdown.parser.LinkMap
|
|
||||||
import org.intellij.markdown.parser.MarkerProcessor
|
|
||||||
import org.intellij.markdown.parser.MarkerProcessorFactory
|
|
||||||
import org.intellij.markdown.parser.ProductionHolder
|
|
||||||
import org.intellij.markdown.parser.constraints.CommonMarkdownConstraints
|
|
||||||
import org.intellij.markdown.parser.markerblocks.MarkerBlockProvider
|
|
||||||
import org.intellij.markdown.parser.markerblocks.providers.HtmlBlockProvider
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
private val baseHtmlGeneratingProvidersMap =
|
|
||||||
GFMFlavourDescriptor().createHtmlGeneratingProviders(LinkMap(emptyMap()), null) + hashMapOf(
|
|
||||||
MarkdownTokenTypes.HTML_TAG to DocSanitizingTagGeneratingProvider(),
|
|
||||||
MarkdownElementTypes.PARAGRAPH to DocParagraphGeneratingProvider(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
internal class DocFlavourDescriptor(private val project: Project, private val defaultLanguage: Language?) : GFMFlavourDescriptor() {
|
|
||||||
override val markerProcessorFactory: MarkerProcessorFactory
|
|
||||||
get() = object : MarkerProcessorFactory {
|
|
||||||
override fun createMarkerProcessor(productionHolder: ProductionHolder): MarkerProcessor<*> =
|
|
||||||
DocumentationMarkerProcessor(productionHolder, GFMConstraints.BASE)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createHtmlGeneratingProviders(linkMap: LinkMap, baseURI: URI?): Map<IElementType, GeneratingProvider> =
|
|
||||||
MergedMap(hashMapOf(MarkdownElementTypes.FULL_REFERENCE_LINK to
|
|
||||||
ReferenceLinksGeneratingProvider(linkMap, baseURI, absolutizeAnchorLinks).makeXssSafe(useSafeLinks),
|
|
||||||
MarkdownElementTypes.SHORT_REFERENCE_LINK to
|
|
||||||
ReferenceLinksGeneratingProvider(linkMap, baseURI, absolutizeAnchorLinks).makeXssSafe(useSafeLinks),
|
|
||||||
MarkdownElementTypes.IMAGE to ImageGeneratingProvider(linkMap, baseURI).makeXssSafe(useSafeLinks),
|
|
||||||
|
|
||||||
MarkdownElementTypes.CODE_BLOCK to DocCodeBlockGeneratingProvider(project, defaultLanguage),
|
|
||||||
MarkdownElementTypes.CODE_FENCE to DocCodeBlockGeneratingProvider(project, defaultLanguage),
|
|
||||||
MarkdownElementTypes.CODE_SPAN to DocCodeSpanGeneratingProvider(project, defaultLanguage)),
|
|
||||||
baseHtmlGeneratingProvidersMap)
|
|
||||||
|
|
||||||
private class DocumentationMarkerProcessor(productionHolder: ProductionHolder,
|
|
||||||
constraintsBase: CommonMarkdownConstraints) : CommonMarkMarkerProcessor(productionHolder,
|
|
||||||
constraintsBase) {
|
|
||||||
override fun getMarkerBlockProviders(): List<MarkerBlockProvider<StateInfo>> =
|
|
||||||
super.getMarkerBlockProviders().filter { it !is HtmlBlockProvider } + DocHtmlBlockProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MergedMap(val map1: Map<IElementType, GeneratingProvider>,
|
|
||||||
val map2: Map<IElementType, GeneratingProvider>) : Map<IElementType, GeneratingProvider> {
|
|
||||||
|
|
||||||
override fun isEmpty(): Boolean =
|
|
||||||
map1.isEmpty() && map2.isEmpty()
|
|
||||||
|
|
||||||
override fun get(key: IElementType): GeneratingProvider? =
|
|
||||||
map1[key] ?: map2[key]
|
|
||||||
|
|
||||||
override fun containsValue(value: GeneratingProvider): Boolean =
|
|
||||||
map1.containsValue(value) || map2.containsValue(value)
|
|
||||||
|
|
||||||
override fun containsKey(key: IElementType): Boolean =
|
|
||||||
map1.containsKey(key) || map2.containsKey(key)
|
|
||||||
|
|
||||||
override val entries: Set<Map.Entry<IElementType, GeneratingProvider>>
|
|
||||||
get() = throw UnsupportedOperationException()
|
|
||||||
override val keys: Set<IElementType>
|
|
||||||
get() = throw UnsupportedOperationException()
|
|
||||||
override val size: Int
|
|
||||||
get() = throw UnsupportedOperationException()
|
|
||||||
override val values: Collection<GeneratingProvider>
|
|
||||||
get() = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
|
||||||
package com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl
|
|
||||||
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter
|
|
||||||
import com.intellij.lang.Language
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.lang.documentation.QuickDocHighlightingHelper
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.lang.documentation.QuickDocHighlightingHelper.guessLanguage
|
|
||||||
import com.intellij.openapi.project.Project
|
|
||||||
import com.intellij.openapi.util.text.StringUtil
|
|
||||||
import com.intellij.util.containers.CollectionFactory
|
|
||||||
import com.intellij.util.containers.ContainerUtil
|
|
||||||
import org.intellij.markdown.MarkdownTokenTypes
|
|
||||||
import org.intellij.markdown.ast.ASTNode
|
|
||||||
import org.intellij.markdown.ast.LeafASTNode
|
|
||||||
import org.intellij.markdown.ast.accept
|
|
||||||
import org.intellij.markdown.ast.getTextInNode
|
|
||||||
import org.intellij.markdown.html.GeneratingProvider
|
|
||||||
import org.intellij.markdown.html.HtmlGenerator
|
|
||||||
import org.intellij.markdown.html.TrimmingInlineHolderProvider
|
|
||||||
|
|
||||||
private val TAG_REPLACE_MAP = CollectionFactory.createCharSequenceMap<String>(false).also {
|
|
||||||
it["div"] = "span"
|
|
||||||
it["em"] = "i"
|
|
||||||
it["strong"] = "b"
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DocSanitizingTagGeneratingProvider : GeneratingProvider {
|
|
||||||
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
|
|
||||||
val nodeText = node.getTextInNode(text)
|
|
||||||
if (nodeText.contentEquals("</p>", true)) return
|
|
||||||
val matcher = DocMarkdownToHtmlConverter.TAG_PATTERN.matcher(nodeText)
|
|
||||||
if (matcher.matches()) {
|
|
||||||
val tagName = matcher.group(1)
|
|
||||||
val replaceWith = TAG_REPLACE_MAP[tagName]
|
|
||||||
if (replaceWith != null) {
|
|
||||||
visitor.consumeHtml(nodeText.subSequence(0, matcher.start(1)))
|
|
||||||
visitor.consumeHtml(replaceWith)
|
|
||||||
visitor.consumeHtml(nodeText.subSequence(matcher.end(1), nodeText.length))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (DocMarkdownToHtmlConverter.ACCEPTABLE_TAGS.contains(tagName)) {
|
|
||||||
visitor.consumeHtml(nodeText)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visitor.consumeHtml(StringUtil.escapeXmlEntities(nodeText.toString()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DocCodeBlockGeneratingProvider(private val project: Project, private val defaultLanguage: Language?) : GeneratingProvider {
|
|
||||||
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
|
|
||||||
val contents = StringBuilder()
|
|
||||||
var language: String? = null
|
|
||||||
node.children.forEach { child ->
|
|
||||||
when (child.type) {
|
|
||||||
MarkdownTokenTypes.CODE_FENCE_CONTENT, MarkdownTokenTypes.CODE_LINE, MarkdownTokenTypes.EOL ->
|
|
||||||
contents.append(child.getTextInNode(text))
|
|
||||||
MarkdownTokenTypes.FENCE_LANG ->
|
|
||||||
language = HtmlGenerator.leafText(text, child).toString().trim().takeWhile { !it.isWhitespace() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visitor.consumeHtml(QuickDocHighlightingHelper.getStyledCodeBlock(
|
|
||||||
project, guessLanguage(language) ?: defaultLanguage, contents.toString()))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DocCodeSpanGeneratingProvider(private val project: Project, private val defaultLanguage: Language?) : GeneratingProvider {
|
|
||||||
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
|
|
||||||
val nodes = node.children.subList(1, node.children.size - 1)
|
|
||||||
val output = nodes
|
|
||||||
.filter { it.type != MarkdownTokenTypes.BLOCK_QUOTE }
|
|
||||||
.joinToString(separator = "") { it.getTextInNode(text) }.trim()
|
|
||||||
visitor.consumeHtml(QuickDocHighlightingHelper.getStyledInlineCode(project, defaultLanguage, output))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DocParagraphGeneratingProvider : TrimmingInlineHolderProvider() {
|
|
||||||
override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
|
|
||||||
visitor.consumeTagOpen(node, "p")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun closeTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
|
|
||||||
visitor.consumeTagClose("p")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
|
|
||||||
val childrenToRender = childrenToRender(node)
|
|
||||||
if (childrenToRender.isEmpty()) return
|
|
||||||
|
|
||||||
openTag(visitor, text, node)
|
|
||||||
|
|
||||||
for (child in childrenToRender(node)) {
|
|
||||||
if (child is LeafASTNode) {
|
|
||||||
visitor.visitLeaf(child)
|
|
||||||
} else {
|
|
||||||
child.accept(visitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeTag(visitor, text, node)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
|
||||||
package com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter
|
|
||||||
import org.intellij.markdown.lexer.Compat.assert
|
|
||||||
import org.intellij.markdown.parser.LookaheadText
|
|
||||||
import org.intellij.markdown.parser.MarkerProcessor
|
|
||||||
import org.intellij.markdown.parser.ProductionHolder
|
|
||||||
import org.intellij.markdown.parser.constraints.MarkdownConstraints
|
|
||||||
import org.intellij.markdown.parser.markerblocks.MarkerBlock
|
|
||||||
import org.intellij.markdown.parser.markerblocks.MarkerBlockProvider
|
|
||||||
import org.intellij.markdown.parser.markerblocks.impl.HtmlBlockMarkerBlock
|
|
||||||
|
|
||||||
internal object DocHtmlBlockProvider : MarkerBlockProvider<MarkerProcessor.StateInfo> {
|
|
||||||
override fun createMarkerBlocks(pos: LookaheadText.Position,
|
|
||||||
productionHolder: ProductionHolder,
|
|
||||||
stateInfo: MarkerProcessor.StateInfo): List<MarkerBlock> {
|
|
||||||
val matchingGroup = matches(pos, stateInfo.currentConstraints)
|
|
||||||
if (matchingGroup in 0..3) {
|
|
||||||
return listOf(HtmlBlockMarkerBlock(stateInfo.currentConstraints, productionHolder, OPEN_CLOSE_REGEXES[matchingGroup].second, pos))
|
|
||||||
}
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun interruptsParagraph(pos: LookaheadText.Position, constraints: MarkdownConstraints): Boolean {
|
|
||||||
return matches(pos, constraints) in 0..4
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun matches(pos: LookaheadText.Position, constraints: MarkdownConstraints): Int {
|
|
||||||
if (!MarkerBlockProvider.isStartOfLineWithConstraints(pos, constraints)) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
val text = pos.currentLineFromPosition
|
|
||||||
val offset = MarkerBlockProvider.passSmallIndent(text)
|
|
||||||
if (offset >= text.length || text[offset] != '<') {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
val matchResult = FIND_START_REGEX.find(text.substring(offset))
|
|
||||||
?: return -1
|
|
||||||
assert(matchResult.groups.size == OPEN_CLOSE_REGEXES.size + 2) { "There are some excess capturing groups probably!" }
|
|
||||||
for (i in OPEN_CLOSE_REGEXES.indices) {
|
|
||||||
if (matchResult.groups[i + 2] != null) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert(false) { "Match found but all groups are empty!" }
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
/** see {@link http://spec.commonmark.org/0.21/#html-blocks}
|
|
||||||
*
|
|
||||||
* nulls mean "Next line should be blank"
|
|
||||||
* */
|
|
||||||
private val OPEN_CLOSE_REGEXES: List<Pair<Regex, Regex?>> = listOf(
|
|
||||||
Pair(Regex("<(?:script|pre|style)(?: |>|$)", RegexOption.IGNORE_CASE),
|
|
||||||
Regex("</(?:script|style|pre)>", RegexOption.IGNORE_CASE)),
|
|
||||||
Pair(Regex("<!--"), Regex("-->")),
|
|
||||||
Pair(Regex("<\\?"), Regex("\\?>")),
|
|
||||||
Pair(Regex("<![A-Z]"), Regex(">")),
|
|
||||||
Pair(Regex("<!\\[CDATA\\["), Regex("]]>")),
|
|
||||||
Pair(Regex("</?(?:${DocMarkdownToHtmlConverter.ACCEPTABLE_BLOCK_TAGS.joinToString("|")})(?: |/?>|$)", RegexOption.IGNORE_CASE), null)
|
|
||||||
)
|
|
||||||
|
|
||||||
private val FIND_START_REGEX = Regex(
|
|
||||||
"^(${OPEN_CLOSE_REGEXES.joinToString(separator = "|", transform = { "(${it.first.pattern})" })})"
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
|
||||||
package com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter
|
|
||||||
import org.intellij.markdown.MarkdownTokenTypes
|
|
||||||
import org.intellij.markdown.ast.ASTNode
|
|
||||||
import org.intellij.markdown.ast.getTextInNode
|
|
||||||
import org.intellij.markdown.html.DUMMY_ATTRIBUTES_CUSTOMIZER
|
|
||||||
import org.intellij.markdown.html.HtmlGenerator
|
|
||||||
|
|
||||||
internal class DocTagRenderer(private val wholeText: String)
|
|
||||||
: HtmlGenerator.DefaultTagRenderer(DUMMY_ATTRIBUTES_CUSTOMIZER, false) {
|
|
||||||
|
|
||||||
override fun openTag(node: ASTNode, tagName: CharSequence,
|
|
||||||
vararg attributes: CharSequence?,
|
|
||||||
autoClose: Boolean): CharSequence {
|
|
||||||
if (tagName.contentEquals("p", true)) {
|
|
||||||
val first = node.children.firstOrNull()
|
|
||||||
if (first != null && first.type === MarkdownTokenTypes.HTML_TAG) {
|
|
||||||
val text = first.getTextInNode(wholeText)
|
|
||||||
val matcher = DocMarkdownToHtmlConverter.TAG_PATTERN.matcher(text)
|
|
||||||
if (matcher.matches()) {
|
|
||||||
val nestedTag = matcher.group(1)
|
|
||||||
if (DocMarkdownToHtmlConverter.ACCEPTABLE_BLOCK_TAGS.contains(nestedTag)) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tagName.contentEquals("code", true) && node.type === MarkdownTokenTypes.CODE_FENCE_CONTENT) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return super.openTag(node, convertTag(tagName), *attributes, autoClose = autoClose)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun closeTag(tagName: CharSequence): CharSequence {
|
|
||||||
if (tagName.contentEquals("p", true)) return ""
|
|
||||||
return super.closeTag(convertTag(tagName))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun convertTag(tagName: CharSequence): CharSequence {
|
|
||||||
if (tagName.contentEquals("strong", true)) {
|
|
||||||
return "b"
|
|
||||||
}
|
|
||||||
else if (tagName.contentEquals("em", true)) {
|
|
||||||
return "i"
|
|
||||||
}
|
|
||||||
return tagName
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package com.falsepattern.zigbrains.common;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evil hack for bypassing plugin verifier restrictions via generics. Unfortunately this is necessary due to public api limitations.
|
|
||||||
*/
|
|
||||||
public record ObjectHolder<T>(@NotNull T value) {
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
inlayprovider=ZLS Inlay Provider
|
inlayprovider=ZLS Inlay Provider
|
||||||
notif-zb=ZigBrains general notification
|
notif-zb=ZigBrains general notification
|
||||||
notif-zig-project=Zig project notification
|
notif-zig-project=Zig project notification
|
||||||
notif-zls-error=ZLS error notification
|
notif-zls-error=ZLS notification
|
||||||
notif-debug-info=ZigBrains debugger info
|
notif-debug-info=ZigBrains debugger info
|
||||||
notif-debug-warn=ZigBrains debugger warning
|
notif-debug-warn=ZigBrains debugger warning
|
||||||
notif-debug-error=ZigBrains debugger error
|
notif-debug-error=ZigBrains debugger error
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package com.falsepattern.zigbrains.clion;
|
package com.falsepattern.zigbrains.clion;
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.ObjectHolder;
|
|
||||||
import com.falsepattern.zigbrains.debugbridge.ZigDebuggerDriverConfigurationProvider;
|
import com.falsepattern.zigbrains.debugbridge.ZigDebuggerDriverConfigurationProvider;
|
||||||
import com.falsepattern.zigbrains.debugger.settings.ZigDebuggerSettings;
|
import com.falsepattern.zigbrains.debugger.settings.ZigDebuggerSettings;
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
import com.intellij.openapi.diagnostic.Logger;
|
||||||
|
@ -29,11 +28,13 @@ import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class ZigClionDebuggerDriverConfigurationProvider implements ZigDebuggerDriverConfigurationProvider {
|
public class ZigClionDebuggerDriverConfigurationProvider implements ZigDebuggerDriverConfigurationProvider {
|
||||||
private static final Logger LOG = Logger.getInstance(ZigClionDebuggerDriverConfigurationProvider.class);
|
private static final Logger LOG = Logger.getInstance(ZigClionDebuggerDriverConfigurationProvider.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable ObjectHolder<DebuggerDriverConfiguration> getDebuggerConfiguration(Project project, boolean isElevated, boolean emulateTerminal) {
|
public @Nullable Supplier<DebuggerDriverConfiguration> getDebuggerConfiguration(Project project, boolean isElevated, boolean emulateTerminal) {
|
||||||
if (SystemInfo.isWindows)
|
if (SystemInfo.isWindows)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
@ -51,9 +52,10 @@ public class ZigClionDebuggerDriverConfigurationProvider implements ZigDebuggerD
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CPPToolchains.Toolchain finalToolchain = toolchain;
|
||||||
return switch (toolchain.getDebuggerKind()) {
|
return switch (toolchain.getDebuggerKind()) {
|
||||||
case CUSTOM_GDB, BUNDLED_GDB -> new ObjectHolder<>(new CLionGDBDriverConfiguration(project, toolchain));
|
case CUSTOM_GDB, BUNDLED_GDB -> () -> new CLionGDBDriverConfiguration(project, finalToolchain);
|
||||||
case BUNDLED_LLDB -> new ObjectHolder<>(new CLionLLDBDriverConfiguration(project, toolchain));
|
case BUNDLED_LLDB -> () -> new CLionLLDBDriverConfiguration(project, finalToolchain);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,27 +16,26 @@
|
||||||
|
|
||||||
package com.falsepattern.zigbrains.debugbridge;
|
package com.falsepattern.zigbrains.debugbridge;
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.ObjectHolder;
|
|
||||||
import com.intellij.openapi.extensions.ExtensionPointName;
|
import com.intellij.openapi.extensions.ExtensionPointName;
|
||||||
import com.intellij.openapi.project.Project;
|
import com.intellij.openapi.project.Project;
|
||||||
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration;
|
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Objects;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public interface ZigDebuggerDriverConfigurationProvider {
|
public interface ZigDebuggerDriverConfigurationProvider {
|
||||||
ExtensionPointName<ZigDebuggerDriverConfigurationProvider> EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.debuggerDriverProvider");
|
ExtensionPointName<ZigDebuggerDriverConfigurationProvider> EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.debuggerDriverProvider");
|
||||||
|
|
||||||
static @NotNull Stream<Supplier<DebuggerDriverConfiguration>> findDebuggerConfigurations(Project project, boolean isElevated, boolean emulateTerminal) {
|
static @NotNull Stream<DebuggerDriverConfiguration> findDebuggerConfigurations(Project project, boolean isElevated, boolean emulateTerminal) {
|
||||||
return EXTENSION_POINT_NAME.getExtensionList()
|
return EXTENSION_POINT_NAME.getExtensionList()
|
||||||
.stream()
|
.stream()
|
||||||
.map(it -> (Supplier<DebuggerDriverConfiguration>) () -> Optional.ofNullable(it.getDebuggerConfiguration(project, isElevated, emulateTerminal))
|
.map(it -> it.getDebuggerConfiguration(project, isElevated, emulateTerminal))
|
||||||
.map(ObjectHolder::value)
|
.filter(Objects::nonNull)
|
||||||
.orElse(null));
|
.map(Supplier::get);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable ObjectHolder<DebuggerDriverConfiguration> getDebuggerConfiguration(Project project, boolean isElevated, boolean emulateTerminal);
|
@Nullable Supplier<DebuggerDriverConfiguration> getDebuggerConfiguration(Project project, boolean isElevated, boolean emulateTerminal);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.falsepattern.zigbrains.debugger;
|
package com.falsepattern.zigbrains.debugger;
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.ZigBundle;
|
import com.falsepattern.zigbrains.ZigBundle;
|
||||||
import com.falsepattern.zigbrains.common.ObjectHolder;
|
|
||||||
import com.falsepattern.zigbrains.debugbridge.ZigDebuggerDriverConfigurationProvider;
|
import com.falsepattern.zigbrains.debugbridge.ZigDebuggerDriverConfigurationProvider;
|
||||||
import com.falsepattern.zigbrains.debugger.settings.ZigDebuggerSettings;
|
import com.falsepattern.zigbrains.debugger.settings.ZigDebuggerSettings;
|
||||||
import com.falsepattern.zigbrains.debugger.toolchain.DebuggerAvailability;
|
import com.falsepattern.zigbrains.debugger.toolchain.DebuggerAvailability;
|
||||||
|
@ -26,6 +25,7 @@ import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class ZigDefaultDebuggerDriverConfigurationProvider implements ZigDebuggerDriverConfigurationProvider {
|
public class ZigDefaultDebuggerDriverConfigurationProvider implements ZigDebuggerDriverConfigurationProvider {
|
||||||
private static boolean availabilityCheck(Project project, DebuggerKind kind) {
|
private static boolean availabilityCheck(Project project, DebuggerKind kind) {
|
||||||
|
@ -79,7 +79,7 @@ public class ZigDefaultDebuggerDriverConfigurationProvider implements ZigDebugge
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable ObjectHolder<DebuggerDriverConfiguration> getDebuggerConfiguration(Project project, boolean isElevated, boolean emulateTerminal) {
|
public @Nullable Supplier<DebuggerDriverConfiguration> getDebuggerConfiguration(Project project, boolean isElevated, boolean emulateTerminal) {
|
||||||
val settings = ZigDebuggerSettings.getInstance();
|
val settings = ZigDebuggerSettings.getInstance();
|
||||||
val service = ZigDebuggerToolchainService.getInstance();
|
val service = ZigDebuggerToolchainService.getInstance();
|
||||||
val kind = settings.debuggerKind;
|
val kind = settings.debuggerKind;
|
||||||
|
@ -88,14 +88,14 @@ public class ZigDefaultDebuggerDriverConfigurationProvider implements ZigDebugge
|
||||||
}
|
}
|
||||||
val availability = service.debuggerAvailability(kind);
|
val availability = service.debuggerAvailability(kind);
|
||||||
return switch (availability.kind()) {
|
return switch (availability.kind()) {
|
||||||
case Bundled -> new ObjectHolder<>(switch (kind) {
|
case Bundled -> () -> (switch (kind) {
|
||||||
case LLDB -> new ZigLLDBDriverConfiguration(isElevated, emulateTerminal);
|
case LLDB -> new ZigLLDBDriverConfiguration(isElevated, emulateTerminal);
|
||||||
case GDB -> new ZigGDBDriverConfiguration(isElevated, emulateTerminal);
|
case GDB -> new ZigGDBDriverConfiguration(isElevated, emulateTerminal);
|
||||||
case MSVC -> throw new AssertionError("MSVC is never bundled");
|
case MSVC -> throw new AssertionError("MSVC is never bundled");
|
||||||
});
|
});
|
||||||
case Binaries -> {
|
case Binaries -> {
|
||||||
val bin = (DebuggerAvailability.Binaries) availability;
|
val bin = (DebuggerAvailability.Binaries) availability;
|
||||||
yield new ObjectHolder<>(switch (bin.binariesKind()) {
|
yield () -> (switch (bin.binariesKind()) {
|
||||||
case LLDB -> new ZigCustomBinariesLLDBDriverConfiguration((LLDBBinaries) bin, isElevated, emulateTerminal);
|
case LLDB -> new ZigCustomBinariesLLDBDriverConfiguration((LLDBBinaries) bin, isElevated, emulateTerminal);
|
||||||
case GDB -> new ZigCustomBinariesGDBDriverConfiguration((GDBBinaries) bin, isElevated, emulateTerminal);
|
case GDB -> new ZigCustomBinariesGDBDriverConfiguration((GDBBinaries) bin, isElevated, emulateTerminal);
|
||||||
case MSVC -> new ZigMSVCDriverConfiguration((MSVCBinaries) bin, isElevated, emulateTerminal);
|
case MSVC -> new ZigMSVCDriverConfiguration((MSVCBinaries) bin, isElevated, emulateTerminal);
|
||||||
|
|
|
@ -66,26 +66,19 @@ public abstract class ZigDebugRunnerBase<ProfileState extends ProfileStateBase<?
|
||||||
protected RunContentDescriptor doExecute(ProfileState state, AbstractZigToolchain toolchain, ExecutionEnvironment environment)
|
protected RunContentDescriptor doExecute(ProfileState state, AbstractZigToolchain toolchain, ExecutionEnvironment environment)
|
||||||
throws ExecutionException {
|
throws ExecutionException {
|
||||||
val project = environment.getProject();
|
val project = environment.getProject();
|
||||||
val providers = ZigDebuggerDriverConfigurationProvider.findDebuggerConfigurations(project, false, false)
|
val drivers = ZigDebuggerDriverConfigurationProvider.findDebuggerConfigurations(project, false, false)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
DebuggerDriverConfiguration debuggerDriver = null;
|
for (val debuggerDriver: drivers) {
|
||||||
|
if (debuggerDriver == null)
|
||||||
for (val provider: providers) {
|
continue;
|
||||||
debuggerDriver = provider.get();
|
ZigDebugParametersBase<ProfileState> runParameters = getDebugParameters(state, environment, debuggerDriver, toolchain);
|
||||||
if (debuggerDriver != null)
|
if (runParameters == null) {
|
||||||
break;
|
continue;
|
||||||
|
}
|
||||||
|
return startSession(environment, new ZigLocalDebugProcessStarter(runParameters, state, environment));
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
if (debuggerDriver == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
ZigDebugParametersBase<ProfileState> runParameters = getDebugParameters(state, environment, debuggerDriver, toolchain);
|
|
||||||
if (runParameters == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return startSession(environment, new ZigLocalDebugProcessStarter(runParameters, state, environment));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -31,13 +31,13 @@ import lombok.val;
|
||||||
import org.eclipse.lsp4j.debug.Capabilities;
|
import org.eclipse.lsp4j.debug.Capabilities;
|
||||||
import org.eclipse.lsp4j.debug.OutputEventArguments;
|
import org.eclipse.lsp4j.debug.OutputEventArguments;
|
||||||
import org.eclipse.lsp4j.debug.services.IDebugProtocolServer;
|
import org.eclipse.lsp4j.debug.services.IDebugProtocolServer;
|
||||||
|
import org.eclipse.lsp4j.debug.util.ToStringBuilder;
|
||||||
import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
|
import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
|
||||||
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
|
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
|
||||||
import org.eclipse.lsp4j.jsonrpc.MessageIssueException;
|
import org.eclipse.lsp4j.jsonrpc.MessageIssueException;
|
||||||
import org.eclipse.lsp4j.jsonrpc.debug.messages.DebugResponseMessage;
|
import org.eclipse.lsp4j.jsonrpc.debug.messages.DebugResponseMessage;
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.Message;
|
import org.eclipse.lsp4j.jsonrpc.messages.Message;
|
||||||
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
|
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
|
||||||
import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lspcommon.connection;
|
|
||||||
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class symbolizing a stream to a process
|
|
||||||
* <p>
|
|
||||||
* commands - The commands to start the process
|
|
||||||
* workingDir - The working directory of the process
|
|
||||||
*/
|
|
||||||
public class ProcessStreamConnectionProvider implements StreamConnectionProvider {
|
|
||||||
|
|
||||||
private Logger LOG = Logger.getInstance(ProcessStreamConnectionProvider.class);
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private ProcessBuilder builder;
|
|
||||||
@Nullable
|
|
||||||
protected Process process = null;
|
|
||||||
private List<String> commands;
|
|
||||||
private String workingDir;
|
|
||||||
|
|
||||||
public ProcessStreamConnectionProvider(List<String> commands, String workingDir) {
|
|
||||||
this.commands = commands;
|
|
||||||
this.workingDir = workingDir;
|
|
||||||
this.builder = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProcessStreamConnectionProvider(@NotNull ProcessBuilder processBuilder) {
|
|
||||||
this.builder = processBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() throws IOException {
|
|
||||||
if ((workingDir == null || commands == null || commands.isEmpty()) && builder == null) {
|
|
||||||
throw new IOException("Unable to start language server: " + this.toString());
|
|
||||||
}
|
|
||||||
ProcessBuilder builder = createProcessBuilder();
|
|
||||||
LOG.info("Starting server process with commands " + commands + " and workingDir " + workingDir);
|
|
||||||
process = builder.start();
|
|
||||||
if (!process.isAlive()) {
|
|
||||||
throw new IOException("Unable to start language server: " + this.toString());
|
|
||||||
} else {
|
|
||||||
LOG.info("Server process started " + process);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProcessBuilder createProcessBuilder() {
|
|
||||||
if (builder != null) {
|
|
||||||
return builder;
|
|
||||||
} else {
|
|
||||||
commands.forEach(c -> c = c.replace("\'", ""));
|
|
||||||
ProcessBuilder builder = new ProcessBuilder(commands);
|
|
||||||
builder.directory(new File(workingDir));
|
|
||||||
builder.redirectError(ProcessBuilder.Redirect.INHERIT);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public InputStream getInputStream() {
|
|
||||||
return process != null ? process.getInputStream() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public InputStream getErrorStream() {
|
|
||||||
return process != null ? process.getErrorStream() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public OutputStream getOutputStream() {
|
|
||||||
return process != null ? process.getOutputStream() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
if (process != null) {
|
|
||||||
process.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onExit(Runnable runnable) {
|
|
||||||
if (process != null) {
|
|
||||||
process.onExit().thenRun(runnable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (obj instanceof ProcessStreamConnectionProvider) {
|
|
||||||
ProcessStreamConnectionProvider other = (ProcessStreamConnectionProvider) obj;
|
|
||||||
return commands.size() == other.commands.size() && new HashSet<>(commands).equals(new HashSet<>(other.commands))
|
|
||||||
&& workingDir.equals(other.workingDir);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hashCode(commands) ^ Objects.hashCode(workingDir);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lspcommon.connection;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public interface StreamConnectionProvider {
|
|
||||||
|
|
||||||
void start() throws IOException;
|
|
||||||
|
|
||||||
InputStream getInputStream();
|
|
||||||
|
|
||||||
default @Nullable InputStream getErrorStream() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
OutputStream getOutputStream();
|
|
||||||
|
|
||||||
void stop();
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,393 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.FileUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
|
|
||||||
import com.falsepattern.zigbrains.lsp.extensions.LSPExtensionManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.Timeout;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import lombok.val;
|
|
||||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
|
||||||
import org.apache.commons.lang3.tuple.MutablePair;
|
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
|
||||||
import org.eclipse.lsp4j.DidChangeConfigurationParams;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import static com.falsepattern.zigbrains.common.util.ApplicationUtil.pool;
|
|
||||||
|
|
||||||
public class IntellijLanguageClient {
|
|
||||||
|
|
||||||
private static Logger LOG = Logger.getInstance(IntellijLanguageClient.class);
|
|
||||||
private static final Map<Pair<String, String>, LanguageServerWrapper> extToLanguageWrapper = new ConcurrentHashMap<>();
|
|
||||||
private static Map<String, Set<LanguageServerWrapper>> projectToLanguageWrappers = new ConcurrentHashMap<>();
|
|
||||||
private static Map<Pair<String, String>, LanguageServerDefinition> extToServerDefinition = new ConcurrentHashMap<>();
|
|
||||||
private static Map<String, LSPExtensionManager> extToExtManager = new ConcurrentHashMap<>();
|
|
||||||
private static final Predicate<LanguageServerWrapper> RUNNING = (s) -> s.getStatus() != ServerStatus.STOPPED;
|
|
||||||
|
|
||||||
// @Override
|
|
||||||
// public void initComponent() {
|
|
||||||
// try {
|
|
||||||
// // Adds project listener.
|
|
||||||
// ApplicationManager.getApplication().getMessageBus().connect().subscribe(ProjectManager.TOPIC,
|
|
||||||
// new LSPProjectManagerListener());
|
|
||||||
// // Adds editor listener.
|
|
||||||
// EditorFactory.getInstance().addEditorFactoryListener(new LSPEditorListener(), this);
|
|
||||||
// // Adds VFS listener.
|
|
||||||
// VirtualFileManager.getInstance().addVirtualFileListener(new VFSListener());
|
|
||||||
// // Adds document event listener.
|
|
||||||
// ApplicationManager.getApplication().getMessageBus().connect().subscribe(AppTopics.FILE_DOCUMENT_SYNC,
|
|
||||||
// new LSPFileDocumentManagerListener());
|
|
||||||
//
|
|
||||||
// // in case if JVM forcefully exit.
|
|
||||||
// Runtime.getRuntime().addShutdownHook(new Thread(() -> projectToLanguageWrappers.values().stream()
|
|
||||||
// .flatMap(Collection::stream).filter(RUNNING).forEach(s -> s.stop(true))));
|
|
||||||
//
|
|
||||||
// LOG.info("Intellij Language Client initialized successfully");
|
|
||||||
// } catch (Exception e) {
|
|
||||||
// LOG.warn("Fatal error occurred when initializing Intellij language client.", e);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use it to initialize the server connection for the given project (useful if no editor is launched)
|
|
||||||
*/
|
|
||||||
public void initProjectConnections(@NotNull Project project) {
|
|
||||||
String projectStr = FileUtils.projectToUri(project);
|
|
||||||
// find serverdefinition keys for this project and try to start a wrapper
|
|
||||||
extToServerDefinition.entrySet().stream().filter(e -> e.getKey().getRight().equals(projectStr)).forEach(entry -> {
|
|
||||||
updateLanguageWrapperContainers(project, entry.getKey(), entry.getValue()).start();
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new server definition, attached to the given file extension.
|
|
||||||
* This definition will be applicable for any project, since a specific project is not defined.
|
|
||||||
* Plugin developers can register their application-level language server definitions using this API.
|
|
||||||
*
|
|
||||||
* @param definition The server definition
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static void addServerDefinition(@NotNull LanguageServerDefinition definition) {
|
|
||||||
addServerDefinition(definition, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new server definition, attached to the given file extension and the project.
|
|
||||||
* Plugin developers can register their project-level language server definitions using this API.
|
|
||||||
*
|
|
||||||
* @param definition The server definition
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static void addServerDefinition(@NotNull LanguageServerDefinition definition, @Nullable Project project) {
|
|
||||||
if (project != null) {
|
|
||||||
processDefinition(definition, FileUtils.projectToUri(project));
|
|
||||||
FileUtils.reloadEditors(project);
|
|
||||||
} else {
|
|
||||||
processDefinition(definition, "");
|
|
||||||
FileUtils.reloadAllEditors();
|
|
||||||
}
|
|
||||||
LOG.info("Added definition for " + definition);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new LSP extension manager, attached to the given file extension.
|
|
||||||
* Plugin developers should register their custom language server extensions using this API.
|
|
||||||
*
|
|
||||||
* @param ext File extension type
|
|
||||||
* @param manager LSP extension manager (Should be implemented by the developer)
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static void addExtensionManager(@NotNull String ext, @NotNull LSPExtensionManager manager) {
|
|
||||||
if (extToExtManager.get(ext) != null) {
|
|
||||||
LOG.warn("An extension manager is already registered for \"" + ext + "\" extension");
|
|
||||||
}
|
|
||||||
extToExtManager.put(ext, manager);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return All instantiated ServerWrappers
|
|
||||||
*/
|
|
||||||
public static Set<LanguageServerWrapper> getAllServerWrappersFor(String projectUri) {
|
|
||||||
Set<LanguageServerWrapper> allWrappers = new HashSet<>();
|
|
||||||
extToLanguageWrapper.forEach((stringStringPair, languageServerWrapper) -> {
|
|
||||||
if (FileUtils.projectToUri(languageServerWrapper.getProject()).equals(projectUri)) {
|
|
||||||
allWrappers.add(languageServerWrapper);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return allWrappers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return All registered LSP protocol extension managers.
|
|
||||||
*/
|
|
||||||
public static LSPExtensionManager getExtensionManagerFor(String fileExt) {
|
|
||||||
if (extToExtManager.containsKey(fileExt)) {
|
|
||||||
return extToExtManager.get(fileExt);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param virtualFile The virtual file instance to be validated
|
|
||||||
* @return True if there is a LanguageServer supporting this extension, false otherwise
|
|
||||||
*/
|
|
||||||
public static boolean isExtensionSupported(VirtualFile virtualFile) {
|
|
||||||
return extToServerDefinition.keySet().stream().anyMatch(keyMap ->
|
|
||||||
keyMap.getLeft().equals(virtualFile.getExtension()) || (virtualFile.getName().matches(keyMap.getLeft())));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when an editor is opened. Instantiates a LanguageServerWrapper if necessary, and adds the Editor to the Wrapper
|
|
||||||
*
|
|
||||||
* @param editor the editor
|
|
||||||
*/
|
|
||||||
public static void editorOpened(Editor editor) {
|
|
||||||
VirtualFile file = FileDocumentManager.getInstance().getFile(editor.getDocument());
|
|
||||||
if (!FileUtils.isFileSupported(file)) {
|
|
||||||
LOG.debug("Handling open on a editor which host a LightVirtual/Null file");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Project project = editor.getProject();
|
|
||||||
if (project == null) {
|
|
||||||
LOG.debug("Opened an unsupported editor, which does not have an attached project.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String projectUri = FileUtils.projectToUri(project);
|
|
||||||
if (projectUri == null) {
|
|
||||||
LOG.warn("File for editor " + editor.getDocument().getText() + " is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pool(() -> {
|
|
||||||
String ext = file.getExtension();
|
|
||||||
final String fileName = file.getName();
|
|
||||||
LOG.info("Opened " + fileName);
|
|
||||||
|
|
||||||
// The ext can either be a file extension or a file pattern(regex expression).
|
|
||||||
// First try for the extension since it is the most comment usage, if not try to
|
|
||||||
// match file name.
|
|
||||||
LanguageServerDefinition serverDefinition = extToServerDefinition.get(new ImmutablePair<>(ext, projectUri));
|
|
||||||
if (serverDefinition == null) {
|
|
||||||
// Fallback to file name pattern matching, where the map key is a regex.
|
|
||||||
Optional<Pair<String, String>> keyForFile = extToServerDefinition.keySet().stream().
|
|
||||||
filter(keyPair -> fileName.matches(keyPair.getLeft()) && keyPair.getRight().equals(projectUri))
|
|
||||||
.findFirst();
|
|
||||||
if (keyForFile.isPresent()) {
|
|
||||||
serverDefinition = extToServerDefinition.get(keyForFile.get());
|
|
||||||
// ext must be the key since we are in file name mode.
|
|
||||||
ext = keyForFile.get().getLeft();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If cannot find a project-specific server definition for the given file and project, repeat the
|
|
||||||
// above process to find an application level server definition for the given file extension/regex.
|
|
||||||
if (serverDefinition == null) {
|
|
||||||
serverDefinition = extToServerDefinition.get(new ImmutablePair<>(ext, ""));
|
|
||||||
}
|
|
||||||
if (serverDefinition == null) {
|
|
||||||
// Fallback to file name pattern matching, where the map key is a regex.
|
|
||||||
Optional<Pair<String, String>> keyForFile = extToServerDefinition.keySet().stream().
|
|
||||||
filter(keyPair -> fileName.matches(keyPair.getLeft()) && keyPair.getRight().isEmpty())
|
|
||||||
.findFirst();
|
|
||||||
if (keyForFile.isPresent()) {
|
|
||||||
serverDefinition = extToServerDefinition.get(keyForFile.get());
|
|
||||||
// ext must be the key since we are in file name mode.
|
|
||||||
ext = keyForFile.get().getLeft();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverDefinition == null) {
|
|
||||||
LOG.warn("Could not find a server definition for " + ext);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Update project mapping for language servers.
|
|
||||||
LanguageServerWrapper wrapper = updateLanguageWrapperContainers(project, new ImmutablePair<>(ext, projectUri), serverDefinition);
|
|
||||||
|
|
||||||
LOG.info("Adding file " + fileName);
|
|
||||||
wrapper.connect(editor);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static synchronized LanguageServerWrapper updateLanguageWrapperContainers(Project project, final Pair<String, String> key, LanguageServerDefinition serverDefinition) {
|
|
||||||
String projectUri = FileUtils.projectToUri(project);
|
|
||||||
LanguageServerWrapper wrapper = extToLanguageWrapper.get(key);
|
|
||||||
String ext = key.getLeft();
|
|
||||||
if (wrapper == null) {
|
|
||||||
LOG.info("Instantiating wrapper for " + ext + " : " + projectUri);
|
|
||||||
if (extToExtManager.get(ext) != null) {
|
|
||||||
wrapper = new LanguageServerWrapper(serverDefinition, project, extToExtManager.get(ext));
|
|
||||||
} else {
|
|
||||||
wrapper = new LanguageServerWrapper(serverDefinition, project);
|
|
||||||
}
|
|
||||||
String[] exts = serverDefinition.ext.split(LanguageServerDefinition.SPLIT_CHAR);
|
|
||||||
for (String ex : exts) {
|
|
||||||
extToLanguageWrapper.put(new ImmutablePair<>(ex, projectUri), wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<LanguageServerWrapper> wrappers = projectToLanguageWrappers
|
|
||||||
.computeIfAbsent(projectUri, k -> new HashSet<>());
|
|
||||||
wrappers.add(wrapper);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
LOG.info("Wrapper already existing for " + ext + " , " + projectUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when an editor is closed. Notifies the LanguageServerWrapper if needed
|
|
||||||
*
|
|
||||||
* @param editor the editor.
|
|
||||||
*/
|
|
||||||
public static void editorClosed(Editor editor) {
|
|
||||||
VirtualFile file = FileUtils.virtualFileFromEditor(editor);
|
|
||||||
if (!FileUtils.isFileSupported(file)) {
|
|
||||||
LOG.debug("Handling close on a editor which host a LightVirtual/Null file");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pool(() -> {
|
|
||||||
LanguageServerWrapper serverWrapper = LanguageServerWrapper.forEditor(editor);
|
|
||||||
if (serverWrapper != null) {
|
|
||||||
LOG.info("Disconnecting " + FileUtils.editorToURIString(editor));
|
|
||||||
serverWrapper.disconnect(editor);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns current timeout values.
|
|
||||||
*
|
|
||||||
* @return A map of Timeout types and corresponding values(in milliseconds).
|
|
||||||
*/
|
|
||||||
public static Map<Timeouts, Integer> getTimeouts() {
|
|
||||||
return Timeout.getTimeouts();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns current timeout value of a given timeout type.
|
|
||||||
*
|
|
||||||
* @return A map of Timeout types and corresponding values(in milliseconds).
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static int getTimeout(Timeouts timeoutType) {
|
|
||||||
return getTimeouts().get(timeoutType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overrides default timeout values with a given set of timeouts.
|
|
||||||
*
|
|
||||||
* @param newTimeouts A map of Timeout types and corresponding values to be set.
|
|
||||||
*/
|
|
||||||
public static void setTimeouts(Map<Timeouts, Integer> newTimeouts) {
|
|
||||||
Timeout.setTimeouts(newTimeouts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param timeout Timeout type
|
|
||||||
* @param value new timeout value to be set (in milliseconds).
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static void setTimeout(Timeouts timeout, int value) {
|
|
||||||
Map<Timeouts, Integer> newTimeout = new HashMap<>();
|
|
||||||
newTimeout.put(timeout, value);
|
|
||||||
setTimeouts(newTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void removeWrapper(LanguageServerWrapper wrapper) {
|
|
||||||
if (wrapper.getProject() != null) {
|
|
||||||
String[] extensions = wrapper.getServerDefinition().ext.split(LanguageServerDefinition.SPLIT_CHAR);
|
|
||||||
val rootPath = wrapper.getProjectRootPath();
|
|
||||||
if (rootPath == null) {
|
|
||||||
LOG.error("Project root path is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
val absolutePath = FileUtil.pathToUri(rootPath);
|
|
||||||
for (String ext : extensions) {
|
|
||||||
MutablePair<String, String> extProjectPair = new MutablePair<>(ext, absolutePath);
|
|
||||||
extToLanguageWrapper.remove(extProjectPair);
|
|
||||||
extToServerDefinition.remove(extProjectPair);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG.error("No attached projects found for wrapper");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<String, Set<LanguageServerWrapper>> getProjectToLanguageWrappers() {
|
|
||||||
return projectToLanguageWrappers;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static void didChangeConfiguration(@NotNull DidChangeConfigurationParams params, @NotNull Project project) {
|
|
||||||
final Set<LanguageServerWrapper> serverWrappers = IntellijLanguageClient.getProjectToLanguageWrappers()
|
|
||||||
.get(FileUtils.projectToUri(project));
|
|
||||||
serverWrappers.forEach(s -> s.getRequestManager().didChangeConfiguration(params));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the registered extension manager for this language server.
|
|
||||||
*
|
|
||||||
* @param definition The LanguageServerDefinition
|
|
||||||
*/
|
|
||||||
public static Optional<LSPExtensionManager> getExtensionManagerForDefinition(@NotNull LanguageServerDefinition definition) {
|
|
||||||
return Optional.ofNullable(extToExtManager.get(definition.ext.split(",")[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Override
|
|
||||||
// public void disposeComponent() {
|
|
||||||
// Disposer.dispose(this);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @Override
|
|
||||||
// public void dispose() {
|
|
||||||
// Disposer.dispose(this);
|
|
||||||
// }
|
|
||||||
|
|
||||||
private static void processDefinition(LanguageServerDefinition definition, String projectUri) {
|
|
||||||
String[] extensions = definition.ext.split(LanguageServerDefinition.SPLIT_CHAR);
|
|
||||||
for (String ext : extensions) {
|
|
||||||
Pair<String, String> keyPair = new ImmutablePair<>(ext, projectUri);
|
|
||||||
if (extToServerDefinition.get(keyPair) == null) {
|
|
||||||
extToServerDefinition.put(keyPair, definition);
|
|
||||||
LOG.info("Added server definition for " + ext);
|
|
||||||
} else {
|
|
||||||
extToServerDefinition.replace(keyPair, definition);
|
|
||||||
LOG.info("Updated server definition for " + ext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.actions;
|
|
||||||
|
|
||||||
import com.intellij.codeInsight.actions.ReformatCodeAction;
|
|
||||||
import com.intellij.codeInsight.actions.ShowReformatFileDialog;
|
|
||||||
import com.intellij.codeInsight.hint.actions.ShowImplementationsAction;
|
|
||||||
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction;
|
|
||||||
import com.intellij.codeInsight.navigation.actions.GotoImplementationAction;
|
|
||||||
import com.intellij.openapi.actionSystem.ActionManager;
|
|
||||||
import com.intellij.openapi.actionSystem.AnAction;
|
|
||||||
import com.intellij.openapi.actionSystem.impl.DynamicActionConfigurationCustomizer;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.val;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
public class ActionCustomizer implements DynamicActionConfigurationCustomizer {
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
private static class ActionWrapper<T extends AnAction> {
|
|
||||||
public final Class<T> klass;
|
|
||||||
public final Function<T, WrappedAction<T>> wrapper;
|
|
||||||
}
|
|
||||||
private static final Map<String, ActionWrapper<?>> actions = new HashMap<>();
|
|
||||||
static {
|
|
||||||
actions.put("GotoDeclaration", new ActionWrapper<>(GotoDeclarationAction.class, LSPGotoDeclarationAction::new));
|
|
||||||
actions.put("GotoImplementation", new ActionWrapper<>(GotoImplementationAction.class, LSPGotoImplementationAction::new));
|
|
||||||
actions.put("ReformatCode", new ActionWrapper<>(ReformatCodeAction.class, LSPReformatAction::new));
|
|
||||||
actions.put("QuickImplementations", new ActionWrapper<>(ShowImplementationsAction.class, LSPShowImplementationsAction::new));
|
|
||||||
actions.put("ShowReformatFileDialog", new ActionWrapper<>(ShowReformatFileDialog.class, LSPShowReformatDialogAction::new));
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void registerActions(@NotNull ActionManager manager) {
|
|
||||||
for (val entry: actions.entrySet()) {
|
|
||||||
wrap(manager, entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unregisterActions(@NotNull ActionManager manager) {
|
|
||||||
for (val name: actions.keySet()) {
|
|
||||||
unwrap(manager, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T extends AnAction> void wrap(ActionManager manager, String name, ActionWrapper<T> constructor) {
|
|
||||||
val oldAction = manager.getAction(name);
|
|
||||||
|
|
||||||
if (constructor.klass.isInstance(oldAction)) {
|
|
||||||
val wrapped = constructor.wrapper.apply(constructor.klass.cast(oldAction));
|
|
||||||
wrapped.copyFrom(oldAction);
|
|
||||||
manager.replaceAction(name, wrapped);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void unwrap(ActionManager manager, String name) {
|
|
||||||
val oldAction = manager.getAction(name);
|
|
||||||
if (oldAction instanceof WrappedAction<?> w) {
|
|
||||||
manager.replaceAction(name, w.wrapped);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.actions;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction;
|
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.project.DumbAware;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import lombok.val;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
public class LSPGotoDeclarationAction extends WrappedAction<GotoDeclarationAction> implements DumbAware {
|
|
||||||
public LSPGotoDeclarationAction(GotoDeclarationAction wrapped) {
|
|
||||||
super(wrapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void actionPerformedLSP(@NotNull AnActionEvent e, EditorEventManager manager, PsiFile file) {
|
|
||||||
val offset = manager.editor.getCaretModel().getOffset();
|
|
||||||
val psiElement = file.findElementAt(offset);
|
|
||||||
if (psiElement == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
manager.gotoDeclarationOrUsages(psiElement);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.actions;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.intellij.codeInsight.navigation.actions.GotoImplementationAction;
|
|
||||||
import com.intellij.idea.ActionsBundle;
|
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
|
||||||
import com.intellij.openapi.actionSystem.PerformWithDocumentsCommitted;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import lombok.val;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
public class LSPGotoImplementationAction extends WrappedAction<GotoImplementationAction> implements PerformWithDocumentsCommitted {
|
|
||||||
public LSPGotoImplementationAction(GotoImplementationAction wrapped) {
|
|
||||||
super(wrapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void actionPerformedLSP(AnActionEvent e, EditorEventManager manager, PsiFile file) {
|
|
||||||
val offset = manager.editor.getCaretModel().getOffset();
|
|
||||||
val psiElement = file.findElementAt(offset);
|
|
||||||
if (psiElement == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
manager.gotoDefinition(psiElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void updateLSP(AnActionEvent e, EditorEventManager manager, PsiFile file) {
|
|
||||||
if (e.getPresentation().getTextWithMnemonic() == null) {
|
|
||||||
e.getPresentation().setText(ActionsBundle.actionText("GotoImplementation"));
|
|
||||||
e.getPresentation().setDescription(ActionsBundle.actionDescription("GotoImplementation"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,146 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.actions;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.intellij.codeInsight.hint.HintManager;
|
|
||||||
import com.intellij.codeInsight.hint.HintManagerImpl;
|
|
||||||
import com.intellij.find.FindBundle;
|
|
||||||
import com.intellij.find.findUsages.FindUsagesOptions;
|
|
||||||
import com.intellij.find.findUsages.PsiElement2UsageTargetAdapter;
|
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
|
||||||
import com.intellij.openapi.actionSystem.CommonDataKeys;
|
|
||||||
import com.intellij.openapi.application.ModalityState;
|
|
||||||
import com.intellij.openapi.application.ReadAction;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.editor.LogicalPosition;
|
|
||||||
import com.intellij.openapi.project.DumbAwareAction;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.util.Pair;
|
|
||||||
import com.intellij.openapi.util.text.StringUtil;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.psi.PsiElement;
|
|
||||||
import com.intellij.ui.JBColor;
|
|
||||||
import com.intellij.ui.LightweightHint;
|
|
||||||
import com.intellij.usageView.UsageInfo;
|
|
||||||
import com.intellij.usageView.UsageViewUtil;
|
|
||||||
import com.intellij.usages.Usage;
|
|
||||||
import com.intellij.usages.UsageInfo2UsageAdapter;
|
|
||||||
import com.intellij.usages.UsageTarget;
|
|
||||||
import com.intellij.usages.UsageViewManager;
|
|
||||||
import com.intellij.usages.UsageViewPresentation;
|
|
||||||
import com.intellij.util.concurrency.AppExecutorUtil;
|
|
||||||
import lombok.val;
|
|
||||||
|
|
||||||
import javax.swing.JLabel;
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.awt.Point;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action for references / see usages (SHIFT+ALT+F7)
|
|
||||||
*/
|
|
||||||
public class LSPReferencesAction extends DumbAwareAction {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(AnActionEvent e) {
|
|
||||||
Editor editor = e.getData(CommonDataKeys.EDITOR);
|
|
||||||
if (editor != null) {
|
|
||||||
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (eventManager == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List<PsiElement2UsageTargetAdapter> targets = new ArrayList<>();
|
|
||||||
Pair<List<PsiElement>, List<VirtualFile>> references = eventManager
|
|
||||||
.references(editor.getCaretModel().getCurrentCaret().getOffset(), true, true);
|
|
||||||
if (references.first != null) {
|
|
||||||
references.first.forEach(element -> targets.add(new PsiElement2UsageTargetAdapter(element, true)));
|
|
||||||
}
|
|
||||||
showReferences(editor, targets, editor.getCaretModel().getCurrentCaret().getLogicalPosition());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void forManagerAndOffset(EditorEventManager manager, int offset) {
|
|
||||||
ReadAction.nonBlocking(() -> {
|
|
||||||
val references = manager.references(offset, true, true);
|
|
||||||
val targets = new ArrayList<PsiElement2UsageTargetAdapter>();
|
|
||||||
if (references.first != null) {
|
|
||||||
references.first.forEach(element -> targets.add(new PsiElement2UsageTargetAdapter(element, true)));
|
|
||||||
}
|
|
||||||
return targets;
|
|
||||||
})
|
|
||||||
.expireWhen(() -> manager.editor.isDisposed())
|
|
||||||
.finishOnUiThread(ModalityState.nonModal(), targets -> {
|
|
||||||
val editor = manager.editor;
|
|
||||||
if (editor.isDisposed())
|
|
||||||
return;
|
|
||||||
showReferences(editor, targets, editor.offsetToLogicalPosition(offset));
|
|
||||||
})
|
|
||||||
.submit(AppExecutorUtil.getAppExecutorService());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showReferences(Editor editor, List<PsiElement2UsageTargetAdapter> targets, LogicalPosition position) {
|
|
||||||
if (targets.isEmpty()) {
|
|
||||||
short constraint = HintManager.ABOVE;
|
|
||||||
int flags = HintManager.HIDE_BY_ANY_KEY | HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_SCROLLING;
|
|
||||||
JLabel label = new JLabel("No references found");
|
|
||||||
label.setBackground(new JBColor(new Color(150, 0, 0), new Color(150, 0, 0)));
|
|
||||||
LightweightHint hint = new LightweightHint(label);
|
|
||||||
Point p = HintManagerImpl.getHintPosition(hint, editor, position, constraint);
|
|
||||||
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, p, flags, 0, false,
|
|
||||||
HintManagerImpl.createHintHint(editor, p, hint, constraint).setContentActive(false));
|
|
||||||
} else {
|
|
||||||
List<Usage> usages = new ArrayList<>();
|
|
||||||
targets.forEach(ut -> {
|
|
||||||
PsiElement elem = ut.getElement();
|
|
||||||
usages.add(new UsageInfo2UsageAdapter(new UsageInfo(elem, -1, -1, false)));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (editor == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Project project = editor.getProject();
|
|
||||||
if (project == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
UsageViewPresentation presentation = createPresentation(targets.get(0).getElement(),
|
|
||||||
new FindUsagesOptions(editor.getProject()), false);
|
|
||||||
UsageViewManager.getInstance(project)
|
|
||||||
.showUsages(new UsageTarget[0], usages.toArray(new Usage[0]),
|
|
||||||
presentation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private UsageViewPresentation createPresentation(PsiElement psiElement, FindUsagesOptions options,
|
|
||||||
boolean toOpenInNewTab) {
|
|
||||||
UsageViewPresentation presentation = new UsageViewPresentation();
|
|
||||||
String scopeString = options.searchScope.getDisplayName();
|
|
||||||
presentation.setScopeText(scopeString);
|
|
||||||
String usagesString = options.generateUsagesString();
|
|
||||||
presentation.setSearchString(usagesString);
|
|
||||||
String title = FindBundle.message("find.usages.of.element.in.scope.panel.title", usagesString,
|
|
||||||
UsageViewUtil.getLongName(psiElement), scopeString);
|
|
||||||
presentation.setTabText(title);
|
|
||||||
presentation.setTabName(FindBundle
|
|
||||||
.message("find.usages.of.element.tab.name", usagesString, UsageViewUtil.getShortName(psiElement)));
|
|
||||||
presentation.setTargetsNodeText(StringUtil.capitalize(UsageViewUtil.getType(psiElement)));
|
|
||||||
presentation.setOpenInNewTab(toOpenInNewTab);
|
|
||||||
presentation.setShowCancelButton(true);
|
|
||||||
return presentation;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.actions;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.ApplicationUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.ReformatHandler;
|
|
||||||
import com.intellij.codeInsight.actions.ReformatCodeAction;
|
|
||||||
import com.intellij.ide.lightEdit.LightEditCompatible;
|
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
|
||||||
import com.intellij.openapi.project.DumbAware;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import lombok.val;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action overriding the default reformat action
|
|
||||||
* Fallback to the default action if the language is already supported or not supported by any language server
|
|
||||||
*/
|
|
||||||
public class LSPReformatAction extends WrappedAction<ReformatCodeAction> implements DumbAware, LightEditCompatible {
|
|
||||||
public LSPReformatAction(ReformatCodeAction wrapped) {
|
|
||||||
super(wrapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void actionPerformedLSP(AnActionEvent e, EditorEventManager manager, PsiFile file) {
|
|
||||||
val editor = manager.editor;
|
|
||||||
ApplicationUtil.writeAction(() -> FileDocumentManager.getInstance().saveDocument(editor.getDocument()));
|
|
||||||
// if editor hasSelection, only reformat selection, not reformat the whole file
|
|
||||||
if (editor.getSelectionModel().hasSelection()) {
|
|
||||||
ReformatHandler.reformatSelection(editor);
|
|
||||||
} else {
|
|
||||||
ReformatHandler.reformatFile(editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.actions;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.intellij.codeInsight.hint.actions.ShowImplementationsAction;
|
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
|
||||||
import com.intellij.openapi.project.DumbAware;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import lombok.val;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public class LSPShowImplementationsAction extends WrappedAction<ShowImplementationsAction> implements DumbAware {
|
|
||||||
public LSPShowImplementationsAction(ShowImplementationsAction wrapped) {
|
|
||||||
super(wrapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void actionPerformedLSP(@NotNull AnActionEvent e, EditorEventManager manager, PsiFile file) {
|
|
||||||
val offset = manager.editor.getCaretModel().getOffset();
|
|
||||||
val psiElement = file.findElementAt(offset);
|
|
||||||
if (psiElement == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
manager.gotoDefinition(psiElement);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.actions;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.intellij.codeInsight.actions.LayoutCodeDialog;
|
|
||||||
import com.intellij.codeInsight.actions.LayoutCodeOptions;
|
|
||||||
import com.intellij.codeInsight.actions.ShowReformatFileDialog;
|
|
||||||
import com.intellij.codeInsight.actions.TextRangeType;
|
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
|
||||||
import com.intellij.openapi.project.DumbAware;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import lombok.val;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class overriding the default action handling the Reformat dialog event (CTRL+ALT+SHIFT+L by default)
|
|
||||||
* Fallback to the default action if the language is already supported or not supported by any language server
|
|
||||||
*/
|
|
||||||
public class LSPShowReformatDialogAction extends WrappedAction<ShowReformatFileDialog> implements DumbAware {
|
|
||||||
|
|
||||||
private String HELP_ID = "editing.codeReformatting";
|
|
||||||
private Logger LOG = Logger.getInstance(LSPShowReformatDialogAction.class);
|
|
||||||
|
|
||||||
public LSPShowReformatDialogAction(ShowReformatFileDialog wrapped) {
|
|
||||||
super(wrapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void actionPerformedLSP(AnActionEvent e, EditorEventManager manager, PsiFile psiFile) {
|
|
||||||
val editor = manager.editor;
|
|
||||||
val project = manager.getProject();
|
|
||||||
VirtualFile virFile = FileDocumentManager.getInstance().getFile(editor.getDocument());
|
|
||||||
if (!IntellijLanguageClient.isExtensionSupported(virFile)) {
|
|
||||||
wrapped.actionPerformed(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
boolean hasSelection = editor.getSelectionModel().hasSelection();
|
|
||||||
LayoutCodeDialog dialog = new LayoutCodeDialog(project, psiFile, hasSelection, HELP_ID);
|
|
||||||
dialog.show();
|
|
||||||
if (!dialog.isOK()) {
|
|
||||||
// if user chose cancel , the dialog in super.actionPerformed(e) will show again
|
|
||||||
// super.actionPerformed(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LayoutCodeOptions options = dialog.getRunOptions();
|
|
||||||
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (eventManager != null) {
|
|
||||||
if (options.getTextRangeType() == TextRangeType.SELECTED_TEXT) {
|
|
||||||
eventManager.reformatSelection();
|
|
||||||
} else {
|
|
||||||
eventManager.reformat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,247 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.actions;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.intellij.openapi.actionSystem.ActionUpdateThread;
|
|
||||||
import com.intellij.openapi.actionSystem.AnAction;
|
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
|
||||||
import com.intellij.openapi.actionSystem.CommonDataKeys;
|
|
||||||
import com.intellij.openapi.actionSystem.OverridingAction;
|
|
||||||
import com.intellij.openapi.actionSystem.ShortcutSet;
|
|
||||||
import com.intellij.psi.PsiDocumentManager;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import lombok.val;
|
|
||||||
import org.jetbrains.annotations.Nls;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
public abstract class WrappedAction<T extends AnAction> extends AnAction implements OverridingAction {
|
|
||||||
public final T wrapped;
|
|
||||||
|
|
||||||
// private static class Reflector {
|
|
||||||
// static Field templatePresentation;
|
|
||||||
// static Field myShortcutSet;
|
|
||||||
// static Field myEnabledInModalContext;
|
|
||||||
// static Field myIsDefaultIcon;
|
|
||||||
// static Field myWorksInInjected;
|
|
||||||
// static Field myActionTextOverrides;
|
|
||||||
// static Field mySynonyms;
|
|
||||||
// static Field[] fields;
|
|
||||||
//
|
|
||||||
// static {
|
|
||||||
// val theClass = AnAction.class;
|
|
||||||
// val validFields = new ArrayList<Field>();
|
|
||||||
// try {
|
|
||||||
// templatePresentation = theClass.getDeclaredField("templatePresentation");
|
|
||||||
// templatePresentation.setAccessible(true);
|
|
||||||
// validFields.add(templatePresentation);
|
|
||||||
// } catch (NoSuchFieldException e) {
|
|
||||||
// e.printStackTrace();
|
|
||||||
// }
|
|
||||||
// try {
|
|
||||||
// myShortcutSet = theClass.getDeclaredField("myShortcutSet");
|
|
||||||
// myShortcutSet.setAccessible(true);
|
|
||||||
// validFields.add(myShortcutSet);
|
|
||||||
// } catch (NoSuchFieldException e) {
|
|
||||||
// e.printStackTrace();
|
|
||||||
// }
|
|
||||||
// try {
|
|
||||||
// myEnabledInModalContext = theClass.getDeclaredField("myEnabledInModalContext");
|
|
||||||
// myEnabledInModalContext.setAccessible(true);
|
|
||||||
// validFields.add(myEnabledInModalContext);
|
|
||||||
// } catch (NoSuchFieldException e) {
|
|
||||||
// e.printStackTrace();
|
|
||||||
// }
|
|
||||||
// try {
|
|
||||||
// myIsDefaultIcon = theClass.getDeclaredField("myIsDefaultIcon");
|
|
||||||
// myIsDefaultIcon.setAccessible(true);
|
|
||||||
// validFields.add(myIsDefaultIcon);
|
|
||||||
// } catch (NoSuchFieldException e) {
|
|
||||||
// e.printStackTrace();
|
|
||||||
// }
|
|
||||||
// try {
|
|
||||||
// myWorksInInjected = theClass.getDeclaredField("myWorksInInjected");
|
|
||||||
// myWorksInInjected.setAccessible(true);
|
|
||||||
// validFields.add(myWorksInInjected);
|
|
||||||
// } catch (NoSuchFieldException e) {
|
|
||||||
// e.printStackTrace();
|
|
||||||
// }
|
|
||||||
// try {
|
|
||||||
// myActionTextOverrides = theClass.getDeclaredField("myActionTextOverrides");
|
|
||||||
// myActionTextOverrides.setAccessible(true);
|
|
||||||
// validFields.add(myActionTextOverrides);
|
|
||||||
// } catch (NoSuchFieldException e) {
|
|
||||||
// e.printStackTrace();
|
|
||||||
// }
|
|
||||||
// try {
|
|
||||||
// mySynonyms = theClass.getDeclaredField("mySynonyms");
|
|
||||||
// mySynonyms.setAccessible(true);
|
|
||||||
// validFields.add(mySynonyms);
|
|
||||||
// } catch (NoSuchFieldException e) {
|
|
||||||
// e.printStackTrace();
|
|
||||||
// }
|
|
||||||
// fields = validFields.toArray(new Field[0]);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
public WrappedAction(T wrapped) {
|
|
||||||
this.wrapped = wrapped;
|
|
||||||
// if (Reflector.fields != null) {
|
|
||||||
// for (val field: Reflector.fields) {
|
|
||||||
// assimilate(field);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
private void assimilate(Field field) {
|
|
||||||
if (field == null)
|
|
||||||
return;
|
|
||||||
try {
|
|
||||||
field.set(this, field.get(wrapped));
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isDumbAware() {
|
|
||||||
return wrapped.isDumbAware();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull ActionUpdateThread getActionUpdateThread() {
|
|
||||||
return wrapped.getActionUpdateThread();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean displayTextInToolbar() {
|
|
||||||
return super.displayTextInToolbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean useSmallerFontForTextInToolbar() {
|
|
||||||
return super.useSmallerFontForTextInToolbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setDefaultIcon(boolean isDefaultIconSet) {
|
|
||||||
wrapped.setDefaultIcon(isDefaultIconSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isDefaultIcon() {
|
|
||||||
return wrapped.isDefaultIcon();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setInjectedContext(boolean worksInInjected) {
|
|
||||||
wrapped.setInjectedContext(worksInInjected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isInInjectedContext() {
|
|
||||||
return wrapped.isInInjectedContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addSynonym(@NotNull Supplier<@Nls String> text) {
|
|
||||||
wrapped.addSynonym(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull List<Supplier<String>> getSynonyms() {
|
|
||||||
return wrapped.getSynonyms();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final void update(@NotNull AnActionEvent e) {
|
|
||||||
val manager = tryGetEventManager(e);
|
|
||||||
if (manager == null) {
|
|
||||||
wrapped.update(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
val file = tryGetPSIFileLSPAware(manager);
|
|
||||||
if (file == null) {
|
|
||||||
wrapped.update(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateLSP(e, manager, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final void beforeActionPerformedUpdate(@NotNull AnActionEvent e) {
|
|
||||||
val manager = tryGetEventManager(e);
|
|
||||||
if (manager == null) {
|
|
||||||
wrapped.beforeActionPerformedUpdate(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
val file = tryGetPSIFileLSPAware(manager);
|
|
||||||
if (file == null) {
|
|
||||||
wrapped.beforeActionPerformedUpdate(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
beforeActionPerformedUpdateLSP(e, manager, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final void actionPerformed(@NotNull AnActionEvent e) {
|
|
||||||
val manager = tryGetEventManager(e);
|
|
||||||
if (manager == null) {
|
|
||||||
wrapped.actionPerformed(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
val file = tryGetPSIFileLSPAware(manager);
|
|
||||||
if (file == null) {
|
|
||||||
wrapped.actionPerformed(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
actionPerformedLSP(e, manager, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static EditorEventManager tryGetEventManager(AnActionEvent e) {
|
|
||||||
val editor = e.getData(CommonDataKeys.EDITOR);
|
|
||||||
if (editor == null)
|
|
||||||
return null;
|
|
||||||
return EditorEventManagerBase.forEditor(editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PsiFile tryGetPSIFileLSPAware(EditorEventManager manager) {
|
|
||||||
val project = manager.getProject();
|
|
||||||
val editor = manager.editor;
|
|
||||||
if (project == null)
|
|
||||||
return null;
|
|
||||||
val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
|
|
||||||
if (file == null || !IntellijLanguageClient.isExtensionSupported(file.getVirtualFile()))
|
|
||||||
return null;
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void actionPerformedLSP(AnActionEvent e, EditorEventManager manager, PsiFile file);
|
|
||||||
|
|
||||||
protected void beforeActionPerformedUpdateLSP(AnActionEvent e, EditorEventManager manager, PsiFile file) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void updateLSP(AnActionEvent e, EditorEventManager manager, PsiFile file) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The client context which is received by {@link DefaultLanguageClient}. The context contain
|
|
||||||
* information about the runtime and its components.
|
|
||||||
*
|
|
||||||
* @author gayanper
|
|
||||||
*/
|
|
||||||
public interface ClientContext {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link EditorEventManager} for the given document URI.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
EditorEventManager getEditorEventManagerFor(@NotNull String documentUri);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link Project} associated with the LanuageClient.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
Project getProject();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link RequestManager} associated with the Language Server Connection.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
RequestManager getRequestManager();
|
|
||||||
}
|
|
|
@ -1,282 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.ApplicationUtil;
|
|
||||||
import com.falsepattern.zigbrains.common.util.FileUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.WorkspaceEditHandler;
|
|
||||||
import com.intellij.notification.Notification;
|
|
||||||
import com.intellij.notification.NotificationAction;
|
|
||||||
import com.intellij.notification.NotificationGroup;
|
|
||||||
import com.intellij.notification.NotificationGroupManager;
|
|
||||||
import com.intellij.notification.NotificationType;
|
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
|
||||||
import com.intellij.openapi.application.ApplicationManager;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.ui.Messages;
|
|
||||||
import com.intellij.util.ui.UIUtil;
|
|
||||||
import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
|
|
||||||
import org.eclipse.lsp4j.ApplyWorkspaceEditResponse;
|
|
||||||
import org.eclipse.lsp4j.ConfigurationParams;
|
|
||||||
import org.eclipse.lsp4j.Diagnostic;
|
|
||||||
import org.eclipse.lsp4j.MessageActionItem;
|
|
||||||
import org.eclipse.lsp4j.MessageParams;
|
|
||||||
import org.eclipse.lsp4j.MessageType;
|
|
||||||
import org.eclipse.lsp4j.PublishDiagnosticsParams;
|
|
||||||
import org.eclipse.lsp4j.RegistrationParams;
|
|
||||||
import org.eclipse.lsp4j.ShowMessageRequestParams;
|
|
||||||
import org.eclipse.lsp4j.Unregistration;
|
|
||||||
import org.eclipse.lsp4j.UnregistrationParams;
|
|
||||||
import org.eclipse.lsp4j.WorkspaceFolder;
|
|
||||||
import org.eclipse.lsp4j.services.LanguageClient;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import javax.swing.Icon;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.FutureTask;
|
|
||||||
|
|
||||||
public class DefaultLanguageClient implements LanguageClient {
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
final private Logger LOG = Logger.getInstance(DefaultLanguageClient.class);
|
|
||||||
@NotNull
|
|
||||||
private final NotificationGroup STICKY_NOTIFICATION_GROUP = NotificationGroupManager.getInstance().getNotificationGroup("lsp");
|
|
||||||
@NotNull
|
|
||||||
final private Map<String, DynamicRegistrationMethods> registrations = new ConcurrentHashMap<>();
|
|
||||||
@NotNull
|
|
||||||
private final ClientContext context;
|
|
||||||
protected boolean isModal = false;
|
|
||||||
|
|
||||||
public DefaultLanguageClient(@NotNull ClientContext context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<ApplyWorkspaceEditResponse> applyEdit(ApplyWorkspaceEditParams params) {
|
|
||||||
boolean response = WorkspaceEditHandler.applyEdit(params.getEdit(), "LSP edits");
|
|
||||||
return CompletableFuture.supplyAsync(() -> new ApplyWorkspaceEditResponse(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<Object>> configuration(ConfigurationParams configurationParams) {
|
|
||||||
return LanguageClient.super.configuration(configurationParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<WorkspaceFolder>> workspaceFolders() {
|
|
||||||
return LanguageClient.super.workspaceFolders();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Void> registerCapability(RegistrationParams params) {
|
|
||||||
return CompletableFuture.runAsync(() -> params.getRegistrations().forEach(r -> {
|
|
||||||
String id = r.getId();
|
|
||||||
Optional<DynamicRegistrationMethods> method = DynamicRegistrationMethods.forName(r.getMethod());
|
|
||||||
method.ifPresent(dynamicRegistrationMethods -> registrations.put(id, dynamicRegistrationMethods));
|
|
||||||
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Void> unregisterCapability(UnregistrationParams params) {
|
|
||||||
return CompletableFuture.runAsync(() -> params.getUnregisterations().forEach((Unregistration r) -> {
|
|
||||||
String id = r.getId();
|
|
||||||
Optional<DynamicRegistrationMethods> method = DynamicRegistrationMethods.forName(r.getMethod());
|
|
||||||
if (registrations.containsKey(id)) {
|
|
||||||
registrations.remove(id);
|
|
||||||
} else {
|
|
||||||
Map<DynamicRegistrationMethods, String> inverted = new HashMap<>();
|
|
||||||
for (Map.Entry<String, DynamicRegistrationMethods> entry : registrations.entrySet()) {
|
|
||||||
inverted.put(entry.getValue(), entry.getKey());
|
|
||||||
}
|
|
||||||
if (method.isPresent() && inverted.containsKey(method.get())) {
|
|
||||||
registrations.remove(inverted.get(method.get()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void telemetryEvent(Object o) {
|
|
||||||
LOG.info(o.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void publishDiagnostics(PublishDiagnosticsParams publishDiagnosticsParams) {
|
|
||||||
String uri = FileUtil.sanitizeURI(publishDiagnosticsParams.getUri());
|
|
||||||
List<Diagnostic> diagnostics = publishDiagnosticsParams.getDiagnostics();
|
|
||||||
EditorEventManagerBase.diagnostics(uri, diagnostics);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showMessage(MessageParams messageParams) {
|
|
||||||
String title = "Language Server message";
|
|
||||||
String message = messageParams.getMessage();
|
|
||||||
|
|
||||||
if (isModal) {
|
|
||||||
ApplicationUtil.invokeLater(() -> {
|
|
||||||
MessageType msgType = messageParams.getType();
|
|
||||||
switch (msgType) {
|
|
||||||
case Error:
|
|
||||||
Messages.showErrorDialog(message, title);
|
|
||||||
break;
|
|
||||||
case Warning:
|
|
||||||
Messages.showWarningDialog(message, title);
|
|
||||||
break;
|
|
||||||
case Info:
|
|
||||||
case Log:
|
|
||||||
Messages.showInfoMessage(message, title);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LOG.warn("No message type for " + message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
NotificationType type = getNotificationType(messageParams.getType());
|
|
||||||
final Notification notification = new Notification(
|
|
||||||
"lsp", messageParams.getType().toString(), messageParams.getMessage(), type);
|
|
||||||
notification.notify(context.getProject());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams showMessageRequestParams) {
|
|
||||||
List<MessageActionItem> actions = showMessageRequestParams.getActions();
|
|
||||||
String title = "Language Server " + showMessageRequestParams.getType().toString();
|
|
||||||
String message = showMessageRequestParams.getMessage();
|
|
||||||
MessageType msgType = showMessageRequestParams.getType();
|
|
||||||
|
|
||||||
String[] options = new String[actions == null ? 0 : actions.size()];
|
|
||||||
for (int i = 0, size = options.length; i < size; i++) {
|
|
||||||
options[i] = actions.get(i).getTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
int exitCode;
|
|
||||||
FutureTask<Integer> task;
|
|
||||||
if (isModal) {
|
|
||||||
Icon icon;
|
|
||||||
switch (msgType) {
|
|
||||||
case Error:
|
|
||||||
icon = UIUtil.getErrorIcon();
|
|
||||||
break;
|
|
||||||
case Warning:
|
|
||||||
icon = UIUtil.getWarningIcon();
|
|
||||||
break;
|
|
||||||
case Info:
|
|
||||||
case Log:
|
|
||||||
icon = UIUtil.getInformationIcon();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
icon = null;
|
|
||||||
LOG.warn("No message type for " + message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
task = new FutureTask<>(
|
|
||||||
() -> Messages.showDialog(message, title, options, 0, icon));
|
|
||||||
ApplicationManager.getApplication().invokeAndWait(task);
|
|
||||||
|
|
||||||
try {
|
|
||||||
exitCode = task.get();
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
LOG.warn(e.getMessage());
|
|
||||||
exitCode = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
final Notification notification = STICKY_NOTIFICATION_GROUP.createNotification(title, message, getNotificationType(msgType));
|
|
||||||
final CompletableFuture<Integer> integerCompletableFuture = new CompletableFuture<>();
|
|
||||||
for (int i = 0, optionsSize = options.length; i < optionsSize; i++) {
|
|
||||||
int finalI = i;
|
|
||||||
notification.addAction(new NotificationAction(options[i]) {
|
|
||||||
@Override
|
|
||||||
public boolean isDumbAware() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) {
|
|
||||||
integerCompletableFuture.complete(finalI);
|
|
||||||
notification.expire();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
notification.whenExpired(() -> {
|
|
||||||
if (!integerCompletableFuture.isDone()) {
|
|
||||||
integerCompletableFuture.complete(-1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
notification.notify(context.getProject());
|
|
||||||
|
|
||||||
try {
|
|
||||||
exitCode = integerCompletableFuture.get();
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
LOG.warn(e.getMessage());
|
|
||||||
exitCode = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return CompletableFuture.completedFuture(actions == null || exitCode < 0 ? null : actions.get(exitCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected NotificationType getNotificationType(@NotNull MessageType messageType) {
|
|
||||||
switch (messageType) {
|
|
||||||
case Error:
|
|
||||||
return NotificationType.ERROR;
|
|
||||||
case Warning:
|
|
||||||
return NotificationType.WARNING;
|
|
||||||
case Info:
|
|
||||||
case Log:
|
|
||||||
default:
|
|
||||||
return NotificationType.INFORMATION;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void logMessage(MessageParams messageParams) {
|
|
||||||
String message = messageParams.getMessage();
|
|
||||||
MessageType msgType = messageParams.getType();
|
|
||||||
|
|
||||||
switch (msgType) {
|
|
||||||
case Error:
|
|
||||||
LOG.error(message);
|
|
||||||
break;
|
|
||||||
case Warning:
|
|
||||||
LOG.warn(message);
|
|
||||||
break;
|
|
||||||
case Info:
|
|
||||||
case Log:
|
|
||||||
LOG.info(message);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LOG.warn("Unknown message type '" + msgType + "' for " + message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
protected final ClientContext getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enum for methods which may support DynamicRegistration
|
|
||||||
*/
|
|
||||||
public enum DynamicRegistrationMethods {
|
|
||||||
|
|
||||||
DID_CHANGE_CONFIGURATION("workspace/didChangeConfiguration"),
|
|
||||||
DID_CHANGE_WATCHED_FILES("workspace/didChangeWatchedFiles"),
|
|
||||||
SYMBOL("workspace/symbol"),
|
|
||||||
EXECUTE_COMMAND("workspace/executeCommand"),
|
|
||||||
SYNCHRONIZATION("textDocument/synchronization"),
|
|
||||||
COMPLETION("textDocument/completion"),
|
|
||||||
HOVER("textDocument/hover"),
|
|
||||||
SIGNATURE_HELP("textDocument/signatureHelp"),
|
|
||||||
REFERENCES("textDocument/references"),
|
|
||||||
DOCUMENT_HIGHLIGHT("textDocument/documentHighlight"),
|
|
||||||
DOCUMENT_SYMBOL("textDocument/documentSymbol"),
|
|
||||||
FORMATTING("textDocument/formatting"),
|
|
||||||
RANGE_FORMATTING("textDocument/rangeFormatting"),
|
|
||||||
ONTYPE_FORMATTING("textDocument/onTypeFormatting"),
|
|
||||||
DEFINITION("textDocument/definition"),
|
|
||||||
CODE_ACTION("textDocument/codeAction"),
|
|
||||||
CODE_LENS("textDocument/codeLens"),
|
|
||||||
DOCUMENT_LINK("textDocument/documentLink"),
|
|
||||||
RENAME("textDocument/rename");
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
|
|
||||||
DynamicRegistrationMethods(final String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Optional<DynamicRegistrationMethods> forName(final String name) {
|
|
||||||
return Arrays.stream(DynamicRegistrationMethods.values()).filter(n -> n.name.equals(name)).findAny();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
public class ServerWrapperBaseClientContext implements ClientContext {
|
|
||||||
|
|
||||||
private final LanguageServerWrapper wrapper;
|
|
||||||
|
|
||||||
public ServerWrapperBaseClientContext(@NotNull LanguageServerWrapper wrapper) {
|
|
||||||
this.wrapper = wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public EditorEventManager getEditorEventManagerFor(@NotNull String documentUri) {
|
|
||||||
return wrapper.getEditorManagerFor(documentUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public Project getProject() {
|
|
||||||
return wrapper.getProject();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public RequestManager getRequestManager() {
|
|
||||||
return wrapper.getRequestManager();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client.languageserver;
|
|
||||||
|
|
||||||
import org.eclipse.lsp4j.CodeLensOptions;
|
|
||||||
import org.eclipse.lsp4j.CompletionOptions;
|
|
||||||
import org.eclipse.lsp4j.DocumentLinkOptions;
|
|
||||||
import org.eclipse.lsp4j.DocumentOnTypeFormattingOptions;
|
|
||||||
import org.eclipse.lsp4j.ExecuteCommandOptions;
|
|
||||||
import org.eclipse.lsp4j.ServerCapabilities;
|
|
||||||
import org.eclipse.lsp4j.SignatureHelpOptions;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentSyncKind;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class containing the options of the language server.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class ServerOptions {
|
|
||||||
|
|
||||||
//Todo - Revisit and implement with accessors
|
|
||||||
public TextDocumentSyncKind syncKind;
|
|
||||||
public ServerCapabilities capabilities;
|
|
||||||
public CompletionOptions completionOptions;
|
|
||||||
public SignatureHelpOptions signatureHelpOptions;
|
|
||||||
public CodeLensOptions codeLensOptions;
|
|
||||||
public DocumentOnTypeFormattingOptions documentOnTypeFormattingOptions;
|
|
||||||
public DocumentLinkOptions documentLinkOptions;
|
|
||||||
public ExecuteCommandOptions executeCommandOptions;
|
|
||||||
|
|
||||||
public ServerOptions(ServerCapabilities serverCapabilities) {
|
|
||||||
|
|
||||||
this.capabilities = serverCapabilities;
|
|
||||||
|
|
||||||
if (capabilities.getTextDocumentSync().isRight()) {
|
|
||||||
this.syncKind = capabilities.getTextDocumentSync().getRight().getChange();
|
|
||||||
} else if (capabilities.getTextDocumentSync().isLeft()) {
|
|
||||||
this.syncKind = capabilities.getTextDocumentSync().getLeft();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.completionOptions = capabilities.getCompletionProvider();
|
|
||||||
this.signatureHelpOptions = capabilities.getSignatureHelpProvider();
|
|
||||||
this.codeLensOptions = capabilities.getCodeLensProvider();
|
|
||||||
this.documentOnTypeFormattingOptions = capabilities.getDocumentOnTypeFormattingProvider();
|
|
||||||
this.documentLinkOptions = capabilities.getDocumentLinkProvider();
|
|
||||||
this.executeCommandOptions = capabilities.getExecuteCommandProvider();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client.languageserver;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An enum representing a server status
|
|
||||||
*/
|
|
||||||
public enum ServerStatus {
|
|
||||||
NONEXISTENT,
|
|
||||||
STOPPED,
|
|
||||||
STARTING,
|
|
||||||
STARTED,
|
|
||||||
INITIALIZED,
|
|
||||||
STOPPING
|
|
||||||
}
|
|
|
@ -1,740 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
|
|
||||||
import org.eclipse.lsp4j.ApplyWorkspaceEditResponse;
|
|
||||||
import org.eclipse.lsp4j.CodeAction;
|
|
||||||
import org.eclipse.lsp4j.CodeActionOptions;
|
|
||||||
import org.eclipse.lsp4j.CodeActionParams;
|
|
||||||
import org.eclipse.lsp4j.CodeLens;
|
|
||||||
import org.eclipse.lsp4j.CodeLensParams;
|
|
||||||
import org.eclipse.lsp4j.ColorInformation;
|
|
||||||
import org.eclipse.lsp4j.ColorPresentation;
|
|
||||||
import org.eclipse.lsp4j.ColorPresentationParams;
|
|
||||||
import org.eclipse.lsp4j.Command;
|
|
||||||
import org.eclipse.lsp4j.CompletionItem;
|
|
||||||
import org.eclipse.lsp4j.CompletionList;
|
|
||||||
import org.eclipse.lsp4j.CompletionOptions;
|
|
||||||
import org.eclipse.lsp4j.CompletionParams;
|
|
||||||
import org.eclipse.lsp4j.DeclarationParams;
|
|
||||||
import org.eclipse.lsp4j.DefinitionParams;
|
|
||||||
import org.eclipse.lsp4j.DidChangeConfigurationParams;
|
|
||||||
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
|
|
||||||
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentColorParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentFormattingParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentHighlight;
|
|
||||||
import org.eclipse.lsp4j.DocumentHighlightParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentLink;
|
|
||||||
import org.eclipse.lsp4j.DocumentLinkParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentRangeFormattingParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentSymbol;
|
|
||||||
import org.eclipse.lsp4j.DocumentSymbolParams;
|
|
||||||
import org.eclipse.lsp4j.ExecuteCommandParams;
|
|
||||||
import org.eclipse.lsp4j.FoldingRange;
|
|
||||||
import org.eclipse.lsp4j.FoldingRangeRequestParams;
|
|
||||||
import org.eclipse.lsp4j.Hover;
|
|
||||||
import org.eclipse.lsp4j.HoverParams;
|
|
||||||
import org.eclipse.lsp4j.ImplementationParams;
|
|
||||||
import org.eclipse.lsp4j.InitializeParams;
|
|
||||||
import org.eclipse.lsp4j.InitializeResult;
|
|
||||||
import org.eclipse.lsp4j.InitializedParams;
|
|
||||||
import org.eclipse.lsp4j.InlayHint;
|
|
||||||
import org.eclipse.lsp4j.InlayHintParams;
|
|
||||||
import org.eclipse.lsp4j.Location;
|
|
||||||
import org.eclipse.lsp4j.LocationLink;
|
|
||||||
import org.eclipse.lsp4j.MessageActionItem;
|
|
||||||
import org.eclipse.lsp4j.MessageParams;
|
|
||||||
import org.eclipse.lsp4j.PublishDiagnosticsParams;
|
|
||||||
import org.eclipse.lsp4j.ReferenceParams;
|
|
||||||
import org.eclipse.lsp4j.RegistrationParams;
|
|
||||||
import org.eclipse.lsp4j.RenameParams;
|
|
||||||
import org.eclipse.lsp4j.ServerCapabilities;
|
|
||||||
import org.eclipse.lsp4j.ShowMessageRequestParams;
|
|
||||||
import org.eclipse.lsp4j.SignatureHelp;
|
|
||||||
import org.eclipse.lsp4j.SignatureHelpParams;
|
|
||||||
import org.eclipse.lsp4j.SymbolInformation;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentPositionParams;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentSyncOptions;
|
|
||||||
import org.eclipse.lsp4j.TextEdit;
|
|
||||||
import org.eclipse.lsp4j.TypeDefinitionParams;
|
|
||||||
import org.eclipse.lsp4j.UnregistrationParams;
|
|
||||||
import org.eclipse.lsp4j.WillSaveTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.WorkspaceEdit;
|
|
||||||
import org.eclipse.lsp4j.WorkspaceSymbol;
|
|
||||||
import org.eclipse.lsp4j.WorkspaceSymbolParams;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
|
||||||
import org.eclipse.lsp4j.services.LanguageClient;
|
|
||||||
import org.eclipse.lsp4j.services.LanguageServer;
|
|
||||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
|
||||||
import org.eclipse.lsp4j.services.WorkspaceService;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default implementation for LSP requests/notifications handling.
|
|
||||||
*/
|
|
||||||
public class DefaultRequestManager implements RequestManager {
|
|
||||||
|
|
||||||
private final Logger LOG = Logger.getInstance(DefaultRequestManager.class);
|
|
||||||
|
|
||||||
private final LanguageServerWrapper wrapper;
|
|
||||||
private final LanguageServer server;
|
|
||||||
private final LanguageClient client;
|
|
||||||
private final ServerCapabilities serverCapabilities;
|
|
||||||
private final TextDocumentSyncOptions textDocumentOptions;
|
|
||||||
private final WorkspaceService workspaceService;
|
|
||||||
private final TextDocumentService textDocumentService;
|
|
||||||
|
|
||||||
public DefaultRequestManager(LanguageServerWrapper wrapper, LanguageServer server, LanguageClient client,
|
|
||||||
ServerCapabilities serverCapabilities) {
|
|
||||||
|
|
||||||
this.wrapper = wrapper;
|
|
||||||
this.server = server;
|
|
||||||
this.client = client;
|
|
||||||
this.serverCapabilities = serverCapabilities;
|
|
||||||
|
|
||||||
textDocumentOptions = serverCapabilities.getTextDocumentSync().isRight() ? serverCapabilities.getTextDocumentSync().getRight() : null;
|
|
||||||
workspaceService = server.getWorkspaceService();
|
|
||||||
textDocumentService = server.getTextDocumentService();
|
|
||||||
}
|
|
||||||
|
|
||||||
public LanguageServerWrapper getWrapper() {
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LanguageClient getClient() {
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LanguageServer getServer() {
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServerCapabilities getServerCapabilities() {
|
|
||||||
return serverCapabilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client
|
|
||||||
@Override
|
|
||||||
public void showMessage(MessageParams messageParams) {
|
|
||||||
client.showMessage(messageParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams showMessageRequestParams) {
|
|
||||||
return client.showMessageRequest(showMessageRequestParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void logMessage(MessageParams messageParams) {
|
|
||||||
client.logMessage(messageParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void telemetryEvent(Object o) {
|
|
||||||
client.telemetryEvent(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Void> registerCapability(RegistrationParams params) {
|
|
||||||
return client.registerCapability(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Void> unregisterCapability(UnregistrationParams params) {
|
|
||||||
return client.unregisterCapability(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<ApplyWorkspaceEditResponse> applyEdit(ApplyWorkspaceEditParams params) {
|
|
||||||
return client.applyEdit(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void publishDiagnostics(PublishDiagnosticsParams publishDiagnosticsParams) {
|
|
||||||
client.publishDiagnostics(publishDiagnosticsParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Void> refreshSemanticTokens() {
|
|
||||||
return client.refreshSemanticTokens();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server
|
|
||||||
|
|
||||||
// General
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return server.initialize(params);
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialized(InitializedParams params) {
|
|
||||||
if (wrapper.getStatus() == ServerStatus.STARTED) {
|
|
||||||
try {
|
|
||||||
server.initialized(params);
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Object> shutdown() {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return server.shutdown();
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void exit() {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
server.exit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextDocumentService getTextDocumentService() {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return textDocumentService;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public WorkspaceService getWorkspaceService() {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return workspaceService;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workspace service
|
|
||||||
@Override
|
|
||||||
public void didChangeConfiguration(DidChangeConfigurationParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
workspaceService.didChangeConfiguration(params);
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
workspaceService.didChangeWatchedFiles(params);
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletableFuture<Either<List<? extends SymbolInformation>, List<? extends WorkspaceSymbol>>> symbol(WorkspaceSymbolParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return Optional.ofNullable(serverCapabilities.getWorkspaceSymbolProvider())
|
|
||||||
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
|
|
||||||
workspaceService.symbol(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return serverCapabilities.getExecuteCommandProvider() != null ? workspaceService.executeCommand(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text document service
|
|
||||||
@Override
|
|
||||||
public void didOpen(DidOpenTextDocumentParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
if (Optional.ofNullable(textDocumentOptions).map(TextDocumentSyncOptions::getOpenClose).orElse(false)) {
|
|
||||||
textDocumentService.didOpen(params);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void didChange(DidChangeTextDocumentParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
if (textDocumentOptions == null || textDocumentOptions.getChange() != null) {
|
|
||||||
textDocumentService.didChange(params);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void willSave(WillSaveTextDocumentParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
if (Optional.ofNullable(textDocumentOptions).map(x -> x.getWillSave()).orElse(false)) {
|
|
||||||
textDocumentService.willSave(params);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<TextEdit>> willSaveWaitUntil(WillSaveTextDocumentParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return Optional.ofNullable(textDocumentOptions).map(x -> x.getWillSaveWaitUntil()).orElse(false) ?
|
|
||||||
textDocumentService.willSaveWaitUntil(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void didSave(DidSaveTextDocumentParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
if (Optional.ofNullable(textDocumentOptions).map(x -> x.getSave()).isPresent()) {
|
|
||||||
textDocumentService.didSave(params);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void didClose(DidCloseTextDocumentParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
if (Optional.ofNullable(textDocumentOptions).map(TextDocumentSyncOptions::getOpenClose).orElse(false)) {
|
|
||||||
textDocumentService.didClose(params);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return (serverCapabilities.getCompletionProvider() != null) ? textDocumentService.completion(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return (Optional.ofNullable(serverCapabilities.getCompletionProvider()).map(CompletionOptions::getResolveProvider).orElse(false)) ?
|
|
||||||
textDocumentService.resolveCompletionItem(unresolved) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Hover> hover(TextDocumentPositionParams params) {
|
|
||||||
return hover(new HoverParams(params.getTextDocument(), params.getPosition()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Hover> hover(HoverParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return
|
|
||||||
Optional.ofNullable(serverCapabilities.getHoverProvider())
|
|
||||||
.map(e -> e.getRight() != null || (e.getLeft() != null && e.getLeft())).orElse(false) ?
|
|
||||||
textDocumentService.hover(params) : null;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<SignatureHelp> signatureHelp(TextDocumentPositionParams params) {
|
|
||||||
return signatureHelp(new SignatureHelpParams(params.getTextDocument(), params.getPosition()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<SignatureHelp> signatureHelp(SignatureHelpParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return (serverCapabilities.getSignatureHelpProvider() != null) ? textDocumentService.signatureHelp(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<? extends Location>> references(ReferenceParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return Optional.ofNullable(serverCapabilities.getReferencesProvider())
|
|
||||||
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
|
|
||||||
textDocumentService.references(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(TextDocumentPositionParams params) {
|
|
||||||
return documentHighlight(new DocumentHighlightParams(params.getTextDocument(), params.getPosition()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(DocumentHighlightParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return Optional.ofNullable(serverCapabilities.getDocumentHighlightProvider())
|
|
||||||
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
|
|
||||||
textDocumentService.documentHighlight(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(DocumentSymbolParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return Optional.ofNullable(serverCapabilities.getDocumentSymbolProvider())
|
|
||||||
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
|
|
||||||
textDocumentService.documentSymbol(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return Optional.ofNullable(serverCapabilities.getDocumentFormattingProvider())
|
|
||||||
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
|
|
||||||
textDocumentService.formatting(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return (serverCapabilities.getDocumentRangeFormattingProvider() != null) ? textDocumentService.rangeFormatting(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<? extends TextEdit>> onTypeFormatting(DocumentOnTypeFormattingParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return (serverCapabilities.getDocumentOnTypeFormattingProvider() != null) ?
|
|
||||||
textDocumentService.onTypeFormatting(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(TextDocumentPositionParams params) {
|
|
||||||
return definition(new DefinitionParams(params.getTextDocument(), params.getPosition()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(DefinitionParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return Optional.ofNullable(serverCapabilities.getDefinitionProvider())
|
|
||||||
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
|
|
||||||
textDocumentService.definition(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> declaration(DeclarationParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return Optional.ofNullable(serverCapabilities.getDefinitionProvider())
|
|
||||||
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
|
|
||||||
textDocumentService.declaration(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return checkCodeActionProvider(serverCapabilities.getCodeActionProvider()) ? textDocumentService.codeAction(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return (serverCapabilities.getCodeLensProvider() != null) ? textDocumentService.codeLens(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<CodeLens> resolveCodeLens(CodeLens unresolved) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return (serverCapabilities.getCodeLensProvider() != null && serverCapabilities.getCodeLensProvider()
|
|
||||||
.getResolveProvider()) ? textDocumentService.resolveCodeLens(unresolved) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<DocumentLink>> documentLink(DocumentLinkParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return (serverCapabilities.getDocumentLinkProvider() != null) ?
|
|
||||||
textDocumentService.documentLink(params) :
|
|
||||||
null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<DocumentLink> documentLinkResolve(DocumentLink unresolved) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return serverCapabilities.getDocumentLinkProvider() != null && Optional.ofNullable(serverCapabilities
|
|
||||||
.getDocumentLinkProvider().getResolveProvider()).orElse(false) ?
|
|
||||||
textDocumentService.documentLinkResolve(unresolved) :
|
|
||||||
null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<WorkspaceEdit> rename(RenameParams params) {
|
|
||||||
// if (checkStatus()) {
|
|
||||||
// try {
|
|
||||||
// return (checkProvider((Either<Boolean, StaticRegistrationOptions>)serverCapabilities.getRenameProvider())) ?
|
|
||||||
// textDocumentService.rename(params) :
|
|
||||||
// null;
|
|
||||||
// } catch (Exception e) {
|
|
||||||
// crashed(e);
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> implementation(ImplementationParams params) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> typeDefinition(TypeDefinitionParams params) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<ColorInformation>> documentColor(DocumentColorParams params) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<ColorPresentation>> colorPresentation(ColorPresentationParams params) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<InlayHint>> inlayHint(InlayHintParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
return (getServerCapabilities().getInlayHintProvider() != null)
|
|
||||||
? getTextDocumentService().inlayHint(params) : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<List<FoldingRange>> foldingRange(FoldingRangeRequestParams params) {
|
|
||||||
if (checkStatus()) {
|
|
||||||
try {
|
|
||||||
var future = serverCapabilities.getFoldingRangeProvider() != null ?
|
|
||||||
textDocumentService.foldingRange(params) :
|
|
||||||
null;
|
|
||||||
return future == null ? null : future.thenApply((range) -> range == null ? Collections.emptyList() : range);
|
|
||||||
} catch (Exception e) {
|
|
||||||
crashed(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean checkStatus() {
|
|
||||||
return wrapper.getStatus() == ServerStatus.INITIALIZED;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void crashed(Exception e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
wrapper.crashed(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkCodeActionProvider(Either<Boolean, CodeActionOptions> provider) {
|
|
||||||
return provider != null && ((provider.isLeft() && provider.getLeft()) || (provider.isRight()
|
|
||||||
&& provider.getRight() != null));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,238 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager;
|
|
||||||
|
|
||||||
import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
|
|
||||||
import org.eclipse.lsp4j.ApplyWorkspaceEditResponse;
|
|
||||||
import org.eclipse.lsp4j.CodeAction;
|
|
||||||
import org.eclipse.lsp4j.CodeActionParams;
|
|
||||||
import org.eclipse.lsp4j.CodeLens;
|
|
||||||
import org.eclipse.lsp4j.CodeLensParams;
|
|
||||||
import org.eclipse.lsp4j.ColorInformation;
|
|
||||||
import org.eclipse.lsp4j.ColorPresentation;
|
|
||||||
import org.eclipse.lsp4j.ColorPresentationParams;
|
|
||||||
import org.eclipse.lsp4j.Command;
|
|
||||||
import org.eclipse.lsp4j.CompletionItem;
|
|
||||||
import org.eclipse.lsp4j.CompletionList;
|
|
||||||
import org.eclipse.lsp4j.CompletionParams;
|
|
||||||
import org.eclipse.lsp4j.DefinitionParams;
|
|
||||||
import org.eclipse.lsp4j.DidChangeConfigurationParams;
|
|
||||||
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
|
|
||||||
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentColorParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentFormattingParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentHighlight;
|
|
||||||
import org.eclipse.lsp4j.DocumentHighlightParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentLink;
|
|
||||||
import org.eclipse.lsp4j.DocumentLinkParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentRangeFormattingParams;
|
|
||||||
import org.eclipse.lsp4j.DocumentSymbol;
|
|
||||||
import org.eclipse.lsp4j.DocumentSymbolParams;
|
|
||||||
import org.eclipse.lsp4j.ExecuteCommandParams;
|
|
||||||
import org.eclipse.lsp4j.FoldingRange;
|
|
||||||
import org.eclipse.lsp4j.FoldingRangeRequestParams;
|
|
||||||
import org.eclipse.lsp4j.Hover;
|
|
||||||
import org.eclipse.lsp4j.HoverParams;
|
|
||||||
import org.eclipse.lsp4j.ImplementationParams;
|
|
||||||
import org.eclipse.lsp4j.InitializeParams;
|
|
||||||
import org.eclipse.lsp4j.InitializeResult;
|
|
||||||
import org.eclipse.lsp4j.InitializedParams;
|
|
||||||
import org.eclipse.lsp4j.Location;
|
|
||||||
import org.eclipse.lsp4j.LocationLink;
|
|
||||||
import org.eclipse.lsp4j.MessageActionItem;
|
|
||||||
import org.eclipse.lsp4j.MessageParams;
|
|
||||||
import org.eclipse.lsp4j.PublishDiagnosticsParams;
|
|
||||||
import org.eclipse.lsp4j.ReferenceParams;
|
|
||||||
import org.eclipse.lsp4j.RegistrationParams;
|
|
||||||
import org.eclipse.lsp4j.RenameParams;
|
|
||||||
import org.eclipse.lsp4j.ShowMessageRequestParams;
|
|
||||||
import org.eclipse.lsp4j.SignatureHelp;
|
|
||||||
import org.eclipse.lsp4j.SignatureHelpParams;
|
|
||||||
import org.eclipse.lsp4j.SymbolInformation;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentPositionParams;
|
|
||||||
import org.eclipse.lsp4j.TextEdit;
|
|
||||||
import org.eclipse.lsp4j.TypeDefinitionParams;
|
|
||||||
import org.eclipse.lsp4j.UnregistrationParams;
|
|
||||||
import org.eclipse.lsp4j.WillSaveTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.WorkspaceEdit;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
|
||||||
import org.eclipse.lsp4j.services.LanguageClient;
|
|
||||||
import org.eclipse.lsp4j.services.LanguageServer;
|
|
||||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
|
||||||
import org.eclipse.lsp4j.services.WorkspaceService;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base representation of currently supported LSP-based requests and notifications.
|
|
||||||
*/
|
|
||||||
public interface RequestManager extends LanguageClient, TextDocumentService, WorkspaceService, LanguageServer {
|
|
||||||
|
|
||||||
//------------------------------------- Server2Client ---------------------------------------------------------//
|
|
||||||
@Override
|
|
||||||
void showMessage(MessageParams messageParams);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams showMessageRequestParams);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void logMessage(MessageParams messageParams);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void telemetryEvent(Object o);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<Void> registerCapability(RegistrationParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<Void> unregisterCapability(UnregistrationParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<ApplyWorkspaceEditResponse> applyEdit(ApplyWorkspaceEditParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void publishDiagnostics(PublishDiagnosticsParams publishDiagnosticsParams);
|
|
||||||
|
|
||||||
//--------------------------------------Client2Server-------------------------------------------------------------//
|
|
||||||
|
|
||||||
// General
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<InitializeResult> initialize(InitializeParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void initialized(InitializedParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<Object> shutdown();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void exit();
|
|
||||||
|
|
||||||
// Workspace Service
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void didChangeConfiguration(DidChangeConfigurationParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void didChangeWatchedFiles(DidChangeWatchedFilesParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<Object> executeCommand(ExecuteCommandParams params);
|
|
||||||
|
|
||||||
// Text Document Service
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void didOpen(DidOpenTextDocumentParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void didChange(DidChangeTextDocumentParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void willSave(WillSaveTextDocumentParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<TextEdit>> willSaveWaitUntil(WillSaveTextDocumentParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void didSave(DidSaveTextDocumentParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void didClose(DidCloseTextDocumentParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<Hover> hover(HoverParams params);
|
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
CompletableFuture<Hover> hover(TextDocumentPositionParams params);
|
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
CompletableFuture<SignatureHelp> signatureHelp(TextDocumentPositionParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<SignatureHelp> signatureHelp(SignatureHelpParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<? extends Location>> references(ReferenceParams params);
|
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(TextDocumentPositionParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(DocumentHighlightParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(DocumentSymbolParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<? extends TextEdit>> onTypeFormatting(DocumentOnTypeFormattingParams params);
|
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(TextDocumentPositionParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(DefinitionParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<CodeLens> resolveCodeLens(CodeLens unresolved);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<DocumentLink>> documentLink(DocumentLinkParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<DocumentLink> documentLinkResolve(DocumentLink unresolved);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<WorkspaceEdit> rename(RenameParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> implementation(ImplementationParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> typeDefinition(TypeDefinitionParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<ColorInformation>> documentColor(DocumentColorParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<ColorPresentation>> colorPresentation(ColorPresentationParams params);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
CompletableFuture<List<FoldingRange>> foldingRange(FoldingRangeRequestParams params);
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lspcommon.connection.StreamConnectionProvider;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
|
||||||
import org.eclipse.lsp4j.InitializeParams;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A trait representing a ServerDefinition
|
|
||||||
*/
|
|
||||||
public abstract class LanguageServerDefinition {
|
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getInstance(LanguageServerDefinition.class);
|
|
||||||
|
|
||||||
public String ext;
|
|
||||||
protected Map<String, String> languageIds = Collections.emptyMap();
|
|
||||||
private Map<String, StreamConnectionProvider> streamConnectionProviders = new ConcurrentHashMap<>();
|
|
||||||
public static final String SPLIT_CHAR = ",";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a Language server for the given directory and returns a tuple (InputStream, OutputStream)
|
|
||||||
*
|
|
||||||
* @param workingDir The root directory
|
|
||||||
* @return The input and output streams of the server
|
|
||||||
* @throws IOException if the stream connection provider is crashed
|
|
||||||
*/
|
|
||||||
public Pair<InputStream, OutputStream> start(String workingDir) throws IOException {
|
|
||||||
StreamConnectionProvider streamConnectionProvider = streamConnectionProviders.get(workingDir);
|
|
||||||
if (streamConnectionProvider != null) {
|
|
||||||
return new ImmutablePair<>(streamConnectionProvider.getInputStream(), streamConnectionProvider.getOutputStream());
|
|
||||||
} else {
|
|
||||||
streamConnectionProvider = createConnectionProvider(workingDir);
|
|
||||||
streamConnectionProvider.start();
|
|
||||||
streamConnectionProviders.put(workingDir, streamConnectionProvider);
|
|
||||||
return new ImmutablePair<>(streamConnectionProvider.getInputStream(), streamConnectionProvider.getOutputStream());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the Language server corresponding to the given working directory
|
|
||||||
*
|
|
||||||
* @param workingDir The root directory
|
|
||||||
*/
|
|
||||||
public void stop(String workingDir) {
|
|
||||||
StreamConnectionProvider streamConnectionProvider = streamConnectionProviders.get(workingDir);
|
|
||||||
if (streamConnectionProvider != null) {
|
|
||||||
streamConnectionProvider.stop();
|
|
||||||
streamConnectionProviders.remove(workingDir);
|
|
||||||
} else {
|
|
||||||
LOG.warn("No connection for workingDir " + workingDir + " and ext " + ext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this method to modify the {@link InitializeParams} that was initialized by this library. The values
|
|
||||||
* assigned to the passed {@link InitializeParams} after this method ends will be the ones sent to the LSP server.
|
|
||||||
*
|
|
||||||
* @param params the parameters with some prefilled values.
|
|
||||||
*/
|
|
||||||
public void customizeInitializeParams(InitializeParams params) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "ServerDefinition for " + ext;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a StreamConnectionProvider given the working directory
|
|
||||||
*
|
|
||||||
* @param workingDir The root directory
|
|
||||||
* @return The stream connection provider
|
|
||||||
*/
|
|
||||||
public abstract StreamConnectionProvider createConnectionProvider(String workingDir);
|
|
||||||
|
|
||||||
public ServerListener getServerListener() {
|
|
||||||
return ServerListener.DEFAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return language id for the given extension. if there is no langauge ids registered then the
|
|
||||||
* return value will be the value of <code>extension</code>.
|
|
||||||
*/
|
|
||||||
public String languageIdFor(String extension) {
|
|
||||||
return languageIds.getOrDefault(extension, extension);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lspcommon.connection.ProcessStreamConnectionProvider;
|
|
||||||
import com.falsepattern.zigbrains.lspcommon.connection.StreamConnectionProvider;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class representing {@link java.lang.ProcessBuilder} based metadata to launch a language server.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class ProcessBuilderServerDefinition extends LanguageServerDefinition {
|
|
||||||
|
|
||||||
protected ProcessBuilder processBuilder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates new instance with the given language id which is different from the file extension.
|
|
||||||
*
|
|
||||||
* @param ext The extension.
|
|
||||||
* @param languageIds The language server ids mapping to extension(s).
|
|
||||||
* @param process The process builder instance to be started.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
public ProcessBuilderServerDefinition(String ext, Map<String, String> languageIds, ProcessBuilder process) {
|
|
||||||
this.ext = ext;
|
|
||||||
this.languageIds = languageIds;
|
|
||||||
this.processBuilder = process;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates new instance.
|
|
||||||
*
|
|
||||||
* @param ext The extension.
|
|
||||||
* @param process The process builder instance to be started.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public ProcessBuilderServerDefinition(String ext, ProcessBuilder process) {
|
|
||||||
this(ext, Collections.emptyMap(), process);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return "ProcessBuilderServerDefinition : " + String.join(" ", processBuilder.command());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StreamConnectionProvider createConnectionProvider(String workingDir) {
|
|
||||||
return new ProcessStreamConnectionProvider(processBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (obj instanceof ProcessBuilderServerDefinition) {
|
|
||||||
ProcessBuilderServerDefinition processBuilderDef = (ProcessBuilderServerDefinition) obj;
|
|
||||||
return ext.equals(processBuilderDef.ext) && processBuilder.equals(processBuilderDef.processBuilder);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return ext.hashCode() + 3 * processBuilder.hashCode();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lspcommon.connection.ProcessStreamConnectionProvider;
|
|
||||||
import com.falsepattern.zigbrains.lspcommon.connection.StreamConnectionProvider;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class representing raw command based metadata to launch a language server.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class RawCommandServerDefinition extends LanguageServerDefinition {
|
|
||||||
|
|
||||||
protected String[] command;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates new instance with the given languag id which is different from the file extension.
|
|
||||||
*
|
|
||||||
* @param ext The extension
|
|
||||||
* @param languageIds The language server ids mapping to extension(s).
|
|
||||||
* @param command The command to run
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
public RawCommandServerDefinition(String ext, Map<String, String> languageIds, String[] command) {
|
|
||||||
this.ext = ext;
|
|
||||||
this.languageIds = languageIds;
|
|
||||||
this.command = command;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates new instance.
|
|
||||||
*
|
|
||||||
* @param ext The extension
|
|
||||||
* @param command The command to run
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public RawCommandServerDefinition(String ext, String[] command) {
|
|
||||||
this(ext, Collections.emptyMap(), command);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return "RawCommandServerDefinition : " + String.join(" ", command);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StreamConnectionProvider createConnectionProvider(String workingDir) {
|
|
||||||
return new ProcessStreamConnectionProvider(Arrays.asList(command), workingDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (obj instanceof RawCommandServerDefinition) {
|
|
||||||
RawCommandServerDefinition commandsDef = (RawCommandServerDefinition) obj;
|
|
||||||
return ext.equals(commandsDef.ext) && Arrays.equals(command, commandsDef.command);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return ext.hashCode() + 3 * Arrays.hashCode(command);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition;
|
|
||||||
|
|
||||||
import org.eclipse.lsp4j.InitializeResult;
|
|
||||||
import org.eclipse.lsp4j.services.LanguageServer;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public interface ServerListener {
|
|
||||||
|
|
||||||
ServerListener DEFAULT = new ServerListener() {
|
|
||||||
};
|
|
||||||
|
|
||||||
default void initialize(@NotNull LanguageServer server, @NotNull InitializeResult result) {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,834 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client.languageserver.wrapper;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.ApplicationUtil;
|
|
||||||
import com.falsepattern.zigbrains.common.util.FileUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.DefaultLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.ServerWrapperBaseClientContext;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerOptions;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.DefaultRequestManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.falsepattern.zigbrains.lsp.extensions.LSPExtensionManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.listeners.DocumentListenerImpl;
|
|
||||||
import com.falsepattern.zigbrains.lsp.listeners.LSPCaretListenerImpl;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.Timeout;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
|
|
||||||
import com.falsepattern.zigbrains.lsp.statusbar.LSPServerStatusWidget;
|
|
||||||
import com.falsepattern.zigbrains.lsp.statusbar.LSPServerStatusWidgetFactory;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.LSPException;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
|
|
||||||
import com.intellij.openapi.application.ApplicationInfo;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditor;
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.project.ProjectUtil;
|
|
||||||
import com.intellij.openapi.ui.MessageType;
|
|
||||||
import com.intellij.openapi.ui.Messages;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.psi.PsiDocumentManager;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import com.intellij.remoteServer.util.CloudNotifier;
|
|
||||||
import com.intellij.util.PlatformIcons;
|
|
||||||
import lombok.val;
|
|
||||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
|
||||||
import org.eclipse.lsp4j.ClientCapabilities;
|
|
||||||
import org.eclipse.lsp4j.ClientInfo;
|
|
||||||
import org.eclipse.lsp4j.CodeActionCapabilities;
|
|
||||||
import org.eclipse.lsp4j.CodeActionKindCapabilities;
|
|
||||||
import org.eclipse.lsp4j.CodeActionLiteralSupportCapabilities;
|
|
||||||
import org.eclipse.lsp4j.CompletionCapabilities;
|
|
||||||
import org.eclipse.lsp4j.CompletionItemCapabilities;
|
|
||||||
import org.eclipse.lsp4j.DefinitionCapabilities;
|
|
||||||
import org.eclipse.lsp4j.DidChangeWatchedFilesCapabilities;
|
|
||||||
import org.eclipse.lsp4j.DocumentHighlightCapabilities;
|
|
||||||
import org.eclipse.lsp4j.ExecuteCommandCapabilities;
|
|
||||||
import org.eclipse.lsp4j.FoldingRangeCapabilities;
|
|
||||||
import org.eclipse.lsp4j.FoldingRangeKind;
|
|
||||||
import org.eclipse.lsp4j.FoldingRangeKindSupportCapabilities;
|
|
||||||
import org.eclipse.lsp4j.FoldingRangeSupportCapabilities;
|
|
||||||
import org.eclipse.lsp4j.FormattingCapabilities;
|
|
||||||
import org.eclipse.lsp4j.HoverCapabilities;
|
|
||||||
import org.eclipse.lsp4j.InitializeParams;
|
|
||||||
import org.eclipse.lsp4j.InitializeResult;
|
|
||||||
import org.eclipse.lsp4j.InitializedParams;
|
|
||||||
import org.eclipse.lsp4j.InlayHintCapabilities;
|
|
||||||
import org.eclipse.lsp4j.OnTypeFormattingCapabilities;
|
|
||||||
import org.eclipse.lsp4j.RangeFormattingCapabilities;
|
|
||||||
import org.eclipse.lsp4j.ReferencesCapabilities;
|
|
||||||
import org.eclipse.lsp4j.RenameCapabilities;
|
|
||||||
import org.eclipse.lsp4j.ServerCapabilities;
|
|
||||||
import org.eclipse.lsp4j.SignatureHelpCapabilities;
|
|
||||||
import org.eclipse.lsp4j.SymbolCapabilities;
|
|
||||||
import org.eclipse.lsp4j.SynchronizationCapabilities;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentClientCapabilities;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentSyncKind;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentSyncOptions;
|
|
||||||
import org.eclipse.lsp4j.WorkspaceClientCapabilities;
|
|
||||||
import org.eclipse.lsp4j.WorkspaceEditCapabilities;
|
|
||||||
import org.eclipse.lsp4j.WorkspaceFolder;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.Launcher;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.Message;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage;
|
|
||||||
import org.eclipse.lsp4j.services.LanguageClient;
|
|
||||||
import org.eclipse.lsp4j.services.LanguageServer;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
import static com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus.INITIALIZED;
|
|
||||||
import static com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus.STARTED;
|
|
||||||
import static com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus.STARTING;
|
|
||||||
import static com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus.STOPPED;
|
|
||||||
import static com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus.STOPPING;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The implementation of a LanguageServerWrapper (specific to a serverDefinition and a project)
|
|
||||||
*/
|
|
||||||
public class LanguageServerWrapper {
|
|
||||||
public final LanguageServerDefinition serverDefinition;
|
|
||||||
private final LSPExtensionManager extManager;
|
|
||||||
private final Project project;
|
|
||||||
private final HashSet<Editor> toConnect = new HashSet<>();
|
|
||||||
@Nullable
|
|
||||||
private final Path projectRootPath;
|
|
||||||
private final HashSet<String> urisUnderLspControl = new HashSet<>();
|
|
||||||
private final HashSet<Editor> connectedEditors = new HashSet<>();
|
|
||||||
private final Map<String, Set<EditorEventManager>> uriToEditorManagers = new HashMap<>();
|
|
||||||
private LanguageServer languageServer;
|
|
||||||
private LanguageClient client;
|
|
||||||
private RequestManager requestManager;
|
|
||||||
private InitializeResult initializeResult;
|
|
||||||
private Future<?> launcherFuture;
|
|
||||||
private CompletableFuture<InitializeResult> initializeFuture;
|
|
||||||
private int crashCount = 0;
|
|
||||||
private volatile boolean alreadyShownTimeout = false;
|
|
||||||
private volatile boolean alreadyShownCrash = false;
|
|
||||||
private volatile ServerStatus status = STOPPED;
|
|
||||||
private final AtomicReference<Thread> shutdownHook = new AtomicReference<>();
|
|
||||||
private static final Map<Pair<String, String>, LanguageServerWrapper> uriToLanguageServerWrapper =
|
|
||||||
new ConcurrentHashMap<>();
|
|
||||||
private static final Map<Project, LanguageServerWrapper> projectToLanguageServerWrapper = new ConcurrentHashMap<>();
|
|
||||||
private static final Logger LOG = Logger.getInstance(LanguageServerWrapper.class);
|
|
||||||
private static final CloudNotifier notifier = new CloudNotifier("Language Server Protocol client");
|
|
||||||
|
|
||||||
public LanguageServerWrapper(@NotNull LanguageServerDefinition serverDefinition, @NotNull Project project) {
|
|
||||||
this(serverDefinition, project, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LanguageServerWrapper(@NotNull LanguageServerDefinition serverDefinition, @NotNull Project project,
|
|
||||||
@Nullable LSPExtensionManager extManager) {
|
|
||||||
this.serverDefinition = serverDefinition;
|
|
||||||
this.project = project;
|
|
||||||
// We need to keep the project rootPath in addition to the project instance, since we cannot get the project
|
|
||||||
// base path if the project is disposed.
|
|
||||||
val projectDir = ProjectUtil.guessProjectDir(project);
|
|
||||||
if (projectDir != null) {
|
|
||||||
this.projectRootPath = projectDir.toNioPath();
|
|
||||||
} else {
|
|
||||||
this.projectRootPath = null;
|
|
||||||
}
|
|
||||||
this.extManager = extManager;
|
|
||||||
projectToLanguageServerWrapper.put(project, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param uri A file uri
|
|
||||||
* @param project The related project
|
|
||||||
* @return The wrapper for the given uri, or None
|
|
||||||
*/
|
|
||||||
public static LanguageServerWrapper forUri(String uri, Project project) {
|
|
||||||
return uriToLanguageServerWrapper.get(new ImmutablePair<>(uri, FileUtils.projectToUri(project)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LanguageServerWrapper forVirtualFile(VirtualFile file, Project project) {
|
|
||||||
return uriToLanguageServerWrapper.get(new ImmutablePair<>(FileUtil.URIFromVirtualFile(file), FileUtils.projectToUri(project)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param editor An editor
|
|
||||||
* @return The wrapper for the given editor, or None
|
|
||||||
*/
|
|
||||||
public static LanguageServerWrapper forEditor(Editor editor) {
|
|
||||||
return uriToLanguageServerWrapper.get(new ImmutablePair<>(FileUtils.editorToURIString(editor), FileUtils.editorToProjectFolderUri(editor)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LanguageServerWrapper forProject(Project project) {
|
|
||||||
return projectToLanguageServerWrapper.get(project);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LanguageServerDefinition getServerDefinition() {
|
|
||||||
return serverDefinition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path getProjectRootPath() {
|
|
||||||
return projectRootPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return if the server supports willSaveWaitUntil
|
|
||||||
*/
|
|
||||||
public boolean isWillSaveWaitUntil() {
|
|
||||||
return Optional.ofNullable(getServerCapabilities())
|
|
||||||
.map(ServerCapabilities::getTextDocumentSync)
|
|
||||||
.map(Either::getRight)
|
|
||||||
.map(TextDocumentSyncOptions::getWillSaveWaitUntil)
|
|
||||||
.orElse(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Warning: this is a long running operation
|
|
||||||
*
|
|
||||||
* @return the languageServer capabilities, or null if initialization job didn't complete
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public ServerCapabilities getServerCapabilities() {
|
|
||||||
if (initializeResult != null)
|
|
||||||
return initializeResult.getCapabilities();
|
|
||||||
else {
|
|
||||||
try {
|
|
||||||
start();
|
|
||||||
if (initializeFuture != null) {
|
|
||||||
initializeFuture.get(Timeout.getTimeout(Timeouts.INIT), TimeUnit.MILLISECONDS);
|
|
||||||
notifySuccess(Timeouts.INIT);
|
|
||||||
}
|
|
||||||
} catch (TimeoutException e) {
|
|
||||||
notifyFailure(Timeouts.INIT);
|
|
||||||
String msg = String.format("%s \n is not initialized after %d seconds",
|
|
||||||
serverDefinition.toString(), Timeout.getTimeout(Timeouts.INIT) / 1000);
|
|
||||||
LOG.warn(msg, e);
|
|
||||||
ApplicationUtil.invokeLater(() -> {
|
|
||||||
if (!alreadyShownTimeout) {
|
|
||||||
notifier.showMessage(msg, MessageType.WARNING);
|
|
||||||
alreadyShownTimeout = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stop(false);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
stop(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return initializeResult != null ? initializeResult.getCapabilities() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notifyResult(Timeouts timeouts, boolean success) {
|
|
||||||
getWidgets().forEach(widget -> widget.notifyResult(timeouts, success));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notifySuccess(Timeouts timeouts) {
|
|
||||||
notifyResult(timeouts, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notifyFailure(Timeouts timeouts) {
|
|
||||||
notifyResult(timeouts, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the EditorEventManager for a given uri
|
|
||||||
* <p>
|
|
||||||
* WARNING: actually a file can be present in multiple editors, this function just gives you one editor. use {@link #getEditorManagersFor(String)} instead
|
|
||||||
* only use for document level events such as open, close, ...
|
|
||||||
*
|
|
||||||
* @param uri the URI as a string
|
|
||||||
* @return the EditorEventManager (or null)
|
|
||||||
*/
|
|
||||||
public EditorEventManager getEditorManagerFor(String uri) {
|
|
||||||
FileEditor selectedEditor = FileEditorManager.getInstance(project).getSelectedEditor();
|
|
||||||
|
|
||||||
if (selectedEditor == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
VirtualFile currentOpenFile = selectedEditor.getFile();
|
|
||||||
VirtualFile requestedFile = FileUtil.virtualFileFromURI(uri);
|
|
||||||
if (currentOpenFile == null || requestedFile == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (requestedFile.equals(currentOpenFile)) {
|
|
||||||
return EditorEventManagerBase.forEditor((Editor) FileEditorManager.getInstance(project).getSelectedEditor());
|
|
||||||
}
|
|
||||||
if (uriToEditorManagers.containsKey(uri) && !uriToEditorManagers.get(uri).isEmpty()) {
|
|
||||||
return (EditorEventManager) uriToEditorManagers.get(uri).toArray()[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<EditorEventManager> getEditorManagersFor(String uri) {
|
|
||||||
return uriToEditorManagers.get(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The request manager for this wrapper
|
|
||||||
*/
|
|
||||||
public RequestManager getRequestManager() {
|
|
||||||
return requestManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return whether the underlying connection to language languageServer is still active
|
|
||||||
*/
|
|
||||||
public boolean isActive() {
|
|
||||||
return launcherFuture != null && !launcherFuture.isDone() && !launcherFuture.isCancelled()
|
|
||||||
&& !alreadyShownTimeout && !alreadyShownCrash;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connects an editor to the languageServer
|
|
||||||
*
|
|
||||||
* @param editor the editor
|
|
||||||
*/
|
|
||||||
public void connect(Editor editor) {
|
|
||||||
if (editor == null) {
|
|
||||||
LOG.warn("editor is null for " + serverDefinition);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!FileUtils.isEditorSupported(editor)) {
|
|
||||||
LOG.debug("Editor hosts a unsupported file type by the LS library.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String uri = FileUtils.editorToURIString(editor);
|
|
||||||
if (connectedEditors.contains(editor)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ImmutablePair<String, String> key = new ImmutablePair<>(uri, FileUtils.editorToProjectFolderUri(editor));
|
|
||||||
|
|
||||||
uriToLanguageServerWrapper.put(key, this);
|
|
||||||
|
|
||||||
start();
|
|
||||||
if (initializeFuture != null) {
|
|
||||||
ServerCapabilities capabilities = getServerCapabilities();
|
|
||||||
if (capabilities == null) {
|
|
||||||
LOG.warn("Capabilities are null for " + serverDefinition);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeFuture.thenRun(() -> {
|
|
||||||
if (connectedEditors.contains(editor)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Either<TextDocumentSyncKind, TextDocumentSyncOptions> syncOptions = capabilities.getTextDocumentSync();
|
|
||||||
if (syncOptions != null) {
|
|
||||||
//Todo - Implement
|
|
||||||
// SelectionListenerImpl selectionListener = new SelectionListenerImpl();
|
|
||||||
DocumentListenerImpl documentListener = new DocumentListenerImpl();
|
|
||||||
LSPCaretListenerImpl caretListener = new LSPCaretListenerImpl();
|
|
||||||
|
|
||||||
ServerOptions serverOptions = new ServerOptions(capabilities);
|
|
||||||
EditorEventManager manager;
|
|
||||||
if (extManager != null) {
|
|
||||||
manager = extManager.getExtendedEditorEventManagerFor(editor, documentListener,
|
|
||||||
caretListener, requestManager, serverOptions,
|
|
||||||
this);
|
|
||||||
if (manager == null) {
|
|
||||||
manager = new EditorEventManager(editor, documentListener, caretListener,
|
|
||||||
requestManager, serverOptions, this);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
manager = new EditorEventManager(editor, documentListener, caretListener,
|
|
||||||
requestManager, serverOptions, this);
|
|
||||||
}
|
|
||||||
// selectionListener.setManager(manager);
|
|
||||||
documentListener.setManager(manager);
|
|
||||||
caretListener.setManager(manager);
|
|
||||||
manager.registerListeners();
|
|
||||||
if (!urisUnderLspControl.contains(uri)) {
|
|
||||||
manager.documentEventManager.registerListeners();
|
|
||||||
}
|
|
||||||
urisUnderLspControl.add(uri);
|
|
||||||
connectedEditors.add(editor);
|
|
||||||
if (uriToEditorManagers.containsKey(uri)) {
|
|
||||||
uriToEditorManagers.get(uri).add(manager);
|
|
||||||
} else {
|
|
||||||
Set<EditorEventManager> set = new HashSet<>();
|
|
||||||
set.add(manager);
|
|
||||||
uriToEditorManagers.put(uri, set);
|
|
||||||
manager.documentOpened();
|
|
||||||
}
|
|
||||||
manager.initComplete();
|
|
||||||
LOG.info("Created a manager for " + uri);
|
|
||||||
synchronized (toConnect) {
|
|
||||||
toConnect.remove(editor);
|
|
||||||
}
|
|
||||||
for (Editor ed : new HashSet<>(toConnect)) {
|
|
||||||
connect(ed);
|
|
||||||
}
|
|
||||||
// Triggers annotators since this is the first editor which starts the LS
|
|
||||||
// and annotators are executed before LS is bootstrap to provide diagnostics.
|
|
||||||
ApplicationUtil.computableReadAction(() -> {
|
|
||||||
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
|
|
||||||
if (psiFile != null) {
|
|
||||||
DaemonCodeAnalyzer.getInstance(project).restart(psiFile);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
synchronized (toConnect) {
|
|
||||||
toConnect.add(editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The shutdown request is sent from the client to the server. It asks the server to shut down, but to not exit \
|
|
||||||
* (otherwise the response might not be delivered correctly to the client).
|
|
||||||
* Only if the exit flag is true, particular server instance will exit.
|
|
||||||
*/
|
|
||||||
public void stop(boolean exit) {
|
|
||||||
if (this.status == STOPPED || this.status == STOPPING) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setStatus(STOPPING);
|
|
||||||
|
|
||||||
if (initializeFuture != null) {
|
|
||||||
initializeFuture.cancel(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (languageServer != null) {
|
|
||||||
CompletableFuture<Object> shutdown = languageServer.shutdown();
|
|
||||||
shutdown.get(Timeout.getTimeout(Timeouts.SHUTDOWN), TimeUnit.MILLISECONDS);
|
|
||||||
notifySuccess(Timeouts.SHUTDOWN);
|
|
||||||
if (exit) {
|
|
||||||
languageServer.exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// most likely closed externally.
|
|
||||||
notifyFailure(Timeouts.SHUTDOWN);
|
|
||||||
LOG.warn("exception occured while trying to shut down", e);
|
|
||||||
} finally {
|
|
||||||
if (launcherFuture != null) {
|
|
||||||
launcherFuture.cancel(true);
|
|
||||||
}
|
|
||||||
if (serverDefinition != null) {
|
|
||||||
serverDefinition.stop(projectRootPath != null ? projectRootPath.toString() : null);
|
|
||||||
}
|
|
||||||
for (Editor ed : new HashSet<>(connectedEditors)) {
|
|
||||||
disconnect(ed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// sadly this whole editor closing stuff runs asynchronously, so we cannot be sure the state is really clean here...
|
|
||||||
// therefore clear the mapping from here as it should be empty by now.
|
|
||||||
uriToEditorManagers.clear();
|
|
||||||
urisUnderLspControl.clear();
|
|
||||||
launcherFuture = null;
|
|
||||||
initializeResult = null;
|
|
||||||
initializeFuture = null;
|
|
||||||
languageServer = null;
|
|
||||||
setStatus(STOPPED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the wrapper is already connected to the document at the given path.
|
|
||||||
*
|
|
||||||
* @param location file location
|
|
||||||
* @return True if the given file is connected.
|
|
||||||
*/
|
|
||||||
public boolean isConnectedTo(String location) {
|
|
||||||
return urisUnderLspControl.contains(location);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the LanguageServer
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public LanguageServer getServer() {
|
|
||||||
start();
|
|
||||||
if (initializeFuture != null && !initializeFuture.isDone()) {
|
|
||||||
initializeFuture.join();
|
|
||||||
}
|
|
||||||
return languageServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the LanguageServer
|
|
||||||
*/
|
|
||||||
public void start() {
|
|
||||||
if (status == STOPPED && !alreadyShownCrash && !alreadyShownTimeout) {
|
|
||||||
setStatus(STARTING);
|
|
||||||
try {
|
|
||||||
Pair<InputStream, OutputStream> streams = serverDefinition.start(projectRootPath != null ? projectRootPath.toString() : null);
|
|
||||||
InputStream inputStream = streams.getKey();
|
|
||||||
OutputStream outputStream = streams.getValue();
|
|
||||||
InitializeParams initParams = getInitParams();
|
|
||||||
ExecutorService executorService = Executors.newCachedThreadPool();
|
|
||||||
MessageHandler messageHandler = new MessageHandler(serverDefinition.getServerListener(), () -> getStatus() != STOPPED);
|
|
||||||
if (extManager != null && extManager.getExtendedServerInterface() != null) {
|
|
||||||
Class<? extends LanguageServer> remoteServerInterFace = extManager.getExtendedServerInterface();
|
|
||||||
client = extManager.getExtendedClientFor(new ServerWrapperBaseClientContext(this));
|
|
||||||
|
|
||||||
val launcher = new Launcher.Builder<LanguageServer>()
|
|
||||||
.setLocalService(client)
|
|
||||||
.setRemoteInterface(remoteServerInterFace)
|
|
||||||
.setInput(inputStream)
|
|
||||||
.setOutput(outputStream)
|
|
||||||
.setExecutorService(executorService)
|
|
||||||
.wrapMessages(messageHandler)
|
|
||||||
.configureGson(GsonBuilder::disableHtmlEscaping)
|
|
||||||
.create();
|
|
||||||
languageServer = launcher.getRemoteProxy();
|
|
||||||
launcherFuture = launcher.startListening();
|
|
||||||
} else {
|
|
||||||
client = new DefaultLanguageClient(new ServerWrapperBaseClientContext(this));
|
|
||||||
val launcher = new Launcher.Builder<LanguageServer>()
|
|
||||||
.setLocalService(client)
|
|
||||||
.setRemoteInterface(LanguageServer.class)
|
|
||||||
.setInput(inputStream)
|
|
||||||
.setOutput(outputStream)
|
|
||||||
.setExecutorService(executorService)
|
|
||||||
.wrapMessages(messageHandler)
|
|
||||||
.configureGson(GsonBuilder::disableHtmlEscaping)
|
|
||||||
.create();
|
|
||||||
languageServer = launcher.getRemoteProxy();
|
|
||||||
launcherFuture = launcher.startListening();
|
|
||||||
}
|
|
||||||
messageHandler.setLanguageServer(languageServer);
|
|
||||||
|
|
||||||
initializeFuture = languageServer.initialize(initParams).thenApply(res -> {
|
|
||||||
initializeResult = res;
|
|
||||||
LOG.info("Got initializeResult for " + serverDefinition + " ; " + projectRootPath);
|
|
||||||
if (extManager != null) {
|
|
||||||
requestManager = extManager.getExtendedRequestManagerFor(this, languageServer, client, res.getCapabilities());
|
|
||||||
if (requestManager == null) {
|
|
||||||
requestManager = new DefaultRequestManager(this, languageServer, client, res.getCapabilities());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
requestManager = new DefaultRequestManager(this, languageServer, client, res.getCapabilities());
|
|
||||||
}
|
|
||||||
setStatus(STARTED);
|
|
||||||
// send the initialized message since some language servers depends on this message
|
|
||||||
requestManager.initialized(new InitializedParams());
|
|
||||||
setStatus(INITIALIZED);
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
} catch (LSPException | IOException | URISyntaxException e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
ApplicationUtil.invokeLater(() ->
|
|
||||||
notifier.showMessage(String.format("Can't start server due to %s", e.getMessage()),
|
|
||||||
MessageType.WARNING));
|
|
||||||
removeServerWrapper();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private InitializeParams getInitParams() throws URISyntaxException {
|
|
||||||
InitializeParams initParams = new InitializeParams();
|
|
||||||
String projectRootUri = FileUtil.pathToUri(projectRootPath);
|
|
||||||
if (projectRootUri.endsWith("/")) {
|
|
||||||
projectRootUri = projectRootUri.substring(0, projectRootUri.length() - 1);
|
|
||||||
}
|
|
||||||
WorkspaceFolder workspaceFolder = new WorkspaceFolder(projectRootUri, this.project.getName());
|
|
||||||
initParams.setWorkspaceFolders(Collections.singletonList(workspaceFolder));
|
|
||||||
initParams.setProcessId((int) ProcessHandle.current().pid());
|
|
||||||
initParams.setLocale("en-us");
|
|
||||||
|
|
||||||
// workspace capabilities
|
|
||||||
WorkspaceClientCapabilities workspaceClientCapabilities = new WorkspaceClientCapabilities();
|
|
||||||
workspaceClientCapabilities.setApplyEdit(true);
|
|
||||||
workspaceClientCapabilities.setDidChangeWatchedFiles(new DidChangeWatchedFilesCapabilities());
|
|
||||||
workspaceClientCapabilities.setExecuteCommand(new ExecuteCommandCapabilities());
|
|
||||||
workspaceClientCapabilities.setWorkspaceEdit(new WorkspaceEditCapabilities());
|
|
||||||
workspaceClientCapabilities.setSymbol(new SymbolCapabilities());
|
|
||||||
workspaceClientCapabilities.setWorkspaceFolders(true);
|
|
||||||
workspaceClientCapabilities.setConfiguration(false);
|
|
||||||
|
|
||||||
// text document capabilities
|
|
||||||
TextDocumentClientCapabilities textDocumentClientCapabilities = new TextDocumentClientCapabilities();
|
|
||||||
textDocumentClientCapabilities.setCodeAction(new CodeActionCapabilities());
|
|
||||||
textDocumentClientCapabilities.getCodeAction().setCodeActionLiteralSupport(new CodeActionLiteralSupportCapabilities(new CodeActionKindCapabilities()));
|
|
||||||
textDocumentClientCapabilities.setCompletion(new CompletionCapabilities(new CompletionItemCapabilities(true)));
|
|
||||||
textDocumentClientCapabilities.setDefinition(new DefinitionCapabilities());
|
|
||||||
textDocumentClientCapabilities.setDocumentHighlight(new DocumentHighlightCapabilities());
|
|
||||||
textDocumentClientCapabilities.setFormatting(new FormattingCapabilities());
|
|
||||||
textDocumentClientCapabilities.setHover(new HoverCapabilities());
|
|
||||||
textDocumentClientCapabilities.setInlayHint(new InlayHintCapabilities());
|
|
||||||
textDocumentClientCapabilities.setOnTypeFormatting(new OnTypeFormattingCapabilities());
|
|
||||||
textDocumentClientCapabilities.setRangeFormatting(new RangeFormattingCapabilities());
|
|
||||||
textDocumentClientCapabilities.setReferences(new ReferencesCapabilities());
|
|
||||||
textDocumentClientCapabilities.setRename(new RenameCapabilities());
|
|
||||||
textDocumentClientCapabilities.setSignatureHelp(new SignatureHelpCapabilities());
|
|
||||||
textDocumentClientCapabilities.setSynchronization(new SynchronizationCapabilities(true, true, true));
|
|
||||||
|
|
||||||
FoldingRangeCapabilities foldingRangeCapabilities = new FoldingRangeCapabilities();
|
|
||||||
foldingRangeCapabilities.setFoldingRangeKind(new FoldingRangeKindSupportCapabilities(List.of(
|
|
||||||
FoldingRangeKind.Comment, FoldingRangeKind.Imports, FoldingRangeKind.Region)));
|
|
||||||
foldingRangeCapabilities.setFoldingRange(new FoldingRangeSupportCapabilities(true));
|
|
||||||
textDocumentClientCapabilities.setFoldingRange(foldingRangeCapabilities);
|
|
||||||
|
|
||||||
initParams.setCapabilities(
|
|
||||||
new ClientCapabilities(workspaceClientCapabilities, textDocumentClientCapabilities, null));
|
|
||||||
initParams.setClientInfo(new ClientInfo(ApplicationInfo.getInstance().getVersionName(), ApplicationInfo.getInstance().getFullVersion()));
|
|
||||||
|
|
||||||
serverDefinition.customizeInitializeParams(initParams);
|
|
||||||
return initParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void logMessage(Message message) {
|
|
||||||
if (message instanceof ResponseMessage) {
|
|
||||||
ResponseMessage responseMessage = (ResponseMessage) message;
|
|
||||||
if (responseMessage.getError() != null && (responseMessage.getId()
|
|
||||||
.equals(Integer.toString(ResponseErrorCode.RequestCancelled.getValue())))) {
|
|
||||||
LOG.error(new ResponseErrorException(responseMessage.getError()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Project getProject() {
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServerStatus getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setStatus(ServerStatus status) {
|
|
||||||
this.status = status;
|
|
||||||
getWidgets().forEach(widget -> widget.setStatus(status));
|
|
||||||
synchronized (shutdownHook) {
|
|
||||||
if ((status == STARTED || status == INITIALIZED) && shutdownHook.get() == null) {
|
|
||||||
shutdownHook.set(new Thread(() -> {
|
|
||||||
shutdownHook.set(null);
|
|
||||||
stop(true);
|
|
||||||
}));
|
|
||||||
Runtime.getRuntime().addShutdownHook(shutdownHook.get());
|
|
||||||
} else if (status == STOPPED && shutdownHook.get() != null) {
|
|
||||||
try {
|
|
||||||
Runtime.getRuntime().removeShutdownHook(shutdownHook.get());
|
|
||||||
} catch (IllegalStateException ignored) {} //Shouldn't happen
|
|
||||||
shutdownHook.set(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void crashed(Exception e) {
|
|
||||||
crashCount += 1;
|
|
||||||
if (crashCount <= 3) {
|
|
||||||
reconnect();
|
|
||||||
} else {
|
|
||||||
ApplicationUtil.invokeLater(() -> {
|
|
||||||
if (alreadyShownCrash) {
|
|
||||||
reconnect();
|
|
||||||
} else {
|
|
||||||
int response = Messages.showYesNoDialog(String.format(
|
|
||||||
"LanguageServer for definition %s, project %s keeps crashing due to \n%s\n"
|
|
||||||
, serverDefinition.toString(), project.getName(), e.getMessage()),
|
|
||||||
"Language Server Client Warning", "Keep Connected", "Disconnect", PlatformIcons.CHECK_ICON);
|
|
||||||
if (response == Messages.NO) {
|
|
||||||
int confirm = Messages.showYesNoDialog("All the language server based plugin features will be disabled.\n" +
|
|
||||||
"Do you wish to continue?", "", PlatformIcons.WARNING_INTRODUCTION_ICON);
|
|
||||||
if (confirm == Messages.YES) {
|
|
||||||
// Disconnects from the language server.
|
|
||||||
stop(true);
|
|
||||||
} else {
|
|
||||||
reconnect();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
alreadyShownCrash = true;
|
|
||||||
crashCount = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reconnect() {
|
|
||||||
// Need to copy by value since connected editors gets cleared during 'stop()' invocation.
|
|
||||||
final Set<String> connected = new HashSet<>(urisUnderLspControl);
|
|
||||||
stop(true);
|
|
||||||
for (String uri : connected) {
|
|
||||||
connect(uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getConnectedFiles() {
|
|
||||||
List<String> connected = new ArrayList<>();
|
|
||||||
urisUnderLspControl.forEach(s -> {
|
|
||||||
try {
|
|
||||||
connected.add(new URI(FileUtil.sanitizeURI(s)).toString());
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return connected;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnects an editor from the LanguageServer
|
|
||||||
*
|
|
||||||
* @param editor The editor
|
|
||||||
*/
|
|
||||||
public void disconnect(Editor editor) {
|
|
||||||
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
connectedEditors.remove(editor);
|
|
||||||
if (manager != null) {
|
|
||||||
manager.removeListeners();
|
|
||||||
String uri = FileUtils.editorToURIString(editor);
|
|
||||||
Set<EditorEventManager> set = uriToEditorManagers.get(uri);
|
|
||||||
if (set != null) {
|
|
||||||
set.remove(manager);
|
|
||||||
if (set.isEmpty()) {
|
|
||||||
manager.documentClosed();
|
|
||||||
manager.documentEventManager.removeListeners();
|
|
||||||
|
|
||||||
uriToEditorManagers.remove(uri);
|
|
||||||
urisUnderLspControl.remove(uri);
|
|
||||||
uriToLanguageServerWrapper.remove(new ImmutablePair<>(uri, FileUtils.editorToProjectFolderUri(editor)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectedEditors.isEmpty()) {
|
|
||||||
stop(true);
|
|
||||||
|
|
||||||
getWidgets().forEach(widget -> widget.setStatus(ServerStatus.NONEXISTENT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnects an editor from the LanguageServer
|
|
||||||
* <p>
|
|
||||||
* WARNING: only use this method if you have no editor instance and you restart all connections to the language server for all open editors
|
|
||||||
* prefer using disconnect(editor)
|
|
||||||
*
|
|
||||||
* @param uri The file uri
|
|
||||||
* @param projectUri The project root uri
|
|
||||||
*/
|
|
||||||
public void disconnect(String uri, String projectUri) {
|
|
||||||
uriToLanguageServerWrapper.remove(new ImmutablePair<>(FileUtil.sanitizeURI(uri), FileUtil.sanitizeURI(projectUri)));
|
|
||||||
|
|
||||||
Set<EditorEventManager> managers = uriToEditorManagers.get(uri);
|
|
||||||
if (managers == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (EditorEventManager manager : managers) {
|
|
||||||
manager.removeListeners();
|
|
||||||
manager.documentEventManager.removeListeners();
|
|
||||||
connectedEditors.remove(manager.editor);
|
|
||||||
Set<EditorEventManager> editorEventManagers = uriToEditorManagers.get(uri);
|
|
||||||
if (editorEventManagers != null) {
|
|
||||||
editorEventManagers.remove(manager);
|
|
||||||
if (editorEventManagers.isEmpty()) {
|
|
||||||
uriToEditorManagers.remove(uri);
|
|
||||||
manager.documentClosed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
urisUnderLspControl.remove(uri);
|
|
||||||
uriToLanguageServerWrapper.remove(new ImmutablePair<>(FileUtil.sanitizeURI(uri), FileUtil.sanitizeURI(projectUri)));
|
|
||||||
}
|
|
||||||
if (connectedEditors.isEmpty()) {
|
|
||||||
stop(true);
|
|
||||||
|
|
||||||
getWidgets().forEach(widget -> widget.setStatus(ServerStatus.NONEXISTENT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeServerWrapper() {
|
|
||||||
stop(true);
|
|
||||||
IntellijLanguageClient.removeWrapper(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void connect(String uri) {
|
|
||||||
List<Editor> editors = FileUtils.getAllOpenedEditorsForUri(project, uri);
|
|
||||||
|
|
||||||
for (Editor editor : editors) {
|
|
||||||
connect(editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is the language server in a state where it can be restartable. Normally language server is
|
|
||||||
* restartable if it has timeout or has a startup error.
|
|
||||||
*/
|
|
||||||
public boolean isRestartable() {
|
|
||||||
return status == STOPPED && (alreadyShownTimeout || alreadyShownCrash);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset language server wrapper state so it can be started again if it was failed earlier.
|
|
||||||
*/
|
|
||||||
public void restart() {
|
|
||||||
ApplicationUtil.pool(() -> {
|
|
||||||
if (isRestartable()) {
|
|
||||||
alreadyShownCrash = false;
|
|
||||||
alreadyShownTimeout = false;
|
|
||||||
} else {
|
|
||||||
stop(true);
|
|
||||||
}
|
|
||||||
FileUtils.reloadEditors(project);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<LSPServerStatusWidget> getWidgets() {
|
|
||||||
return Optional.ofNullable(project.getUserData(LSPServerStatusWidgetFactory.LSP_WIDGETS)).orElse(List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the extension manager associated with this language server wrapper.
|
|
||||||
*
|
|
||||||
* @return The result can be null if there is not extension manager defined.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public final LSPExtensionManager getExtensionManager() {
|
|
||||||
return extManager;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.client.languageserver.wrapper;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.ServerListener;
|
|
||||||
import org.eclipse.lsp4j.InitializeResult;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.Message;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage;
|
|
||||||
import org.eclipse.lsp4j.services.LanguageServer;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.function.BooleanSupplier;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
class MessageHandler implements Function<MessageConsumer, MessageConsumer> {
|
|
||||||
|
|
||||||
private ServerListener listener;
|
|
||||||
private BooleanSupplier isRunning;
|
|
||||||
private LanguageServer languageServer;
|
|
||||||
|
|
||||||
MessageHandler(@NotNull ServerListener listener, @NotNull BooleanSupplier isRunning) {
|
|
||||||
this.listener = listener;
|
|
||||||
this.isRunning = isRunning;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MessageConsumer apply(MessageConsumer messageConsumer) {
|
|
||||||
return message -> {
|
|
||||||
if(isRunning.getAsBoolean()) {
|
|
||||||
handleMessage(message);
|
|
||||||
messageConsumer.consume(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleMessage(Message message) {
|
|
||||||
if (message instanceof ResponseMessage) {
|
|
||||||
ResponseMessage responseMessage = (ResponseMessage) message;
|
|
||||||
if (responseMessage.getResult() instanceof InitializeResult) {
|
|
||||||
listener.initialize(languageServer, (InitializeResult) responseMessage.getResult());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setLanguageServer(@NotNull LanguageServer languageServer) {
|
|
||||||
this.languageServer = languageServer;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
|
|
||||||
import com.intellij.codeInsight.completion.CompletionContributor;
|
|
||||||
import com.intellij.codeInsight.completion.CompletionParameters;
|
|
||||||
import com.intellij.codeInsight.completion.CompletionProvider;
|
|
||||||
import com.intellij.codeInsight.completion.CompletionResultSet;
|
|
||||||
import com.intellij.codeInsight.completion.PlainPrefixMatcher;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.util.ProcessingContext;
|
|
||||||
import org.eclipse.lsp4j.Position;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The completion contributor for the LSP
|
|
||||||
*/
|
|
||||||
class LSPCompletionContributor extends CompletionContributor {
|
|
||||||
private static final Logger LOG = Logger.getInstance(LSPCompletionContributor.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
|
|
||||||
CompletionProvider<CompletionParameters> provider = new CompletionProvider<>() {
|
|
||||||
@Override
|
|
||||||
protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet result) {
|
|
||||||
try {
|
|
||||||
Editor editor = parameters.getEditor();
|
|
||||||
int offset = parameters.getOffset();
|
|
||||||
Position serverPos = DocumentUtils.offsetToLSPPos(editor, offset);
|
|
||||||
|
|
||||||
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (manager != null) {
|
|
||||||
result.addAllElements(manager.completion(serverPos));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warn("LSP Completions ended with an error", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Editor editor = parameters.getEditor();
|
|
||||||
int offset = parameters.getOffset();
|
|
||||||
|
|
||||||
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (manager != null) {
|
|
||||||
String prefix = manager.getCompletionPrefix(editor, offset);
|
|
||||||
|
|
||||||
provider.addCompletionVariants(parameters, new ProcessingContext(), result.withPrefixMatcher(new PlainPrefixMatcher(prefix)));
|
|
||||||
if (result.isStopped()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
super.fillCompletionVariants(parameters, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.openapi.application.ApplicationManager;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.ScrollType;
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
|
||||||
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
|
|
||||||
import com.intellij.openapi.vfs.VfsUtil;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.platform.backend.documentation.DocumentationLinkHandler;
|
|
||||||
import com.intellij.platform.backend.documentation.DocumentationTarget;
|
|
||||||
import com.intellij.platform.backend.documentation.LinkResolveResult;
|
|
||||||
import lombok.val;
|
|
||||||
import org.eclipse.lsp4j.Position;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
import static com.intellij.codeInsight.documentation.DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL;
|
|
||||||
|
|
||||||
public class LSPDocumentationLinkHandler implements DocumentationLinkHandler {
|
|
||||||
private static final String prefix = PSI_ELEMENT_PROTOCOL + "zigbrains://";
|
|
||||||
private final static Logger LOG = Logger.getInstance(LSPDocumentationLinkHandler.class);
|
|
||||||
@Override
|
|
||||||
public @Nullable LinkResolveResult resolveLink(@NotNull DocumentationTarget target, @NotNull String url) {
|
|
||||||
if (!url.startsWith(PSI_ELEMENT_PROTOCOL) || ! (target instanceof LSPDocumentationTargetProvider.LSPDocumentationTarget tgt)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
url = url.replace(prefix, "file://");
|
|
||||||
val separator = url.indexOf("#L");
|
|
||||||
if (separator < 0)
|
|
||||||
return null;
|
|
||||||
val link = url.substring(0, separator);
|
|
||||||
final int line;
|
|
||||||
{
|
|
||||||
int theLine = 1;
|
|
||||||
try {
|
|
||||||
theLine = Integer.parseInt(url.substring(separator + 2));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
LOG.error("Could not parse file line: " + url.substring(separator + 2));
|
|
||||||
}
|
|
||||||
line = theLine - 1;
|
|
||||||
}
|
|
||||||
val app = ApplicationManager.getApplication();
|
|
||||||
app.executeOnPooledThread(() -> {
|
|
||||||
val project = tgt.file.getProject();
|
|
||||||
VirtualFile file;
|
|
||||||
try {
|
|
||||||
file = VfsUtil.findFileByURL(new URL(link));
|
|
||||||
} catch (MalformedURLException e1) {
|
|
||||||
LOG.warn("Syntax Exception occurred for uri: " + link);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (file == null)
|
|
||||||
return;
|
|
||||||
val descriptor = new OpenFileDescriptor(project, file);
|
|
||||||
app.invokeLater(() -> {
|
|
||||||
FileEditorManager.getInstance(project).openTextEditor(descriptor, true);
|
|
||||||
val editor = FileUtils.editorFromVirtualFile(file, project);
|
|
||||||
if (editor == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
val logicalPos = DocumentUtils.getTabsAwarePosition(editor, new Position(line, 0));
|
|
||||||
if (logicalPos == null)
|
|
||||||
return;
|
|
||||||
editor.getCaretModel().moveToLogicalPosition(logicalPos);
|
|
||||||
editor.getScrollingModel().scrollTo(logicalPos, ScrollType.CENTER);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,157 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.ApplicationUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.HoverHandler;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.Timeout;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter;
|
|
||||||
import com.intellij.model.Pointer;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.util.TextRange;
|
|
||||||
import com.intellij.platform.backend.documentation.DocumentationResult;
|
|
||||||
import com.intellij.platform.backend.documentation.DocumentationTarget;
|
|
||||||
import com.intellij.platform.backend.documentation.DocumentationTargetProvider;
|
|
||||||
import com.intellij.platform.backend.presentation.TargetPresentation;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import com.intellij.psi.SmartPointerManager;
|
|
||||||
import com.intellij.psi.SmartPsiFileRange;
|
|
||||||
import lombok.val;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.eclipse.lsp4j.HoverParams;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
import static com.intellij.codeInsight.documentation.DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL;
|
|
||||||
|
|
||||||
public class LSPDocumentationTargetProvider implements DocumentationTargetProvider {
|
|
||||||
@Override
|
|
||||||
public @NotNull List<? extends @NotNull DocumentationTarget> documentationTargets(@NotNull PsiFile file, int offset) {
|
|
||||||
if (!FileUtils.isFileSupported(file.getVirtualFile())) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
return Collections.singletonList(new LSPDocumentationTarget(file, offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LSPDocumentationTarget implements DocumentationTarget {
|
|
||||||
private final Pointer<LSPDocumentationTarget> pointer;
|
|
||||||
public final PsiFile file;
|
|
||||||
private final int offset;
|
|
||||||
public LSPDocumentationTarget(PsiFile file, int offset) {
|
|
||||||
this.file = file;
|
|
||||||
this.offset = offset;
|
|
||||||
|
|
||||||
var range = TextRange.from(offset, 0);
|
|
||||||
SmartPsiFileRange base = SmartPointerManager.getInstance(file.getProject()).createSmartPsiFileRangePointer(file, range);
|
|
||||||
pointer = new FileRangePointer(base);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Logger LOG = Logger.getInstance(LSPDocumentationTargetProvider.class);
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public DocumentationResult computeDocumentation() {
|
|
||||||
var editor = FileUtils.editorFromPsiFile(file);
|
|
||||||
if (editor == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var manager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (manager == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var wrapper = manager.wrapper;
|
|
||||||
if (wrapper == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var caretPos = editor.offsetToLogicalPosition(offset);
|
|
||||||
var serverPos = ApplicationUtil.computableReadAction(() -> DocumentUtils.logicalToLSPPos(caretPos, editor));
|
|
||||||
return DocumentationResult.asyncDocumentation(() -> {
|
|
||||||
var identifier = manager.getIdentifier();
|
|
||||||
var request = wrapper.getRequestManager().hover(new HoverParams(identifier, serverPos));
|
|
||||||
if (request == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
var hover = request.get(Timeout.getTimeout(Timeouts.HOVER), TimeUnit.MILLISECONDS);
|
|
||||||
wrapper.notifySuccess(Timeouts.HOVER);
|
|
||||||
if (hover == null) {
|
|
||||||
LOG.debug(String.format("Hover is null for file %s and pos (%d;%d)", identifier.getUri(),
|
|
||||||
serverPos.getLine(), serverPos.getCharacter()));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
val markdown = HoverHandler.getHoverString(hover).replaceAll("file://", PSI_ELEMENT_PROTOCOL + "zigbrains://");
|
|
||||||
val string = ApplicationUtil.computableReadAction(() -> DocMarkdownToHtmlConverter
|
|
||||||
.convert(manager.getProject(), markdown));
|
|
||||||
if (StringUtils.isEmpty(string)) {
|
|
||||||
LOG.warn(String.format("Hover string returned is empty for file %s and pos (%d;%d)",
|
|
||||||
identifier.getUri(), serverPos.getLine(), serverPos.getCharacter()));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return DocumentationResult.documentation(string);
|
|
||||||
} catch (TimeoutException e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
wrapper.notifyFailure(Timeouts.HOVER);
|
|
||||||
} catch (InterruptedException | JsonRpcException | ExecutionException e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
wrapper.crashed(e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public TargetPresentation computePresentation() {
|
|
||||||
return TargetPresentation.builder("Doc from language server").presentation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public Pointer<? extends DocumentationTarget> createPointer() {
|
|
||||||
return pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class FileRangePointer implements Pointer<LSPDocumentationTarget> {
|
|
||||||
private final SmartPsiFileRange base;
|
|
||||||
public FileRangePointer(SmartPsiFileRange base) {
|
|
||||||
this.base = base;
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public @Nullable LSPDocumentationTarget dereference() {
|
|
||||||
if (base.getElement() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (base.getRange() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new LSPDocumentationTarget(base.getElement(), TextRange.create(base.getRange()).getStartOffset());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,224 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.Timeout;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.lang.ASTNode;
|
|
||||||
import com.intellij.lang.folding.CustomFoldingBuilder;
|
|
||||||
import com.intellij.lang.folding.FoldingDescriptor;
|
|
||||||
import com.intellij.openapi.application.ApplicationManager;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Document;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.util.Key;
|
|
||||||
import com.intellij.openapi.util.TextRange;
|
|
||||||
import com.intellij.psi.PsiElement;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import org.eclipse.lsp4j.FoldingRange;
|
|
||||||
import org.eclipse.lsp4j.FoldingRangeRequestParams;
|
|
||||||
import org.eclipse.lsp4j.Position;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentIdentifier;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
public class LSPFoldingRangeProvider extends CustomFoldingBuilder {
|
|
||||||
private static final Key<Boolean> ASYNC_FOLDING_KEY = new Key<>("ASYNC_FOLDING");
|
|
||||||
|
|
||||||
protected Logger LOG = Logger.getInstance(LSPFoldingRangeProvider.class);
|
|
||||||
|
|
||||||
private interface FoldingRangeAcceptor {
|
|
||||||
void accept(int start, int end, @Nullable String replacement);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class AFoldingRange {
|
|
||||||
public final int start;
|
|
||||||
public final int end;
|
|
||||||
public final String collapsedText;
|
|
||||||
|
|
||||||
private AFoldingRange(int start, int end, String collapsedText) {
|
|
||||||
this.start = start;
|
|
||||||
this.end = end;
|
|
||||||
this.collapsedText = collapsedText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void buildLanguageFoldRegions(@NotNull List<FoldingDescriptor> descriptors, @NotNull PsiElement root, @NotNull Document document, boolean quick) {
|
|
||||||
// if the quick flag is set, we do nothing here
|
|
||||||
if (quick) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var async = async(root.getProject());
|
|
||||||
if (!async) {
|
|
||||||
doBuildLanguageFoldRegions((start, end, collapsedText) -> {
|
|
||||||
if (collapsedText != null) {
|
|
||||||
descriptors.add(new FoldingDescriptor(root.getNode(), new TextRange(start, end), null, collapsedText));
|
|
||||||
} else {
|
|
||||||
descriptors.add(new FoldingDescriptor(root.getNode(), new TextRange(start, end)));
|
|
||||||
}
|
|
||||||
}, root, document, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var app = ApplicationManager.getApplication();
|
|
||||||
app.executeOnPooledThread(() -> {
|
|
||||||
var ranges = new ArrayList<AFoldingRange>();
|
|
||||||
doBuildLanguageFoldRegions((start, end, collapsedText) -> ranges.add(
|
|
||||||
new AFoldingRange(start, end, collapsedText == null ? "..." : collapsedText)),
|
|
||||||
root, document, true);
|
|
||||||
var editor = FileUtils.editorFromPsiFile(root.getContainingFile());
|
|
||||||
if (editor == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
app.invokeLater(() -> {
|
|
||||||
if (editor.isDisposed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var foldingModel = editor.getFoldingModel();
|
|
||||||
var oldRegions = Arrays.stream(foldingModel.getAllFoldRegions()).filter(region -> {
|
|
||||||
var data = region.getUserData(ASYNC_FOLDING_KEY);
|
|
||||||
return data != null && data;
|
|
||||||
}).toList();
|
|
||||||
foldingModel.runBatchFoldingOperation(() -> {
|
|
||||||
for (var oldRegion: oldRegions) {
|
|
||||||
foldingModel.removeFoldRegion(oldRegion);
|
|
||||||
}
|
|
||||||
for (var range: ranges) {
|
|
||||||
var region = foldingModel.addFoldRegion(range.start, range.end, range.collapsedText);
|
|
||||||
if (region != null) {
|
|
||||||
region.putUserData(ASYNC_FOLDING_KEY, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doBuildLanguageFoldRegions(@NotNull FoldingRangeAcceptor acceptor, @NotNull PsiElement root, @NotNull Document document, boolean async) {
|
|
||||||
PsiFile psiFile = root.getContainingFile();
|
|
||||||
var editor = FileUtils.editorFromPsiFile(psiFile);
|
|
||||||
var wrapper = LanguageServerWrapper.forVirtualFile(psiFile.getVirtualFile(), root.getProject());
|
|
||||||
if (editor == null || wrapper == null || !editor.getDocument().equals(document)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var manager = wrapper.getRequestManager();
|
|
||||||
if (manager == null) {
|
|
||||||
//IDE startup race condition
|
|
||||||
if (!async)
|
|
||||||
return;
|
|
||||||
|
|
||||||
//We can block the async thread for a moment; wait 2 more seconds
|
|
||||||
for (int i = 0; i < 20 && manager == null; i++) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(100);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
//We got interrupted, bail
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
manager = wrapper.getRequestManager();
|
|
||||||
}
|
|
||||||
if (manager == null)
|
|
||||||
return; //LSP did not connect in time, bail
|
|
||||||
}
|
|
||||||
|
|
||||||
TextDocumentIdentifier textDocumentIdentifier = FileUtils.editorToLSPIdentifier(editor);
|
|
||||||
FoldingRangeRequestParams params = new FoldingRangeRequestParams(textDocumentIdentifier);
|
|
||||||
CompletableFuture<List<FoldingRange>> future = manager.foldingRange(params);
|
|
||||||
|
|
||||||
if (future == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
List<FoldingRange> foldingRanges = future.get(Timeout.getTimeout(Timeouts.FOLDING), TimeUnit.MILLISECONDS);
|
|
||||||
wrapper.notifySuccess(Timeouts.FOLDING);
|
|
||||||
if (foldingRanges == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (FoldingRange foldingRange : foldingRanges) {
|
|
||||||
int start = getStartOffset(editor, foldingRange, document);
|
|
||||||
int end = getEndOffset(editor, foldingRange, document);
|
|
||||||
int length = end - start;
|
|
||||||
if (length <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end > root.getTextLength()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var collapsedText = getCollapsedText(foldingRange);
|
|
||||||
acceptor.accept(start, end, collapsedText);
|
|
||||||
}
|
|
||||||
} catch (TimeoutException | InterruptedException e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
wrapper.notifyFailure(Timeouts.FOLDING);
|
|
||||||
} catch (JsonRpcException | ExecutionException e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
wrapper.crashed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean async(Project project) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected @Nullable String getCollapsedText(@NotNull FoldingRange foldingRange) {
|
|
||||||
return foldingRange.getCollapsedText();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getEndOffset(Editor editor, @NotNull FoldingRange foldingRange, @NotNull Document document) {
|
|
||||||
// EndCharacter is optional. When missing, it should be set to the length of the end line.
|
|
||||||
if (foldingRange.getEndCharacter() == null) {
|
|
||||||
return document.getLineEndOffset(foldingRange.getEndLine());
|
|
||||||
}
|
|
||||||
|
|
||||||
return DocumentUtils.LSPPosToOffset(editor, new Position(foldingRange.getEndLine(), foldingRange.getEndCharacter()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getStartOffset(Editor editor, @NotNull FoldingRange foldingRange, @NotNull Document document) {
|
|
||||||
// StartCharacter is optional. When missing, it should be set to the length of the start line.
|
|
||||||
if (foldingRange.getStartCharacter() == null) {
|
|
||||||
return document.getLineEndOffset(foldingRange.getStartLine());
|
|
||||||
} else {
|
|
||||||
return DocumentUtils.LSPPosToOffset(editor, new Position(foldingRange.getStartLine(), foldingRange.getStartCharacter()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getLanguagePlaceholderText(@NotNull ASTNode node, @NotNull TextRange range) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isRegionCollapsedByDefault(@NotNull ASTNode node) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.codeInsight.hints.declarative.InlayHintsCollector;
|
|
||||||
import com.intellij.codeInsight.hints.declarative.InlayHintsProvider;
|
|
||||||
import com.intellij.codeInsight.hints.declarative.InlayTreeSink;
|
|
||||||
import com.intellij.codeInsight.hints.declarative.InlineInlayPosition;
|
|
||||||
import com.intellij.codeInsight.hints.declarative.OwnBypassCollector;
|
|
||||||
import com.intellij.codeInsight.hints.declarative.impl.DeclarativeInlayHintsPassFactory;
|
|
||||||
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public class LSPInlayHintProvider implements InlayHintsProvider {
|
|
||||||
protected static Logger LOG = Logger.getInstance(LSPInlayHintProvider.class);
|
|
||||||
private static final LSPInlayHintsCollector DEFAULT_COLLECTOR = new LSPInlayHintsCollector();
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public InlayHintsCollector createCollector(@NotNull PsiFile psiFile, @NotNull Editor editor) {
|
|
||||||
if (FileUtils.isFileSupported(psiFile.getVirtualFile())) {
|
|
||||||
return getCollector();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LSPInlayHintsCollector getCollector() {
|
|
||||||
return DEFAULT_COLLECTOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LSPInlayHintsCollector implements OwnBypassCollector {
|
|
||||||
@Override
|
|
||||||
public void collectHintsForFile(@NotNull PsiFile psiFile, @NotNull InlayTreeSink inlayTreeSink) {
|
|
||||||
var editor = FileUtils.editorFromPsiFile(psiFile);
|
|
||||||
if (editor == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var manager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (manager == null || manager.editor != editor) {
|
|
||||||
EditorEventManagerBase.runWhenManagerGetsRegistered(editor,
|
|
||||||
() -> {
|
|
||||||
if (editor.isDisposed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var project = editor.getProject();
|
|
||||||
if (project == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DeclarativeInlayHintsPassFactory.Companion.scheduleRecompute(editor, project);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var res = manager.inlayHint();
|
|
||||||
if (res == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (var hint: res) {
|
|
||||||
var pos = DocumentUtils.LSPPosToOffset(editor, hint.getPosition());
|
|
||||||
var inlayPos = new InlineInlayPosition(pos, false, 0);
|
|
||||||
var tt = hint.getTooltip();
|
|
||||||
if (tt == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String tooltipText;
|
|
||||||
if (tt.isLeft()) {
|
|
||||||
tooltipText = tt.getLeft();
|
|
||||||
} else {
|
|
||||||
var markup = tt.getRight();
|
|
||||||
tooltipText = switch (markup.getKind()) {
|
|
||||||
case "markdown" -> {
|
|
||||||
var markedContent = markup.getValue();
|
|
||||||
if (markedContent.isBlank()) {
|
|
||||||
yield "";
|
|
||||||
}
|
|
||||||
yield DocMarkdownToHtmlConverter.convert(manager.getProject(), markedContent);
|
|
||||||
}
|
|
||||||
default -> markup.getValue();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
inlayTreeSink.addPresentation(inlayPos, Collections.emptyList(), tooltipText, true, (builder) -> {
|
|
||||||
var label = hint.getLabel();
|
|
||||||
StringBuilder text = new StringBuilder();
|
|
||||||
if (label.isLeft()) {
|
|
||||||
text.append(label.getLeft());
|
|
||||||
} else if (label.isRight()) {
|
|
||||||
var parts = label.getRight();
|
|
||||||
for (var part: parts) {
|
|
||||||
text.append(part.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (text.length() == 0) {
|
|
||||||
text.append(" ");
|
|
||||||
}
|
|
||||||
builder.text(text.toString(), null);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,229 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.annotator;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.FileUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.codeInspection.ProblemHighlightType;
|
|
||||||
import com.intellij.lang.annotation.Annotation;
|
|
||||||
import com.intellij.lang.annotation.AnnotationBuilder;
|
|
||||||
import com.intellij.lang.annotation.AnnotationHolder;
|
|
||||||
import com.intellij.lang.annotation.ExternalAnnotator;
|
|
||||||
import com.intellij.lang.annotation.HighlightSeverity;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.util.TextRange;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.psi.PsiDocumentManager;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import com.intellij.util.SmartList;
|
|
||||||
import lombok.val;
|
|
||||||
import org.eclipse.lsp4j.Diagnostic;
|
|
||||||
import org.eclipse.lsp4j.DiagnosticSeverity;
|
|
||||||
import org.eclipse.lsp4j.DiagnosticTag;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.ConcurrentModificationException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class LSPAnnotator extends ExternalAnnotator<Object, Object> {
|
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getInstance(LSPAnnotator.class);
|
|
||||||
private static final Object RESULT = new Object();
|
|
||||||
private static final HashMap<DiagnosticSeverity, HighlightSeverity> lspToIntellijAnnotationsMap = new HashMap<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
lspToIntellijAnnotationsMap.put(DiagnosticSeverity.Error, HighlightSeverity.ERROR);
|
|
||||||
lspToIntellijAnnotationsMap.put(DiagnosticSeverity.Warning, HighlightSeverity.WARNING);
|
|
||||||
|
|
||||||
// seem flipped, but just different semantics lsp<->intellij. Hint is rendered without any squiggle
|
|
||||||
lspToIntellijAnnotationsMap.put(DiagnosticSeverity.Information, HighlightSeverity.WEAK_WARNING);
|
|
||||||
lspToIntellijAnnotationsMap.put(DiagnosticSeverity.Hint, HighlightSeverity.INFORMATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public Object collectInformation(@NotNull PsiFile file, @NotNull Editor editor, boolean hasErrors) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
VirtualFile virtualFile = file.getVirtualFile();
|
|
||||||
|
|
||||||
// If the file is not supported, we skips the annotation by returning null.
|
|
||||||
if (!FileUtils.isFileSupported(virtualFile) || !IntellijLanguageClient.isExtensionSupported(virtualFile)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
|
|
||||||
if (eventManager == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the diagnostics list is locked, we need to skip annotating the file.
|
|
||||||
if (!(eventManager.isDiagnosticSyncRequired() || eventManager.isCodeActionSyncRequired())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return RESULT;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public Object doAnnotate(Object collectedInfo) {
|
|
||||||
return RESULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void apply(@NotNull PsiFile file, Object annotationResult, @NotNull AnnotationHolder holder) {
|
|
||||||
|
|
||||||
LanguageServerWrapper languageServerWrapper = LanguageServerWrapper.forVirtualFile(file.getVirtualFile(), file.getProject());
|
|
||||||
if (languageServerWrapper == null || languageServerWrapper.getStatus() != ServerStatus.INITIALIZED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
VirtualFile virtualFile = file.getVirtualFile();
|
|
||||||
if (FileUtils.isFileSupported(virtualFile) && IntellijLanguageClient.isExtensionSupported(virtualFile)) {
|
|
||||||
String uri = FileUtil.URIFromVirtualFile(virtualFile);
|
|
||||||
// TODO annotations are applied to a file / document not to an editor. so store them by file and not by editor..
|
|
||||||
EditorEventManager eventManager = EditorEventManagerBase.forUri(uri);
|
|
||||||
if (eventManager == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (eventManager.isDiagnosticSyncRequired()) {
|
|
||||||
try {
|
|
||||||
createAnnotations(holder, eventManager);
|
|
||||||
} catch (ConcurrentModificationException e) {
|
|
||||||
// Todo - Add proper fix to handle concurrent modifications gracefully.
|
|
||||||
LOG.warn("Error occurred when updating LSP code actions due to concurrent modifications.", e);
|
|
||||||
} catch (Throwable t) {
|
|
||||||
LOG.warn("Error occurred when updating LSP code actions.", t);
|
|
||||||
}
|
|
||||||
} else if (eventManager.isCodeActionSyncRequired()) {
|
|
||||||
try {
|
|
||||||
updateAnnotations(holder, eventManager);
|
|
||||||
} catch (ConcurrentModificationException e) {
|
|
||||||
// Todo - Add proper fix to handle concurrent modifications gracefully.
|
|
||||||
LOG.warn("Error occurred when updating LSP diagnostics due to concurrent modifications.", e);
|
|
||||||
} catch (Throwable t) {
|
|
||||||
LOG.warn("Error occurred when updating LSP diagnostics.", t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateAnnotations(AnnotationHolder holder, EditorEventManager eventManager) {
|
|
||||||
final List<Annotation> annotations = eventManager.getAnnotations();
|
|
||||||
if (annotations == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var requests = eventManager.fetchQuickFixes();
|
|
||||||
annotations.forEach(annotation -> {
|
|
||||||
if (annotation.getQuickFixes() != null && !annotation.getQuickFixes().isEmpty()) {
|
|
||||||
AnnotationBuilder builder = holder.newAnnotation(annotation.getSeverity(), annotation.getMessage());
|
|
||||||
builder = builder.range(TextRange.create(annotation.getStartOffset(), annotation.getEndOffset()));
|
|
||||||
for (Annotation.QuickFixInfo quickFixInfo : annotation.getQuickFixes()) {
|
|
||||||
builder = builder.newFix(quickFixInfo.quickFix).range(quickFixInfo.textRange).key(quickFixInfo.key).registerFix();
|
|
||||||
}
|
|
||||||
builder.create();
|
|
||||||
} else if (requests.containsKey(annotation)) {
|
|
||||||
AnnotationBuilder builder = holder.newAnnotation(annotation.getSeverity(), annotation.getMessage());
|
|
||||||
builder = builder.range(TextRange.create(annotation.getStartOffset(), annotation.getEndOffset()));
|
|
||||||
var request = requests.remove(annotation);
|
|
||||||
for (var quickFixInfo: request) {
|
|
||||||
builder = builder.newFix(quickFixInfo.action()).range(quickFixInfo.range()).registerFix();
|
|
||||||
}
|
|
||||||
builder.create();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
protected TextRange getTextRange(Editor editor, Diagnostic diagnostic) {
|
|
||||||
final int start = DocumentUtils.LSPPosToOffset(editor, diagnostic.getRange().getStart());
|
|
||||||
final int end = DocumentUtils.LSPPosToOffset(editor, diagnostic.getRange().getEnd());
|
|
||||||
if (start > end) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new TextRange(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
protected AnnotationBuilder createAnnotation(@NotNull TextRange range, AnnotationHolder holder, Diagnostic diagnostic) {
|
|
||||||
return holder.newAnnotation(lspToIntellijAnnotationsMap.get(diagnostic.getSeverity()), diagnostic.getMessage())
|
|
||||||
.range(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createAnnotations(AnnotationHolder holder, EditorEventManager eventManager) {
|
|
||||||
final List<Diagnostic> diagnostics = eventManager.getDiagnostics();
|
|
||||||
final Editor editor = eventManager.editor;
|
|
||||||
PsiFile file;
|
|
||||||
val document = editor.getDocument();
|
|
||||||
if (document != null) {
|
|
||||||
file = PsiDocumentManager.getInstance(eventManager.getProject()).getPsiFile(document);
|
|
||||||
} else {
|
|
||||||
file = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Annotation> annotations = new ArrayList<>();
|
|
||||||
diagnostics.forEach(d -> {
|
|
||||||
var range = getTextRange(editor, d);
|
|
||||||
if (range == null)
|
|
||||||
return;
|
|
||||||
blk:
|
|
||||||
if (file != null && range.getLength() == 0) {
|
|
||||||
val psiElement = file.findElementAt(range.getStartOffset());
|
|
||||||
if (psiElement != null && !psiElement.getText().equals(".")) {
|
|
||||||
range = psiElement.getTextRange();
|
|
||||||
break blk;
|
|
||||||
}
|
|
||||||
val psiElementForward = file.findElementAt(range.getStartOffset() + 1);
|
|
||||||
if (psiElementForward != null) {
|
|
||||||
range = psiElementForward.getTextRange();
|
|
||||||
break blk;
|
|
||||||
}
|
|
||||||
val psiElementBack = file.findElementAt(range.getStartOffset() - 1);
|
|
||||||
if (psiElementBack != null) {
|
|
||||||
range = psiElementBack.getTextRange();
|
|
||||||
break blk;
|
|
||||||
}
|
|
||||||
if (psiElement != null) {
|
|
||||||
range = psiElement.getTextRange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var annotation = createAnnotation(range, holder, d);
|
|
||||||
if (d.getTags() != null && d.getTags().contains(DiagnosticTag.Deprecated)) {
|
|
||||||
annotation = annotation.highlightType(ProblemHighlightType.LIKE_DEPRECATED);
|
|
||||||
}
|
|
||||||
annotation.create();
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
var theList = (SmartList<Annotation>) holder;
|
|
||||||
annotations.add(theList.get(theList.size() - 1));
|
|
||||||
});
|
|
||||||
|
|
||||||
eventManager.setAnnotations(annotations);
|
|
||||||
eventManager.setAnonHolder(holder);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.fixes;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.WorkspaceEditHandler;
|
|
||||||
import com.intellij.codeInsight.intention.IntentionAction;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import org.eclipse.lsp4j.CodeAction;
|
|
||||||
import org.jetbrains.annotations.Nls;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public class LSPCodeActionFix implements IntentionAction {
|
|
||||||
|
|
||||||
private final String uri;
|
|
||||||
private final CodeAction codeAction;
|
|
||||||
|
|
||||||
public LSPCodeActionFix(String uri, @NotNull CodeAction codeAction) {
|
|
||||||
this.uri = uri;
|
|
||||||
this.codeAction = codeAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nls(capitalization = Nls.Capitalization.Sentence)
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public String getText() {
|
|
||||||
return codeAction.getTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nls
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public String getFamilyName() {
|
|
||||||
return "LSP Fixes";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile psiFile) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) {
|
|
||||||
if (codeAction.getEdit() != null) {
|
|
||||||
WorkspaceEditHandler.applyEdit(codeAction.getEdit(), codeAction.getTitle());
|
|
||||||
}
|
|
||||||
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (manager != null) {
|
|
||||||
manager.executeCommands(Collections.singletonList(codeAction.getCommand()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean startInWriteAction() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.fixes;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.intellij.codeInsight.intention.IntentionAction;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import org.eclipse.lsp4j.Command;
|
|
||||||
import org.jetbrains.annotations.Nls;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import static java.util.Collections.singletonList;
|
|
||||||
|
|
||||||
public class LSPCommandFix implements IntentionAction {
|
|
||||||
|
|
||||||
private final String uri;
|
|
||||||
private final Command command;
|
|
||||||
|
|
||||||
public LSPCommandFix(String uri, @NotNull Command command) {
|
|
||||||
this.uri = uri;
|
|
||||||
this.command = command;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nls(capitalization = Nls.Capitalization.Sentence)
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public String getText() {
|
|
||||||
return command.getTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nls
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public String getFamilyName() {
|
|
||||||
return "LSP Fixes";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile psiFile) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) {
|
|
||||||
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (manager != null) {
|
|
||||||
manager.executeCommands(singletonList(command));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean startInWriteAction() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,119 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.icon;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
|
|
||||||
import com.intellij.icons.AllIcons;
|
|
||||||
import com.intellij.icons.AllIcons.Nodes;
|
|
||||||
import com.intellij.openapi.util.IconLoader;
|
|
||||||
import org.eclipse.lsp4j.CompletionItemKind;
|
|
||||||
import org.eclipse.lsp4j.SymbolKind;
|
|
||||||
|
|
||||||
import javax.swing.Icon;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class LSPDefaultIconProvider extends LSPIconProvider {
|
|
||||||
|
|
||||||
private final static Icon GREEN = IconLoader.getIcon("/images/started.svg", LSPDefaultIconProvider.class);
|
|
||||||
private final static Icon YELLOW = IconLoader.getIcon("/images/starting.svg", LSPDefaultIconProvider.class);
|
|
||||||
private final static Icon RED = IconLoader.getIcon("/images/stopped.svg", LSPDefaultIconProvider.class);
|
|
||||||
private final static Icon GREY = IconLoader.getIcon("/images/idle.svg", LSPDefaultIconProvider.class);
|
|
||||||
|
|
||||||
public Icon getCompletionIcon(CompletionItemKind kind) {
|
|
||||||
|
|
||||||
if (kind == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (kind) {
|
|
||||||
case Class:
|
|
||||||
return Nodes.Class;
|
|
||||||
case Enum:
|
|
||||||
return Nodes.Enum;
|
|
||||||
case Field:
|
|
||||||
return Nodes.Field;
|
|
||||||
case File:
|
|
||||||
return AllIcons.FileTypes.Any_type;
|
|
||||||
case Function:
|
|
||||||
return Nodes.Function;
|
|
||||||
case Interface:
|
|
||||||
return Nodes.Interface;
|
|
||||||
case Keyword:
|
|
||||||
return Nodes.UpLevel;
|
|
||||||
case Method:
|
|
||||||
return Nodes.Method;
|
|
||||||
case Module:
|
|
||||||
return Nodes.Module;
|
|
||||||
case Property:
|
|
||||||
return Nodes.Property;
|
|
||||||
case Reference:
|
|
||||||
return Nodes.MethodReference;
|
|
||||||
case Snippet:
|
|
||||||
return Nodes.Static;
|
|
||||||
case Text:
|
|
||||||
return AllIcons.FileTypes.Text;
|
|
||||||
case Unit:
|
|
||||||
return Nodes.Artifact;
|
|
||||||
case Variable:
|
|
||||||
return Nodes.Variable;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Icon getSymbolIcon(SymbolKind kind) {
|
|
||||||
|
|
||||||
if (kind == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (kind) {
|
|
||||||
case Field:
|
|
||||||
case EnumMember:
|
|
||||||
return Nodes.Field;
|
|
||||||
case Method:
|
|
||||||
return Nodes.Method;
|
|
||||||
case Variable:
|
|
||||||
return Nodes.Variable;
|
|
||||||
case Class:
|
|
||||||
return Nodes.Class;
|
|
||||||
case Constructor:
|
|
||||||
return Nodes.ClassInitializer;
|
|
||||||
case Enum:
|
|
||||||
return Nodes.Enum;
|
|
||||||
default:
|
|
||||||
return Nodes.Tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<ServerStatus, Icon> getStatusIcons() {
|
|
||||||
Map<ServerStatus, Icon> statusIconMap = new HashMap<>();
|
|
||||||
statusIconMap.put(ServerStatus.NONEXISTENT, GREY);
|
|
||||||
statusIconMap.put(ServerStatus.STOPPED, RED);
|
|
||||||
statusIconMap.put(ServerStatus.STARTING, YELLOW);
|
|
||||||
statusIconMap.put(ServerStatus.STARTED, YELLOW);
|
|
||||||
statusIconMap.put(ServerStatus.INITIALIZED, GREEN);
|
|
||||||
statusIconMap.put(ServerStatus.STOPPING, YELLOW);
|
|
||||||
return statusIconMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSpecificFor(LanguageServerDefinition serverDefinition) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.icon;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
|
|
||||||
import org.eclipse.lsp4j.CompletionItemKind;
|
|
||||||
import org.eclipse.lsp4j.SymbolInformation;
|
|
||||||
import org.eclipse.lsp4j.SymbolKind;
|
|
||||||
|
|
||||||
import javax.swing.Icon;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public abstract class LSPIconProvider {
|
|
||||||
|
|
||||||
public abstract Icon getCompletionIcon(CompletionItemKind kind);
|
|
||||||
|
|
||||||
public abstract Map<ServerStatus, Icon> getStatusIcons();
|
|
||||||
|
|
||||||
public abstract Icon getSymbolIcon(SymbolKind kind);
|
|
||||||
|
|
||||||
public abstract boolean isSpecificFor(LanguageServerDefinition serverDefinition);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return icon for symbol based on the symbol information. This method must only be used if you need more than the
|
|
||||||
* symbol kind to decide the icon which needs to be used. Otherwise always prefer implementing
|
|
||||||
* {@link #getSymbolIcon(SymbolKind)}.
|
|
||||||
*
|
|
||||||
* @return default implementation delegates to {@link #getSymbolIcon(SymbolKind)}.
|
|
||||||
*/
|
|
||||||
public Icon getSymbolIcon(SymbolInformation information) {
|
|
||||||
return getSymbolIcon(information.getKind());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.label;
|
|
||||||
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import org.eclipse.lsp4j.SymbolInformation;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
public class LSPDefaultLabelProvider implements LSPLabelProvider {
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public String symbolNameFor(@NotNull SymbolInformation symbolInformation, @NotNull Project project) {
|
|
||||||
return symbolInformation.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public String symbolLocationFor(@NotNull SymbolInformation symbolInformation, @NotNull Project project) {
|
|
||||||
return symbolInformation.getContainerName();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.label;
|
|
||||||
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import org.eclipse.lsp4j.SymbolInformation;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension to override the default Labels for Language Server elements such as symbols and completions.
|
|
||||||
*
|
|
||||||
* @author gayanper
|
|
||||||
*/
|
|
||||||
public interface LSPLabelProvider {
|
|
||||||
/**
|
|
||||||
* Generate the symbol name for the given {@link SymbolInformation}.
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
String symbolNameFor(@NotNull SymbolInformation symbolInformation, @NotNull Project project);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the symbol location for the given {@link SymbolInformation}.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
String symbolLocationFor(@NotNull SymbolInformation symbolInformation, @NotNull Project project);
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,756 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.psi;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.ApplicationUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.lang.ASTNode;
|
|
||||||
import com.intellij.lang.Language;
|
|
||||||
import com.intellij.navigation.ItemPresentation;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
|
||||||
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
|
|
||||||
import com.intellij.openapi.fileTypes.PlainTextLanguage;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.util.Key;
|
|
||||||
import com.intellij.openapi.util.KeyWithDefaultValue;
|
|
||||||
import com.intellij.openapi.util.TextRange;
|
|
||||||
import com.intellij.openapi.util.UserDataHolderBase;
|
|
||||||
import com.intellij.psi.ContributedReferenceHost;
|
|
||||||
import com.intellij.psi.FileViewProvider;
|
|
||||||
import com.intellij.psi.NavigatablePsiElement;
|
|
||||||
import com.intellij.psi.PsiDocumentManager;
|
|
||||||
import com.intellij.psi.PsiElement;
|
|
||||||
import com.intellij.psi.PsiElementVisitor;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import com.intellij.psi.PsiInvalidElementAccessException;
|
|
||||||
import com.intellij.psi.PsiManager;
|
|
||||||
import com.intellij.psi.PsiNameIdentifierOwner;
|
|
||||||
import com.intellij.psi.PsiNamedElement;
|
|
||||||
import com.intellij.psi.PsiPolyVariantReference;
|
|
||||||
import com.intellij.psi.PsiReference;
|
|
||||||
import com.intellij.psi.PsiReferenceService;
|
|
||||||
import com.intellij.psi.ResolveState;
|
|
||||||
import com.intellij.psi.impl.PsiElementBase;
|
|
||||||
import com.intellij.psi.scope.PsiScopeProcessor;
|
|
||||||
import com.intellij.psi.search.GlobalSearchScope;
|
|
||||||
import com.intellij.psi.search.SearchScope;
|
|
||||||
import com.intellij.util.IncorrectOperationException;
|
|
||||||
import com.intellij.util.concurrency.AtomicFieldUpdater;
|
|
||||||
import com.intellij.util.keyFMap.KeyFMap;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import javax.swing.Icon;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple PsiElement for LSP
|
|
||||||
*/
|
|
||||||
public class LSPPsiElement extends PsiElementBase implements PsiNameIdentifierOwner, NavigatablePsiElement {
|
|
||||||
|
|
||||||
private final Key<KeyFMap> COPYABLE_USER_MAP_KEY = Key.create("COPYABLE_USER_MAP_KEY");
|
|
||||||
private final AtomicFieldUpdater<LSPPsiElement, KeyFMap> updater = AtomicFieldUpdater.forFieldOfType(LSPPsiElement.class, KeyFMap.class);
|
|
||||||
private final PsiManager manager;
|
|
||||||
private final LSPPsiReference reference;
|
|
||||||
private final Project project;
|
|
||||||
private String name;
|
|
||||||
private final PsiFile file;
|
|
||||||
public final int start;
|
|
||||||
public final int end;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param name The name (text) of the element
|
|
||||||
* @param project The project it belongs to
|
|
||||||
* @param start The offset in the editor where the element starts
|
|
||||||
* @param end The offset where it ends
|
|
||||||
*/
|
|
||||||
public LSPPsiElement(String name, @NotNull Project project, int start, int end, PsiFile file) {
|
|
||||||
this.project = project;
|
|
||||||
this.name = name;
|
|
||||||
this.start = start;
|
|
||||||
this.end = end;
|
|
||||||
this.file = file;
|
|
||||||
manager = PsiManager.getInstance(project);
|
|
||||||
reference = new LSPPsiReference(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Concurrent writes to this field are via CASes only, using the {@link #updater}
|
|
||||||
*/
|
|
||||||
private volatile KeyFMap myUserMap = KeyFMap.EMPTY_MAP;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the language of the PSI element.
|
|
||||||
*
|
|
||||||
* @return the language instance.
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
public Language getLanguage() {
|
|
||||||
return PlainTextLanguage.INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the PSI manager for the project to which the PSI element belongs.
|
|
||||||
*
|
|
||||||
* @return the PSI manager instance.
|
|
||||||
*/
|
|
||||||
public PsiManager getManager() {
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the array of children for the PSI element. Important: In some implementations children are only composite
|
|
||||||
* elements, i.e. not a leaf elements
|
|
||||||
*
|
|
||||||
* @return the array of child elements.
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
public PsiElement[] getChildren() {
|
|
||||||
return new PsiElement[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the parent of the PSI element.
|
|
||||||
*
|
|
||||||
* @return the parent of the element, or null if the element has no parent.
|
|
||||||
*/
|
|
||||||
public PsiElement getParent() {
|
|
||||||
return getContainingFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the first child of the PSI element.
|
|
||||||
*
|
|
||||||
* @return the first child, or null if the element has no children.
|
|
||||||
*/
|
|
||||||
public PsiElement getFirstChild() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the last child of the PSI element.
|
|
||||||
*
|
|
||||||
* @return the last child, or null if the element has no children.
|
|
||||||
*/
|
|
||||||
public PsiElement getLastChild() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next sibling of the PSI element.
|
|
||||||
*
|
|
||||||
* @return the next sibling, or null if the node is the last in the list of siblings.
|
|
||||||
*/
|
|
||||||
public PsiElement getNextSibling() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the previous sibling of the PSI element.
|
|
||||||
*
|
|
||||||
* @return the previous sibling, or null if the node is the first in the list of siblings.
|
|
||||||
*/
|
|
||||||
public PsiElement getPrevSibling() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the text range in the document occupied by the PSI element.
|
|
||||||
*
|
|
||||||
* @return the text range.
|
|
||||||
*/
|
|
||||||
public TextRange getTextRange() {
|
|
||||||
return new TextRange(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the text offset of the PSI element relative to its parent.
|
|
||||||
*
|
|
||||||
* @return the relative offset.
|
|
||||||
*/
|
|
||||||
public int getStartOffsetInParent() {
|
|
||||||
return start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the length of text of the PSI element.
|
|
||||||
*
|
|
||||||
* @return the text length.
|
|
||||||
*/
|
|
||||||
public int getTextLength() {
|
|
||||||
return end - start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a leaf PSI element at the specified offset from the start of the text range of this node.
|
|
||||||
*
|
|
||||||
* @param offset the relative offset for which the PSI element is requested.
|
|
||||||
* @return the element at the offset, or null if none is found.
|
|
||||||
*/
|
|
||||||
public PsiElement findElementAt(int offset) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a reference at the specified offset from the start of the text range of this node.
|
|
||||||
*
|
|
||||||
* @param offset the relative offset for which the reference is requested.
|
|
||||||
* @return the reference at the offset, or null if none is found.
|
|
||||||
*/
|
|
||||||
public PsiReference findReferenceAt(int offset) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the text of the PSI element as a character array.
|
|
||||||
*
|
|
||||||
* @return the element text as a character array.
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
public char[] textToCharArray() {
|
|
||||||
return name.toCharArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the PSI element which should be used as a navigation target when navigation to this PSI element is
|
|
||||||
* requested. The method can either return {@code this} or substitute a different element if this element does not
|
|
||||||
* have an associated file and offset. (For example, if the source code of a library is attached to a project, the
|
|
||||||
* navigation element for a compiled library class is its source class.)
|
|
||||||
*
|
|
||||||
* @return the navigation target element.
|
|
||||||
*/
|
|
||||||
public PsiElement getNavigationElement() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the PSI element which corresponds to this element and belongs to either the project source path or class
|
|
||||||
* path. The method can either return {@code this} or substitute a different element if this element does not belong
|
|
||||||
* to the source path or class path. (For example, the original element for a library source file is the
|
|
||||||
* corresponding compiled class file.)
|
|
||||||
*
|
|
||||||
* @return the original element.
|
|
||||||
*/
|
|
||||||
public PsiElement getOriginalElement() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the text of this PSI element is equal to the specified character sequence.
|
|
||||||
*
|
|
||||||
* @param text the character sequence to compare with.
|
|
||||||
* @return true if the text is equal, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean textMatches(@NotNull CharSequence text) {
|
|
||||||
return getText() == text;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Q: get rid of these methods?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the text of this PSI element is equal to the text of the specified PSI element.
|
|
||||||
*
|
|
||||||
* @param element the element to compare the text with.
|
|
||||||
* @return true if the text is equal, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean textMatches(PsiElement element) {
|
|
||||||
return getText().equals(element.getText());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the text of this element contains the specified character.
|
|
||||||
*
|
|
||||||
* @param c the character to search for.
|
|
||||||
* @return true if the character is found, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean textContains(char c) {
|
|
||||||
return getText().indexOf(c) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the text of the PSI element.
|
|
||||||
*
|
|
||||||
* @return the element text.
|
|
||||||
*/
|
|
||||||
public String getText() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Passes the element to the specified visitor.
|
|
||||||
*
|
|
||||||
* @param visitor the visitor to pass the element to.
|
|
||||||
*/
|
|
||||||
public void accept(PsiElementVisitor visitor) {
|
|
||||||
visitor.visitElement(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Passes the children of the element to the specified visitor.
|
|
||||||
*
|
|
||||||
* @param visitor the visitor to pass the children to.
|
|
||||||
*/
|
|
||||||
public void acceptChildren(@NotNull PsiElementVisitor visitor) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a copy of the file containing the PSI element and returns the corresponding element in the created copy.
|
|
||||||
* Resolve operations performed on elements in the copy of the file will resolve to elements in the copy, not in the
|
|
||||||
* original file.
|
|
||||||
*
|
|
||||||
* @return the element in the file copy corresponding to this element.
|
|
||||||
*/
|
|
||||||
public PsiElement copy() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a child to this PSI element.
|
|
||||||
*
|
|
||||||
* @param element the child element to add.
|
|
||||||
* @return the element which was actually added (either { @code element} or its copy).
|
|
||||||
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
|
|
||||||
*/
|
|
||||||
public PsiElement add(@NotNull PsiElement element) {
|
|
||||||
throw new IncorrectOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a child to this PSI element, before the specified anchor element.
|
|
||||||
*
|
|
||||||
* @param element the child element to add.
|
|
||||||
* @param anchor the anchor before which the child element is inserted (must be a child of this PSI element)
|
|
||||||
* @return the element which was actually added (either { @code element} or its copy).
|
|
||||||
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
|
|
||||||
*/
|
|
||||||
public PsiElement addBefore(@NotNull PsiElement element, PsiElement anchor) {
|
|
||||||
throw new IncorrectOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a child to this PSI element, after the specified anchor element.
|
|
||||||
*
|
|
||||||
* @param element the child element to add.
|
|
||||||
* @param anchor the anchor after which the child element is inserted (must be a child of this PSI element)
|
|
||||||
* @return the element which was actually added (either { @code element} or its copy).
|
|
||||||
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
|
|
||||||
*/
|
|
||||||
public PsiElement addAfter(@NotNull PsiElement element, PsiElement anchor) {
|
|
||||||
throw new IncorrectOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a range of elements as children to this PSI element.
|
|
||||||
*
|
|
||||||
* @param first the first child element to add.
|
|
||||||
* @param last the last child element to add (must have the same parent as { @code first})
|
|
||||||
* @return the first child element which was actually added (either { @code first} or its copy).
|
|
||||||
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
|
|
||||||
*/
|
|
||||||
public PsiElement addRange(PsiElement first, PsiElement last) {
|
|
||||||
throw new IncorrectOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a range of elements as children to this PSI element, before the specified anchor element.
|
|
||||||
*
|
|
||||||
* @param first the first child element to add.
|
|
||||||
* @param last the last child element to add (must have the same parent as { @code first})
|
|
||||||
* @param anchor the anchor before which the child element is inserted (must be a child of this PSI element)
|
|
||||||
* @return the first child element which was actually added (either { @code first} or its copy).
|
|
||||||
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
|
|
||||||
*/
|
|
||||||
public PsiElement addRangeBefore(@NotNull PsiElement first, @NotNull PsiElement last, PsiElement anchor) {
|
|
||||||
throw new IncorrectOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a range of elements as children to this PSI element, after the specified anchor element.
|
|
||||||
*
|
|
||||||
* @param first the first child element to add.
|
|
||||||
* @param last the last child element to add (must have the same parent as { @code first})
|
|
||||||
* @param anchor the anchor after which the child element is inserted (must be a child of this PSI element)
|
|
||||||
* @return the first child element which was actually added (either { @code first} or its copy).
|
|
||||||
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
|
|
||||||
*/
|
|
||||||
public PsiElement addRangeAfter(PsiElement first, PsiElement last, PsiElement anchor) {
|
|
||||||
throw new IncorrectOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes this PSI element from the tree.
|
|
||||||
*
|
|
||||||
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason (for
|
|
||||||
* example, the file containing the element is read-only).
|
|
||||||
*/
|
|
||||||
public void delete() {
|
|
||||||
throw new IncorrectOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a range of children of this PSI element from the tree.
|
|
||||||
*
|
|
||||||
* @param first the first child to delete (must be a child of this PSI element)
|
|
||||||
* @param last the last child to delete (must be a child of this PSI element)
|
|
||||||
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
|
|
||||||
*/
|
|
||||||
public void deleteChildRange(PsiElement first, PsiElement last) {
|
|
||||||
throw new IncorrectOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces this PSI element (along with all its children) with another element (along with the children).
|
|
||||||
*
|
|
||||||
* @param newElement the element to replace this element with.
|
|
||||||
* @return the element which was actually inserted in the tree (either { @code newElement} or its copy)
|
|
||||||
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
|
|
||||||
*/
|
|
||||||
public PsiElement replace(@NotNull PsiElement newElement) {
|
|
||||||
throw new IncorrectOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if this PSI element is valid. Valid elements and their hierarchy members can be accessed for reading and
|
|
||||||
* writing. Valid elements can still correspond to underlying documents whose text is different, when those
|
|
||||||
* documents have been changed and not yet committed
|
|
||||||
* ({@link PsiDocumentManager#commitDocument(com.intellij.openapi.editor.Document)}).
|
|
||||||
* (In this case an attempt to change PSI will result in an exception).
|
|
||||||
* <p>
|
|
||||||
* Any access to invalid elements results in {@link PsiInvalidElementAccessException}.
|
|
||||||
* <p>
|
|
||||||
* Once invalid, elements can't become valid again.
|
|
||||||
* <p>
|
|
||||||
* Elements become invalid in following cases:
|
|
||||||
* <ul>
|
|
||||||
* <li>They have been deleted via PSI operation ({@link #delete()})</li>
|
|
||||||
* <li>They have been deleted as a result of an incremental reparse (document commit)</li>
|
|
||||||
* <li>Their containing file has been changed externally, or renamed so that its PSI had to be rebuilt from
|
|
||||||
* scratch</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* @return true if the element is valid, false otherwise.
|
|
||||||
* @see com.intellij.psi.util.PsiUtilCore#ensureValid(PsiElement)
|
|
||||||
*/
|
|
||||||
public boolean isValid() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the contents of the element can be modified (if it belongs to a non-read-only source file.)
|
|
||||||
*
|
|
||||||
* @return true if the element can be modified, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean isWritable() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the reference from this PSI element to another PSI element (or elements), if one exists. If the element
|
|
||||||
* has multiple associated references (see {@link #getReferences()} for an example), returns the first associated
|
|
||||||
* reference.
|
|
||||||
*
|
|
||||||
* @return the reference instance, or null if the PSI element does not have any associated references.
|
|
||||||
* @see com.intellij.psi.search.searches.ReferencesSearch
|
|
||||||
*/
|
|
||||||
public PsiReference getReference() {
|
|
||||||
return reference;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all references from this PSI element to other PSI elements. An element can have multiple references when,
|
|
||||||
* for example, the element is a string literal containing multiple sub-strings which are valid full-qualified class
|
|
||||||
* names. If an element contains only one text fragment which acts as a reference but the reference has multiple
|
|
||||||
* possible targets, {@link PsiPolyVariantReference} should be used instead of returning multiple references.
|
|
||||||
* <p>
|
|
||||||
* Actually, it's preferable to call {@link PsiReferenceService#getReferences} instead as it allows adding
|
|
||||||
* references by plugins when the element implements {@link ContributedReferenceHost}.
|
|
||||||
*
|
|
||||||
* @return the array of references, or an empty array if the element has no associated references.
|
|
||||||
* @see PsiReferenceService#getReferences
|
|
||||||
* @see com.intellij.psi.search.searches.ReferencesSearch
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
public PsiReference[] getReferences() {
|
|
||||||
return new PsiReference[]{reference};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Passes the declarations contained in this PSI element and its children for processing to the specified scope
|
|
||||||
* processor.
|
|
||||||
*
|
|
||||||
* @param processor the processor receiving the declarations.
|
|
||||||
* @param lastParent the child of this element has been processed during the previous step of the tree up walk
|
|
||||||
* (declarations under this element do not need to be processed again)
|
|
||||||
* @param place the original element from which the tree up walk was initiated.
|
|
||||||
* @return true if the declaration processing should continue or false if it should be stopped.
|
|
||||||
*/
|
|
||||||
public boolean processDeclarations(@NotNull PsiScopeProcessor processor, @NotNull ResolveState state,
|
|
||||||
PsiElement lastParent,
|
|
||||||
@NotNull PsiElement place) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the element which should be used as the parent of this element in a tree up walk during a resolve
|
|
||||||
* operation. For most elements, this returns {@code getParent()}, but the context can be overridden for some
|
|
||||||
* elements like code fragments.
|
|
||||||
*
|
|
||||||
* @return the resolve context element.
|
|
||||||
*/
|
|
||||||
public PsiElement getContext() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if an actual source or class file corresponds to the element. Non-physical elements include, for example,
|
|
||||||
* PSI elements created for the watch expressions in the debugger. Non-physical elements do not generate tree change
|
|
||||||
* events. Also, {@link PsiDocumentManager#getDocument(PsiFile)} returns null for non-physical elements. Not to be
|
|
||||||
* confused with {@link FileViewProvider#isPhysical()}.
|
|
||||||
*
|
|
||||||
* @return true if the element is physical, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean isPhysical() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the scope in which the declarations for the references in this PSI element are searched.
|
|
||||||
*
|
|
||||||
* @return the resolve scope instance.
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
public GlobalSearchScope getResolveScope() {
|
|
||||||
return getContainingFile().getResolveScope();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the scope in which references to this element are searched.
|
|
||||||
*
|
|
||||||
* @return the search scope instance.
|
|
||||||
* @see { @link com.intellij.psi.search.PsiSearchHelper#getUseScope(PsiElement)}
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
public SearchScope getUseScope() {
|
|
||||||
return getContainingFile().getResolveScope();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the AST node corresponding to the element.
|
|
||||||
*
|
|
||||||
* @return the AST node instance.
|
|
||||||
*/
|
|
||||||
public ASTNode getNode() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* toString() should never be presented to the user.
|
|
||||||
*/
|
|
||||||
public String toString() {
|
|
||||||
return "Name : " + name + " at offset " + start + " to " + end + " in " + project;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method shouldn't be called by clients directly, because there are no guarantees of it being symmetric. It's
|
|
||||||
* called by {@link PsiManager#areElementsEquivalent(PsiElement, PsiElement)} internally, which clients should
|
|
||||||
* invoke instead.<p/>
|
|
||||||
* <p>
|
|
||||||
* Implementations of this method should return {@code true} if the parameter is resolve-equivalent to {@code this},
|
|
||||||
* i.e. it represents the same entity from the language perspective. See also {@link
|
|
||||||
* PsiManager#areElementsEquivalent(PsiElement, PsiElement)} documentation.
|
|
||||||
*/
|
|
||||||
public boolean isEquivalentTo(PsiElement another) {
|
|
||||||
return this == another;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Icon getIcon(int flags) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PsiElement getNameIdentifier() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PsiElement setName(@NotNull String name) {
|
|
||||||
this.name = name;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
|
|
||||||
boolean control = true;
|
|
||||||
while (control) {
|
|
||||||
KeyFMap map = getUserMap();
|
|
||||||
KeyFMap newMap = (value == null) ? map.minus(key) : map.plus(key, value);
|
|
||||||
if ((newMap.equalsByReference(map)) || changeUserMap(map, newMap)) {
|
|
||||||
control = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean changeUserMap(KeyFMap oldMap, KeyFMap newMap) {
|
|
||||||
return updater.compareAndSet(this, oldMap, newMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected KeyFMap getUserMap() {
|
|
||||||
return myUserMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T getCopyableUserData(Key<T> key) {
|
|
||||||
KeyFMap map = getUserData(COPYABLE_USER_MAP_KEY);
|
|
||||||
return (map == null) ? null : map.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T getUserData(@NotNull Key<T> key) {
|
|
||||||
T t = getUserMap().get(key);
|
|
||||||
if (t == null && key instanceof KeyWithDefaultValue) {
|
|
||||||
KeyWithDefaultValue<T> key1 = (KeyWithDefaultValue<T>) key;
|
|
||||||
t = putUserDataIfAbsent(key, key1.getDefaultValue());
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T putUserDataIfAbsent(Key<T> key, T value) {
|
|
||||||
while (true) {
|
|
||||||
KeyFMap map = getUserMap();
|
|
||||||
T oldValue = map.get(key);
|
|
||||||
if (oldValue != null) {
|
|
||||||
return oldValue;
|
|
||||||
}
|
|
||||||
KeyFMap newMap = map.plus(key, value);
|
|
||||||
if ((newMap.equalsByReference(map)) || changeUserMap(map, newMap)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> void putCopyableUserData(Key<T> key, T value) {
|
|
||||||
boolean control = true;
|
|
||||||
while (control) {
|
|
||||||
KeyFMap map = getUserMap();
|
|
||||||
KeyFMap copyableMap = map.get(COPYABLE_USER_MAP_KEY);
|
|
||||||
if (copyableMap == null)
|
|
||||||
copyableMap = KeyFMap.EMPTY_MAP;
|
|
||||||
KeyFMap newCopyableMap = (value == null) ? copyableMap.minus(key) : copyableMap.plus(key, value);
|
|
||||||
KeyFMap newMap = (newCopyableMap.isEmpty()) ?
|
|
||||||
map.minus(COPYABLE_USER_MAP_KEY) :
|
|
||||||
map.plus(COPYABLE_USER_MAP_KEY, newCopyableMap);
|
|
||||||
if ((newMap.equalsByReference(map)) || changeUserMap(map, newMap))
|
|
||||||
control = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> boolean replace(Key<T> key, @Nullable T oldValue, @Nullable T newValue) {
|
|
||||||
while (true) {
|
|
||||||
KeyFMap map = getUserMap();
|
|
||||||
if (map.get(key) != oldValue) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
KeyFMap newMap = (newValue == null) ? map.minus(key) : map.plus(key, newValue);
|
|
||||||
if ((newMap == map) || changeUserMap(map, newMap)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void copyCopyableDataTo(UserDataHolderBase clone) {
|
|
||||||
clone.putUserData(COPYABLE_USER_MAP_KEY, getUserData(COPYABLE_USER_MAP_KEY));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isUserDataEmpty() {
|
|
||||||
return getUserMap().isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ItemPresentation getPresentation() {
|
|
||||||
return new ItemPresentation() {
|
|
||||||
public String getPresentableText() {
|
|
||||||
return getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLocationString() {
|
|
||||||
return getContainingFile().getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Icon getIcon(boolean unused) {
|
|
||||||
return (unused) ? null : null; //iconProvider.getIcon(LSPPsiElement.this)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void navigate(boolean requestFocus) {
|
|
||||||
Editor editor = FileUtils.editorFromPsiFile(getContainingFile());
|
|
||||||
if (editor == null) {
|
|
||||||
OpenFileDescriptor descriptor = new OpenFileDescriptor(getProject(), getContainingFile().getVirtualFile(),
|
|
||||||
getTextOffset());
|
|
||||||
ApplicationUtil.invokeLater(() -> ApplicationUtil
|
|
||||||
.writeAction(() -> FileEditorManager.getInstance(getProject()).openTextEditor(descriptor, false)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the file containing the PSI element.
|
|
||||||
*
|
|
||||||
* @return the file instance, or null if the PSI element is not contained in a file (for example, the element
|
|
||||||
* represents a package or directory).
|
|
||||||
* @throws PsiInvalidElementAccessException if this element is invalid
|
|
||||||
*/
|
|
||||||
public PsiFile getContainingFile() {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the project to which the PSI element belongs.
|
|
||||||
*
|
|
||||||
* @return the project instance.
|
|
||||||
* @throws PsiInvalidElementAccessException if this element is invalid
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
public Project getProject() {
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the offset in the file to which the caret should be placed when performing the navigation to the element.
|
|
||||||
* (For classes implementing {@link PsiNamedElement}, this should return the offset in the file of the name
|
|
||||||
* identifier.)
|
|
||||||
*
|
|
||||||
* @return the offset of the PSI element.
|
|
||||||
*/
|
|
||||||
public int getTextOffset() {
|
|
||||||
return start;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean canNavigateToSource() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean canNavigate() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void clearUserData() {
|
|
||||||
setUserMap(KeyFMap.EMPTY_MAP);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setUserMap(KeyFMap map) {
|
|
||||||
myUserMap = map;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.psi;
|
|
||||||
|
|
||||||
import com.intellij.openapi.util.TextRange;
|
|
||||||
import com.intellij.psi.PsiElement;
|
|
||||||
import com.intellij.psi.PsiPolyVariantReference;
|
|
||||||
import com.intellij.psi.PsiReference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple PsiReference for LSP
|
|
||||||
*/
|
|
||||||
public class LSPPsiReference implements PsiReference {
|
|
||||||
|
|
||||||
private PsiElement element;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple PsiReference for LSP.
|
|
||||||
*
|
|
||||||
* @param element The corresponding PsiElement
|
|
||||||
*/
|
|
||||||
public LSPPsiReference(PsiElement element) {
|
|
||||||
this.element = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the underlying (referencing) element of the reference.
|
|
||||||
*
|
|
||||||
* @return the underlying element of the reference.
|
|
||||||
*/
|
|
||||||
public PsiElement getElement() {
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the part of the underlying element which serves as a reference, or the complete text range of the element
|
|
||||||
* if the entire element is a reference.
|
|
||||||
*
|
|
||||||
* @return Relative range in element
|
|
||||||
*/
|
|
||||||
public TextRange getRangeInElement() {
|
|
||||||
return new TextRange(0, element.getTextLength());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the element which is the target of the reference.
|
|
||||||
*
|
|
||||||
* @return the target element, or null if it was not possible to resolve the reference to a valid target.
|
|
||||||
* @see PsiPolyVariantReference#multiResolve(boolean)
|
|
||||||
*/
|
|
||||||
public PsiElement resolve() {
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of the reference target element which does not depend on import statements and other context
|
|
||||||
* (for example, the full-qualified name of the class if the reference targets a Java class).
|
|
||||||
*
|
|
||||||
* @return the canonical text of the reference.
|
|
||||||
*/
|
|
||||||
public String getCanonicalText() {
|
|
||||||
return element.getText();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the reference target element has been renamed, in order to change the reference text according to the
|
|
||||||
* new name.
|
|
||||||
*
|
|
||||||
* @param newElementName the new name of the target element.
|
|
||||||
* @return the new underlying element of the reference.
|
|
||||||
* @throws IncorrectOperationException if the rename cannot be handled for some reason.
|
|
||||||
*/
|
|
||||||
public PsiElement handleElementRename(String newElementName) {
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the reference so that it starts to point to the specified element. This is called, for example, by the
|
|
||||||
* "Create Class from New" quickfix, to bind the (invalid) reference on which the quickfix was called to the newly
|
|
||||||
* created class.
|
|
||||||
*
|
|
||||||
* @param element the element which should become the target of the reference.
|
|
||||||
* @return the new underlying element of the reference.
|
|
||||||
* @throws IncorrectOperationException if the rebind cannot be handled for some reason.
|
|
||||||
*/
|
|
||||||
public PsiElement bindToElement(PsiElement element) {
|
|
||||||
this.element = element;
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the reference targets the specified element.
|
|
||||||
*
|
|
||||||
* @param element the element to check target for.
|
|
||||||
* @return true if the reference targets that element, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean isReferenceTo(PsiElement element) {
|
|
||||||
return this.element == element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the array of String, {@link PsiElement} and/or {@link LookupElement} instances representing all
|
|
||||||
* identifiers that are visible at the location of the reference. The contents of the returned array is used to
|
|
||||||
* build the lookup list for basic code completion. (The list of visible identifiers may not be filtered by the
|
|
||||||
* completion prefix string - the filtering is performed later by IDEA core.)
|
|
||||||
*
|
|
||||||
* @return the array of available identifiers.
|
|
||||||
*/
|
|
||||||
public Object[] getVariants() {
|
|
||||||
return new Object[] {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns false if the underlying element is guaranteed to be a reference, or true if the underlying element is a
|
|
||||||
* possible reference which should not be reported as an error if it fails to resolve. For example, a text in an XML
|
|
||||||
* file which looks like a full-qualified Java class name is a soft reference.
|
|
||||||
*
|
|
||||||
* @return true if the reference is soft, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean isSoft() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.rename;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.util.Pair;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.psi.PsiElement;
|
|
||||||
import com.intellij.psi.PsiNamedElement;
|
|
||||||
import com.intellij.psi.PsiReference;
|
|
||||||
import com.intellij.psi.search.SearchScope;
|
|
||||||
import com.intellij.refactoring.rename.inplace.MemberInplaceRenamer;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The LSP based in-place renaming implementation.
|
|
||||||
*/
|
|
||||||
public class LSPInplaceRenamer extends MemberInplaceRenamer {
|
|
||||||
|
|
||||||
private Editor editor;
|
|
||||||
|
|
||||||
LSPInplaceRenamer(@NotNull PsiNamedElement elementToRename, PsiElement substituted, Editor editor) {
|
|
||||||
super(elementToRename, substituted, editor, elementToRename.getName(), elementToRename.getName());
|
|
||||||
this.editor = editor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<PsiReference> collectRefs(SearchScope referencesSearchScope) {
|
|
||||||
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (eventManager != null) {
|
|
||||||
Pair<List<PsiElement>, List<VirtualFile>> results = eventManager
|
|
||||||
.referencesForRename(editor.getCaretModel().getCurrentCaret().getOffset(), true, false);
|
|
||||||
List<PsiElement> references = results.getFirst();
|
|
||||||
List<VirtualFile> toClose = results.getSecond();
|
|
||||||
LSPRenameProcessor.addEditors(toClose);
|
|
||||||
return references.stream().map(PsiElement::getReference).collect(Collectors.toList());
|
|
||||||
} else {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,177 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.rename;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.psi.LSPPsiElement;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.intellij.codeInsight.template.impl.TemplateManagerImpl;
|
|
||||||
import com.intellij.codeInsight.template.impl.TemplateState;
|
|
||||||
import com.intellij.openapi.actionSystem.CommonDataKeys;
|
|
||||||
import com.intellij.openapi.actionSystem.DataContext;
|
|
||||||
import com.intellij.openapi.command.impl.StartMarkAction;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.ui.Messages;
|
|
||||||
import com.intellij.openapi.ui.NonEmptyInputValidator;
|
|
||||||
import com.intellij.openapi.util.Pair;
|
|
||||||
import com.intellij.openapi.util.Pass;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.psi.PsiElement;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import com.intellij.psi.PsiNameIdentifierOwner;
|
|
||||||
import com.intellij.psi.PsiNamedElement;
|
|
||||||
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageEditorUtil;
|
|
||||||
import com.intellij.refactoring.rename.PsiElementRenameHandler;
|
|
||||||
import com.intellij.refactoring.rename.RenameHandler;
|
|
||||||
import com.intellij.refactoring.rename.RenamePsiElementProcessor;
|
|
||||||
import com.intellij.refactoring.rename.inplace.InplaceRefactoring;
|
|
||||||
import com.intellij.refactoring.rename.inplace.MemberInplaceRenameHandler;
|
|
||||||
import com.intellij.refactoring.rename.inplace.MemberInplaceRenamer;
|
|
||||||
import lombok.val;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static com.intellij.openapi.command.impl.StartMarkAction.START_MARK_ACTION_KEY;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The LSP based rename handler implementation.
|
|
||||||
*/
|
|
||||||
public class LSPRenameHandler implements RenameHandler {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) {
|
|
||||||
val editor = dataContext.getData(CommonDataKeys.EDITOR);
|
|
||||||
if (editor == null)
|
|
||||||
return;
|
|
||||||
if (elements.length == 1) {
|
|
||||||
new MemberInplaceRenameHandler()
|
|
||||||
.doRename(elements[0], editor, dataContext);
|
|
||||||
} else {
|
|
||||||
invoke(project, editor, dataContext.getData(CommonDataKeys.PSI_FILE),
|
|
||||||
dataContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void invoke(@NotNull Project project, Editor editor, PsiFile file, DataContext dataContext) {
|
|
||||||
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (manager != null) {
|
|
||||||
LSPPsiElement psiElement = getElementAtOffset(manager,
|
|
||||||
editor.getCaretModel().getCurrentCaret().getOffset());
|
|
||||||
if (psiElement != null) {
|
|
||||||
doRename(psiElement, editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private InplaceRefactoring doRename(PsiElement elementToRename, Editor editor) {
|
|
||||||
if (elementToRename instanceof PsiNameIdentifierOwner) {
|
|
||||||
RenamePsiElementProcessor processor = RenamePsiElementProcessor.forElement(elementToRename);
|
|
||||||
if (processor.isInplaceRenameSupported()) {
|
|
||||||
StartMarkAction startMarkAction = editor.getUserData(START_MARK_ACTION_KEY);
|
|
||||||
if (startMarkAction == null || (processor.substituteElementToRename(elementToRename, editor)
|
|
||||||
== elementToRename)) {
|
|
||||||
processor.substituteElementToRename(elementToRename, editor, new Pass<PsiElement>() {
|
|
||||||
@Override
|
|
||||||
public void pass(PsiElement element) {
|
|
||||||
MemberInplaceRenamer renamer = createMemberRenamer(element,
|
|
||||||
(PsiNameIdentifierOwner) elementToRename, editor);
|
|
||||||
boolean startedRename = renamer.performInplaceRename();
|
|
||||||
if (!startedRename) {
|
|
||||||
performDialogRename(editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
InplaceRefactoring inplaceRefactoring = editor.getUserData(InplaceRefactoring.INPLACE_RENAMER);
|
|
||||||
if ((inplaceRefactoring instanceof MemberInplaceRenamer)) {
|
|
||||||
TemplateState templateState = TemplateManagerImpl
|
|
||||||
.getTemplateState(InjectedLanguageEditorUtil.getTopLevelEditor(editor));
|
|
||||||
if (templateState != null) {
|
|
||||||
templateState.gotoEnd(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
performDialogRename(editor);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRenaming(DataContext dataContext) {
|
|
||||||
return isAvailableOnDataContext(dataContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAvailableOnDataContext(DataContext dataContext) {
|
|
||||||
PsiElement element = PsiElementRenameHandler.getElement(dataContext);
|
|
||||||
Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
|
|
||||||
PsiFile file = CommonDataKeys.PSI_FILE.getData(dataContext);
|
|
||||||
|
|
||||||
return editor != null && file != null && isAvailable(element, editor, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAvailable(PsiElement psiElement, Editor editor, PsiFile psiFile) {
|
|
||||||
if (psiElement instanceof PsiFile || psiElement instanceof LSPPsiElement) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return IntellijLanguageClient.isExtensionSupported(psiFile.getVirtualFile());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MemberInplaceRenamer createMemberRenamer(PsiElement element, PsiNameIdentifierOwner elementToRename,
|
|
||||||
Editor editor) {
|
|
||||||
return new LSPInplaceRenamer((PsiNamedElement) element, elementToRename, editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void performDialogRename(Editor editor) {
|
|
||||||
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (manager != null) {
|
|
||||||
String renameTo = Messages.showInputDialog(
|
|
||||||
editor.getProject(), "Enter new name: ", "Rename", Messages.getQuestionIcon(), "",
|
|
||||||
new NonEmptyInputValidator());
|
|
||||||
if (renameTo != null && !renameTo.equals("")) {
|
|
||||||
manager.rename(renameTo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private LSPPsiElement getElementAtOffset(EditorEventManager eventManager, int offset) {
|
|
||||||
Pair<List<PsiElement>, List<VirtualFile>> refResponse = eventManager.references(offset, true, false);
|
|
||||||
List<PsiElement> refs = refResponse.getFirst();
|
|
||||||
if (refs == null || refs.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
PsiElement curElement = refs.stream()
|
|
||||||
.filter(e -> e.getTextRange().getStartOffset() <= offset && offset <= e.getTextRange().getEndOffset())
|
|
||||||
.findAny().orElse(null);
|
|
||||||
if (curElement != null) {
|
|
||||||
var newElement = new LSPPsiElement(curElement.getText(), curElement.getProject(),
|
|
||||||
curElement.getTextRange().getStartOffset(), curElement.getTextRange().getEndOffset(),
|
|
||||||
curElement.getContainingFile());
|
|
||||||
eventManager.renameCache = new Pair<>(new WeakReference<>(newElement), refResponse);
|
|
||||||
return newElement;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.rename;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.psi.LSPPsiElement;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.WorkspaceEditHandler;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.util.Pair;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.psi.PsiElement;
|
|
||||||
import com.intellij.psi.PsiReference;
|
|
||||||
import com.intellij.psi.search.SearchScope;
|
|
||||||
import com.intellij.refactoring.listeners.RefactoringElementListener;
|
|
||||||
import com.intellij.refactoring.rename.RenameDialog;
|
|
||||||
import com.intellij.refactoring.rename.RenamePsiElementProcessor;
|
|
||||||
import com.intellij.usageView.UsageInfo;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LSPRenameProcessor implementation.
|
|
||||||
*/
|
|
||||||
public class LSPRenameProcessor extends RenamePsiElementProcessor {
|
|
||||||
|
|
||||||
private PsiElement curElem;
|
|
||||||
private final Set<PsiElement> elements = new HashSet<>();
|
|
||||||
private static final Set<VirtualFile> openedEditors = new HashSet<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean canProcessElement(@NotNull PsiElement element) {
|
|
||||||
return element instanceof LSPPsiElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public RenameDialog createRenameDialog(@NotNull Project project, @NotNull PsiElement element,
|
|
||||||
PsiElement nameSuggestionContext, Editor editor) {
|
|
||||||
return super.createRenameDialog(project, curElem, nameSuggestionContext, editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public Collection<PsiReference> findReferences(@NotNull PsiElement element, @NotNull SearchScope searchScope,
|
|
||||||
boolean searchInCommentsAndStrings) {
|
|
||||||
if (element instanceof LSPPsiElement) {
|
|
||||||
if (elements.contains(element)) {
|
|
||||||
return elements.stream().map(PsiElement::getReference).filter(Objects::nonNull).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
EditorEventManager
|
|
||||||
manager = EditorEventManagerBase.forEditor(FileUtils.editorFromPsiFile(element.getContainingFile()));
|
|
||||||
if (manager != null) {
|
|
||||||
Pair<List<PsiElement>, List<VirtualFile>> refs = manager.referencesForRename(element, true, false);
|
|
||||||
if (refs.getFirst() != null && refs.getSecond() != null) {
|
|
||||||
addEditors(refs.getSecond());
|
|
||||||
return refs.getFirst().stream().map(PsiElement::getReference).filter(Objects::nonNull).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO may rename invalid elements
|
|
||||||
@Override
|
|
||||||
public void renameElement(@NotNull PsiElement element, @NotNull String newName, @NotNull UsageInfo[] usages,
|
|
||||||
RefactoringElementListener listener) {
|
|
||||||
WorkspaceEditHandler.applyEdit(element, newName, usages, listener, new ArrayList<>(openedEditors));
|
|
||||||
openedEditors.clear();
|
|
||||||
elements.clear();
|
|
||||||
curElem = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isInplaceRenameSupported() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void clearEditors() {
|
|
||||||
openedEditors.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Set<VirtualFile> getEditors() {
|
|
||||||
return openedEditors;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void addEditors(List<VirtualFile> toAdd) {
|
|
||||||
openedEditors.addAll(toAdd);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.symbol;
|
|
||||||
|
|
||||||
import com.intellij.navigation.ItemPresentation;
|
|
||||||
import com.intellij.navigation.NavigationItem;
|
|
||||||
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import javax.swing.Icon;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LSP implementation of NavigationItem for intellij
|
|
||||||
*
|
|
||||||
* @author gayanper
|
|
||||||
*/
|
|
||||||
public class LSPNavigationItem extends OpenFileDescriptor implements NavigationItem {
|
|
||||||
|
|
||||||
private ItemPresentation presentation;
|
|
||||||
|
|
||||||
LSPNavigationItem(String name, String location, Icon icon, @NotNull Project project, @NotNull VirtualFile file,
|
|
||||||
int logicalLine, int logicalColumn) {
|
|
||||||
super(project, file, logicalLine, logicalColumn);
|
|
||||||
presentation = new LSPItemPresentation(location, name, icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return presentation.getPresentableText();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public ItemPresentation getPresentation() {
|
|
||||||
return presentation;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if(obj instanceof LSPNavigationItem) {
|
|
||||||
LSPNavigationItem other = (LSPNavigationItem) obj;
|
|
||||||
return this.getLine() == other.getLine() && this.getColumn() == other.getColumn() &&
|
|
||||||
Objects.equals(this.getName(), other.getName());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(this.getLine(), this.getColumn(), this.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LSPItemPresentation implements ItemPresentation {
|
|
||||||
|
|
||||||
private String location;
|
|
||||||
private String presentableText;
|
|
||||||
private Icon icon;
|
|
||||||
|
|
||||||
LSPItemPresentation(String location, String presentableText, Icon icon) {
|
|
||||||
this.location = location;
|
|
||||||
this.presentableText = presentableText;
|
|
||||||
this.icon = icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public String getPresentableText() {
|
|
||||||
return presentableText;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public String getLocationString() {
|
|
||||||
return location;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public Icon getIcon(boolean unused) {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.symbol;
|
|
||||||
|
|
||||||
import com.intellij.ide.util.gotoByName.ChooseByNamePopup;
|
|
||||||
import com.intellij.navigation.ChooseByNameContributorEx;
|
|
||||||
import com.intellij.navigation.NavigationItem;
|
|
||||||
import com.intellij.psi.search.GlobalSearchScope;
|
|
||||||
import com.intellij.util.Processor;
|
|
||||||
import com.intellij.util.indexing.FindSymbolParameters;
|
|
||||||
import com.intellij.util.indexing.IdFilter;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The symbol provider implementation for LSP client.
|
|
||||||
*
|
|
||||||
* @author gayanper
|
|
||||||
*/
|
|
||||||
public class LSPSymbolContributor implements ChooseByNameContributorEx {
|
|
||||||
|
|
||||||
private final WorkspaceSymbolProvider workspaceSymbolProvider = new WorkspaceSymbolProvider();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void processNames(@NotNull Processor<? super String> processor, @NotNull GlobalSearchScope globalSearchScope, @Nullable IdFilter idFilter) {
|
|
||||||
String queryString = Optional.ofNullable(globalSearchScope.getProject())
|
|
||||||
.map(p -> p.getUserData(ChooseByNamePopup.CURRENT_SEARCH_PATTERN)).orElse("");
|
|
||||||
|
|
||||||
workspaceSymbolProvider.workspaceSymbols(queryString, globalSearchScope.getProject()).stream()
|
|
||||||
.filter(ni -> globalSearchScope.accept(ni.getFile()))
|
|
||||||
.map(NavigationItem::getName)
|
|
||||||
.forEach(processor::process);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void processElementsWithName(@NotNull String s, @NotNull Processor<? super NavigationItem> processor, @NotNull FindSymbolParameters findSymbolParameters) {
|
|
||||||
workspaceSymbolProvider.workspaceSymbols(s, findSymbolParameters.getProject()).stream()
|
|
||||||
.filter(ni -> findSymbolParameters.getSearchScope().accept(ni.getFile()))
|
|
||||||
.forEach(processor::process);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,167 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.contributors.symbol;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.FileUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPIconProvider;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.label.LSPLabelProvider;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.GUIUtils;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import org.eclipse.lsp4j.Location;
|
|
||||||
import org.eclipse.lsp4j.SymbolInformation;
|
|
||||||
import org.eclipse.lsp4j.SymbolTag;
|
|
||||||
import org.eclipse.lsp4j.WorkspaceSymbol;
|
|
||||||
import org.eclipse.lsp4j.WorkspaceSymbolParams;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The workspace symbole provider implementation based on LSP
|
|
||||||
*
|
|
||||||
* @author gayanper
|
|
||||||
*/
|
|
||||||
public class WorkspaceSymbolProvider {
|
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getInstance(WorkspaceSymbolProvider.class);
|
|
||||||
|
|
||||||
public List<LSPNavigationItem> workspaceSymbols(String name, Project project) {
|
|
||||||
final Set<LanguageServerWrapper> serverWrappers = IntellijLanguageClient
|
|
||||||
.getProjectToLanguageWrappers()
|
|
||||||
.getOrDefault(FileUtils.projectToUri(project), Collections.emptySet());
|
|
||||||
|
|
||||||
final WorkspaceSymbolParams symbolParams = new WorkspaceSymbolParams(name);
|
|
||||||
return serverWrappers.stream().filter(s -> s.getStatus() == ServerStatus.INITIALIZED)
|
|
||||||
.flatMap(server -> collectSymbol(server, server.getRequestManager(), symbolParams))
|
|
||||||
.map(s -> createNavigationItem(s, project)).filter(Objects::nonNull).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private LSPNavigationItem createNavigationItem(LSPSymbolResult result, Project project) {
|
|
||||||
final SymbolInformation information = (result.getSymbolInformation() != null) ?
|
|
||||||
result.getSymbolInformation() : from(result.getWorkspaceSymbol());
|
|
||||||
final Location location = information.getLocation();
|
|
||||||
final VirtualFile file = FileUtil.virtualFileFromURI(location.getUri());
|
|
||||||
|
|
||||||
if (file != null) {
|
|
||||||
final LSPIconProvider iconProviderFor = GUIUtils.getIconProviderFor(result.getDefinition());
|
|
||||||
final LSPLabelProvider labelProvider = GUIUtils.getLabelProviderFor(result.getDefinition());
|
|
||||||
return new LSPNavigationItem(labelProvider.symbolNameFor(information, project),
|
|
||||||
labelProvider.symbolLocationFor(information, project), iconProviderFor.getSymbolIcon(information),
|
|
||||||
project, file,
|
|
||||||
location.getRange().getStart().getLine(),
|
|
||||||
location.getRange().getStart().getCharacter());
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SymbolInformation from(WorkspaceSymbol symbol) {
|
|
||||||
SymbolInformation information = new SymbolInformation();
|
|
||||||
information.setContainerName(symbol.getContainerName());
|
|
||||||
information.setKind(symbol.getKind());
|
|
||||||
information.setName(symbol.getName());
|
|
||||||
if(symbol.getLocation().isLeft()) {
|
|
||||||
information.setLocation(symbol.getLocation().getLeft());
|
|
||||||
} else {
|
|
||||||
information.setLocation(new Location());
|
|
||||||
information.getLocation().setUri(symbol.getLocation().getRight().getUri());
|
|
||||||
}
|
|
||||||
information.setTags(symbol.getTags());
|
|
||||||
if(symbol.getTags() != null) {
|
|
||||||
information.setDeprecated(symbol.getTags().contains(SymbolTag.Deprecated));
|
|
||||||
}
|
|
||||||
return information;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("squid:S2142")
|
|
||||||
private Stream<LSPSymbolResult> collectSymbol(LanguageServerWrapper wrapper,
|
|
||||||
RequestManager requestManager,
|
|
||||||
WorkspaceSymbolParams symbolParams) {
|
|
||||||
final CompletableFuture<Either<List<? extends SymbolInformation>, List<? extends WorkspaceSymbol>>> request = requestManager
|
|
||||||
.symbol(symbolParams);
|
|
||||||
|
|
||||||
if (request == null) {
|
|
||||||
return Stream.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Either<List<? extends SymbolInformation>, List<? extends WorkspaceSymbol>> symbolInformations = request
|
|
||||||
.get(20000, TimeUnit.MILLISECONDS);
|
|
||||||
wrapper.notifySuccess(Timeouts.SYMBOLS);
|
|
||||||
if(symbolInformations.isLeft()) {
|
|
||||||
return symbolInformations.getLeft().stream().map(si -> new LSPSymbolResult(si, wrapper.getServerDefinition()));
|
|
||||||
} else if (symbolInformations.isRight()) {
|
|
||||||
return symbolInformations.getRight().stream().map(si -> new LSPSymbolResult(si, wrapper.getServerDefinition()));
|
|
||||||
}
|
|
||||||
} catch (TimeoutException e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
wrapper.notifyFailure(Timeouts.SYMBOLS);
|
|
||||||
} catch (ExecutionException | InterruptedException e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
wrapper.crashed(e);
|
|
||||||
}
|
|
||||||
return Stream.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class LSPSymbolResult {
|
|
||||||
|
|
||||||
private SymbolInformation symbolInformation;
|
|
||||||
private WorkspaceSymbol workspaceSymbol;
|
|
||||||
private LanguageServerDefinition definition;
|
|
||||||
|
|
||||||
public LSPSymbolResult(SymbolInformation symbolInformation,
|
|
||||||
LanguageServerDefinition definition) {
|
|
||||||
this.symbolInformation = symbolInformation;
|
|
||||||
this.definition = definition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LSPSymbolResult(WorkspaceSymbol workspaceSymbol,
|
|
||||||
LanguageServerDefinition definition) {
|
|
||||||
this.workspaceSymbol = workspaceSymbol;
|
|
||||||
this.definition = definition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SymbolInformation getSymbolInformation() {
|
|
||||||
return symbolInformation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LanguageServerDefinition getDefinition() {
|
|
||||||
return definition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public WorkspaceSymbol getWorkspaceSymbol() {
|
|
||||||
return workspaceSymbol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.editor;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.editor.markup.RangeHighlighter;
|
|
||||||
import org.eclipse.lsp4j.Location;
|
|
||||||
|
|
||||||
import java.awt.Cursor;
|
|
||||||
|
|
||||||
public class CtrlRangeMarker {
|
|
||||||
|
|
||||||
Location location;
|
|
||||||
private Editor editor;
|
|
||||||
private RangeHighlighter range;
|
|
||||||
|
|
||||||
CtrlRangeMarker(Location location, Editor editor, RangeHighlighter range) {
|
|
||||||
this.location = location;
|
|
||||||
this.editor = editor;
|
|
||||||
this.range = range;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean highlightContainsOffset(int offset) {
|
|
||||||
if (!isDefinition()) {
|
|
||||||
return range.getStartOffset() <= offset && range.getEndOffset() >= offset;
|
|
||||||
} else {
|
|
||||||
return definitionContainsOffset(offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean definitionContainsOffset(int offset) {
|
|
||||||
return DocumentUtils.LSPPosToOffset(editor, location.getRange().getStart()) <= offset && offset <= DocumentUtils
|
|
||||||
.LSPPosToOffset(editor, location.getRange().getEnd());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the highlighter and restores the default cursor
|
|
||||||
*/
|
|
||||||
void dispose() {
|
|
||||||
if (!isDefinition()) {
|
|
||||||
editor.getMarkupModel().removeHighlighter(range);
|
|
||||||
editor.getContentComponent().setCursor(Cursor.getDefaultCursor());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the marker points to the definition itself
|
|
||||||
*/
|
|
||||||
boolean isDefinition() {
|
|
||||||
return range == null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,149 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.editor;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.ApplicationUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Document;
|
|
||||||
import com.intellij.openapi.editor.event.DocumentEvent;
|
|
||||||
import com.intellij.openapi.editor.event.DocumentListener;
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
|
||||||
import com.intellij.openapi.util.io.FileUtilRt;
|
|
||||||
import lombok.val;
|
|
||||||
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentIdentifier;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentItem;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentSyncKind;
|
|
||||||
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class DocumentEventManager {
|
|
||||||
private final Document document;
|
|
||||||
private final DocumentListener documentListener;
|
|
||||||
private final TextDocumentSyncKind syncKind;
|
|
||||||
private final LanguageServerWrapper wrapper;
|
|
||||||
private final TextDocumentIdentifier identifier;
|
|
||||||
private int version = 0;
|
|
||||||
protected Logger LOG = Logger.getInstance(EditorEventManager.class);
|
|
||||||
|
|
||||||
private final Set<Document> openDocuments = new HashSet<>();
|
|
||||||
|
|
||||||
DocumentEventManager(Document document, DocumentListener documentListener, TextDocumentSyncKind syncKind, LanguageServerWrapper wrapper) {
|
|
||||||
this.document = document;
|
|
||||||
this.documentListener = documentListener;
|
|
||||||
this.syncKind = syncKind;
|
|
||||||
this.wrapper = wrapper;
|
|
||||||
this.identifier = new TextDocumentIdentifier(FileUtils.documentToUri(document));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeListeners() {
|
|
||||||
document.removeDocumentListener(documentListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void registerListeners() {
|
|
||||||
document.addDocumentListener(documentListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDocumentVersion() {
|
|
||||||
return this.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void documentChanged(DocumentEvent event) {
|
|
||||||
|
|
||||||
DidChangeTextDocumentParams changesParams = new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(),
|
|
||||||
Collections.singletonList(new TextDocumentContentChangeEvent()));
|
|
||||||
changesParams.getTextDocument().setUri(identifier.getUri());
|
|
||||||
changesParams.getTextDocument().setVersion(++version);
|
|
||||||
|
|
||||||
// TODO this incremental update logic is kinda broken, investigate later...
|
|
||||||
// if (syncKind == TextDocumentSyncKind.Incremental) {
|
|
||||||
// TextDocumentContentChangeEvent changeEvent = changesParams.getContentChanges().get(0);
|
|
||||||
// CharSequence newText = event.getNewFragment();
|
|
||||||
// int offset = event.getOffset();
|
|
||||||
// int newTextLength = event.getNewLength();
|
|
||||||
//
|
|
||||||
// EditorEventManager editorEventManager = EditorEventManagerBase.forUri(FileUtils.documentToUri(document));
|
|
||||||
// if (editorEventManager == null) {
|
|
||||||
// LOG.warn("no editor associated with document");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// Editor editor = editorEventManager.editor;
|
|
||||||
// Position lspPosition = DocumentUtils.offsetToLSPPos(editor, offset);
|
|
||||||
// if (lspPosition == null) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// int startLine = lspPosition.getLine();
|
|
||||||
// int startColumn = lspPosition.getCharacter();
|
|
||||||
// CharSequence oldText = event.getOldFragment();
|
|
||||||
//
|
|
||||||
// //if text was deleted/replaced, calculate the end position of inserted/deleted text
|
|
||||||
// int endLine, endColumn;
|
|
||||||
// if (oldText.length() > 0) {
|
|
||||||
// endLine = startLine + StringUtil.countNewLines(oldText);
|
|
||||||
// String content = oldText.toString();
|
|
||||||
// String[] oldLines = content.split("\n");
|
|
||||||
// int oldTextLength = oldLines.length == 0 ? 0 : oldLines[oldLines.length - 1].length();
|
|
||||||
// endColumn = content.endsWith("\n") ? 0 : oldLines.length == 1 ? startColumn + oldTextLength : oldTextLength;
|
|
||||||
// } else { //if insert or no text change, the end position is the same
|
|
||||||
// endLine = startLine;
|
|
||||||
// endColumn = startColumn;
|
|
||||||
// }
|
|
||||||
// Range range = new Range(new Position(startLine, startColumn), new Position(endLine, endColumn));
|
|
||||||
// changeEvent.setRange(range);
|
|
||||||
// changeEvent.setText(newText.toString());
|
|
||||||
// } else if (syncKind == TextDocumentSyncKind.Full) {
|
|
||||||
if (syncKind != TextDocumentSyncKind.None) {
|
|
||||||
changesParams.getContentChanges().get(0).setText(document.getText());
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
ApplicationUtil.pool(() -> wrapper.getRequestManager().didChange(changesParams));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void documentOpened() {
|
|
||||||
if (openDocuments.contains(document)) {
|
|
||||||
LOG.warn("trying to send open notification for document which was already opened!");
|
|
||||||
} else {
|
|
||||||
openDocuments.add(document);
|
|
||||||
val theFile = FileDocumentManager.getInstance().getFile(document);
|
|
||||||
if (theFile == null)
|
|
||||||
return;
|
|
||||||
final String extension = FileUtilRt.getExtension(theFile.getName());
|
|
||||||
wrapper.getRequestManager().didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(identifier.getUri(),
|
|
||||||
wrapper.serverDefinition.languageIdFor(extension),
|
|
||||||
++version,
|
|
||||||
document.getText())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void documentClosed() {
|
|
||||||
if (!openDocuments.contains(document)) {
|
|
||||||
LOG.warn("trying to close document which is not open");
|
|
||||||
} else if (EditorEventManagerBase.managersForUri(FileUtils.documentToUri(document)).size() > 1) {
|
|
||||||
LOG.warn("trying to close document which is still open in another editor!");
|
|
||||||
} else {
|
|
||||||
openDocuments.remove(document);
|
|
||||||
wrapper.getRequestManager().didClose(new DidCloseTextDocumentParams(identifier));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,183 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.editor;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.ApplicationUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import lombok.val;
|
|
||||||
import org.eclipse.lsp4j.Diagnostic;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.WeakHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
public class EditorEventManagerBase {
|
|
||||||
|
|
||||||
private static final Map<String, Set<EditorEventManager>> uriToManagers = new ConcurrentHashMap<>();
|
|
||||||
private static final Map<Editor, EditorEventManager> editorToManager = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
private static void prune() {
|
|
||||||
pruneEditorManagerMap();
|
|
||||||
pruneUriManagerMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void pruneUriManagerMap() {
|
|
||||||
synchronized (uriToManagers) {
|
|
||||||
new HashMap<>(uriToManagers).forEach((key, value) -> {
|
|
||||||
new HashSet<>(value).forEach((manager) -> {
|
|
||||||
if (!manager.wrapper.isActive()) {
|
|
||||||
uriToManagers.get(key).remove(manager);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (value.isEmpty()) {
|
|
||||||
uriToManagers.remove(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void pruneEditorManagerMap() {
|
|
||||||
new HashMap<>(editorToManager).forEach((key, value) -> {
|
|
||||||
if (!value.wrapper.isActive()) {
|
|
||||||
editorToManager.remove(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final WeakHashMap<Editor, List<Runnable>> runOnRegistry = new WeakHashMap<>();
|
|
||||||
|
|
||||||
public static void registerManager(EditorEventManager manager) {
|
|
||||||
String uri = FileUtils.editorToURIString(manager.editor);
|
|
||||||
synchronized (uriToManagers) {
|
|
||||||
if (uriToManagers.containsKey(uri)) {
|
|
||||||
uriToManagers.get(uri).add(manager);
|
|
||||||
} else {
|
|
||||||
HashSet<EditorEventManager> set = new HashSet<>();
|
|
||||||
set.add(manager);
|
|
||||||
uriToManagers.put(uri, set);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
synchronized (runOnRegistry) {
|
|
||||||
editorToManager.put(manager.editor, manager);
|
|
||||||
if (runOnRegistry.containsKey(manager.editor)) {
|
|
||||||
var tasks = runOnRegistry.remove(manager.editor);
|
|
||||||
for (var task: tasks) {
|
|
||||||
ApplicationUtil.invokeLater(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void runWhenManagerGetsRegistered(Editor editor, Runnable... runnables) {
|
|
||||||
synchronized (runOnRegistry) {
|
|
||||||
var manager = forEditor(editor);
|
|
||||||
if (manager != null) {
|
|
||||||
for (var task: runnables) {
|
|
||||||
ApplicationUtil.invokeLater(task);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
runOnRegistry.computeIfAbsent(editor, (ignored) -> new ArrayList<>()).addAll(List.of(runnables));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unregisterManager(EditorEventManager manager) {
|
|
||||||
editorToManager.remove(manager.editor);
|
|
||||||
|
|
||||||
String uri = FileUtils.editorToURIString(manager.editor);
|
|
||||||
synchronized (uriToManagers) {
|
|
||||||
val managers = uriToManagers.get(uri);
|
|
||||||
if (managers != null) {
|
|
||||||
managers.remove(manager);
|
|
||||||
if (managers.isEmpty()) {
|
|
||||||
uriToManagers.remove(uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param editor An editor
|
|
||||||
* @return The manager for the given editor, or None
|
|
||||||
*/
|
|
||||||
public static EditorEventManager forEditor(Editor editor) {
|
|
||||||
prune();
|
|
||||||
return editorToManager.get(editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the server that all the documents will be saved
|
|
||||||
*/
|
|
||||||
public static void willSaveAll() {
|
|
||||||
prune();
|
|
||||||
editorToManager.forEach((key, value) -> value.willSave());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void diagnostics(String uri, List<Diagnostic> diagnostics) {
|
|
||||||
getEditorEventManagerCopy(uri).forEach(manager -> {
|
|
||||||
manager.diagnostics(diagnostics);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<EditorEventManager> getEditorEventManagerCopy(String uri) {
|
|
||||||
HashSet<EditorEventManager> result = new HashSet<>();
|
|
||||||
synchronized (uriToManagers) {
|
|
||||||
Set<EditorEventManager> managers = uriToManagers.get(uri);
|
|
||||||
if(managers != null) {
|
|
||||||
result.addAll(managers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void willSave(String uri) {
|
|
||||||
EditorEventManager editorManager = forUri(uri);
|
|
||||||
if(editorManager != null) {
|
|
||||||
editorManager.willSave();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Set<EditorEventManager> managersForUri(String uri) {
|
|
||||||
return getEditorEventManagerCopy(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WARNING: avoid using this function! It only gives you one editorEventManager, not all and not the one of the current editor.
|
|
||||||
* Only use for operations which are file-level (save, open, close,...) otherwise use {@link #managersForUri(String)} or {@link #forEditor(Editor)}
|
|
||||||
*/
|
|
||||||
public static EditorEventManager forUri(String uri) {
|
|
||||||
Set<EditorEventManager> managers = managersForUri(uri);
|
|
||||||
if(managers.size() >= 1) {
|
|
||||||
return managers.iterator().next();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void documentSaved(String uri) {
|
|
||||||
EditorEventManager editorManager = forUri(uri);
|
|
||||||
if(editorManager != null){
|
|
||||||
editorManager.documentSaved();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.editor;
|
|
||||||
|
|
||||||
import com.intellij.codeInsight.intention.IntentionAction;
|
|
||||||
import com.intellij.openapi.util.TextRange;
|
|
||||||
|
|
||||||
public record QuickFixRequest(IntentionAction action, TextRange range) {
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.extensions;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.ClientContext;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.DefaultLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerOptions;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.DefaultRequestManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPDefaultIconProvider;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPIconProvider;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.label.LSPDefaultLabelProvider;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.label.LSPLabelProvider;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.listeners.LSPCaretListenerImpl;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.editor.event.DocumentListener;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import org.eclipse.lsp4j.ServerCapabilities;
|
|
||||||
import org.eclipse.lsp4j.services.LanguageClient;
|
|
||||||
import org.eclipse.lsp4j.services.LanguageServer;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public interface LSPExtensionManager {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LSP allows you to provide custom(language-specific) {@link RequestManager} implementations.
|
|
||||||
* Request manager implementation is required to be modified in such situations where,
|
|
||||||
*
|
|
||||||
* <ul>
|
|
||||||
* <li> Adding support for custom LSP requests/notifications which are not part of the standard LS protocol.</li>
|
|
||||||
* <li> Default handling process of LSP requests/notifications is required to be customized.
|
|
||||||
* </ul>
|
|
||||||
* <p>
|
|
||||||
* As a starting point you can extend
|
|
||||||
* {@link DefaultRequestManager}.
|
|
||||||
*/
|
|
||||||
RequestManager getExtendedRequestManagerFor(LanguageServerWrapper wrapper,
|
|
||||||
LanguageServer server,
|
|
||||||
LanguageClient client,
|
|
||||||
ServerCapabilities serverCapabilities);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LSP allows you to provide custom {@link EditorEventManager} implementations.
|
|
||||||
* Editor event manager implementation is required to be modified in such situations where,
|
|
||||||
*
|
|
||||||
* <ul>
|
|
||||||
* <li> Modifying/optimizing lsp features for custom requirements.
|
|
||||||
* </ul>
|
|
||||||
* <p>
|
|
||||||
* As a starting point you can extend
|
|
||||||
* {@link EditorEventManager}.
|
|
||||||
*/
|
|
||||||
EditorEventManager getExtendedEditorEventManagerFor(Editor editor,
|
|
||||||
DocumentListener documentListener,
|
|
||||||
LSPCaretListenerImpl caretListener,
|
|
||||||
RequestManager requestManager,
|
|
||||||
ServerOptions serverOptions,
|
|
||||||
LanguageServerWrapper wrapper);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LSP allows you to provide extended/custom {@link LanguageServer} interfaces, if required.
|
|
||||||
*/
|
|
||||||
Class<? extends LanguageServer> getExtendedServerInterface();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LSP allows you to provide custom(language-specific) {@link LanguageClient} implementations.
|
|
||||||
* Language client is required to be modified in such situations where,
|
|
||||||
* <ul>
|
|
||||||
* <li> Adding support for custom client notifications which are not part of the standard LS protocol.</li>
|
|
||||||
* <li> Default handling process of LSP client requests/notifications is required to be customized.
|
|
||||||
* </ul>
|
|
||||||
* <p>
|
|
||||||
* As a starting point you can extend
|
|
||||||
* {@link DefaultLanguageClient}.
|
|
||||||
*/
|
|
||||||
LanguageClient getExtendedClientFor(ClientContext context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The icon provider for the Language Server. Override and implement your own or extend the
|
|
||||||
* {@link LSPDefaultIconProvider} to customize the default icons.
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
default LSPIconProvider getIconProvider() {
|
|
||||||
return new LSPDefaultIconProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some language servers might only need to start for files which has a specific content. This method can be used
|
|
||||||
* in such situation to control whether the file must be connected to a language server which is registered for the
|
|
||||||
* extension of this file.
|
|
||||||
*
|
|
||||||
* <b>Note:</b> By default this method returns <code>true</code>
|
|
||||||
*
|
|
||||||
* @param file PsiFile which is about to connect to a language server.
|
|
||||||
* @return <code>true</code> if the file is supported.
|
|
||||||
*/
|
|
||||||
default boolean isFileContentSupported(@NotNull PsiFile file) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The label provider for the Language Server. Implement and override default behavior
|
|
||||||
* if it needs to be customize.
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
default LSPLabelProvider getLabelProvider() {
|
|
||||||
return new LSPDefaultLabelProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.listeners;
|
|
||||||
|
|
||||||
import com.intellij.openapi.editor.event.DocumentEvent;
|
|
||||||
import com.intellij.openapi.editor.event.DocumentListener;
|
|
||||||
|
|
||||||
public class DocumentListenerImpl extends LSPListener implements DocumentListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before the text of the document is changed.
|
|
||||||
*
|
|
||||||
* @param event the event containing the information about the change.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void beforeDocumentChange(DocumentEvent event) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after the text of the document has been changed.
|
|
||||||
*
|
|
||||||
* @param event the event containing the information about the change.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void documentChanged(DocumentEvent event) {
|
|
||||||
if (checkEnabled()) {
|
|
||||||
manager.documentChanged(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.listeners;
|
|
||||||
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.event.CaretEvent;
|
|
||||||
import com.intellij.openapi.editor.event.CaretListener;
|
|
||||||
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.ScheduledFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class LSPCaretListenerImpl extends LSPListener implements CaretListener {
|
|
||||||
|
|
||||||
private Logger LOG = Logger.getInstance(LSPCaretListenerImpl.class);
|
|
||||||
private ScheduledExecutorService scheduler;
|
|
||||||
private ScheduledFuture<?> scheduledFuture;
|
|
||||||
private static final long DEBOUNCE_INTERVAL_MS = 500;
|
|
||||||
|
|
||||||
public LSPCaretListenerImpl() {
|
|
||||||
scheduler = Executors.newScheduledThreadPool(1);
|
|
||||||
scheduledFuture = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void caretPositionChanged(CaretEvent e) {
|
|
||||||
try {
|
|
||||||
if (scheduledFuture != null && !scheduledFuture.isCancelled()) {
|
|
||||||
scheduledFuture.cancel(false);
|
|
||||||
}
|
|
||||||
scheduledFuture = scheduler.schedule(this::debouncedCaretPositionChanged, DEBOUNCE_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
|
||||||
} catch (Exception err) {
|
|
||||||
LOG.warn("Error occurred when trying to update code actions", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void debouncedCaretPositionChanged() {
|
|
||||||
if (checkEnabled()) {
|
|
||||||
manager.requestAndShowCodeActions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.listeners;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.intellij.openapi.editor.EditorKind;
|
|
||||||
import com.intellij.openapi.editor.event.EditorFactoryEvent;
|
|
||||||
import com.intellij.openapi.editor.event.EditorFactoryListener;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public class LSPEditorListener implements EditorFactoryListener {
|
|
||||||
|
|
||||||
public void editorReleased(@NotNull EditorFactoryEvent editorFactoryEvent) {
|
|
||||||
if(editorFactoryEvent.getEditor().getEditorKind() == EditorKind.MAIN_EDITOR){
|
|
||||||
IntellijLanguageClient.editorClosed(editorFactoryEvent.getEditor());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void editorCreated(@NotNull EditorFactoryEvent editorFactoryEvent) {
|
|
||||||
if(editorFactoryEvent.getEditor().getEditorKind() == EditorKind.MAIN_EDITOR) {
|
|
||||||
IntellijLanguageClient.editorOpened(editorFactoryEvent.getEditor());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.listeners;
|
|
||||||
|
|
||||||
import com.intellij.openapi.editor.Document;
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManagerListener;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public class LSPFileDocumentManagerListener implements FileDocumentManagerListener {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeDocumentSaving(@NotNull Document document) {
|
|
||||||
LSPFileEventManager.willSave(document);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unsavedDocumentsDropped() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeAllDocumentsSaving() {
|
|
||||||
LSPFileEventManager.willSaveAllDocuments();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeFileContentReload(VirtualFile virtualFile, @NotNull Document document) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void fileWithNoDocumentChanged(@NotNull VirtualFile virtualFile) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void fileContentReloaded(@NotNull VirtualFile virtualFile, @NotNull Document document) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void fileContentLoaded(@NotNull VirtualFile virtualFile, @NotNull Document document) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,226 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.listeners;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.ApplicationUtil;
|
|
||||||
import com.falsepattern.zigbrains.common.util.FileUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Document;
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
|
||||||
import com.intellij.openapi.project.ProjectManager;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.openapi.vfs.newvfs.events.VFileMoveEvent;
|
|
||||||
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
|
|
||||||
import org.eclipse.lsp4j.FileChangeType;
|
|
||||||
import org.eclipse.lsp4j.FileEvent;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static com.falsepattern.zigbrains.lsp.utils.FileUtils.searchFiles;
|
|
||||||
|
|
||||||
class LSPFileEventManager {
|
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getInstance(LSPFileEventManager.class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates that a document will be saved
|
|
||||||
*
|
|
||||||
* @param doc The document
|
|
||||||
*/
|
|
||||||
static void willSave(Document doc) {
|
|
||||||
String uri = FileUtil.URIFromVirtualFile(FileDocumentManager.getInstance().getFile(doc));
|
|
||||||
EditorEventManagerBase.willSave(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates that all documents will be saved
|
|
||||||
*/
|
|
||||||
static void willSaveAllDocuments() {
|
|
||||||
EditorEventManagerBase.willSaveAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a file is changed. Notifies the server if this file was watched.
|
|
||||||
*
|
|
||||||
* @param file The file
|
|
||||||
*/
|
|
||||||
static void fileChanged(VirtualFile file) {
|
|
||||||
|
|
||||||
if (!FileUtils.isFileSupported(file)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String uri = FileUtil.URIFromVirtualFile(file);
|
|
||||||
if (uri == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplicationUtil.invokeAfterPsiEvents(() -> {
|
|
||||||
EditorEventManagerBase.documentSaved(uri);
|
|
||||||
FileUtils.findProjectsFor(file)
|
|
||||||
.forEach(p -> changedConfiguration(uri, FileUtils.projectToUri(p), FileChangeType.Changed));
|
|
||||||
}, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a file is moved. Notifies the server if this file was watched.
|
|
||||||
*
|
|
||||||
* @param event The file move event
|
|
||||||
*/
|
|
||||||
static void fileMoved(VFileMoveEvent event) {
|
|
||||||
try {
|
|
||||||
VirtualFile file = event.getFile();
|
|
||||||
if (!FileUtils.isFileSupported(file)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String newFileUri = FileUtil.URIFromVirtualFile(file);
|
|
||||||
String oldParentUri = FileUtil.URIFromVirtualFile(event.getOldParent());
|
|
||||||
if (newFileUri == null || oldParentUri == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String oldFileUri = String.format("%s/%s", oldParentUri, event.getFile().getName());
|
|
||||||
closeAndReopenAffectedFile(file, oldFileUri);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warn("LSP file move event failed due to :", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a file is deleted. Notifies the server if this file was watched.
|
|
||||||
*
|
|
||||||
* @param file The file
|
|
||||||
*/
|
|
||||||
static void fileDeleted(VirtualFile file) {
|
|
||||||
if (!FileUtils.isFileSupported(file)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String uri = FileUtil.URIFromVirtualFile(file);
|
|
||||||
if (uri == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ApplicationUtil.invokeAfterPsiEvents(() -> {
|
|
||||||
FileUtils.findProjectsFor(file).forEach(p -> changedConfiguration(uri,
|
|
||||||
FileUtils.projectToUri(p), FileChangeType.Deleted));
|
|
||||||
}, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a file is renamed. Notifies the server if this file was watched.
|
|
||||||
*
|
|
||||||
* @param oldFileName The old file name
|
|
||||||
* @param newFileName the new file name
|
|
||||||
*/
|
|
||||||
static void fileRenamed(String oldFileName, String newFileName) {
|
|
||||||
ApplicationUtil.invokeAfterPsiEvents(() -> {
|
|
||||||
try {
|
|
||||||
// Getting the right file is not trivial here since we only have the file name. Since we have to iterate over
|
|
||||||
// all opened projects and filter based on the file name.
|
|
||||||
Set<VirtualFile> files = Arrays.stream(ProjectManager.getInstance().getOpenProjects())
|
|
||||||
.flatMap(p -> searchFiles(newFileName, p).stream())
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
|
|
||||||
ApplicationUtil.invokeLater(() -> {
|
|
||||||
for (VirtualFile file : files) {
|
|
||||||
if (!FileUtils.isFileSupported(file)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String newFileUri = FileUtil.URIFromVirtualFile(file);
|
|
||||||
String oldFileUri = newFileUri.replace(file.getName(), oldFileName);
|
|
||||||
closeAndReopenAffectedFile(file, oldFileUri);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warn("LSP file rename event failed due to : ", e);
|
|
||||||
}
|
|
||||||
}, true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void closeAndReopenAffectedFile(VirtualFile file, String oldFileUri) {
|
|
||||||
String newFileUri = FileUtil.URIFromVirtualFile(file);
|
|
||||||
|
|
||||||
// Notifies the language server.
|
|
||||||
FileUtils.findProjectsFor(file).forEach(p -> changedConfiguration(oldFileUri,
|
|
||||||
FileUtils.projectToUri(p), FileChangeType.Deleted));
|
|
||||||
FileUtils.findProjectsFor(file).forEach(p -> changedConfiguration(newFileUri,
|
|
||||||
FileUtils.projectToUri(p), FileChangeType.Created));
|
|
||||||
|
|
||||||
FileUtils.findProjectsFor(file).forEach(p -> {
|
|
||||||
// Detaches old file from the wrappers.
|
|
||||||
Set<LanguageServerWrapper> wrappers = IntellijLanguageClient.getAllServerWrappersFor(FileUtils.projectToUri(p));
|
|
||||||
wrappers.forEach(wrapper -> wrapper.disconnect(oldFileUri, FileUtils.projectToUri(p)));
|
|
||||||
if (!newFileUri.equals(oldFileUri)) {
|
|
||||||
// Re-open file to so that the new editor will be connected to the language server.
|
|
||||||
FileEditorManager fileEditorManager = FileEditorManager.getInstance(p);
|
|
||||||
ApplicationUtil.invokeLater(() -> {
|
|
||||||
fileEditorManager.closeFile(file);
|
|
||||||
fileEditorManager.openFile(file, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a file is created. Notifies the server if needed.
|
|
||||||
*
|
|
||||||
* @param file The file
|
|
||||||
*/
|
|
||||||
static void fileCreated(VirtualFile file) {
|
|
||||||
if (!FileUtils.isFileSupported(file)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String uri = FileUtil.URIFromVirtualFile(file);
|
|
||||||
if (uri != null) {
|
|
||||||
ApplicationUtil.invokeAfterPsiEvents(() -> {
|
|
||||||
FileUtils.findProjectsFor(file).forEach(p -> changedConfiguration(uri,
|
|
||||||
FileUtils.projectToUri(p), FileChangeType.Created));
|
|
||||||
}, true, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void changedConfiguration(String uri, String projectUri, FileChangeType typ) {
|
|
||||||
ApplicationUtil.pool(() -> {
|
|
||||||
DidChangeWatchedFilesParams params = getDidChangeWatchedFilesParams(uri, typ);
|
|
||||||
Set<LanguageServerWrapper> wrappers = IntellijLanguageClient.getAllServerWrappersFor(projectUri);
|
|
||||||
if (wrappers == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (LanguageServerWrapper wrapper : wrappers) {
|
|
||||||
if (wrapper.getRequestManager() != null
|
|
||||||
&& wrapper.getStatus() == ServerStatus.INITIALIZED) {
|
|
||||||
wrapper.getRequestManager().didChangeWatchedFiles(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private static DidChangeWatchedFilesParams getDidChangeWatchedFilesParams(String fileUri, FileChangeType typ) {
|
|
||||||
List<FileEvent> event = new ArrayList<>();
|
|
||||||
event.add(new FileEvent(fileUri, typ));
|
|
||||||
return new DidChangeWatchedFilesParams(event);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.listeners;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
|
|
||||||
public class LSPListener {
|
|
||||||
private Logger LOG = Logger.getInstance(LSPListener.class);
|
|
||||||
protected EditorEventManager manager = null;
|
|
||||||
protected boolean enabled = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the manager for this listener
|
|
||||||
*
|
|
||||||
* @param manager The manager
|
|
||||||
*/
|
|
||||||
public void setManager(EditorEventManager manager) {
|
|
||||||
this.manager = manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a manager is set, and logs and error if not the case
|
|
||||||
*
|
|
||||||
* @return true or false depending on if the manager is set
|
|
||||||
*/
|
|
||||||
protected boolean checkEnabled() {
|
|
||||||
if (manager == null) {
|
|
||||||
LOG.error("Manager is null");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void disable() {
|
|
||||||
enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void enable() {
|
|
||||||
enabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.listeners;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.project.ProjectManagerListener;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class LSPProjectManagerListener implements ProjectManagerListener {
|
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getInstance(LSPProjectManagerListener.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void projectClosing(@NotNull Project project) {
|
|
||||||
Set<LanguageServerWrapper> languageServerWrappers = IntellijLanguageClient.getAllServerWrappersFor(FileUtils.projectToUri(project));
|
|
||||||
languageServerWrappers.forEach(wrapper -> {
|
|
||||||
wrapper.stop(false);
|
|
||||||
IntellijLanguageClient.removeWrapper(wrapper);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.listeners;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.codeInsight.AutoPopupController;
|
|
||||||
import com.intellij.codeInsight.editorActions.TypedHandlerDelegate;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class notifies an EditorEventManager that a character has been typed in the editor
|
|
||||||
*/
|
|
||||||
public class LSPTypedHandler extends TypedHandlerDelegate {
|
|
||||||
@Override
|
|
||||||
public @NotNull Result checkAutoPopup(char charTyped, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
|
|
||||||
if (!FileUtils.isFileSupported(file.getVirtualFile())) {
|
|
||||||
return Result.CONTINUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (manager == null) {
|
|
||||||
return Result.CONTINUE;
|
|
||||||
}
|
|
||||||
for (String triggerChar : manager.completionTriggers) {
|
|
||||||
if (triggerChar != null && triggerChar.length() == 1 && triggerChar.charAt(0) == charTyped) {
|
|
||||||
AutoPopupController.getInstance(project).scheduleAutoPopup(editor);
|
|
||||||
return Result.STOP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Result.CONTINUE;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.listeners;
|
|
||||||
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
|
|
||||||
import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent;
|
|
||||||
import com.intellij.openapi.vfs.newvfs.events.VFileCopyEvent;
|
|
||||||
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent;
|
|
||||||
import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent;
|
|
||||||
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
|
|
||||||
import com.intellij.openapi.vfs.newvfs.events.VFileMoveEvent;
|
|
||||||
import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent;
|
|
||||||
import lombok.val;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class VFSListener implements BulkFileListener {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void before(@NotNull List<? extends @NotNull VFileEvent> events) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void after(@NotNull List<? extends @NotNull VFileEvent> events) {
|
|
||||||
for (val event: events) {
|
|
||||||
if (event instanceof VFilePropertyChangeEvent propEvent)
|
|
||||||
propertyChanged(propEvent);
|
|
||||||
else if (event instanceof VFileContentChangeEvent changeEvent)
|
|
||||||
contentsChanged(changeEvent);
|
|
||||||
else if (event instanceof VFileDeleteEvent deleteEvent)
|
|
||||||
fileDeleted(deleteEvent);
|
|
||||||
else if (event instanceof VFileMoveEvent moveEvent)
|
|
||||||
fileMoved(moveEvent);
|
|
||||||
else if (event instanceof VFileCopyEvent copyEvent)
|
|
||||||
fileCopied(copyEvent);
|
|
||||||
else if (event instanceof VFileCreateEvent createEvent)
|
|
||||||
fileCreated(createEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fired when a virtual file is renamed from within IDEA, or its writable status is changed.
|
|
||||||
* For files renamed externally, {@link #fileCreated} and {@link #fileDeleted} events will be fired.
|
|
||||||
*
|
|
||||||
* @param event the event object containing information about the change.
|
|
||||||
*/
|
|
||||||
public void propertyChanged(@NotNull VFilePropertyChangeEvent event) {
|
|
||||||
if (event.getPropertyName().equals(VirtualFile.PROP_NAME)) {
|
|
||||||
LSPFileEventManager.fileRenamed((String) event.getOldValue(), (String) event.getNewValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fired when the contents of a virtual file is changed.
|
|
||||||
*
|
|
||||||
* @param event the event object containing information about the change.
|
|
||||||
*/
|
|
||||||
public void contentsChanged(@NotNull VFileContentChangeEvent event) {
|
|
||||||
LSPFileEventManager.fileChanged(event.getFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fired when a virtual file is deleted.
|
|
||||||
*
|
|
||||||
* @param event the event object containing information about the change.
|
|
||||||
*/
|
|
||||||
public void fileDeleted(@NotNull VFileDeleteEvent event) {
|
|
||||||
LSPFileEventManager.fileDeleted(event.getFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fired when a virtual file is moved from within IDEA.
|
|
||||||
*
|
|
||||||
* @param event the event object containing information about the change.
|
|
||||||
*/
|
|
||||||
public void fileMoved(@NotNull VFileMoveEvent event) {
|
|
||||||
LSPFileEventManager.fileMoved(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fired when a virtual file is copied from within IDEA.
|
|
||||||
*
|
|
||||||
* @param event the event object containing information about the change.
|
|
||||||
*/
|
|
||||||
public void fileCopied(@NotNull VFileCopyEvent event) {
|
|
||||||
LSPFileEventManager.fileCreated(event.findCreatedFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fired when a virtual file is created. This event is not fired for files discovered during initial VFS initialization.
|
|
||||||
*
|
|
||||||
* @param event the event object containing information about the change.
|
|
||||||
*/
|
|
||||||
public void fileCreated(@NotNull VFileCreateEvent event) {
|
|
||||||
LSPFileEventManager.fileCreated(event.getFile());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.requests;
|
|
||||||
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import org.eclipse.lsp4j.Hover;
|
|
||||||
import org.eclipse.lsp4j.MarkedString;
|
|
||||||
import org.eclipse.lsp4j.MarkupContent;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Object used to process Hover responses
|
|
||||||
*/
|
|
||||||
public class HoverHandler {
|
|
||||||
|
|
||||||
private Logger LOG = Logger.getInstance(HoverHandler.class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the hover string corresponding to a Hover response
|
|
||||||
*
|
|
||||||
* @param hover The Hover
|
|
||||||
* @return The string response
|
|
||||||
*/
|
|
||||||
public static String getHoverString(@NonNull Hover hover) {
|
|
||||||
if (hover == null || hover.getContents() == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
Either<List<Either<String, MarkedString>>, MarkupContent> hoverContents = hover.getContents();
|
|
||||||
if (hoverContents.isLeft()) {
|
|
||||||
List<Either<String, MarkedString>> contents = hoverContents.getLeft();
|
|
||||||
if (contents != null && !contents.isEmpty()) {
|
|
||||||
List<String> result = new ArrayList<>();
|
|
||||||
for (Either<String, MarkedString> c : contents) {
|
|
||||||
String string = "";
|
|
||||||
if (c.isLeft() && !c.getLeft().isEmpty()) {
|
|
||||||
string = c.getLeft();
|
|
||||||
} else if (c.isRight()) {
|
|
||||||
MarkedString markedString = c.getRight();
|
|
||||||
string = (markedString.getLanguage() != null && !markedString.getLanguage().isEmpty()) ?
|
|
||||||
"```" + markedString.getLanguage() + " " + markedString.getValue() + "```" :
|
|
||||||
"";
|
|
||||||
}
|
|
||||||
result.add(string);
|
|
||||||
}
|
|
||||||
return String.join("\n", result);
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
} else if (hoverContents.isRight()) {
|
|
||||||
String markedContent = hoverContents.getRight().getValue();
|
|
||||||
if (markedContent.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return markedContent;
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.requests;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
|
|
||||||
public class ReformatHandler {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reformat a file given its editor
|
|
||||||
*
|
|
||||||
* @param editor The editor
|
|
||||||
*/
|
|
||||||
public static void reformatFile(Editor editor) {
|
|
||||||
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (eventManager != null) {
|
|
||||||
eventManager.reformat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reformat a selection in a file given its editor
|
|
||||||
*
|
|
||||||
* @param editor The editor
|
|
||||||
*/
|
|
||||||
public static void reformatSelection(Editor editor) {
|
|
||||||
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (eventManager != null) {
|
|
||||||
eventManager.reformatSelection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.requests;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing the Timeout for the various requests
|
|
||||||
*/
|
|
||||||
public class Timeout {
|
|
||||||
|
|
||||||
private static Map<Timeouts, Integer> timeouts = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
Arrays.stream(Timeouts.values()).forEach(t -> timeouts.put(t, t.getDefaultTimeout()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getTimeout(Timeouts type) {
|
|
||||||
return timeouts.get(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<Timeouts, Integer> getTimeouts() {
|
|
||||||
return timeouts;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setTimeouts(Map<Timeouts, Integer> loaded) {
|
|
||||||
loaded.forEach((t, v) -> timeouts.replace(t, v));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.requests;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enumeration for the timeouts
|
|
||||||
*/
|
|
||||||
public enum Timeouts {
|
|
||||||
CODEACTION(2000), CODELENS(2000), COMPLETION(1000), DECLARATION(2000), DEFINITION(2000), DOC_HIGHLIGHT(1000), EXECUTE_COMMAND(
|
|
||||||
2000), FORMATTING(2000), HOVER(2000), INIT(10000), REFERENCES(2000), SIGNATURE(1000), SHUTDOWN(
|
|
||||||
5000), SYMBOLS(2000), WILLSAVE(2000), FOLDING(1000), HIGHLIGHTING(1000), INLAY_HINTS(2000);
|
|
||||||
|
|
||||||
private final int defaultTimeout;
|
|
||||||
|
|
||||||
Timeouts(final int defaultTimeout) {
|
|
||||||
this.defaultTimeout = defaultTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDefaultTimeout() {
|
|
||||||
return defaultTimeout;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,235 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.requests;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.ApplicationUtil;
|
|
||||||
import com.falsepattern.zigbrains.common.util.FileUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.psi.LSPPsiElement;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
|
|
||||||
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
|
|
||||||
import com.intellij.openapi.command.CommandProcessor;
|
|
||||||
import com.intellij.openapi.command.UndoConfirmationPolicy;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
|
||||||
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.project.ProjectManager;
|
|
||||||
import com.intellij.openapi.project.ProjectUtil;
|
|
||||||
import com.intellij.openapi.util.TextRange;
|
|
||||||
import com.intellij.openapi.vfs.LocalFileSystem;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.psi.PsiElement;
|
|
||||||
import com.intellij.refactoring.listeners.RefactoringElementListener;
|
|
||||||
import com.intellij.usageView.UsageInfo;
|
|
||||||
import lombok.val;
|
|
||||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
|
||||||
import org.eclipse.lsp4j.Range;
|
|
||||||
import org.eclipse.lsp4j.ResourceOperation;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentEdit;
|
|
||||||
import org.eclipse.lsp4j.TextEdit;
|
|
||||||
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
|
|
||||||
import org.eclipse.lsp4j.WorkspaceEdit;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static com.falsepattern.zigbrains.common.util.ApplicationUtil.invokeLater;
|
|
||||||
import static com.falsepattern.zigbrains.common.util.ApplicationUtil.writeAction;
|
|
||||||
import static com.falsepattern.zigbrains.lsp.utils.DocumentUtils.toEither;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An Object handling WorkspaceEdits
|
|
||||||
*/
|
|
||||||
public class WorkspaceEditHandler {
|
|
||||||
private static Logger LOG = Logger.getInstance(WorkspaceEditHandler.class);
|
|
||||||
|
|
||||||
public static void applyEdit(PsiElement elem, String newName, UsageInfo[] infos,
|
|
||||||
RefactoringElementListener listener, List<VirtualFile> openedEditors) {
|
|
||||||
Map<String, List<TextEdit>> edits = new HashMap<>();
|
|
||||||
if (elem instanceof LSPPsiElement) {
|
|
||||||
LSPPsiElement lspElem = (LSPPsiElement) elem;
|
|
||||||
if (Stream.of(infos).allMatch(info -> info.getElement() instanceof LSPPsiElement)) {
|
|
||||||
Stream.of(infos).forEach(ui -> {
|
|
||||||
Editor editor = FileUtils.editorFromVirtualFile(ui.getVirtualFile(), ui.getProject());
|
|
||||||
val element = ui.getElement();
|
|
||||||
if (element == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
TextRange range = element.getTextRange();
|
|
||||||
Range lspRange = new Range(DocumentUtils.offsetToLSPPos(editor, range.getStartOffset()),
|
|
||||||
DocumentUtils.offsetToLSPPos(editor, range.getEndOffset()));
|
|
||||||
TextEdit edit = new TextEdit(lspRange, newName);
|
|
||||||
String uri = null;
|
|
||||||
blk:
|
|
||||||
try {
|
|
||||||
val vfs = ui.getVirtualFile();
|
|
||||||
if (vfs == null) {
|
|
||||||
break blk;
|
|
||||||
}
|
|
||||||
uri = FileUtil.sanitizeURI(
|
|
||||||
new URL(vfs.getUrl().replace(" ", FileUtil.SPACE_ENCODED)).toURI().toString());
|
|
||||||
} catch (MalformedURLException | URISyntaxException e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
}
|
|
||||||
if (edits.containsKey(uri)) {
|
|
||||||
edits.get(uri).add(edit);
|
|
||||||
} else {
|
|
||||||
List<TextEdit> textEdits = new ArrayList<>();
|
|
||||||
textEdits.add(edit);
|
|
||||||
edits.put(uri, textEdits);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
WorkspaceEdit workspaceEdit = new WorkspaceEdit(edits);
|
|
||||||
applyEdit(workspaceEdit, "Rename " + lspElem.getName() + " to " + newName, openedEditors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean applyEdit(WorkspaceEdit edit, String name) {
|
|
||||||
return applyEdit(edit, name, new ArrayList<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies a WorkspaceEdit
|
|
||||||
*
|
|
||||||
* @param edit The edit
|
|
||||||
* @param name edit name
|
|
||||||
* @param toClose files to be closed
|
|
||||||
* @return True if everything was applied, false otherwise
|
|
||||||
*/
|
|
||||||
public static boolean applyEdit(WorkspaceEdit edit, String name, List<VirtualFile> toClose) {
|
|
||||||
final String newName = (name != null) ? name : "LSP edits";
|
|
||||||
if (edit != null) {
|
|
||||||
Map<String, List<TextEdit>> changes = edit.getChanges();
|
|
||||||
List<Either<TextDocumentEdit, ResourceOperation>> dChanges = edit.getDocumentChanges();
|
|
||||||
boolean[] didApply = new boolean[]{true};
|
|
||||||
|
|
||||||
Project[] curProject = new Project[]{null};
|
|
||||||
List<VirtualFile> openedEditors = new ArrayList<>();
|
|
||||||
|
|
||||||
//Get the runnable of edits for each editor to apply them all in one command
|
|
||||||
List<Runnable> toApply = new ArrayList<>();
|
|
||||||
if (dChanges != null) {
|
|
||||||
dChanges.forEach(tEdit -> {
|
|
||||||
if (tEdit.isLeft()) {
|
|
||||||
TextDocumentEdit textEdit = tEdit.getLeft();
|
|
||||||
VersionedTextDocumentIdentifier doc = textEdit.getTextDocument();
|
|
||||||
int version = doc.getVersion() != null ? doc.getVersion() : Integer.MAX_VALUE;
|
|
||||||
String uri = FileUtil.sanitizeURI(doc.getUri());
|
|
||||||
EditorEventManager manager = EditorEventManagerBase.forUri(uri);
|
|
||||||
if (manager != null) {
|
|
||||||
curProject[0] = manager.editor.getProject();
|
|
||||||
toApply.add(manager.getEditsRunnable(version, toEither(textEdit.getEdits()), newName, true));
|
|
||||||
} else {
|
|
||||||
toApply.add(
|
|
||||||
manageUnopenedEditor(textEdit.getEdits(), uri, version, openedEditors, curProject,
|
|
||||||
newName));
|
|
||||||
}
|
|
||||||
} else if (tEdit.isRight()) {
|
|
||||||
ResourceOperation resourceOp = tEdit.getRight();
|
|
||||||
//TODO
|
|
||||||
} else {
|
|
||||||
LOG.warn("Null edit");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} else if (changes != null) {
|
|
||||||
changes.forEach((key, lChanges) -> {
|
|
||||||
String uri = FileUtil.sanitizeURI(key);
|
|
||||||
|
|
||||||
EditorEventManager manager = EditorEventManagerBase.forUri(uri);
|
|
||||||
if (manager != null) {
|
|
||||||
curProject[0] = manager.editor.getProject();
|
|
||||||
toApply.add(manager.getEditsRunnable(Integer.MAX_VALUE, toEither(lChanges), newName, true));
|
|
||||||
} else {
|
|
||||||
toApply.add(manageUnopenedEditor(lChanges, uri, Integer.MAX_VALUE, openedEditors, curProject,
|
|
||||||
newName));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (toApply.contains(null)) {
|
|
||||||
LOG.warn("Didn't apply, null runnable");
|
|
||||||
didApply[0] = false;
|
|
||||||
} else {
|
|
||||||
Runnable runnable = () -> toApply.forEach(Runnable::run);
|
|
||||||
invokeLater(() -> writeAction(() -> {
|
|
||||||
CommandProcessor.getInstance()
|
|
||||||
.executeCommand(curProject[0], runnable, name, "LSPPlugin", UndoConfirmationPolicy.DEFAULT,
|
|
||||||
false);
|
|
||||||
openedEditors.forEach(f -> FileEditorManager.getInstance(curProject[0]).closeFile(f));
|
|
||||||
toClose.forEach(f -> FileEditorManager.getInstance(curProject[0]).closeFile(f));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return didApply[0];
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens an editor when needed and gets the Runnable
|
|
||||||
*
|
|
||||||
* @param edits The text edits
|
|
||||||
* @param uri The uri of the file
|
|
||||||
* @param version The version of the file
|
|
||||||
* @param openedEditors
|
|
||||||
* @param curProject
|
|
||||||
* @param name
|
|
||||||
* @return The runnable containing the edits
|
|
||||||
*/
|
|
||||||
private static Runnable manageUnopenedEditor(List<TextEdit> edits, String uri, int version,
|
|
||||||
List<VirtualFile> openedEditors, Project[] curProject, String name) {
|
|
||||||
Project[] projects = ProjectManager.getInstance().getOpenProjects();
|
|
||||||
//Infer the project from the uri
|
|
||||||
Project project = Stream.of(projects)
|
|
||||||
.map(p -> new ImmutablePair<>(FileUtil.URIFromVirtualFile(ProjectUtil.guessProjectDir(p)), p))
|
|
||||||
.filter(p -> uri.startsWith(p.getLeft())).sorted(Collections.reverseOrder())
|
|
||||||
.map(ImmutablePair::getRight).findFirst().orElse(projects[0]);
|
|
||||||
VirtualFile file = null;
|
|
||||||
try {
|
|
||||||
file = LocalFileSystem.getInstance().findFileByIoFile(new File(new URI(FileUtil.sanitizeURI(uri))));
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
LOG.warn(e);
|
|
||||||
}
|
|
||||||
if (file == null)
|
|
||||||
return () -> {};
|
|
||||||
FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
|
|
||||||
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file);
|
|
||||||
Editor editor = ApplicationUtil
|
|
||||||
.computableWriteAction(() -> fileEditorManager.openTextEditor(descriptor, false));
|
|
||||||
openedEditors.add(file);
|
|
||||||
curProject[0] = editor.getProject();
|
|
||||||
Runnable runnable = null;
|
|
||||||
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
|
|
||||||
if (manager != null) {
|
|
||||||
runnable = manager.getEditsRunnable(version, toEither(edits), name, true);
|
|
||||||
}
|
|
||||||
return runnable;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,302 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.statusbar;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPDefaultIconProvider;
|
|
||||||
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
|
|
||||||
import com.falsepattern.zigbrains.lsp.utils.GUIUtils;
|
|
||||||
import com.intellij.openapi.actionSystem.AnAction;
|
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
|
||||||
import com.intellij.openapi.actionSystem.DataContext;
|
|
||||||
import com.intellij.openapi.actionSystem.DefaultActionGroup;
|
|
||||||
import com.intellij.openapi.project.DumbAware;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.ui.Messages;
|
|
||||||
import com.intellij.openapi.ui.popup.JBPopupFactory;
|
|
||||||
import com.intellij.openapi.ui.popup.ListPopup;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.openapi.wm.StatusBarWidget;
|
|
||||||
import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup;
|
|
||||||
import lombok.val;
|
|
||||||
import org.apache.commons.lang3.tuple.MutablePair;
|
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import javax.swing.BoxLayout;
|
|
||||||
import javax.swing.Icon;
|
|
||||||
import javax.swing.JLabel;
|
|
||||||
import javax.swing.JPanel;
|
|
||||||
import java.awt.Component;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class LSPServerStatusWidget extends EditorBasedStatusBarPopup {
|
|
||||||
|
|
||||||
private final Map<Timeouts, Pair<Integer, Integer>> timeouts = new HashMap<>();
|
|
||||||
private final String projectName;
|
|
||||||
private ServerStatus status = ServerStatus.NONEXISTENT;
|
|
||||||
|
|
||||||
private static final List<WeakReference<LSPServerStatusWidget>> widgets = Collections.synchronizedList(new ArrayList<>());
|
|
||||||
private static final ScheduledExecutorService refresher = Executors.newSingleThreadScheduledExecutor();
|
|
||||||
static {
|
|
||||||
refresher.scheduleWithFixedDelay(() -> {
|
|
||||||
synchronized (widgets) {
|
|
||||||
val iter = widgets.iterator();
|
|
||||||
while (iter.hasNext()) {
|
|
||||||
val weakWidget = iter.next();
|
|
||||||
val widget = weakWidget.get();
|
|
||||||
if (widget == null) {
|
|
||||||
iter.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
widget.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 1, 1, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Project project() {
|
|
||||||
return super.getProject();
|
|
||||||
}
|
|
||||||
|
|
||||||
LSPServerStatusWidget(Project project) {
|
|
||||||
super(project, false);
|
|
||||||
this.projectName = project.getName();
|
|
||||||
|
|
||||||
for (Timeouts t : Timeouts.values()) {
|
|
||||||
timeouts.put(t, new MutablePair<>(0, 0));
|
|
||||||
}
|
|
||||||
widgets.add(new WeakReference<>(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notifyResult(Timeouts timeout, Boolean success) {
|
|
||||||
Pair<Integer, Integer> oldValue = timeouts.get(timeout);
|
|
||||||
if (success) {
|
|
||||||
timeouts.replace(timeout, new MutablePair<>(oldValue.getKey() + 1, oldValue.getValue()));
|
|
||||||
} else {
|
|
||||||
timeouts.replace(timeout, new MutablePair<>(oldValue.getKey(), oldValue.getValue() + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the status of the server
|
|
||||||
*
|
|
||||||
* @param status The new status
|
|
||||||
*/
|
|
||||||
public void setStatus(ServerStatus status) {
|
|
||||||
this.status = status;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public String ID() {
|
|
||||||
return "LSP";
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
protected StatusBarWidget createInstance(@NotNull Project project) {
|
|
||||||
return new LSPServerStatusWidget(project);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LSPServerStatusPanel component;
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
protected JPanel createComponent() {
|
|
||||||
component = new LSPServerStatusPanel();
|
|
||||||
updateComponent();
|
|
||||||
return component;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void updateComponent(@NotNull EditorBasedStatusBarPopup.WidgetState state) {
|
|
||||||
updateComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateComponent() {
|
|
||||||
if (component != null) {
|
|
||||||
component.setToolTipText(getTooltipText());
|
|
||||||
component.setIcon(getIcon());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public WidgetPresentation getPresentation() {
|
|
||||||
return super.getPresentation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Icon getIcon() {
|
|
||||||
LanguageServerWrapper wrapper = LanguageServerWrapper.forProject(project());
|
|
||||||
Map<ServerStatus, Icon> icons = new LSPDefaultIconProvider().getStatusIcons();
|
|
||||||
if (wrapper != null) {
|
|
||||||
icons = GUIUtils.getIconProviderFor(wrapper.getServerDefinition())
|
|
||||||
.getStatusIcons();
|
|
||||||
}
|
|
||||||
return icons.get(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getTooltipText() {
|
|
||||||
LanguageServerWrapper wrapper = LanguageServerWrapper.forProject(project());
|
|
||||||
if (wrapper == null) {
|
|
||||||
return "Language server, project " + projectName;
|
|
||||||
} else {
|
|
||||||
return "Language server for extension " + wrapper.getServerDefinition().ext + ", project " + projectName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isEmpty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
protected ListPopup createPopup(@NotNull DataContext dataContext) {
|
|
||||||
var wrapper = LanguageServerWrapper.forProject(project());
|
|
||||||
if (wrapper == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<AnAction> actions = new ArrayList<>();
|
|
||||||
if (wrapper.getStatus() == ServerStatus.INITIALIZED) {
|
|
||||||
actions.add(new ShowConnectedFiles());
|
|
||||||
}
|
|
||||||
actions.add(new ShowTimeouts());
|
|
||||||
|
|
||||||
actions.add(new Restart());
|
|
||||||
|
|
||||||
JBPopupFactory.ActionSelectionAid mnemonics = JBPopupFactory.ActionSelectionAid.MNEMONICS;
|
|
||||||
String title = "Server Actions";
|
|
||||||
DefaultActionGroup group = new DefaultActionGroup(actions);
|
|
||||||
return JBPopupFactory.getInstance()
|
|
||||||
.createActionGroupPopup(title, group, dataContext, mnemonics, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
protected WidgetState getWidgetState(@Nullable VirtualFile virtualFile) {
|
|
||||||
if (virtualFile == null) {
|
|
||||||
return WidgetState.HIDDEN;
|
|
||||||
}
|
|
||||||
val manager = IntellijLanguageClient.getExtensionManagerFor(virtualFile.getExtension());
|
|
||||||
if (manager != null) {
|
|
||||||
val ws = new WidgetState(getTooltipText(), null, true);
|
|
||||||
ws.setIcon(getIcon());
|
|
||||||
return ws;
|
|
||||||
} else {
|
|
||||||
return WidgetState.HIDDEN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ShowConnectedFiles extends AnAction implements DumbAware {
|
|
||||||
ShowConnectedFiles() {
|
|
||||||
super("&Show Connected Files", "Show the files connected to the server", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(@NotNull AnActionEvent e) {
|
|
||||||
var wrapper = LanguageServerWrapper.forProject(project());
|
|
||||||
if (wrapper == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
StringBuilder connectedFiles = new StringBuilder("Connected files :");
|
|
||||||
wrapper.getConnectedFiles().forEach(f -> connectedFiles.append(System.lineSeparator()).append(f));
|
|
||||||
Messages.showInfoMessage(connectedFiles.toString(), "Connected Files");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShowTimeouts extends AnAction implements DumbAware {
|
|
||||||
ShowTimeouts() {
|
|
||||||
super("&Show Timeouts", "Show the timeouts proportions of the server", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(@NotNull AnActionEvent e) {
|
|
||||||
StringBuilder message = new StringBuilder();
|
|
||||||
message.append("<html>");
|
|
||||||
message.append("Timeouts (failed requests) :<br>");
|
|
||||||
timeouts.forEach((t, v) -> {
|
|
||||||
int timeouts = v.getRight();
|
|
||||||
message.append(t.name(), 0, 1).append(t.name().substring(1).toLowerCase()).append(" => ");
|
|
||||||
int total = v.getLeft() + timeouts;
|
|
||||||
if (total != 0) {
|
|
||||||
if (timeouts > 0) {
|
|
||||||
message.append("<font color=\"red\">");
|
|
||||||
}
|
|
||||||
message.append(timeouts).append("/").append(total).append(" (")
|
|
||||||
.append(100 * (double) timeouts / total).append("%)<br>");
|
|
||||||
if (timeouts > 0) {
|
|
||||||
message.append("</font>");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.append("0/0 (0%)<br>");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
message.append("</html>");
|
|
||||||
Messages.showInfoMessage(message.toString(), "Timeouts");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Restart extends AnAction implements DumbAware {
|
|
||||||
|
|
||||||
Restart() {
|
|
||||||
super("&Restart", "Restarts the language server.", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
|
|
||||||
var wrapper = LanguageServerWrapper.forProject(project());
|
|
||||||
if (wrapper != null) {
|
|
||||||
wrapper.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private class LSPServerStatusPanel extends JPanel {
|
|
||||||
private final JLabel myIconLabel;
|
|
||||||
|
|
||||||
LSPServerStatusPanel() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
setOpaque(false);
|
|
||||||
setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
|
|
||||||
setAlignmentY(Component.CENTER_ALIGNMENT);
|
|
||||||
myIconLabel = new JLabel("");
|
|
||||||
|
|
||||||
add(myIconLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIcon(@Nullable Icon icon) {
|
|
||||||
myIconLabel.setIcon(icon);
|
|
||||||
myIconLabel.setVisible(icon != null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.statusbar;
|
|
||||||
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.util.Key;
|
|
||||||
import com.intellij.openapi.wm.StatusBarWidget;
|
|
||||||
import com.intellij.openapi.wm.impl.status.widget.StatusBarEditorBasedWidgetFactory;
|
|
||||||
import lombok.val;
|
|
||||||
import org.jetbrains.annotations.Nls;
|
|
||||||
import org.jetbrains.annotations.NonNls;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class LSPServerStatusWidgetFactory extends StatusBarEditorBasedWidgetFactory {
|
|
||||||
public static final Key<List<LSPServerStatusWidget>> LSP_WIDGETS = Key.create("ZB_LSP_KEYS");
|
|
||||||
@Override
|
|
||||||
public @NonNls
|
|
||||||
@NotNull
|
|
||||||
String getId() {
|
|
||||||
return "LSP";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nls
|
|
||||||
@NotNull
|
|
||||||
String getDisplayName() {
|
|
||||||
return "Language Server";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull StatusBarWidget createWidget(@NotNull Project project) {
|
|
||||||
var keys = project.getUserData(LSP_WIDGETS);
|
|
||||||
if (keys == null) {
|
|
||||||
keys = new ArrayList<>();
|
|
||||||
project.putUserData(LSP_WIDGETS, keys);
|
|
||||||
}
|
|
||||||
val widget = new LSPServerStatusWidget(project);
|
|
||||||
keys.add(widget);
|
|
||||||
return widget;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disposeWidget(@NotNull StatusBarWidget widget) {
|
|
||||||
if (widget instanceof LSPServerStatusWidget w) {
|
|
||||||
val project = w.project();
|
|
||||||
val keys = project.getUserData(LSP_WIDGETS);
|
|
||||||
if (keys != null) {
|
|
||||||
keys.remove(widget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.disposeWidget(widget);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,169 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.utils;
|
|
||||||
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Document;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.editor.LogicalPosition;
|
|
||||||
import com.intellij.openapi.util.TextRange;
|
|
||||||
import com.intellij.openapi.util.text.StringUtil;
|
|
||||||
import com.intellij.util.DocumentUtil;
|
|
||||||
import org.eclipse.lsp4j.InsertReplaceEdit;
|
|
||||||
import org.eclipse.lsp4j.Position;
|
|
||||||
import org.eclipse.lsp4j.TextEdit;
|
|
||||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static com.falsepattern.zigbrains.common.util.ApplicationUtil.computableReadAction;
|
|
||||||
import static java.lang.Math.max;
|
|
||||||
import static java.lang.Math.min;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Various methods to convert offsets / logical position / server position
|
|
||||||
*/
|
|
||||||
public class DocumentUtils {
|
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getInstance(DocumentUtils.class);
|
|
||||||
public static final String WIN_SEPARATOR = "\r\n";
|
|
||||||
public static final String LINUX_SEPARATOR = "\n";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms a LogicalPosition (IntelliJ) to an LSP Position
|
|
||||||
*
|
|
||||||
* @param position the LogicalPosition
|
|
||||||
* @param editor The editor
|
|
||||||
* @return the Position
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public static Position logicalToLSPPos(LogicalPosition position, Editor editor) {
|
|
||||||
return offsetToLSPPos(editor, editor.logicalPositionToOffset(position));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms a LogicalPosition (IntelliJ) to an LSP Position
|
|
||||||
*
|
|
||||||
* @param position the LogicalPosition
|
|
||||||
* @param editor The editor
|
|
||||||
* @return the Position
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public static Position offsetToLSPPos(LogicalPosition position, Editor editor) {
|
|
||||||
return offsetToLSPPos(editor, editor.logicalPositionToOffset(position));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates a Position given an editor and an offset
|
|
||||||
*
|
|
||||||
* @param editor The editor
|
|
||||||
* @param offset The offset
|
|
||||||
* @return an LSP position
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public static Position offsetToLSPPos(Editor editor, int offset) {
|
|
||||||
return computableReadAction(() -> {
|
|
||||||
if (editor.isDisposed()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Document doc = editor.getDocument();
|
|
||||||
int line = doc.getLineNumber(offset);
|
|
||||||
int lineStart = doc.getLineStartOffset(line);
|
|
||||||
String lineTextBeforeOffset = doc.getText(TextRange.create(lineStart, offset));
|
|
||||||
int column = lineTextBeforeOffset.length();
|
|
||||||
return new Position(line, column);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int LSPPosToOffset(Document document, Position pos) {
|
|
||||||
return document.getLineStartOffset(pos.getLine()) + pos.getCharacter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms an LSP position to an editor offset
|
|
||||||
*
|
|
||||||
* @param editor The editor
|
|
||||||
* @param pos The LSPPos
|
|
||||||
* @return The offset
|
|
||||||
*/
|
|
||||||
public static int LSPPosToOffset(Editor editor, Position pos) {
|
|
||||||
return computableReadAction(() -> {
|
|
||||||
if (editor == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (editor.isDisposed()) {
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
// lsp and intellij start lines/columns zero-based
|
|
||||||
Document doc = editor.getDocument();
|
|
||||||
int line = max(0, min(pos.getLine(), doc.getLineCount()));
|
|
||||||
if (line >= doc.getLineCount()) {
|
|
||||||
return doc.getTextLength();
|
|
||||||
}
|
|
||||||
String lineText = doc.getText(DocumentUtil.getLineTextRange(doc, line));
|
|
||||||
|
|
||||||
final int positionInLine = max(0, min(lineText.length(), pos.getCharacter()));
|
|
||||||
int tabs = StringUtil.countChars(lineText, '\t', 0, positionInLine, false);
|
|
||||||
int tabSize = getTabSize(editor);
|
|
||||||
int column = positionInLine + tabs * (tabSize - 1);
|
|
||||||
int offset = editor.logicalPositionToOffset(new LogicalPosition(line, column));
|
|
||||||
if (pos.getCharacter() >= lineText.length()) {
|
|
||||||
LOG.debug(String.format("LSPPOS outofbounds: %s, line : %s, column : %d, offset : %d", pos,
|
|
||||||
lineText, column, offset));
|
|
||||||
}
|
|
||||||
int docLength = doc.getTextLength();
|
|
||||||
if (offset > docLength) {
|
|
||||||
LOG.debug(String.format("Offset greater than text length : %d > %d", offset, docLength));
|
|
||||||
}
|
|
||||||
return Math.min(max(offset, 0), docLength);
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static LogicalPosition getTabsAwarePosition(Editor editor, Position pos) {
|
|
||||||
return computableReadAction(() -> {
|
|
||||||
if (editor.isDisposed()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Document doc = editor.getDocument();
|
|
||||||
int line = max(0, min(pos.getLine(), doc.getLineCount() - 1));
|
|
||||||
String lineText = doc.getText(DocumentUtil.getLineTextRange(doc, line));
|
|
||||||
final int positionInLine = max(0, min(lineText.length(), pos.getCharacter()));
|
|
||||||
int tabs = StringUtil.countChars(lineText, '\t', 0, positionInLine, false);
|
|
||||||
int tabSize = getTabSize(editor);
|
|
||||||
int column = positionInLine + tabs * (tabSize - 1);
|
|
||||||
return new LogicalPosition(line, column);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the amount of whitespaces a tab represents.
|
|
||||||
*/
|
|
||||||
public static int getTabSize(Editor editor) {
|
|
||||||
return computableReadAction(() -> editor.getSettings().getTabSize(editor.getProject()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean shouldUseSpaces(Editor editor) {
|
|
||||||
return computableReadAction(() -> !editor.getSettings().isUseTabCharacter(editor.getProject()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Either<TextEdit, InsertReplaceEdit>> toEither(List<? extends TextEdit> edits) {
|
|
||||||
return edits.stream().map(Either::<TextEdit, InsertReplaceEdit>forLeft).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,273 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.utils;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.common.util.FileUtil;
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.extensions.LSPExtensionManager;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Document;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditor;
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
|
||||||
import com.intellij.openapi.fileEditor.TextEditor;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.project.ProjectManager;
|
|
||||||
import com.intellij.openapi.project.ProjectUtil;
|
|
||||||
import com.intellij.openapi.util.io.FileUtilRt;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.psi.PsiDocumentManager;
|
|
||||||
import com.intellij.psi.PsiFile;
|
|
||||||
import com.intellij.psi.search.FilenameIndex;
|
|
||||||
import com.intellij.psi.search.GlobalSearchScope;
|
|
||||||
import com.intellij.testFramework.LightVirtualFileBase;
|
|
||||||
import lombok.val;
|
|
||||||
import org.eclipse.lsp4j.TextDocumentIdentifier;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static com.falsepattern.zigbrains.common.util.ApplicationUtil.computableReadAction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Various file / uri related methods
|
|
||||||
*/
|
|
||||||
public class FileUtils {
|
|
||||||
private static final Logger LOG = Logger.getInstance(FileUtils.class);
|
|
||||||
|
|
||||||
public static List<Editor> getAllOpenedEditors(Project project) {
|
|
||||||
return computableReadAction(() -> {
|
|
||||||
List<Editor> editors = new ArrayList<>();
|
|
||||||
FileEditor[] allEditors = FileEditorManager.getInstance(project).getAllEditors();
|
|
||||||
for (FileEditor fEditor : allEditors) {
|
|
||||||
if (fEditor instanceof TextEditor) {
|
|
||||||
Editor editor = ((TextEditor) fEditor).getEditor();
|
|
||||||
if (editor.isDisposed() || !isEditorSupported(editor)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
editors.add(editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return editors;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Editor> getAllOpenedEditorsForUri(@NotNull Project project, String uri) {
|
|
||||||
VirtualFile file = FileUtil.virtualFileFromURI(uri);
|
|
||||||
if (file == null)
|
|
||||||
return Collections.emptyList();
|
|
||||||
return getAllOpenedEditorsForVirtualFile(project, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Editor> getAllOpenedEditorsForVirtualFile(@NotNull Project project, @NotNull VirtualFile file) {
|
|
||||||
return computableReadAction(() -> {
|
|
||||||
List<Editor> editors = new ArrayList<>();
|
|
||||||
FileEditor[] allEditors = FileEditorManager.getInstance(project).getAllEditors(file);
|
|
||||||
for (FileEditor fEditor : allEditors) {
|
|
||||||
if (fEditor instanceof TextEditor) {
|
|
||||||
Editor editor = ((TextEditor) fEditor).getEditor();
|
|
||||||
if (editor.isDisposed() || !isEditorSupported(editor)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
editors.add(editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return editors;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static Editor editorFromPsiFile(PsiFile psiFile) {
|
|
||||||
return editorFromVirtualFile(psiFile.getVirtualFile(), psiFile.getProject());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Editor editorFromUri(String uri, Project project) {
|
|
||||||
return editorFromVirtualFile(FileUtil.virtualFileFromURI(uri), project);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static Editor editorFromVirtualFile(VirtualFile file, Project project) {
|
|
||||||
FileEditor[] allEditors = FileEditorManager.getInstance(project).getAllEditors(file);
|
|
||||||
if (allEditors.length > 0 && allEditors[0] instanceof TextEditor) {
|
|
||||||
return ((TextEditor) allEditors[0]).getEditor();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms an editor (Document) identifier to an LSP identifier
|
|
||||||
*
|
|
||||||
* @param editor The editor
|
|
||||||
* @return The TextDocumentIdentifier
|
|
||||||
*/
|
|
||||||
public static TextDocumentIdentifier editorToLSPIdentifier(Editor editor) {
|
|
||||||
return new TextDocumentIdentifier(editorToURIString(editor));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the URI string corresponding to an Editor (Document)
|
|
||||||
*
|
|
||||||
* @param editor The Editor
|
|
||||||
* @return The URI
|
|
||||||
*/
|
|
||||||
public static String editorToURIString(Editor editor) {
|
|
||||||
return FileUtil.sanitizeURI(
|
|
||||||
FileUtil.URIFromVirtualFile(FileDocumentManager.getInstance().getFile(editor.getDocument())));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static VirtualFile virtualFileFromEditor(Editor editor) {
|
|
||||||
return FileDocumentManager.getInstance().getFile(editor.getDocument());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the project base dir uri given an editor
|
|
||||||
*
|
|
||||||
* @param editor The editor
|
|
||||||
* @return The project whose the editor belongs
|
|
||||||
*/
|
|
||||||
public static String editorToProjectFolderUri(Editor editor) {
|
|
||||||
return FileUtil.pathToUri(editorToProjectFolderPath(editor));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String editorToProjectFolderPath(Editor editor) {
|
|
||||||
if (editor == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
val project = editor.getProject();
|
|
||||||
if (project == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
val projectDir = ProjectUtil.guessProjectDir(editor.getProject());
|
|
||||||
if (projectDir == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return projectDir.toNioPath().toAbsolutePath().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String projectToUri(Project project) {
|
|
||||||
if (project == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
val path = ProjectUtil.guessProjectDir(project);
|
|
||||||
if (path == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return FileUtil.pathToUri(path.toNioPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String documentToUri(Document document) {
|
|
||||||
return FileUtil.sanitizeURI(FileUtil.URIFromVirtualFile(FileDocumentManager.getInstance().getFile(document)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find projects which contains the given file. This search runs among all open projects.
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
public static Set<Project> findProjectsFor(@NotNull VirtualFile file) {
|
|
||||||
return Arrays.stream(ProjectManager.getInstance().getOpenProjects())
|
|
||||||
.filter(p -> searchFiles(file.getName(), p).stream().anyMatch(f -> f.getPath().equals(file.getPath())))
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Collection<VirtualFile> searchFiles(String fileName, Project p) {
|
|
||||||
try {
|
|
||||||
return computableReadAction(() -> FilenameIndex.getVirtualFilesByName(fileName, GlobalSearchScope.projectScope(p)));
|
|
||||||
} catch (Throwable t) {
|
|
||||||
// Todo - Find a proper way to handle when IDEA file indexing is in-progress.
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This can be used to instantly apply a language server definition without restarting the IDE.
|
|
||||||
*/
|
|
||||||
public static void reloadAllEditors() {
|
|
||||||
Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
|
|
||||||
for (Project project : openProjects) {
|
|
||||||
reloadEditors(project);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This can be used to instantly apply a project-specific language server definition without restarting the
|
|
||||||
* project/IDE.
|
|
||||||
*
|
|
||||||
* @param project The project instance which need to be restarted
|
|
||||||
*/
|
|
||||||
public static void reloadEditors(@NotNull Project project) {
|
|
||||||
try {
|
|
||||||
List<Editor> allOpenedEditors = FileUtils.getAllOpenedEditors(project);
|
|
||||||
allOpenedEditors.forEach(IntellijLanguageClient::editorClosed);
|
|
||||||
allOpenedEditors.forEach(IntellijLanguageClient::editorOpened);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warn(String.format("Refreshing project: %s is failed due to: ", project.getName()), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given virtual file instance is supported by this LS client library.
|
|
||||||
*/
|
|
||||||
public static boolean isFileSupported(@Nullable VirtualFile file) {
|
|
||||||
if (file == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file instanceof LightVirtualFileBase) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.getUrl().isEmpty() || file.getUrl().startsWith("jar:")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return IntellijLanguageClient.isExtensionSupported(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the file in editor is supported by this LS client library.
|
|
||||||
*/
|
|
||||||
public static boolean isEditorSupported(@NotNull Editor editor) {
|
|
||||||
return isFileSupported(virtualFileFromEditor(editor)) &&
|
|
||||||
isFileContentSupported(editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always returns true unless the user has registered filtering to validate file content via LS protocol extension
|
|
||||||
// manager implementation.
|
|
||||||
private static boolean isFileContentSupported(Editor editor) {
|
|
||||||
return computableReadAction(() -> {
|
|
||||||
if (editor.getProject() == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
PsiFile file = PsiDocumentManager.getInstance(editor.getProject()).getPsiFile(editor.getDocument());
|
|
||||||
if (file == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
LSPExtensionManager lspExtManager = IntellijLanguageClient.getExtensionManagerFor(FileUtilRt.getExtension(file.getName()));
|
|
||||||
if (lspExtManager == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return lspExtManager.isFileContentSupported(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,142 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.utils;
|
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
|
|
||||||
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPDefaultIconProvider;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPIconProvider;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.label.LSPDefaultLabelProvider;
|
|
||||||
import com.falsepattern.zigbrains.lsp.contributors.label.LSPLabelProvider;
|
|
||||||
import com.falsepattern.zigbrains.lsp.extensions.LSPExtensionManager;
|
|
||||||
import com.intellij.codeInsight.hint.HintManager;
|
|
||||||
import com.intellij.codeInsight.hint.HintManagerImpl;
|
|
||||||
import com.intellij.ide.browsers.BrowserLauncher;
|
|
||||||
import com.intellij.openapi.diagnostic.Logger;
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
|
||||||
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
|
|
||||||
import com.intellij.openapi.project.Project;
|
|
||||||
import com.intellij.openapi.ui.Messages;
|
|
||||||
import com.intellij.openapi.vfs.VfsUtil;
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.ui.Hint;
|
|
||||||
import com.intellij.ui.LightweightHint;
|
|
||||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
|
||||||
|
|
||||||
import javax.swing.JTextPane;
|
|
||||||
import javax.swing.event.HyperlinkEvent;
|
|
||||||
import javax.swing.text.html.HTMLEditorKit;
|
|
||||||
import java.awt.Point;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static com.falsepattern.zigbrains.common.util.ApplicationUtil.writeAction;
|
|
||||||
|
|
||||||
public final class GUIUtils {
|
|
||||||
private static final LSPDefaultIconProvider DEFAULT_ICON_PROVIDER = new LSPDefaultIconProvider();
|
|
||||||
|
|
||||||
private static final LSPLabelProvider DEFAULT_LABEL_PROVIDER = new LSPDefaultLabelProvider();
|
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getInstance(GUIUtils.class);
|
|
||||||
|
|
||||||
private GUIUtils() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Hint createAndShowEditorHint(Editor editor, String string, Point point) {
|
|
||||||
return createAndShowEditorHint(editor, string, point, HintManager.ABOVE,
|
|
||||||
HintManager.HIDE_BY_ANY_KEY | HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_SCROLLING);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Hint createAndShowEditorHint(Editor editor, String string, Point point, int flags) {
|
|
||||||
return createAndShowEditorHint(editor, string, point, HintManager.ABOVE, flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a hint in the editor
|
|
||||||
*
|
|
||||||
* @param editor The editor
|
|
||||||
* @param string The message / text of the hint
|
|
||||||
* @param point The position of the hint
|
|
||||||
* @param constraint The constraint (under/above)
|
|
||||||
* @param flags The flags (when the hint will disappear)
|
|
||||||
* @return The hint
|
|
||||||
*/
|
|
||||||
public static Hint createAndShowEditorHint(Editor editor, String string, Point point, short constraint, int flags) {
|
|
||||||
JTextPane textPane = new JTextPane();
|
|
||||||
textPane.setEditorKit(new HTMLEditorKit());
|
|
||||||
textPane.setText(string);
|
|
||||||
int width = textPane.getPreferredSize().width;
|
|
||||||
if (width > 600) {
|
|
||||||
// max-width does not seem to be supported, so use this rather ugly hack...
|
|
||||||
textPane.setText(string.replace("<style>", "<style>p {width: 600px}\n"));
|
|
||||||
}
|
|
||||||
textPane.setEditable(false);
|
|
||||||
textPane.addHyperlinkListener(e -> {
|
|
||||||
if ((e.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
|
|
||||||
&& Objects.nonNull(e.getURL())) {
|
|
||||||
try {
|
|
||||||
if ("http".equals(e.getURL().getProtocol())) {
|
|
||||||
BrowserLauncher.getInstance().browse(e.getURL().toURI());
|
|
||||||
} else {
|
|
||||||
final Project project = editor.getProject();
|
|
||||||
Optional<? extends Pair<Project, VirtualFile>> fileToOpen = Optional.ofNullable(project).map(
|
|
||||||
p -> Optional.ofNullable(VfsUtil.findFileByURL(e.getURL()))
|
|
||||||
.map(f -> new ImmutablePair<>(p, f))).orElse(Optional.empty());
|
|
||||||
|
|
||||||
fileToOpen.ifPresent(f -> {
|
|
||||||
final OpenFileDescriptor descriptor = new OpenFileDescriptor(f.getLeft(), f.getRight());
|
|
||||||
writeAction(() -> FileEditorManager.getInstance(f.getLeft()).openTextEditor(descriptor, true));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (URISyntaxException ex) {
|
|
||||||
Messages.showErrorDialog("Invalid syntax in URL", "Open URL Error");
|
|
||||||
LOGGER.debug("Invalid URL was found.", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
LightweightHint hint = new LightweightHint(textPane);
|
|
||||||
Point p = HintManagerImpl.getHintPosition(hint, editor, editor.xyToLogicalPosition(point), constraint);
|
|
||||||
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, p, flags, 0, false,
|
|
||||||
HintManagerImpl.createHintHint(editor, p, hint, constraint).setContentActive(false));
|
|
||||||
return hint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a suitable LSPIconProvider given a ServerDefinition
|
|
||||||
*
|
|
||||||
* @param serverDefinition The serverDefinition
|
|
||||||
* @return The LSPIconProvider, or LSPDefaultIconProvider if none are found
|
|
||||||
*/
|
|
||||||
public static LSPIconProvider getIconProviderFor(LanguageServerDefinition serverDefinition) {
|
|
||||||
return IntellijLanguageClient.getExtensionManagerForDefinition(serverDefinition)
|
|
||||||
.map(LSPExtensionManager::getIconProvider).orElse(DEFAULT_ICON_PROVIDER);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a suitable LSPLabelProvider given a ServerDefinition
|
|
||||||
*
|
|
||||||
* @param serverDefinition The serverDefinition
|
|
||||||
* @return The LSPLabelProvider, or the default if none are found
|
|
||||||
*/
|
|
||||||
public static LSPLabelProvider getLabelProviderFor(LanguageServerDefinition serverDefinition) {
|
|
||||||
return IntellijLanguageClient.getExtensionManagerForDefinition(serverDefinition)
|
|
||||||
.map(LSPExtensionManager::getLabelProvider).orElse(DEFAULT_LABEL_PROVIDER);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.utils;
|
|
||||||
|
|
||||||
public abstract class LSPException extends RuntimeException {
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.utils;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class OSUtils {
|
|
||||||
|
|
||||||
private static final String WINDOWS = "windows";
|
|
||||||
private static final String UNIX = "unix";
|
|
||||||
private static final String MAC = "mac";
|
|
||||||
|
|
||||||
private static final String OS = System.getProperty("os.name").toLowerCase(Locale.getDefault());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns name of the operating system running. null if not a unsupported operating system.
|
|
||||||
*
|
|
||||||
* @return operating system
|
|
||||||
*/
|
|
||||||
public static String getOperatingSystem() {
|
|
||||||
if (OSUtils.isWindows()) {
|
|
||||||
return WINDOWS;
|
|
||||||
} else if (OSUtils.isUnix()) {
|
|
||||||
return UNIX;
|
|
||||||
} else if (OSUtils.isMac()) {
|
|
||||||
return MAC;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isWindows() {
|
|
||||||
return (OS.contains("win"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isMac() {
|
|
||||||
return (OS.contains("mac"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isUnix() {
|
|
||||||
return (OS.contains("nix") || OS.contains("nux") || OS.contains("aix"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023-2024 FalsePattern
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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 com.falsepattern.zigbrains.lsp.utils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Object containing some useful methods for the plugin
|
|
||||||
*/
|
|
||||||
public class Utils {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms an array into a string (using mkString, useful for Java)
|
|
||||||
*
|
|
||||||
* @param arr The array
|
|
||||||
* @param sep A separator
|
|
||||||
* @return The result of mkString
|
|
||||||
*/
|
|
||||||
public String arrayToString(Object[] arr, String sep) {
|
|
||||||
sep = (sep != null) ? sep : "";
|
|
||||||
return String.join(sep, Arrays.toString(arr));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Concatenate multiple arrays
|
|
||||||
*
|
|
||||||
* @param arr The arrays
|
|
||||||
* @return The concatenated arrays
|
|
||||||
*/
|
|
||||||
public Object[] concatenateArrays(Object[]... arr) {
|
|
||||||
List<Object> result = new ArrayList<>(Arrays.asList(arr));
|
|
||||||
return result.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> stringToList(String str, String sep) {
|
|
||||||
sep = (sep != null) ? sep : System.lineSeparator();
|
|
||||||
return new ArrayList<>(Arrays.asList(str.split(sep)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String[] parseArgs(String[] strArr) {
|
|
||||||
List<String> buffer = new ArrayList<>();
|
|
||||||
boolean isSingleQuote = false;
|
|
||||||
boolean isDoubleQuote = false;
|
|
||||||
boolean wasEscaped = false;
|
|
||||||
|
|
||||||
StringBuilder curStr = new StringBuilder();
|
|
||||||
for (String str : strArr) {
|
|
||||||
for (int i = 0; i < str.length(); i++) {
|
|
||||||
switch (str.charAt(i)) {
|
|
||||||
case '\'':
|
|
||||||
if (!wasEscaped) {
|
|
||||||
isSingleQuote = !isSingleQuote;
|
|
||||||
}
|
|
||||||
wasEscaped = false;
|
|
||||||
curStr.append('\'');
|
|
||||||
break;
|
|
||||||
case '\"':
|
|
||||||
if (!wasEscaped) {
|
|
||||||
isDoubleQuote = !isDoubleQuote;
|
|
||||||
}
|
|
||||||
wasEscaped = false;
|
|
||||||
curStr.append('\"');
|
|
||||||
break;
|
|
||||||
case ' ':
|
|
||||||
if (isSingleQuote || isDoubleQuote) {
|
|
||||||
curStr.append(" ");
|
|
||||||
} else {
|
|
||||||
buffer.add(curStr.toString());
|
|
||||||
curStr.setLength(0);
|
|
||||||
}
|
|
||||||
wasEscaped = false;
|
|
||||||
break;
|
|
||||||
case '\\':
|
|
||||||
if (wasEscaped) {
|
|
||||||
wasEscaped = false;
|
|
||||||
} else {
|
|
||||||
wasEscaped = true;
|
|
||||||
}
|
|
||||||
curStr.append('\\');
|
|
||||||
break;
|
|
||||||
case 'c':
|
|
||||||
curStr.append('c');
|
|
||||||
wasEscaped = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (curStr.length() != 0) {
|
|
||||||
buffer.add(curStr.toString());
|
|
||||||
curStr.setLength(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String[] result = new String[buffer.size()];
|
|
||||||
buffer.toArray(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 2048 2048">
|
|
||||||
<circle fill-opacity="1" r="896" cx="1024" cy="1024" fill="#808080"></circle>
|
|
||||||
<g transform="scale(1)"><path fill-opacity="1" transform="translate(409.6, 1617.44) rotate(180) scale(-1, 1)" fill="#000000" d="M102 724h1060v-269h-1060v269z"></path></g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 349 B |
|
@ -1,4 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 2048 2048">
|
|
||||||
<circle fill-opacity="1" r="896" cx="1024" cy="1024" fill="#50b050" style="stroke: rgb(50, 100, 50); stroke-width: 128px; stroke-opacity: 1;"></circle>
|
|
||||||
<g transform="scale(1)"></g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 281 B |
|
@ -1,4 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 2048 2048">
|
|
||||||
<circle fill-opacity="1" r="896" cx="1024" cy="1024" fill="#b0b050" style="stroke: rgb(100, 100, 50); stroke-width: 128px; stroke-opacity: 1;"></circle>
|
|
||||||
<g transform="scale(1)"><path fill-opacity="1" transform="translate(409.6, 1556) rotate(180) scale(-1, 1)" fill="#000000" d="M316 890h-227v127h412l58 -58v-412h-127v227l-33 -28q-45 -39 -75 -111q-25 -62 -25 -132q0 -68 25 -129.5t74 -111.5q49 -47 111 -74q59 -25 131 -25t128 25q65 30 110 74q50 50 75 111.5t25 129.5q0 70 -25 132q-24 61 -76 111l119 115q74 -77 111 -164q37 -91 37 -194 q0 -101 -37 -192q-37 -89 -111 -164q-72 -71 -163 -110q-88 -37 -192 -37q-108 0 -191 37q-95 43 -163 110q-72 72 -111 164q-38 88 -38 192q0 103 38 194t111 164z"></path></g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 797 B |
|
@ -1,4 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 2048 2048">
|
|
||||||
<circle fill-opacity="1" r="896" cx="1024" cy="1024" fill="#b05050" style="stroke: rgb(100, 50, 50); stroke-width: 128px; stroke-opacity: 1;"></circle>
|
|
||||||
<g transform="scale(1)"><path fill-opacity="1" transform="translate(430.08, 1595.08) rotate(180) scale(-1, 1)" fill="#000000" d="M389 571L29 1118H375L592 762L811 1118H1157L793 571L1174 0H827L592 383L356 0H10L389 571Z"></path></g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 482 B |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue