diff --git a/.editorconfig b/.editorconfig index 995423a..14d6d9f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,3 +11,6 @@ trim_trailing_whitespace = false [*.scm] indent_size = 4 + +[*.mjs] +indent_size = 4 diff --git a/README.md b/README.md index ee638e2..6fb1c97 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Haxe language support for Zed -> [!NOTE] -> This extension is now published! Open the `Extensions` panel (`Ctrl+Shift+X`) and search for "Haxe". +![Screenshot of Zed editor](media/example-gruvbox.webp) ## Usage @@ -9,7 +8,11 @@ The syntax highlighting should appear immediately. The backing language server should be downloaded automatically in the background. -To make the LSP use a specific `.hxml` configuration, create a `.zed/settings.json` file in your project root: +This extension detects all `.hxml` files in the workspace root, +and then tries to choose the best one as configuration. +This decision might be wrong! +To explicitly use a specific `.hxml` file, +create a `.zed/settings.json` file in your project root: ```json { @@ -26,7 +29,7 @@ To make the LSP use a specific `.hxml` configuration, create a `.zed/settings.js
Additional settings you may want to pass to the language server: (non-exhaustive) - [(reference)](https://github.com/vshaxe/haxe-language-server/blob/9c3114de15bfd8096833ee50aab131459347e3f7/src/haxeLanguageServer/Configuration.hx#L134) + [(reference)](https://github.com/vshaxe/haxe-language-server/blob/65ba91ce13e413fe721d371cdf9e39024a53f2ec/src/haxeLanguageServer/Configuration.hx#L136) ```json { @@ -48,8 +51,43 @@ To make the LSP use a specific `.hxml` configuration, create a `.zed/settings.js } } ``` +
+## Usage (Lime/OpenFL/HaxeFlixel) + +The Haxe language server does not natively understand Lime projects, +supporting `.hxml` files only. +However, Lime lets you easily generate them. + +Assuming that + +- you have a `Project.xml` file in your workspace root, and +- you're targetting `html5`, + +you can run in your favorite shell: + +```sh +lime build html5 +lime display html5 > html5.hxml +``` + +This creates a `html5.hxml` file telling Haxe which libraries to include +and what platform to target. +See [the instructions above](#usage) on how to tell Zed to use this file. + +## Usage (other) + +Many other Haxe toolchains can create `.hxml` files, including: + +```sh +nme prepare html5 +``` + +```sh +ceramic clay hxml web > web.hxml +``` + ## Install nightly To use dev Zed extensions, you will need to have [Rust compiler installed](https://rustup.rs/). diff --git a/extension.toml b/extension.toml index 3351f57..05b10fc 100644 --- a/extension.toml +++ b/extension.toml @@ -17,3 +17,8 @@ commit = "6199e87f50afedab695de1c835430da424ed4ca9" [language_servers.haxe-language-server] name = "Haxe Language Server" languages = ["Haxe", "HXML"] + +[[capabilities]] +kind = "process:exec" +command = "*" +args = ["-e", "*", "--", "**"] diff --git a/media/example-gruvbox.webp b/media/example-gruvbox.webp new file mode 100644 index 0000000..7652827 Binary files /dev/null and b/media/example-gruvbox.webp differ diff --git a/src/extension.rs b/src/extension.rs index 7e265b2..dbbf10f 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -10,7 +10,10 @@ use zed_extension_api::{ Result, Worktree, }; -use crate::language_server::{self}; +use crate::{ + helper_scripts, + language_server::{self}, +}; pub struct HaxeExtension { language_server_binary_path: Option, @@ -65,7 +68,7 @@ impl zed::Extension for HaxeExtension { // Try to fetch the latest version of the language server and download it let version = match language_server::download_from_gh_if_missing(self, Some(id)) { Ok(version) => { - let last_version_path = self.working_dir.join("version.txt"); + let last_version_path = (&self.working_dir).join("version.txt"); std::fs::write(last_version_path, &version).ok(); self.language_server_version = Some(version.clone()); version @@ -120,39 +123,83 @@ impl zed::Extension for HaxeExtension { Value::Null => { // Ideally, the language server has *some* .hxml config file present. // The problem is, Zed does not provide a way to list files in the worktree. - // This makes it impossible to find the correct .hxml file to use as config. - - // However, we can check for existence of files we know exact names of, - // which can tell us that the current setup is completely wrong: + // We need to delegate to a Node script: + let mut command = zed::Command { + command: zed::node_binary_path()?, + args: vec![ + "-e".to_string(), // Eval mode + helper_scripts::DETECT_PROJECT_FILES.to_string(), // Inline the script + "--".to_string(), // Custom args + trim_leading_slash_on_windows(worktree.root_path()), // Where to look + ], + env: vec![], + }; - if worktree.read_text_file("Project.xml").is_ok() { - return Err(concat!( - "Lime/OpenFL projects are not fully supported at this time!\n\n", - "You should generate a .hxml file using `lime display html5 > html5.hxml`\n", - "and consult the documentation for how to pass it to the language server:\n", - "" + match command.output() { + Ok(output) => { + let status_code = output.status.unwrap_or(0); + let file_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + match status_code { + 0 => { + // As a fallback, use our (almost) blank, default config + // we created while our extension was loading: + *display_args = json!([trim_leading_slash_on_windows( + self.working_dir() + .join("default-config.hxml") + .to_string_lossy() + .to_string() + )]); + } + 101 => { + // HXML file found + *display_args = json!([file_path]); + } + 102 => { + // Lime's XML file found + return Err(concat!( +"Lime/OpenFL projects are not fully supported at this time!\n\n", +"You should generate a .hxml file using `lime display html5 > html5.hxml`\n", +"and consult the documentation for how to pass it to the language server:\n", +"" ) - .into()); - } - - if worktree.read_text_file("ceramic.yml").is_ok() { - return Err(concat!( - "Ceramic projects are not fully supported at this time!\n\n", - "You should generate a .hxml file using `ceramic clay hxml web > web.hxml`\n", - "and consult the documentation for how to pass it to the language server:\n", - "" + .to_string()); + } + 103 => { + // Ceramic's YML file found + return Err(concat!( +"Ceramic projects are not fully supported at this time!\n\n", +"You should generate a .hxml file using `ceramic clay hxml web > web.hxml`\n", +"and consult the documentation for how to pass it to the language server:\n", +"" + ) + .to_string()); + } + 104 => { + // NME's NMML file found + return Err(concat!( +"NME projects are not fully supported at this time!\n\n", +"You should run `nme prepare`, which generates a `bin/[platform]/haxe/build.hxml` file\n", +"and consult the documentation for how to pass it to the language server:\n", +"" ) - .into()); + .to_string()); + } + _ => { + return Err(format!( + "Internal error in discover script (return code {}):\n\nstderr:\n{}\nstdout:\n{}", + status_code, + String::from_utf8_lossy(&output.stderr), + String::from_utf8_lossy(&output.stdout) + )); + } + } + } + Err(e) => { + return Err(format!( + "Could not run the project discover script:\n\n{e:?}" + )); + } } - - // As a fallback, use our (almost) blank, default config - // we created while our extension was loading: - *display_args = json!([trim_leading_slash_on_windows( - self.working_dir() - .join("default-config.hxml") - .to_string_lossy() - .to_string() - )]); } _ => {} } diff --git a/src/helper_scripts/detect-project-files.mjs b/src/helper_scripts/detect-project-files.mjs new file mode 100644 index 0000000..910f9aa --- /dev/null +++ b/src/helper_scripts/detect-project-files.mjs @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +import { readdir } from "node:fs/promises"; +import process, { argv, chdir, stderr, stdout } from "node:process"; + +const workspacePath = argv[1]; // Node strips everything before "--" +chdir(workspacePath); + +const SCORES = [ + { sub: "include", delta: -50 }, + { sub: "test", delta: -10 }, + { sub: "debug", delta: -10 }, + { sub: "sample", delta: -5 }, + { sub: "build", delta: 30 }, + { sub: "compile", delta: 30 }, + { sub: "main", delta: 50 }, + { sub: "lsp", delta: 100 }, + { sub: "devenv", delta: 100 }, +]; + +const entries = await readdir(workspacePath, { withFileTypes: true }); +const files = entries.filter((e) => e.isFile()).map((e) => e.name); + +const hxmlPaths = []; +for (const relativePath of files.filter((p) => + p.toLowerCase().endsWith(".hxml") +)) { + let score = 0; + const pathLowerCase = relativePath.toLowerCase(); + if (pathLowerCase === "extraparams.hxml") { + continue; + } + for (const { sub, delta } of SCORES) { + if (pathLowerCase.includes(sub)) { + score += delta; + } + } + hxmlPaths.push({ relativePath, score }); +} + +// Calling `process.exit` is discouraged as it may not flush stdout writes +process.exitCode = (() => { + hxmlPaths.sort((a, b) => b.score - a.score); + if (hxmlPaths.length >= 1) { + stdout.write(hxmlPaths[0].relativePath, "utf8"); + return 101; + } + + const projectXml = files.find((p) => p.toLowerCase() === "project.xml"); + if (projectXml) { + stdout.write(projectXml, "utf8"); + return 102; + } + + const ceramicYml = files.find((p) => p.toLowerCase() === "ceramic.yml"); + if (ceramicYml) { + stdout.write(ceramicYml, "utf8"); + return 103; + } + + const nmml = files.find((p) => p.toLowerCase().endsWith(".nmml")); + if (nmml) { + stdout.write(nmml, "utf8"); + return 104; + } + + return 0; +})(); + +stderr.end(); +stdout.end(); diff --git a/src/helper_scripts/mod.rs b/src/helper_scripts/mod.rs new file mode 100644 index 0000000..1b64ac0 --- /dev/null +++ b/src/helper_scripts/mod.rs @@ -0,0 +1 @@ +pub const DETECT_PROJECT_FILES: &'static str = include_str!("detect-project-files.mjs"); diff --git a/src/lib.rs b/src/lib.rs index b0370c2..f5b9e4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ use zed_extension_api::{self as zed}; mod extension; +mod helper_scripts; mod language_server; zed::register_extension!(crate::extension::HaxeExtension);