Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ trim_trailing_whitespace = false

[*.scm]
indent_size = 4

[*.mjs]
indent_size = 4
46 changes: 42 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# 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

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
{
Expand All @@ -26,7 +29,7 @@ To make the LSP use a specific `.hxml` configuration, create a `.zed/settings.js
<details>
<summary>Additional settings you may want to pass to the language server: (non-exhaustive)</summary>

[(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
{
Expand All @@ -48,8 +51,43 @@ To make the LSP use a specific `.hxml` configuration, create a `.zed/settings.js
}
}
```

</details>

## 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/).
Expand Down
5 changes: 5 additions & 0 deletions extension.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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", "*", "--", "**"]
Binary file added media/example-gruvbox.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 78 additions & 31 deletions src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
"<https://github.com/Frixuu/Zed-Haxe#usage>"
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",
"<https://github.com/Frixuu/Zed-Haxe#usage>"
)
.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",
"<https://github.com/Frixuu/Zed-Haxe#usage>"
.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",
"<https://github.com/Frixuu/Zed-Haxe#usage>"
)
.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",
"<https://github.com/Frixuu/Zed-Haxe#usage>"
)
.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()
)]);
}
_ => {}
}
Expand Down
70 changes: 70 additions & 0 deletions src/helper_scripts/detect-project-files.mjs
Original file line number Diff line number Diff line change
@@ -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();
1 change: 1 addition & 0 deletions src/helper_scripts/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub const DETECT_PROJECT_FILES: &'static str = include_str!("detect-project-files.mjs");
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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);