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 @@ - + + + + + \ 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 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; + } + } +}