diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
index 0be76d4ef1dc3..9b333abf243ab 100644
--- a/.idea/copyright/profiles_settings.xml
+++ b/.idea/copyright/profiles_settings.xml
@@ -1,5 +1,9 @@
-
+
+
+
+
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
index 29f178cfd05a0..2ed35da17cf09 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -12,6 +12,7 @@
+
diff --git a/build/src/org/jetbrains/intellij/build/HulyProperties.kt b/build/src/org/jetbrains/intellij/build/HulyProperties.kt
index c912cdd5699c9..6a4ea759c1a35 100644
--- a/build/src/org/jetbrains/intellij/build/HulyProperties.kt
+++ b/build/src/org/jetbrains/intellij/build/HulyProperties.kt
@@ -70,6 +70,7 @@ val HULY_BUNDLED_PLUGINS: PersistentList = DEFAULT_BUNDLED_PLUGINS + seq
"intellij.performanceTesting",
"intellij.turboComplete",
"redhat.lsp4ij",
+ "hulylabs.hulycode.lang-configurator",
)
internal suspend fun createHulyBuildContext(
diff --git a/intellij.idea.community.main.iml b/intellij.idea.community.main.iml
index 82c1e7c94493d..ad5ff1e990c6a 100644
--- a/intellij.idea.community.main.iml
+++ b/intellij.idea.community.main.iml
@@ -229,5 +229,6 @@
+
\ No newline at end of file
diff --git a/plugins/hulylabs.langconfigurator/hulylabs.langconfigurator.iml b/plugins/hulylabs.langconfigurator/hulylabs.langconfigurator.iml
new file mode 100644
index 0000000000000..f4265e01adcd7
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/hulylabs.langconfigurator.iml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 95c63c1a55b22dd6453890a419cc1a640f790bbf7d8ae82db1e30aefefb08888
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 5629d59b4d3bad66719d3883b01497f28a960f8323d44e0904c6f93f6d3b9702
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/hulylabs.langconfigurator/resources/META-INF/plugin.xml b/plugins/hulylabs.langconfigurator/resources/META-INF/plugin.xml
new file mode 100644
index 0000000000000..dd82f1bd1a148
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/resources/META-INF/plugin.xml
@@ -0,0 +1,24 @@
+
+
+
+ Huly Code Lang Configurator
+ Other Tools
+ hulylabs.hulycode.plugins.hulylangconfigurator
+ Huly Labs
+
+ Configures languages support for opened files.
+
+ com.intellij.modules.platform
+
+ messages.HulyLangConfiguratorBundle
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/hulylabs.langconfigurator/resources/lsp-templates/rust-analyzer/clientSettings.json b/plugins/hulylabs.langconfigurator/resources/lsp-templates/rust-analyzer/clientSettings.json
new file mode 100644
index 0000000000000..2a717ac8d0752
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/resources/lsp-templates/rust-analyzer/clientSettings.json
@@ -0,0 +1,6 @@
+{
+ "caseSensitive": true,
+ "workspaceSymbol": {
+ "supportsGotoClass": true
+ }
+}
\ No newline at end of file
diff --git a/plugins/hulylabs.langconfigurator/resources/lsp-templates/rust-analyzer/initializationOptions.json b/plugins/hulylabs.langconfigurator/resources/lsp-templates/rust-analyzer/initializationOptions.json
new file mode 100644
index 0000000000000..350b099bbf4be
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/resources/lsp-templates/rust-analyzer/initializationOptions.json
@@ -0,0 +1,84 @@
+{
+ "imports": {
+ "granularity": {
+ "group": "module"
+ },
+ "prefix": "self"
+ },
+ "cargo": {
+ "buildScripts": {
+ "enable": true
+ }
+ },
+ "completion": {
+ "autoimport": {
+ "enable": true
+ },
+ "autoself": {
+ "enable": true
+ },
+ "callable": {
+ "snippets": "fill_arguments"
+ },
+ "fullFunctionSignatures": {
+ "enable": false
+ },
+ "limit": null,
+ "postfix": {
+ "enable": true
+ },
+ "privateEditable": {
+ "enable": false
+ },
+ "snippets": {
+ "custom": {
+ "Arc::new": {
+ "postfix": "arc",
+ "body": "Arc::new(${receiver})",
+ "requires": "std::sync::Arc",
+ "description": "Put the expression into an `Arc`",
+ "scope": "expr"
+ },
+ "Rc::new": {
+ "postfix": "rc",
+ "body": "Rc::new(${receiver})",
+ "requires": "std::rc::Rc",
+ "description": "Put the expression into an `Rc`",
+ "scope": "expr"
+ },
+ "Box::pin": {
+ "postfix": "pinbox",
+ "body": "Box::pin(${receiver})",
+ "requires": "std::boxed::Box",
+ "description": "Put the expression into a pinned `Box`",
+ "scope": "expr"
+ },
+ "Ok": {
+ "postfix": "ok",
+ "body": "Ok(${receiver})",
+ "description": "Wrap the expression in a `Result::Ok`",
+ "scope": "expr"
+ },
+ "Err": {
+ "postfix": "err",
+ "body": "Err(${receiver})",
+ "description": "Wrap the expression in a `Result::Err`",
+ "scope": "expr"
+ },
+ "Some": {
+ "postfix": "some",
+ "body": "Some(${receiver})",
+ "description": "Wrap the expression in an `Option::Some`",
+ "scope": "expr"
+ }
+ }
+ }
+ },
+ "termSearch": {
+ "enable": false,
+ "fuel": 200
+ },
+ "procMacro": {
+ "enable": true
+ }
+}
\ No newline at end of file
diff --git a/plugins/hulylabs.langconfigurator/resources/lsp-templates/rust-analyzer/template.yaml b/plugins/hulylabs.langconfigurator/resources/lsp-templates/rust-analyzer/template.yaml
new file mode 100644
index 0000000000000..0cd08a878ab29
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/resources/lsp-templates/rust-analyzer/template.yaml
@@ -0,0 +1,22 @@
+id: rust-analyzer
+name: Rust Language Server
+
+programArgs:
+ default: "sh -c $APPLICATION_CONFIG_DIR$/lsp4ij/rust-analyzer/rust-analyzer"
+ windows: "$APPLICATION_CONFIG_DIR$/lsp4ij/rust-analyzer/rust-analyzer.exe"
+
+binaryUrls:
+ windows-x86_64: "https://github.com/rust-lang/rust-analyzer/releases/download/2024-12-09/rust-analyzer-x86_64-pc-windows-msvc.zip"
+ windows-aarch64: "https://github.com/rust-lang/rust-analyzer/releases/download/2024-12-09/rust-analyzer-aarch64-pc-windows-msvc.zip"
+ linux-x86_64: "https://github.com/rust-lang/rust-analyzer/releases/download/2024-12-09/rust-analyzer-x86_64-unknown-linux-gnu.gz"
+ linux-aarch64: "https://github.com/rust-lang/rust-analyzer/releases/download/2024-12-09/rust-analyzer-aarch64-unknown-linux-gnu.gz"
+ mac-x86_64: "https://github.com/rust-lang/rust-analyzer/releases/download/2024-12-09/rust-analyzer-x86_64-apple-darwin.gz"
+ mac-aarch64: "https://github.com/rust-lang/rust-analyzer/releases/download/2024-12-09/rust-analyzer-aarch64-apple-darwin.gz"
+
+binaryExecutable: "rust-analyzer"
+
+mappingSettings:
+ - languageId: "rust"
+ fileType:
+ name: Rust
+ patterns: ["*.rs"]
diff --git a/plugins/hulylabs.langconfigurator/resources/lsp-templates/typescript-language-server/clientSettings.json b/plugins/hulylabs.langconfigurator/resources/lsp-templates/typescript-language-server/clientSettings.json
new file mode 100644
index 0000000000000..2a717ac8d0752
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/resources/lsp-templates/typescript-language-server/clientSettings.json
@@ -0,0 +1,6 @@
+{
+ "caseSensitive": true,
+ "workspaceSymbol": {
+ "supportsGotoClass": true
+ }
+}
\ No newline at end of file
diff --git a/plugins/hulylabs.langconfigurator/resources/lsp-templates/typescript-language-server/settings.json b/plugins/hulylabs.langconfigurator/resources/lsp-templates/typescript-language-server/settings.json
new file mode 100644
index 0000000000000..b079dc84c250b
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/resources/lsp-templates/typescript-language-server/settings.json
@@ -0,0 +1,24 @@
+{
+ "completions": {
+ "completeFunctionCalls": true
+ },
+ "typescript": {
+ "inlayHints": {
+ "includeInlayEnumMemberValueHints": true,
+ "includeInlayFunctionLikeReturnTypeHints": true,
+ "includeInlayFunctionParameterTypeHints": true,
+ "includeInlayParameterNameHints": "all",
+ "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
+ "includeInlayPropertyDeclarationTypeHints": true,
+ "includeInlayVariableTypeHints": true,
+ "includeInlayVariableTypeHintsWhenTypeMatchesName": true
+ },
+ "implementationsCodeLens": {
+ "enabled": true
+ },
+ "referencesCodeLens": {
+ "enabled": true,
+ "showOnAllFunctions": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/hulylabs.langconfigurator/resources/lsp-templates/typescript-language-server/template.yaml b/plugins/hulylabs.langconfigurator/resources/lsp-templates/typescript-language-server/template.yaml
new file mode 100644
index 0000000000000..9794bc331729d
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/resources/lsp-templates/typescript-language-server/template.yaml
@@ -0,0 +1,25 @@
+id: typescript-language-server
+name: TypeScript Language Server
+
+programArgs:
+ default: "sh -c \"typescript-language-server --stdio\""
+ windows: "typescript-language-server --stdio"
+
+installCommand: "npm install -g typescript-language-server typescript"
+
+mappingSettings:
+ - languageId: javascript
+ fileType:
+ name: JavaScript
+ - languageId: javascriptreact
+ fileType:
+ name: JavaScript-React
+ patterns: ["*.jsx"]
+ - languageId: typescript
+ fileType:
+ name: TypeScript
+ patterns: ["*.ts"]
+ - languageId: typescriptreact
+ fileType:
+ name: TypeScript-React
+ patterns: ["*.tsx"]
diff --git a/plugins/hulylabs.langconfigurator/resources/lsp-templates/zig-language-server/template.yaml b/plugins/hulylabs.langconfigurator/resources/lsp-templates/zig-language-server/template.yaml
new file mode 100644
index 0000000000000..71252e7648eef
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/resources/lsp-templates/zig-language-server/template.yaml
@@ -0,0 +1,19 @@
+id: zig-language-server
+name: ZIG Language Server
+
+programArgs:
+ default: "sh -c \"$APPLICATION_CONFIG_DIR$/lsp4ij/zig-language-server/zls --config-path $APPLICATION_CONFIG_DIR$/lsp4ij/zig-language-server/zls.json\""
+ windows: "$APPLICATION_CONFIG_DIR$/lsp4ij/zig-language-server/zls --config-path $APPLICATION_CONFIG_DIR$/lsp4ij/zig-language-server/zls.json"
+
+binaryUrls:
+ windows-x86_64: "https://github.com/zigtools/zls/releases/download/0.13.0/zls-x86_64-windows.zip"
+ linux-x86_64: "https://github.com/zigtools/zls/releases/download/0.13.0/zls-x86_64-linux.tar.xz"
+ lunix-aarch64: "https://github.com/zigtools/zls/releases/download/0.13.0/zls-aarch64-linux.tar.xz"
+ mac-x86_64: "https://github.com/zigtools/zls/releases/download/0.13.0/zls-x86_64-macos.tar.xz"
+ mac-aarch64: "https://github.com/zigtools/zls/releases/download/0.13.0/zls-aarch64-macos.tar.xz"
+
+mappingSettings:
+ - languageId: zig
+ fileType:
+ name: ZIG
+ patterns: ["*.zig"]
\ No newline at end of file
diff --git a/plugins/hulylabs.langconfigurator/resources/messages/HulyLangConfiguratorBundle.properties b/plugins/hulylabs.langconfigurator/resources/messages/HulyLangConfiguratorBundle.properties
new file mode 100644
index 0000000000000..389d14bdb6313
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/resources/messages/HulyLangConfiguratorBundle.properties
@@ -0,0 +1,8 @@
+# Copyright © 2024 Hardcore Engineering Inc. Use of this source code is governed by the Apache 2.0 license.
+
+message.langconfigurator.title=Language server configurator
+message.addlang.title=Would you like to add a Language Server configuration for this type of file?
+message.addlang.yes=Add
+message.addlang.dismiss=Dismiss
+message.addlang.cannot.configre=Error configuring language server
+message.addlang.configure.success=Language server configuration successfully added
diff --git a/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/LanguageServerConfigSetupNotificationProvider.java b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/LanguageServerConfigSetupNotificationProvider.java
new file mode 100644
index 0000000000000..c3d39435ccda4
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/LanguageServerConfigSetupNotificationProvider.java
@@ -0,0 +1,77 @@
+// Copyright © 2024 HulyLabs. Use of this source code is governed by the Apache 2.0 license.
+package com.hulylabs.intellij.plugins.langconfigurator;
+
+import com.hulylabs.intellij.plugins.langconfigurator.templates.HulyLanguageServerTemplate;
+import com.hulylabs.intellij.plugins.langconfigurator.templates.HulyLanguageServerTemplateManager;
+import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
+import com.intellij.ide.util.PropertiesComponent;
+import com.intellij.notification.NotificationGroup;
+import com.intellij.notification.NotificationGroupManager;
+import com.intellij.notification.NotificationType;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.ui.EditorNotificationPanel;
+import com.intellij.ui.EditorNotificationProvider;
+import com.intellij.ui.EditorNotifications;
+import com.redhat.devtools.lsp4ij.LanguageServersRegistry;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.Locale;
+import java.util.function.Function;
+
+import static com.hulylabs.intellij.plugins.langconfigurator.messages.HulyLangConfiguratorBundle.message;
+
+
+public class LanguageServerConfigSetupNotificationProvider implements EditorNotificationProvider {
+ private static final String IGNORED_EXTS_KEY_PREFIX = "hulylangconfigurator.ignoreexts.";
+
+ @Override
+ public @Nullable Function super @NotNull FileEditor, ? extends @Nullable JComponent> collectNotificationData(@NotNull Project project,
+ @NotNull VirtualFile file) {
+ String ext = file.getExtension() != null ? file.getExtension().toLowerCase(Locale.ROOT) : "";
+ if (PropertiesComponent.getInstance(project).getBoolean(IGNORED_EXTS_KEY_PREFIX + ext)) return null;
+
+ HulyLanguageServerTemplateManager manager = ApplicationManager.getApplication().getService(HulyLanguageServerTemplateManager.class);
+ if (manager == null) return null;
+
+ HulyLanguageServerTemplate template = manager.getTemplate(file);
+ if (template == null) return null;
+
+ if (LanguageServersRegistry.getInstance().isFileSupported(file, project)) return null;
+
+ return fileEditor -> {
+ EditorNotificationPanel panel = new EditorNotificationPanel(fileEditor, EditorNotificationPanel.Status.Info);
+ panel.setText(message("message.addlang.title"));
+ Runnable onSuccess = () -> {
+ EditorNotifications.getInstance(project).updateAllNotifications();
+ PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
+ if (psiFile != null) DaemonCodeAnalyzer.getInstance(project).restart(psiFile);
+ getNotificationGroup().createNotification(message("message.langconfigurator.title"), message("message.addlang.configure.success"),
+ NotificationType.INFORMATION).notify(project);
+ };
+ Runnable onFailure = () -> {
+ getNotificationGroup().createNotification(message("message.langconfigurator.title"), message("message.addlang.cannot.configre"),
+ NotificationType.ERROR)
+ .notify(project);
+ };
+ panel.createActionLabel(message("message.addlang.yes"), () -> {
+ LanguageServerTemplateInstaller.install(project, template, onSuccess, onFailure);
+ });
+ panel.createActionLabel(message("message.addlang.dismiss"), () -> {
+ PropertiesComponent.getInstance(project).setValue(IGNORED_EXTS_KEY_PREFIX + ext, true);
+ EditorNotifications.getInstance(project).updateAllNotifications();
+ });
+ return panel;
+ };
+ }
+
+ private static NotificationGroup getNotificationGroup() {
+ return NotificationGroupManager.getInstance().getNotificationGroup("Language Server Configuration");
+ }
+}
diff --git a/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/LanguageServerTemplateInstaller.java b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/LanguageServerTemplateInstaller.java
new file mode 100644
index 0000000000000..ca6ed68b6069b
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/LanguageServerTemplateInstaller.java
@@ -0,0 +1,212 @@
+// Copyright © 2024 HulyLabs. Use of this source code is governed by the Apache 2.0 license.
+package com.hulylabs.intellij.plugins.langconfigurator;
+
+import com.hulylabs.intellij.plugins.langconfigurator.templates.HulyLanguageServerTemplate;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.configurations.PathEnvironmentVariableUtil;
+import com.intellij.execution.process.OSProcessHandler;
+import com.intellij.execution.process.ProcessAdapter;
+import com.intellij.execution.process.ProcessEvent;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.download.DownloadableFileDescription;
+import com.intellij.util.download.DownloadableFileService;
+import com.intellij.util.download.FileDownloader;
+import com.intellij.util.io.Decompressor;
+import com.redhat.devtools.lsp4ij.LanguageServersRegistry;
+import com.redhat.devtools.lsp4ij.server.definition.launching.UserDefinedLanguageServerDefinition;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.tukaani.xz.XZInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.zip.GZIPInputStream;
+
+public class LanguageServerTemplateInstaller {
+ private static final Logger LOG = Logger.getInstance(LanguageServerTemplateInstaller.class);
+
+ public static void install(@NotNull Project project,
+ @NotNull HulyLanguageServerTemplate template,
+ @NotNull Runnable onSuccess,
+ @NotNull Runnable onFailure) {
+ installBinary(project, template, () -> {
+ addTemplate(project, template);
+ ApplicationManager.getApplication().invokeLater(onSuccess);
+ }, onFailure);
+ }
+
+ public static void installBinary(@NotNull Project project,
+ @NotNull HulyLanguageServerTemplate template,
+ @NotNull Runnable onSuccess,
+ @NotNull Runnable onFailure) {
+ if (template.installCommand != null) {
+ String command = template.installCommand;
+ try {
+ String[] commandParts = command.split(" ");
+ String exePath = PathEnvironmentVariableUtil.findExecutableInWindowsPath(commandParts[0]);
+ GeneralCommandLine commandLine =
+ new GeneralCommandLine().withExePath(exePath).withParameters(Arrays.stream(commandParts).skip(1).toList())
+ .withWorkDirectory(project.getBasePath()).withCharset(StandardCharsets.UTF_8).withRedirectErrorStream(true);
+ LOG.info("Install command: " + commandLine.getCommandLineString());
+ ProcessHandler processHandler = new OSProcessHandler(commandLine);
+ processHandler.addProcessListener(new ProcessAdapter() {
+ @Override
+ public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) {
+ LOG.info(event.getText());
+ super.onTextAvailable(event, outputType);
+ }
+
+ @Override
+ public void processTerminated(@NotNull ProcessEvent event) {
+ if (event.getExitCode() != 0) {
+ LOG.warn("Install command failed with exit code: " + event.getExitCode());
+ ApplicationManager.getApplication().invokeLater(onFailure);
+ }
+ else {
+ ApplicationManager.getApplication().invokeLater(onSuccess);
+ }
+ }
+ });
+ processHandler.startNotify();
+ }
+ catch (ExecutionException e) {
+ LOG.warn("Can't execute install server process", e);
+ ApplicationManager.getApplication().invokeLater(onFailure);
+ }
+ }
+ else if (template.getBinaryUrl() != null) {
+ File directory = new File(PathManager.getConfigPath(), "lsp4ij/" + template.id);
+ if (!directory.exists()) {
+ directory.mkdirs();
+ }
+ DownloadableFileService service = DownloadableFileService.getInstance();
+ String downloadName;
+ try {
+ downloadName = new URL(template.getBinaryUrl()).getFile();
+ }
+ catch (MalformedURLException e) {
+ LOG.warn("Can't parse binary url", e);
+ ApplicationManager.getApplication().invokeLater(onFailure);
+ return;
+ }
+ DownloadableFileDescription description = service.createFileDescription(template.getBinaryUrl(), downloadName);
+ FileDownloader downloader = service.createDownloader(Collections.singletonList(description), downloadName);
+ Task.Backgroundable task = new Task.Backgroundable(project, "Downloading") {
+ @Override
+ public void run(@NotNull ProgressIndicator indicator) {
+ try {
+ List> pairs = downloader.download(directory);
+ Pair first = ContainerUtil.getFirstItem(pairs);
+ File file = first != null ? first.first : null;
+ if (file != null) {
+ decompressBinary(file, directory, template.binaryExecutable);
+ ApplicationManager.getApplication().invokeLater(onSuccess);
+ }
+ }
+ catch (IOException e) {
+ LOG.warn("Can't download language-server binary", e);
+ ApplicationManager.getApplication().invokeLater(onFailure);
+ }
+ }
+ };
+ BackgroundableProcessIndicator processIndicator = new BackgroundableProcessIndicator(task);
+ processIndicator.setIndeterminate(false);
+ ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, processIndicator);
+ }
+ }
+
+ static void addTemplate(@NotNull Project project, @NotNull HulyLanguageServerTemplate template) {
+ String serverId = UUID.randomUUID().toString();
+
+ UserDefinedLanguageServerDefinition definition =
+ new UserDefinedLanguageServerDefinition(
+ serverId,
+ template.name,
+ "",
+ template.getCommandLine(),
+ Collections.emptyMap(),
+ false,
+ template.settingsJson,
+ null,
+ template.initializationOptionsJson,
+ template.clientSettingsJson
+ );
+ LanguageServersRegistry.getInstance().addServerDefinition(project, definition, template.getServerMappingSettings());
+ }
+
+ static void decompressBinary(File archFile, File directory, @Nullable String binaryExecutable) throws IOException {
+ String fileName = archFile.getName().toLowerCase(Locale.ROOT);
+ if (fileName.endsWith(".zip")) {
+ new Decompressor.Zip(archFile.toPath()).extract(directory.toPath());
+ }
+ else if (fileName.endsWith(".tar")) {
+ new Decompressor.Tar(archFile.toPath()).extract(directory.toPath());
+ }
+ else if (fileName.endsWith(".gz")) {
+ Path outFiled = decompressGzip(archFile.toPath());
+ if (outFiled.getFileName().toString().endsWith(".tar")) {
+ new Decompressor.Tar(outFiled).extract(directory.toPath());
+ FileUtil.delete(outFiled);
+ }
+ else {
+ Path resultFile = directory.toPath().resolve(binaryExecutable != null ? binaryExecutable : outFiled.getFileName().toString());
+ Files.move(outFiled, resultFile);
+ FileUtil.setExecutable(resultFile.toFile());
+ }
+ }
+ else if (fileName.endsWith(".xz")) {
+ Path outFiled = decompressXz(archFile.toPath());
+ if (outFiled.getFileName().toString().endsWith(".tar")) {
+ new Decompressor.Tar(outFiled).extract(directory.toPath());
+ FileUtil.delete(outFiled);
+ }
+ else {
+ Path resultFile = directory.toPath().resolve(binaryExecutable != null ? binaryExecutable : outFiled.getFileName().toString());
+ Files.move(outFiled, resultFile);
+ FileUtil.setExecutable(resultFile.toFile());
+ }
+ }
+ else {
+ throw new IOException("Unsupported package type: " + fileName);
+ }
+ FileUtil.delete(archFile);
+ }
+
+ static Path decompressGzip(Path filePath) throws IOException {
+ try (GZIPInputStream gzipInputStream = new GZIPInputStream(new BufferedInputStream(Files.newInputStream(filePath)))) {
+ String fileName = filePath.getFileName().toString();
+ Path outFileName = filePath.getParent().resolve(fileName.substring(0, fileName.lastIndexOf('.')));
+ Files.copy(gzipInputStream, outFileName);
+ return outFileName;
+ }
+ }
+
+ static Path decompressXz(Path filePath) throws IOException {
+ try (XZInputStream gzipInputStream = new XZInputStream(new BufferedInputStream(Files.newInputStream(filePath)))) {
+ String fileName = filePath.getFileName().toString();
+ Path outFileName = filePath.getParent().resolve(fileName.substring(0, fileName.lastIndexOf('.')));
+ Files.copy(gzipInputStream, outFileName);
+ return outFileName;
+ }
+ }
+}
diff --git a/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/messages/HulyLangConfiguratorBundle.java b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/messages/HulyLangConfiguratorBundle.java
new file mode 100644
index 0000000000000..de5ca460a7d05
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/messages/HulyLangConfiguratorBundle.java
@@ -0,0 +1,20 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.hulylabs.intellij.plugins.langconfigurator.messages;
+
+import com.intellij.DynamicBundle;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.PropertyKey;
+
+public final class HulyLangConfiguratorBundle {
+ private static final @NonNls String BUNDLE = "messages.HulyLangConfiguratorBundle";
+ private static final DynamicBundle INSTANCE = new DynamicBundle(HulyLangConfiguratorBundle.class, BUNDLE);
+
+ private HulyLangConfiguratorBundle() {
+ }
+
+ public static @NotNull @Nls String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) {
+ return INSTANCE.getMessage(key, params);
+ }
+}
diff --git a/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyLanguageServerTemplate.java b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyLanguageServerTemplate.java
new file mode 100644
index 0000000000000..52e0ac55a8508
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyLanguageServerTemplate.java
@@ -0,0 +1,43 @@
+// Copyright © 2024 HulyLabs. Use of this source code is governed by the Apache 2.0 license.
+package com.hulylabs.intellij.plugins.langconfigurator.templates;
+
+import com.intellij.openapi.util.SystemInfo;
+import com.intellij.util.system.CpuArch;
+import com.redhat.devtools.lsp4ij.launching.ServerMappingSettings;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class HulyLanguageServerTemplate {
+ private static final String DEFAULT_KEY = "default";
+ private static final String OS_KEY = SystemInfo.isWindows ? "windows" : (SystemInfo.isMac ? "mac" : (SystemInfo.isLinux ? "linux" : ""));
+ private static final String OS_ARCH_KEY = OS_KEY + "-" + (CpuArch.isArm64() ? "aarch64" : "x86_64");
+
+ public String id;
+ public String name;
+ public String installCommand;
+ public Map programArgs;
+ public Map binaryUrls;
+ public String binaryExecutable;
+ public String settingsJson;
+ public String initializationOptionsJson;
+ public String clientSettingsJson;
+ public List mappingSettings;
+
+ @NotNull
+ public String getCommandLine() {
+ return programArgs != null ? programArgs.getOrDefault(OS_KEY, programArgs.getOrDefault(DEFAULT_KEY, "")) : "";
+ }
+
+ @Nullable
+ public String getBinaryUrl() {
+ return binaryUrls != null ? binaryUrls.get(OS_ARCH_KEY) : null;
+ }
+
+ public List getServerMappingSettings() {
+ return mappingSettings.stream().map(HulyServerMappingSettings::toServerMappingSettings).filter(Objects::nonNull).toList();
+ }
+}
diff --git a/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyLanguageServerTemplateManager.java b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyLanguageServerTemplateManager.java
new file mode 100644
index 0000000000000..bf1a13c5a4921
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyLanguageServerTemplateManager.java
@@ -0,0 +1,135 @@
+// Copyright © 2024 HulyLabs. Use of this source code is governed by the Apache 2.0 license.
+package com.hulylabs.intellij.plugins.langconfigurator.templates;
+
+import com.esotericsoftware.yamlbeans.YamlReader;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.JarFileSystem;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+public class HulyLanguageServerTemplateManager {
+ private static final Logger LOG = Logger.getInstance(HulyLanguageServerTemplateManager.class);
+ private static final String TEMPLATES_DIR = "lsp-templates";
+
+ private final Map templatesByExt = new HashMap<>();
+ private final Map templatesByLangName = new HashMap<>();
+
+ private HulyLanguageServerTemplateManager() {
+ VirtualFile templateRoot = getTemplateRoot();
+ if (templateRoot != null) {
+ for (VirtualFile templateDir : templateRoot.getChildren()) {
+ if (templateDir.isDirectory()) {
+ try {
+ HulyLanguageServerTemplate template = importTemplate(templateDir);
+ if (template != null) {
+ template.mappingSettings.forEach(mapping -> {
+ if (mapping.language != null) {
+ templatesByLangName.put(mapping.language, template);
+ }
+ if (mapping.fileType != null) {
+ if (mapping.fileType.patterns != null) {
+ mapping.fileType.patterns.forEach(
+ pattern -> templatesByExt.put(pattern.replaceFirst("\\*\\.", "").toLowerCase(Locale.ROOT), template));
+ }
+ else if (mapping.fileType.name != null) {
+ templatesByLangName.put(mapping.fileType.name, template);
+ }
+ }
+ });
+ }
+ else {
+ LOG.warn(String.format("No template found in %s", templateDir));
+ }
+ }
+ catch (IOException ex) {
+ LOG.warn(ex.getLocalizedMessage(), ex);
+ }
+ }
+ }
+ }
+ else {
+ LOG.warn("No templateRoot found, no templates");
+ }
+ }
+
+ @Nullable
+ public VirtualFile getTemplateRoot() {
+ URL url = HulyLanguageServerTemplateManager.class.getClassLoader().getResource(TEMPLATES_DIR);
+ if (url == null) {
+ LOG.warn("No " + TEMPLATES_DIR + " directory/url found");
+ return null;
+ }
+ try {
+ // url looks like jar:file:/Users/username/Library/Application%20Support//plugins//lib/.jar!/data/lsp-templates
+ String filePart = url.toURI().getRawSchemeSpecificPart(); // get un-decoded, URI compatible part
+ LOG.debug("Templates filePart : {}", filePart);
+ String resourcePath = new URI(filePart).getSchemeSpecificPart();
+ LOG.debug("Templates resources path from uri : {}", resourcePath);
+ return VfsUtil.findFileByURL(url);
+ //if (resourcePath.contains(".jar")) {
+ // return JarFileSystem.getInstance().findFileByPath(resourcePath);
+ //}
+ //else {
+ // return LocalFileSystem.getInstance().findFileByPath(resourcePath);
+ //}
+ }
+ catch (URISyntaxException e) {
+ LOG.warn(e.getMessage());
+ }
+ return null;
+ }
+
+ @Nullable
+ public HulyLanguageServerTemplate getTemplate(@NotNull VirtualFile file) {
+ String ext = file.getExtension();
+ String langName = file.getFileType().getName();
+ return templatesByLangName.getOrDefault(langName, ext != null ? templatesByExt.get(ext.toLowerCase(Locale.ROOT)) : null);
+ }
+
+
+ @Nullable
+ private static HulyLanguageServerTemplate importTemplate(@NotNull VirtualFile templateFolder) throws IOException {
+ VirtualFile templateFile = templateFolder.findChild("template.yaml");
+ if (templateFile == null) return null;
+ HulyLanguageServerTemplate template;
+ try (InputStream stream = templateFile.getInputStream()) {
+ YamlReader reader = new YamlReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
+ template = reader.read(HulyLanguageServerTemplate.class);
+ }
+ VirtualFile clientSettingsFile = templateFolder.findChild("clientSettings.json");
+ if (clientSettingsFile != null) {
+ try (InputStream stream = clientSettingsFile.getInputStream()) {
+ template.clientSettingsJson = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+ VirtualFile initializationOptionsFile = templateFolder.findChild("initializationOptions.json");
+ if (initializationOptionsFile != null) {
+ try (InputStream stream = initializationOptionsFile.getInputStream()) {
+ template.initializationOptionsJson = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+ VirtualFile settingsFile = templateFolder.findChild("settings.json");
+ if (settingsFile != null) {
+ try (InputStream stream = settingsFile.getInputStream()) {
+ template.settingsJson = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+ return template;
+ }
+}
diff --git a/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyServerMappingFileType.java b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyServerMappingFileType.java
new file mode 100644
index 0000000000000..9865146d7dd58
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyServerMappingFileType.java
@@ -0,0 +1,9 @@
+// Copyright © 2024 HulyLabs. Use of this source code is governed by the Apache 2.0 license.
+package com.hulylabs.intellij.plugins.langconfigurator.templates;
+
+import java.util.List;
+
+public class HulyServerMappingFileType {
+ public String name;
+ public List patterns;
+}
diff --git a/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyServerMappingSettings.java b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyServerMappingSettings.java
new file mode 100644
index 0000000000000..d715e810d8132
--- /dev/null
+++ b/plugins/hulylabs.langconfigurator/src/com/hulylabs/intellij/plugins/langconfigurator/templates/HulyServerMappingSettings.java
@@ -0,0 +1,24 @@
+// Copyright © 2024 HulyLabs. Use of this source code is governed by the Apache 2.0 license.
+package com.hulylabs.intellij.plugins.langconfigurator.templates;
+
+import com.redhat.devtools.lsp4ij.launching.ServerMappingSettings;
+import org.jetbrains.annotations.Nullable;
+
+public class HulyServerMappingSettings {
+ public String language;
+ public String languageId;
+ public HulyServerMappingFileType fileType;
+
+ @Nullable
+ public ServerMappingSettings toServerMappingSettings() {
+ if (language != null) {
+ return ServerMappingSettings.createLanguageMappingSettings(language, languageId);
+ } else if (fileType != null && fileType.name != null && fileType.patterns == null) {
+ return ServerMappingSettings.createFileTypeMappingSettings(fileType.name, languageId);
+ } else if (fileType != null && fileType.patterns != null) {
+ return ServerMappingSettings.createFileNamePatternsMappingSettings(fileType.patterns, languageId);
+ } else {
+ return null;
+ }
+ }
+}