From 7c3ed5ae87d5fb0422b0df684f7d8338fd33f243 Mon Sep 17 00:00:00 2001 From: Vasiliy Kulikov Date: Thu, 10 Sep 2026 21:11:46 +0300 Subject: [PATCH 01/10] Support .tar.xz archives in the Mason installer GitHub-release assets packaged as .tar.xz (e.g. zls) silently failed to extract since only .tar.gz and .zip were handled, leaving an empty install directory and a misleading "did not produce a runnable executable" error. Decode via the pure-Rust lzma-rs crate, reusing the existing per-entry tar unpacking and a bounded writer to keep the same decompression-bomb guard the other formats already have. --- Cargo.lock | 32 +++++++++++++++++++++ Cargo.toml | 1 + src/mason/install/artifacts.rs | 51 ++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index cffea46..331b617 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -220,6 +226,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.0" @@ -783,6 +804,7 @@ dependencies = [ "fs_extra", "humantime", "lsp-types", + "lzma-rs", "regex", "reqwest", "serde", @@ -811,6 +833,16 @@ dependencies = [ "serde_repr", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "memchr" version = "2.8.0" diff --git a/Cargo.toml b/Cargo.toml index fb1982a..9282c7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ command-group = "5.0.1" flate2 = "1.1.5" humantime = "2.3.0" lsp-types = "0.97.0" +lzma-rs = "0.3.0" regex = "1.11.1" reqwest = { version = "0.12.24", default-features = false, features = ["blocking", "json", "rustls-tls"] } serde = { version = "1.0.228", features = ["derive"] } diff --git a/src/mason/install/artifacts.rs b/src/mason/install/artifacts.rs index b4d18ea..38e940c 100644 --- a/src/mason/install/artifacts.rs +++ b/src/mason/install/artifacts.rs @@ -259,6 +259,8 @@ pub(super) fn install_downloaded_artifact( if relative_name_lower.ends_with(".tar.gz") { extract_tar_gz(root, bytes) + } else if relative_name_lower.ends_with(".tar.xz") { + extract_tar_xz(root, bytes) } else if extension.is_some_and(|value| value.eq_ignore_ascii_case("zip")) { extract_zip(root, bytes) } else if extension.is_some_and(|value| value.eq_ignore_ascii_case("gz")) { @@ -272,6 +274,55 @@ pub(super) fn install_downloaded_artifact( fn extract_tar_gz(root: &Path, bytes: &[u8]) -> Result<()> { let reader = GzDecoder::new(Cursor::new(bytes)); + unpack_tar(root, reader) +} + +fn extract_tar_xz(root: &Path, bytes: &[u8]) -> Result<()> { + let mut decompressed = BoundedWriter::new(MAX_DECOMPRESSED_BYTES); + lzma_rs::xz_decompress(&mut Cursor::new(bytes), &mut decompressed).map_err( + format_root_error("failed to decompress downloaded xz archive in", root), + )?; + unpack_tar(root, Cursor::new(decompressed.into_inner())) +} + +/// A `Write` sink that errors once more than `limit` bytes have been written, guarding xz +/// decompression (which lzma-rs only exposes as "decompress fully into a sink") against +/// decompression bombs the way the streaming tar.gz/zip paths already are per-entry. +struct BoundedWriter { + buffer: Vec, + limit: u64, +} + +impl BoundedWriter { + fn new(limit: u64) -> Self { + Self { + buffer: Vec::new(), + limit, + } + } + + fn into_inner(self) -> Vec { + self.buffer + } +} + +impl std::io::Write for BoundedWriter { + fn write(&mut self, data: &[u8]) -> std::io::Result { + if self.buffer.len() as u64 + data.len() as u64 > self.limit { + return Err(std::io::Error::other( + "decompressed archive exceeds size limit", + )); + } + self.buffer.extend_from_slice(data); + Ok(data.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +fn unpack_tar(root: &Path, reader: impl Read) -> Result<()> { let mut archive = Archive::new(reader); for entry in archive.entries().map_err(format_root_error( "failed to open downloaded tar archive in", From b23dea94641dbfe893ec28adfaed98b5a03c8d28 Mon Sep 17 00:00:00 2001 From: Vasiliy Kulikov Date: Thu, 10 Sep 2026 21:12:25 +0300 Subject: [PATCH 02/10] Promote zig/zls to full real-server E2E query coverage Add zig and ruby runtimes to scripts/download_dev_env.sh / activate.sh (ruby-builder's prebuilt binaries need their shebang rewritten and LD_LIBRARY_PATH/RUBYLIB set, since RUNPATH and the default $LOAD_PATH are baked in for GitHub Actions' hostedtoolcache layout; mirrored via a new ruby_env case in the E2E harness alongside the existing dotnet_root one). Give zig a real fixture project (build.zig + main.zig) and promote its language kind from metadata to source with a query profile, since `capabilities`-only smoke is only valid for metadata languages - `queries` requires an actual project to run references/definition/ callers/etc against. zls has no background-work progress signal, so build-index gets the same failure exception as clangd/pyright/etc. Also start a ruby fixture (lib/order.rb, lib/report.rb, Gemfile) as a head start for later - ruby's LSP servers (solargraph, rubocop, sorbet, standardrb, steep) all install via `pkg:gem`, which the Mason installer doesn't support yet, so ruby.yaml stays on capabilities-only for now. --- activate.sh | 21 ++++++++++++- playground/ruby/Gemfile | 3 ++ playground/ruby/lib/order.rb | 36 ++++++++++++++++++++++ playground/ruby/lib/report.rb | 3 ++ playground/ruby/main.rb | 5 ++++ playground/zig/build.zig | 16 ++++++++++ playground/zig/main.zig | 49 ++++++++++++++++++++++++++++++ scripts/download_dev_env.sh | 56 +++++++++++++++++++++++++++++++++-- tests/e2e/cases/zig.yaml | 22 ++++++++++++-- tests/e2e/harness.rs | 41 +++++++++++++++++++++++++ 10 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 playground/ruby/Gemfile create mode 100644 playground/ruby/lib/order.rb create mode 100644 playground/ruby/lib/report.rb create mode 100644 playground/zig/build.zig diff --git a/activate.sh b/activate.sh index 8e1fe48..bc58a6d 100644 --- a/activate.sh +++ b/activate.sh @@ -1,6 +1,25 @@ # Source this from the repo root to put the tools installed by `make download-dev-env` -# (go, java, node, dotnet) on PATH: `source activate.sh` +# (go, java, node, dotnet, zig, ruby) on PATH: `source activate.sh` case ":$PATH:" in *":$PWD/.env/bin:"*) ;; *) export PATH="$PWD/.env/bin:$PATH" ;; esac + +# ruby-builder's prebuilt ruby needs its lib dir on LD_LIBRARY_PATH (RUNPATH points at the +# hosted-toolcache path it was built for) and RUBYLIB (default $LOAD_PATH is baked in too). +if [[ -d "$PWD/.env/ruby/lib" ]]; then + export LD_LIBRARY_PATH="$PWD/.env/ruby/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + rubylib_entries=() + for dir in "$PWD"/.env/ruby/lib/ruby/*/; do + [[ -d "$dir" ]] || continue + rubylib_entries+=("${dir%/}") + for arch_dir in "$dir"*linux*/; do + [[ -d "$arch_dir" ]] && rubylib_entries+=("${arch_dir%/}") + done + done + if (( ${#rubylib_entries[@]} > 0 )); then + rubylib_joined=$(IFS=:; echo "${rubylib_entries[*]}") + export RUBYLIB="$rubylib_joined${RUBYLIB:+:$RUBYLIB}" + fi + unset rubylib_entries rubylib_joined dir arch_dir +fi diff --git a/playground/ruby/Gemfile b/playground/ruby/Gemfile new file mode 100644 index 0000000..99ae841 --- /dev/null +++ b/playground/ruby/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +ruby ">= 3.0" diff --git a/playground/ruby/lib/order.rb b/playground/ruby/lib/order.rb new file mode 100644 index 0000000..b92ceed --- /dev/null +++ b/playground/ruby/lib/order.rb @@ -0,0 +1,36 @@ +class OrderItem + attr_reader :name, :quantity, :price + + def initialize(name, quantity, price) + @name = name + @quantity = quantity + @price = price + end + + def total + quantity * price + end +end + +class Order + attr_reader :customer, :items + + def initialize(customer, items) + @customer = customer + @items = items + end + + def total + items.sum(&:total) + end +end + +def build_sample_order + Order.new( + "Carol", + [ + OrderItem.new("Mouse", 1, 35.0), + OrderItem.new("Pad", 1, 12.5) + ] + ) +end diff --git a/playground/ruby/lib/report.rb b/playground/ruby/lib/report.rb new file mode 100644 index 0000000..5972d77 --- /dev/null +++ b/playground/ruby/lib/report.rb @@ -0,0 +1,3 @@ +def format_order(order) + "#{order.customer} has #{order.items.length} items worth #{format('%.2f', order.total)}" +end diff --git a/playground/ruby/main.rb b/playground/ruby/main.rb index e69de29..d2f714e 100644 --- a/playground/ruby/main.rb +++ b/playground/ruby/main.rb @@ -0,0 +1,5 @@ +require_relative "lib/order" +require_relative "lib/report" + +order = build_sample_order +puts format_order(order) diff --git a/playground/zig/build.zig b/playground/zig/build.zig new file mode 100644 index 0000000..30ee240 --- /dev/null +++ b/playground/zig/build.zig @@ -0,0 +1,16 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "playground", + .root_module = b.createModule(.{ + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + }), + }); + b.installArtifact(exe); +} diff --git a/playground/zig/main.zig b/playground/zig/main.zig index e69de29..f3cb6c7 100644 --- a/playground/zig/main.zig +++ b/playground/zig/main.zig @@ -0,0 +1,49 @@ +const std = @import("std"); + +const OrderItem = struct { + name: []const u8, + quantity: u32, + price: f64, + + fn total(self: OrderItem) f64 { + return @as(f64, @floatFromInt(self.quantity)) * self.price; + } +}; + +const Order = struct { + customer: []const u8, + items: []const OrderItem, + + fn total(self: Order) f64 { + var sum: f64 = 0; + for (self.items) |item| { + sum += item.total(); + } + return sum; + } +}; + +fn sample_order() Order { + return Order{ + .customer = "Carol", + .items = &[_]OrderItem{ + OrderItem{ .name = "Mouse", .quantity = 1, .price = 35.0 }, + OrderItem{ .name = "Pad", .quantity = 1, .price = 12.5 }, + }, + }; +} + +fn format_order(order: Order, buffer: []u8) ![]u8 { + return std.fmt.bufPrint(buffer, "{s} has {d} items worth {d:.2}", .{ + order.customer, + order.items.len, + order.total(), + }); +} + +pub fn main() !void { + const order = sample_order(); + var buffer: [128]u8 = undefined; + const report = try format_order(order, &buffer); + std.debug.print("{s}\n", .{report}); +} diff --git a/scripts/download_dev_env.sh b/scripts/download_dev_env.sh index 9cfdb7d..7592f47 100755 --- a/scripts/download_dev_env.sh +++ b/scripts/download_dev_env.sh @@ -1,12 +1,13 @@ #!/usr/bin/env bash # -# Downloads the external runtimes needed by the real-server E2E tests (go, java, node, dotnet) -# into a project-local .env/ directory, so contributors don't need to install them system-wide. +# Downloads the external runtimes needed by the real-server E2E tests (go, java, node, dotnet, +# zig, ruby) into a project-local .env/ directory, so contributors don't need to install them +# system-wide. # # Default versions mirror .github/workflows/ci.yml / e2e.yml (actions/setup-go, # actions/setup-java, actions/setup-node, actions/setup-dotnet). Keep them in sync manually if # CI's pins change; override per-tool via GO_VERSION / JAVA_VERSION / NODE_VERSION / -# DOTNET_CHANNEL. +# DOTNET_CHANNEL / ZIG_VERSION / RUBY_VERSION. # # Usage: scripts/download_dev_env.sh # Then: source activate.sh (from the repo root) @@ -17,6 +18,8 @@ GO_VERSION="${GO_VERSION:-1.27.1}" JAVA_VERSION="${JAVA_VERSION:-21}" NODE_VERSION="${NODE_VERSION:-24}" DOTNET_CHANNEL="${DOTNET_CHANNEL:-10.0}" +ZIG_VERSION="${ZIG_VERSION:-0.15.2}" +RUBY_VERSION="${RUBY_VERSION:-3.3.6}" repo_root=$(git rev-parse --show-toplevel) env_dir="$repo_root/.env" @@ -141,10 +144,57 @@ install_dotnet() { link "$tool_dir/dotnet" dotnet } +install_zig() { + local tool_dir="$env_dir/zig" + if is_up_to_date "$tool_dir" "$ZIG_VERSION"; then + echo "zig $ZIG_VERSION already installed, skipping" + return + fi + echo "Installing zig $ZIG_VERSION..." + local archive="$tmp_dir/zig.tar.xz" + curl -fsSL -o "$archive" "https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-${os}-${ZIG_VERSION}.tar.xz" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xJf "$archive" -C "$tool_dir" --strip-components=1 + echo "$ZIG_VERSION" > "$(version_stamp "$tool_dir")" + link "$tool_dir/zig" zig +} + +install_ruby() { + # Prebuilt CRuby from ruby/ruby-builder (same source actions/setup-ruby uses), matched to + # this host's Ubuntu release so the dynamically linked build actually runs. + local tool_dir="$env_dir/ruby" + if is_up_to_date "$tool_dir" "$RUBY_VERSION"; then + echo "ruby $RUBY_VERSION already installed, skipping" + return + fi + local ubuntu_codename + ubuntu_codename=$(. /etc/os-release && echo "$VERSION_ID") + echo "Installing ruby $RUBY_VERSION (ubuntu-${ubuntu_codename})..." + local archive="$tmp_dir/ruby.tar.gz" + curl -fsSL -o "$archive" "https://github.com/ruby/ruby-builder/releases/download/ruby-${RUBY_VERSION}/ruby-${RUBY_VERSION}-ubuntu-${ubuntu_codename}-x64.tar.gz" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xzf "$archive" -C "$tool_dir" --strip-components=1 + # ruby-builder bakes its hosted-toolcache build path into every bin/ shebang and into the + # binary's RUNPATH; rewrite the shebangs to this install's real ruby, and rely on + # LD_LIBRARY_PATH (set by the E2E harness / activate.sh) rather than RUNPATH for libruby. + for script in "$tool_dir"/bin/*; do + [[ -f "$script" ]] || continue + sed -i "1s|^#!.*/bin/ruby\$|#!${tool_dir}/bin/ruby|" "$script" + done + echo "$RUBY_VERSION" > "$(version_stamp "$tool_dir")" + link "$tool_dir/bin/ruby" ruby + link "$tool_dir/bin/gem" gem + link "$tool_dir/bin/bundle" bundle +} + install_go install_java install_node install_dotnet +install_zig +install_ruby echo echo "Dev env ready under $env_dir" diff --git a/tests/e2e/cases/zig.yaml b/tests/e2e/cases/zig.yaml index efb6e48..a9a3ade 100644 --- a/tests/e2e/cases/zig.yaml +++ b/tests/e2e/cases/zig.yaml @@ -1,9 +1,27 @@ language: id: zig - kind: metadata + kind: source project: playground/zig + query-profile: + symbol-query: Order + callable-query: sample_order + format-file: main.zig + expected-names: + - Order + - OrderItem + - sample_order + - format_order pairs: - language: zig server: zls smoke: - status: capabilities + status: queries + exceptions: + - command: build-index + outcome: failure + message: background-work progress + reason: zls exposes no background-work progress signal + lifecycle: + status: scenarios + direct-run: + status: exchange diff --git a/tests/e2e/harness.rs b/tests/e2e/harness.rs index 9a8edea..b69baf0 100644 --- a/tests/e2e/harness.rs +++ b/tests/e2e/harness.rs @@ -42,6 +42,15 @@ pub(crate) struct E2eContext { // The staged `dotnet` apphost resolves its runtime via DOTNET_ROOT rather than PATH, so its // install root must be threaded through explicitly once `stage_host_program` resolves it. dotnet_root: RefCell>, + // Prebuilt ruby-builder binaries carry a hard-coded RUNPATH and default $LOAD_PATH baked in + // at build time, which don't match wherever this sandbox happens to stage them; + // LD_LIBRARY_PATH/RUBYLIB are the overrides. + ruby_env: RefCell>, +} + +struct RubyEnv { + lib_dir: PathBuf, + rubylib: std::ffi::OsString, } pub(crate) struct E2eOutput { @@ -93,6 +102,7 @@ impl E2eContext { build_dir, data_dir: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data"), dotnet_root: RefCell::new(None), + ruby_env: RefCell::new(None), }) } @@ -184,6 +194,32 @@ impl E2eContext { { *self.dotnet_root.borrow_mut() = Some(root.to_path_buf()); } + if name == "ruby" + && let Some(root) = resolved.parent().and_then(Path::parent) + { + let lib_dir = root.join("lib"); + let stdlib_root = lib_dir.join("ruby"); + let mut rubylib_entries = Vec::new(); + if let Ok(versions) = std::fs::read_dir(&stdlib_root) { + for version_dir in versions.flatten().map(|entry| entry.path()) { + rubylib_entries.push(version_dir.clone()); + if let Ok(archs) = std::fs::read_dir(&version_dir) { + rubylib_entries.extend(archs.flatten().map(|entry| entry.path()).filter( + |path| { + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.contains("linux")) + }, + )); + } + } + } + if lib_dir.is_dir() && !rubylib_entries.is_empty() { + let rubylib = std::env::join_paths(&rubylib_entries) + .map_err(|error| format!("failed to build RUBYLIB for staged ruby: {error}"))?; + *self.ruby_env.borrow_mut() = Some(RubyEnv { lib_dir, rubylib }); + } + } self.link_host_program(&resolved, &self.bin_dir.join(name)) .map_err(|error| { @@ -272,6 +308,11 @@ impl E2eContext { if let Some(root) = self.dotnet_root.borrow().as_deref() { command.env("DOTNET_ROOT", root); } + if let Some(ruby_env) = self.ruby_env.borrow().as_ref() { + command + .env("LD_LIBRARY_PATH", &ruby_env.lib_dir) + .env("RUBYLIB", &ruby_env.rubylib); + } command } From d513559ea1bbb51cb6b80f35c7e4fb3689335e17 Mon Sep 17 00:00:00 2001 From: Vasiliy Kulikov Date: Thu, 10 Sep 2026 21:22:02 +0300 Subject: [PATCH 03/10] Bump data submodule for zig preferred-server entry --- data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data b/data index 27c97a8..b407ddc 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 27c97a864a4de20181f1098438e1582ffb187b9a +Subproject commit b407ddc2c10caf86c6a0443ee1b6315e66e02e5a From 97e7add36bd4c79bae2d68443a45e67b48580c53 Mon Sep 17 00:00:00 2001 From: Vasiliy Kulikov Date: Thu, 10 Sep 2026 21:42:26 +0300 Subject: [PATCH 04/10] Promote perl/perlnavigator to full real-server E2E query coverage Give perl a real fixture project (lib/Order.pm, lib/OrderItem.pm, lib/Report.pm, main.pl) and promote its language kind from metadata to source with a query profile. Declare node/npm/perl as host programs for perlnavigator's provisioning (it's an npm package that shells out to a system perl for compilation checks and Perl::Critic), and except definition (perlnavigator doesn't resolve goto-definition for a sub called from the same file it's defined in) and build-index (no background-work progress signal, same as several other servers). --- data | 2 +- playground/perl/lib/Order.pm | 18 ++++++++++++++++++ playground/perl/lib/OrderItem.pm | 11 +++++++++++ playground/perl/lib/Report.pm | 18 ++++++++++++++++++ playground/perl/main.pl | 17 +++++++++++++++++ tests/e2e/cases/perl.yaml | 25 +++++++++++++++++++++++-- tests/e2e/cases/suite.yaml | 13 +++++++++++++ 7 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 playground/perl/lib/Order.pm create mode 100644 playground/perl/lib/OrderItem.pm create mode 100644 playground/perl/lib/Report.pm diff --git a/data b/data index b407ddc..e48460e 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit b407ddc2c10caf86c6a0443ee1b6315e66e02e5a +Subproject commit e48460eec6c575d482495209fdc8625d654d3eb3 diff --git a/playground/perl/lib/Order.pm b/playground/perl/lib/Order.pm new file mode 100644 index 0000000..034c750 --- /dev/null +++ b/playground/perl/lib/Order.pm @@ -0,0 +1,18 @@ +package Order; + +use strict; +use warnings; + +sub new { + my ($class, $customer, $items) = @_; + return bless { customer => $customer, items => $items }, $class; +} + +sub total { + my ($self) = @_; + my $sum = 0; + $sum += $_->{quantity} * $_->{price} for @{ $self->{items} }; + return $sum; +} + +1; diff --git a/playground/perl/lib/OrderItem.pm b/playground/perl/lib/OrderItem.pm new file mode 100644 index 0000000..06711ed --- /dev/null +++ b/playground/perl/lib/OrderItem.pm @@ -0,0 +1,11 @@ +package OrderItem; + +use strict; +use warnings; + +sub new { + my ($class, $name, $quantity, $price) = @_; + return bless { name => $name, quantity => $quantity, price => $price }, $class; +} + +1; diff --git a/playground/perl/lib/Report.pm b/playground/perl/lib/Report.pm new file mode 100644 index 0000000..2e3c277 --- /dev/null +++ b/playground/perl/lib/Report.pm @@ -0,0 +1,18 @@ +package Report; + +use strict; +use warnings; +use Exporter qw(import); + +our @EXPORT_OK = qw(format_order); + +sub format_order { + my ($order) = @_; + my $count = scalar @{ $order->{items} }; + return sprintf( + "%s has %d items worth %.2f", + $order->{customer}, $count, $order->total + ); +} + +1; diff --git a/playground/perl/main.pl b/playground/perl/main.pl index e69de29..7319594 100644 --- a/playground/perl/main.pl +++ b/playground/perl/main.pl @@ -0,0 +1,17 @@ +use strict; +use warnings; +use lib 'lib'; +use Order; +use OrderItem; +use Report qw(format_order); + +sub build_sample_order { + my @items = ( + OrderItem->new("Mouse", 1, 35.0), + OrderItem->new("Pad", 1, 12.5), + ); + return Order->new("Carol", \@items); +} + +my $order = build_sample_order(); +print format_order($order), "\n"; diff --git a/tests/e2e/cases/perl.yaml b/tests/e2e/cases/perl.yaml index 21015c3..ca67c57 100644 --- a/tests/e2e/cases/perl.yaml +++ b/tests/e2e/cases/perl.yaml @@ -1,9 +1,30 @@ language: id: perl - kind: metadata + kind: source project: playground/perl + query-profile: + symbol-query: Order + callable-query: build_sample_order + format-file: main.pl + expected-names: + - Order + - OrderItem + - build_sample_order + - format_order pairs: - language: perl server: perlnavigator smoke: - status: capabilities + status: queries + exceptions: + - command: definition + outcome: empty-matches + reason: perlnavigator does not resolve goto-definition for a sub called from the same file it is defined in + - command: build-index + outcome: failure + message: background-work progress + reason: perlnavigator exposes no background-work progress signal + lifecycle: + status: scenarios + direct-run: + status: exchange diff --git a/tests/e2e/cases/suite.yaml b/tests/e2e/cases/suite.yaml index 2b33e3b..8931389 100644 --- a/tests/e2e/cases/suite.yaml +++ b/tests/e2e/cases/suite.yaml @@ -986,6 +986,19 @@ servers: owner-language: perl provisioning: status: download + host-programs: + - name: node + resolve: + - which + - node + - name: npm + resolve: + - which + - npm + - name: perl + resolve: + - which + - perl - id: perlpls owner-language: perl provisioning: From 852fc0780bd2c3bac10033dafc2b6482e5477a25 Mon Sep 17 00:00:00 2001 From: Vasiliy Kulikov Date: Thu, 10 Sep 2026 22:09:51 +0300 Subject: [PATCH 05/10] Split scripts/download_dev_env.sh per-language installers into includes The single file was growing by one install_ function per new E2E runtime; move each into scripts/download_dev_env/.inc (sourced by the main script, which keeps the shared helpers and orchestration) so adding a language is a new small file instead of a bigger diff to one. --- scripts/download_dev_env.sh | 132 +++------------------------- scripts/download_dev_env/dotnet.inc | 12 +++ scripts/download_dev_env/go.inc | 16 ++++ scripts/download_dev_env/java.inc | 19 ++++ scripts/download_dev_env/node.inc | 27 ++++++ scripts/download_dev_env/ruby.inc | 28 ++++++ scripts/download_dev_env/zig.inc | 15 ++++ 7 files changed, 127 insertions(+), 122 deletions(-) create mode 100644 scripts/download_dev_env/dotnet.inc create mode 100644 scripts/download_dev_env/go.inc create mode 100644 scripts/download_dev_env/java.inc create mode 100644 scripts/download_dev_env/node.inc create mode 100644 scripts/download_dev_env/ruby.inc create mode 100644 scripts/download_dev_env/zig.inc diff --git a/scripts/download_dev_env.sh b/scripts/download_dev_env.sh index 7592f47..60ab9d6 100755 --- a/scripts/download_dev_env.sh +++ b/scripts/download_dev_env.sh @@ -9,6 +9,9 @@ # CI's pins change; override per-tool via GO_VERSION / JAVA_VERSION / NODE_VERSION / # DOTNET_CHANNEL / ZIG_VERSION / RUBY_VERSION. # +# Each runtime's installer lives in scripts/download_dev_env/.inc; add a new one there and +# source+call it below to add another language. +# # Usage: scripts/download_dev_env.sh # Then: source activate.sh (from the repo root) @@ -24,6 +27,7 @@ RUBY_VERSION="${RUBY_VERSION:-3.3.6}" repo_root=$(git rev-parse --show-toplevel) env_dir="$repo_root/.env" bin_dir="$env_dir/bin" +inc_dir="$repo_root/scripts/download_dev_env" tmp_dir=$(mktemp -d) trap 'rm -rf "$tmp_dir"' EXIT @@ -66,128 +70,12 @@ link() { ln -sf "$target" "$bin_dir/$name" } -install_go() { - local tool_dir="$env_dir/go" - if is_up_to_date "$tool_dir" "$GO_VERSION"; then - echo "go $GO_VERSION already installed, skipping" - return - fi - echo "Installing go $GO_VERSION..." - local archive="$tmp_dir/go.tar.gz" - curl -fsSL -o "$archive" "https://go.dev/dl/go${GO_VERSION}.${os}-${arch/x64/amd64}.tar.gz" - rm -rf "$tool_dir" - mkdir -p "$tool_dir" - tar -xzf "$archive" -C "$tool_dir" --strip-components=1 - echo "$GO_VERSION" > "$(version_stamp "$tool_dir")" - link "$tool_dir/bin/go" go - link "$tool_dir/bin/gofmt" gofmt -} - -install_java() { - local tool_dir="$env_dir/java" - local api_url="https://api.adoptium.net/v3/binary/latest/${JAVA_VERSION}/ga/${os}/${arch}/jdk/hotspot/normal/eclipse" - local resolved_version - resolved_version=$(curl -fsSIL -o /dev/null -w '%{url_effective}' "$api_url") - if is_up_to_date "$tool_dir" "$resolved_version"; then - echo "java (Temurin $JAVA_VERSION) already installed, skipping" - return - fi - echo "Installing java (Temurin $JAVA_VERSION)..." - local archive="$tmp_dir/java.tar.gz" - curl -fsSL -o "$archive" "$api_url" - rm -rf "$tool_dir" - mkdir -p "$tool_dir" - tar -xzf "$archive" -C "$tool_dir" --strip-components=1 - echo "$resolved_version" > "$(version_stamp "$tool_dir")" - link "$tool_dir/bin/java" java - link "$tool_dir/bin/javac" javac -} - -install_node() { - local tool_dir="$env_dir/node" - local index_url="https://nodejs.org/dist/index.json" - local resolved_version - resolved_version=$(curl -fsSL "$index_url" \ - | grep -o "\"version\":\"v${NODE_VERSION}\.[0-9]*\.[0-9]*\"" \ - | head -n1 \ - | sed -E 's/.*"v([0-9.]+)".*/\1/' || true) - if [[ -z "$resolved_version" ]]; then - echo "download-dev-env: could not resolve latest node ${NODE_VERSION}.x from $index_url" >&2 - exit 1 - fi - if is_up_to_date "$tool_dir" "$resolved_version"; then - echo "node $resolved_version already installed, skipping" - return - fi - echo "Installing node $resolved_version..." - local archive="$tmp_dir/node.tar.xz" - curl -fsSL -o "$archive" "https://nodejs.org/dist/v${resolved_version}/node-v${resolved_version}-${os}-${arch}.tar.xz" - rm -rf "$tool_dir" - mkdir -p "$tool_dir" - tar -xJf "$archive" -C "$tool_dir" --strip-components=1 - echo "$resolved_version" > "$(version_stamp "$tool_dir")" - link "$tool_dir/bin/node" node - link "$tool_dir/bin/npm" npm - link "$tool_dir/bin/npx" npx -} - -install_dotnet() { - # dotnet-install.sh is already idempotent: it detects a matching SDK already present in - # --install-dir and skips reinstalling it. - local tool_dir="$env_dir/dotnet" - local install_script="$tmp_dir/dotnet-install.sh" - curl -fsSL -o "$install_script" "https://dot.net/v1/dotnet-install.sh" - chmod +x "$install_script" - echo "Installing dotnet (channel $DOTNET_CHANNEL)..." - mkdir -p "$tool_dir" - "$install_script" --channel "$DOTNET_CHANNEL" --install-dir "$tool_dir" --no-path - link "$tool_dir/dotnet" dotnet -} - -install_zig() { - local tool_dir="$env_dir/zig" - if is_up_to_date "$tool_dir" "$ZIG_VERSION"; then - echo "zig $ZIG_VERSION already installed, skipping" - return - fi - echo "Installing zig $ZIG_VERSION..." - local archive="$tmp_dir/zig.tar.xz" - curl -fsSL -o "$archive" "https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-${os}-${ZIG_VERSION}.tar.xz" - rm -rf "$tool_dir" - mkdir -p "$tool_dir" - tar -xJf "$archive" -C "$tool_dir" --strip-components=1 - echo "$ZIG_VERSION" > "$(version_stamp "$tool_dir")" - link "$tool_dir/zig" zig -} - -install_ruby() { - # Prebuilt CRuby from ruby/ruby-builder (same source actions/setup-ruby uses), matched to - # this host's Ubuntu release so the dynamically linked build actually runs. - local tool_dir="$env_dir/ruby" - if is_up_to_date "$tool_dir" "$RUBY_VERSION"; then - echo "ruby $RUBY_VERSION already installed, skipping" - return - fi - local ubuntu_codename - ubuntu_codename=$(. /etc/os-release && echo "$VERSION_ID") - echo "Installing ruby $RUBY_VERSION (ubuntu-${ubuntu_codename})..." - local archive="$tmp_dir/ruby.tar.gz" - curl -fsSL -o "$archive" "https://github.com/ruby/ruby-builder/releases/download/ruby-${RUBY_VERSION}/ruby-${RUBY_VERSION}-ubuntu-${ubuntu_codename}-x64.tar.gz" - rm -rf "$tool_dir" - mkdir -p "$tool_dir" - tar -xzf "$archive" -C "$tool_dir" --strip-components=1 - # ruby-builder bakes its hosted-toolcache build path into every bin/ shebang and into the - # binary's RUNPATH; rewrite the shebangs to this install's real ruby, and rely on - # LD_LIBRARY_PATH (set by the E2E harness / activate.sh) rather than RUNPATH for libruby. - for script in "$tool_dir"/bin/*; do - [[ -f "$script" ]] || continue - sed -i "1s|^#!.*/bin/ruby\$|#!${tool_dir}/bin/ruby|" "$script" - done - echo "$RUBY_VERSION" > "$(version_stamp "$tool_dir")" - link "$tool_dir/bin/ruby" ruby - link "$tool_dir/bin/gem" gem - link "$tool_dir/bin/bundle" bundle -} +source "$inc_dir/go.inc" +source "$inc_dir/java.inc" +source "$inc_dir/node.inc" +source "$inc_dir/dotnet.inc" +source "$inc_dir/zig.inc" +source "$inc_dir/ruby.inc" install_go install_java diff --git a/scripts/download_dev_env/dotnet.inc b/scripts/download_dev_env/dotnet.inc new file mode 100644 index 0000000..bec3a83 --- /dev/null +++ b/scripts/download_dev_env/dotnet.inc @@ -0,0 +1,12 @@ +install_dotnet() { + # dotnet-install.sh is already idempotent: it detects a matching SDK already present in + # --install-dir and skips reinstalling it. + local tool_dir="$env_dir/dotnet" + local install_script="$tmp_dir/dotnet-install.sh" + curl -fsSL -o "$install_script" "https://dot.net/v1/dotnet-install.sh" + chmod +x "$install_script" + echo "Installing dotnet (channel $DOTNET_CHANNEL)..." + mkdir -p "$tool_dir" + "$install_script" --channel "$DOTNET_CHANNEL" --install-dir "$tool_dir" --no-path + link "$tool_dir/dotnet" dotnet +} diff --git a/scripts/download_dev_env/go.inc b/scripts/download_dev_env/go.inc new file mode 100644 index 0000000..384818a --- /dev/null +++ b/scripts/download_dev_env/go.inc @@ -0,0 +1,16 @@ +install_go() { + local tool_dir="$env_dir/go" + if is_up_to_date "$tool_dir" "$GO_VERSION"; then + echo "go $GO_VERSION already installed, skipping" + return + fi + echo "Installing go $GO_VERSION..." + local archive="$tmp_dir/go.tar.gz" + curl -fsSL -o "$archive" "https://go.dev/dl/go${GO_VERSION}.${os}-${arch/x64/amd64}.tar.gz" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xzf "$archive" -C "$tool_dir" --strip-components=1 + echo "$GO_VERSION" > "$(version_stamp "$tool_dir")" + link "$tool_dir/bin/go" go + link "$tool_dir/bin/gofmt" gofmt +} diff --git a/scripts/download_dev_env/java.inc b/scripts/download_dev_env/java.inc new file mode 100644 index 0000000..cc57729 --- /dev/null +++ b/scripts/download_dev_env/java.inc @@ -0,0 +1,19 @@ +install_java() { + local tool_dir="$env_dir/java" + local api_url="https://api.adoptium.net/v3/binary/latest/${JAVA_VERSION}/ga/${os}/${arch}/jdk/hotspot/normal/eclipse" + local resolved_version + resolved_version=$(curl -fsSIL -o /dev/null -w '%{url_effective}' "$api_url") + if is_up_to_date "$tool_dir" "$resolved_version"; then + echo "java (Temurin $JAVA_VERSION) already installed, skipping" + return + fi + echo "Installing java (Temurin $JAVA_VERSION)..." + local archive="$tmp_dir/java.tar.gz" + curl -fsSL -o "$archive" "$api_url" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xzf "$archive" -C "$tool_dir" --strip-components=1 + echo "$resolved_version" > "$(version_stamp "$tool_dir")" + link "$tool_dir/bin/java" java + link "$tool_dir/bin/javac" javac +} diff --git a/scripts/download_dev_env/node.inc b/scripts/download_dev_env/node.inc new file mode 100644 index 0000000..65f7995 --- /dev/null +++ b/scripts/download_dev_env/node.inc @@ -0,0 +1,27 @@ +install_node() { + local tool_dir="$env_dir/node" + local index_url="https://nodejs.org/dist/index.json" + local resolved_version + resolved_version=$(curl -fsSL "$index_url" \ + | grep -o "\"version\":\"v${NODE_VERSION}\.[0-9]*\.[0-9]*\"" \ + | head -n1 \ + | sed -E 's/.*"v([0-9.]+)".*/\1/' || true) + if [[ -z "$resolved_version" ]]; then + echo "download-dev-env: could not resolve latest node ${NODE_VERSION}.x from $index_url" >&2 + exit 1 + fi + if is_up_to_date "$tool_dir" "$resolved_version"; then + echo "node $resolved_version already installed, skipping" + return + fi + echo "Installing node $resolved_version..." + local archive="$tmp_dir/node.tar.xz" + curl -fsSL -o "$archive" "https://nodejs.org/dist/v${resolved_version}/node-v${resolved_version}-${os}-${arch}.tar.xz" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xJf "$archive" -C "$tool_dir" --strip-components=1 + echo "$resolved_version" > "$(version_stamp "$tool_dir")" + link "$tool_dir/bin/node" node + link "$tool_dir/bin/npm" npm + link "$tool_dir/bin/npx" npx +} diff --git a/scripts/download_dev_env/ruby.inc b/scripts/download_dev_env/ruby.inc new file mode 100644 index 0000000..efe077f --- /dev/null +++ b/scripts/download_dev_env/ruby.inc @@ -0,0 +1,28 @@ +install_ruby() { + # Prebuilt CRuby from ruby/ruby-builder (same source actions/setup-ruby uses), matched to + # this host's Ubuntu release so the dynamically linked build actually runs. + local tool_dir="$env_dir/ruby" + if is_up_to_date "$tool_dir" "$RUBY_VERSION"; then + echo "ruby $RUBY_VERSION already installed, skipping" + return + fi + local ubuntu_codename + ubuntu_codename=$(. /etc/os-release && echo "$VERSION_ID") + echo "Installing ruby $RUBY_VERSION (ubuntu-${ubuntu_codename})..." + local archive="$tmp_dir/ruby.tar.gz" + curl -fsSL -o "$archive" "https://github.com/ruby/ruby-builder/releases/download/ruby-${RUBY_VERSION}/ruby-${RUBY_VERSION}-ubuntu-${ubuntu_codename}-x64.tar.gz" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xzf "$archive" -C "$tool_dir" --strip-components=1 + # ruby-builder bakes its hosted-toolcache build path into every bin/ shebang and into the + # binary's RUNPATH; rewrite the shebangs to this install's real ruby, and rely on + # LD_LIBRARY_PATH (set by the E2E harness / activate.sh) rather than RUNPATH for libruby. + for script in "$tool_dir"/bin/*; do + [[ -f "$script" ]] || continue + sed -i "1s|^#!.*/bin/ruby\$|#!${tool_dir}/bin/ruby|" "$script" + done + echo "$RUBY_VERSION" > "$(version_stamp "$tool_dir")" + link "$tool_dir/bin/ruby" ruby + link "$tool_dir/bin/gem" gem + link "$tool_dir/bin/bundle" bundle +} diff --git a/scripts/download_dev_env/zig.inc b/scripts/download_dev_env/zig.inc new file mode 100644 index 0000000..4abbb76 --- /dev/null +++ b/scripts/download_dev_env/zig.inc @@ -0,0 +1,15 @@ +install_zig() { + local tool_dir="$env_dir/zig" + if is_up_to_date "$tool_dir" "$ZIG_VERSION"; then + echo "zig $ZIG_VERSION already installed, skipping" + return + fi + echo "Installing zig $ZIG_VERSION..." + local archive="$tmp_dir/zig.tar.xz" + curl -fsSL -o "$archive" "https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-${os}-${ZIG_VERSION}.tar.xz" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xJf "$archive" -C "$tool_dir" --strip-components=1 + echo "$ZIG_VERSION" > "$(version_stamp "$tool_dir")" + link "$tool_dir/zig" zig +} From 2d17b78aa1fdbcbf4ac704101fa1a117d4233d63 Mon Sep 17 00:00:00 2001 From: Vasiliy Kulikov Date: Thu, 10 Sep 2026 22:15:36 +0300 Subject: [PATCH 06/10] Send the correct textDocument/didOpen languageId for .vim files language_id() only covered 10 extensions and fell back to "plaintext" for everything else, including .vim; vim-language-server filters on languageId and silently returns no results for documents it doesn't recognize. Discovered while triaging vim/vimls for E2E query coverage (not promoted this round - the server still returns empty document symbols and can't resolve workspace/symbol for name-based lookups, so it needs more investigation before it's a clean queries candidate). --- playground/vim/main.vim | 14 ++++++++++++++ src/lsp/client/requests.rs | 1 + 2 files changed, 15 insertions(+) diff --git a/playground/vim/main.vim b/playground/vim/main.vim index e69de29..693b9de 100644 --- a/playground/vim/main.vim +++ b/playground/vim/main.vim @@ -0,0 +1,14 @@ +function! OrderTotal(quantity, price) + return a:quantity * a:price +endfunction + +function! BuildSampleOrder() + return "Carol" +endfunction + +function! FormatOrder(customer, quantity, price) + let total = OrderTotal(a:quantity, a:price) + return a:customer . " has " . a:quantity . " items worth " . total +endfunction + +echo FormatOrder(BuildSampleOrder(), 2, 35.0) diff --git a/src/lsp/client/requests.rs b/src/lsp/client/requests.rs index 32a2efe..619ffa3 100644 --- a/src/lsp/client/requests.rs +++ b/src/lsp/client/requests.rs @@ -268,6 +268,7 @@ fn language_id(path: &Path) -> &'static str { Some("py") => "python", Some("rs") => "rust", Some("ts" | "mts" | "cts") => "typescript", + Some("vim") => "vim", _ => "plaintext", } } From eefc65da5fedf4439867733a263dea292ccbfe8d Mon Sep 17 00:00:00 2001 From: Vasiliy Kulikov Date: Thu, 10 Sep 2026 22:20:27 +0300 Subject: [PATCH 07/10] Promote luau/luau_lsp to full real-server E2E query coverage Give luau a real fixture project (main.luau, Order/OrderItem types + build_sample_order/format_order functions) and promote its language kind from metadata to source with a query profile. luau-lsp is a standalone binary (no extra host programs needed) with full definition/references/call-hierarchy support; except callees (build_sample_order has no outgoing call-hierarchy edges, same pattern as rust/zig) and build-index (no background-work progress signal). --- data | 2 +- playground/luau/main.luau | 35 +++++++++++++++++++++++++++++++++++ tests/e2e/cases/luau.yaml | 25 +++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/data b/data index e48460e..86ea8d2 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit e48460eec6c575d482495209fdc8625d654d3eb3 +Subproject commit 86ea8d29af3f47285d46529632d0a98b94d58b89 diff --git a/playground/luau/main.luau b/playground/luau/main.luau index e69de29..8bc4308 100644 --- a/playground/luau/main.luau +++ b/playground/luau/main.luau @@ -0,0 +1,35 @@ +type OrderItem = { + name: string, + quantity: number, + price: number, +} + +type Order = { + customer: string, + items: { OrderItem }, +} + +local function order_total(order: Order): number + local sum = 0 + for _, item in order.items do + sum += item.quantity * item.price + end + return sum +end + +local function build_sample_order(): Order + return { + customer = "Carol", + items = { + { name = "Mouse", quantity = 1, price = 35.0 }, + { name = "Pad", quantity = 1, price = 12.5 }, + }, + } +end + +local function format_order(order: Order): string + return string.format("%s has %d items worth %.2f", order.customer, #order.items, order_total(order)) +end + +local order = build_sample_order() +print(format_order(order)) diff --git a/tests/e2e/cases/luau.yaml b/tests/e2e/cases/luau.yaml index e3a1321..89592b5 100644 --- a/tests/e2e/cases/luau.yaml +++ b/tests/e2e/cases/luau.yaml @@ -1,9 +1,30 @@ language: id: luau - kind: metadata + kind: source project: playground/luau + query-profile: + symbol-query: Order + callable-query: build_sample_order + format-file: main.luau + expected-names: + - Order + - OrderItem + - build_sample_order + - format_order pairs: - language: luau server: luau_lsp smoke: - status: capabilities + status: queries + exceptions: + - command: callees + outcome: empty-matches + reason: build_sample_order constructs data directly and has no outgoing call-hierarchy edges + - command: build-index + outcome: failure + message: background-work progress + reason: luau-lsp exposes no background-work progress signal + lifecycle: + status: scenarios + direct-run: + status: exchange From 79c5303efed804c37c32ddc5e50cff63df6b033a Mon Sep 17 00:00:00 2001 From: Vasiliy Kulikov Date: Thu, 10 Sep 2026 22:27:15 +0300 Subject: [PATCH 08/10] Promote odin/ols to full real-server E2E query coverage Give odin a real fixture project (main.odin, Order/OrderItem structs + build_sample_order/format_order procs) and promote its language kind from metadata to source with a query profile. Also add "odin" to the textDocument/didOpen languageId table (was falling back to "plaintext" like .vim files did) - ols doesn't actually require it, but it's correct regardless. Except grep (ols returns no workspace/symbol matches for this fixture, even though definition/references work by other means) and build-index (no background-work progress signal, same pattern as several other servers). --- data | 2 +- playground/odin/main.odin | 42 ++++++++++++++++++++++++++++++++++++++ src/lsp/client/requests.rs | 1 + tests/e2e/cases/odin.yaml | 25 +++++++++++++++++++++-- 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/data b/data index 86ea8d2..4fb4539 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 86ea8d29af3f47285d46529632d0a98b94d58b89 +Subproject commit 4fb453903beed464c803bb715b7739fb3b58bcb8 diff --git a/playground/odin/main.odin b/playground/odin/main.odin index e69de29..a0b6656 100644 --- a/playground/odin/main.odin +++ b/playground/odin/main.odin @@ -0,0 +1,42 @@ +package main + +import "core:fmt" + +OrderItem :: struct { + name: string, + quantity: int, + price: f64, +} + +Order :: struct { + customer: string, + items: []OrderItem, +} + +order_total :: proc(order: Order) -> f64 { + sum := 0.0 + for item in order.items { + sum += f64(item.quantity) * item.price + } + return sum +} + +build_sample_order :: proc() -> Order { + return Order{ + customer = "Carol", + items = []OrderItem{ + {name = "Mouse", quantity = 1, price = 35.0}, + {name = "Pad", quantity = 1, price = 12.5}, + }, + } +} + +format_order :: proc(order: Order) -> string { + total := order_total(order) + return fmt.tprintf("%s has %d items worth %.2f", order.customer, len(order.items), total) +} + +main :: proc() { + order := build_sample_order() + fmt.println(format_order(order)) +} diff --git a/src/lsp/client/requests.rs b/src/lsp/client/requests.rs index 619ffa3..9ad8c3c 100644 --- a/src/lsp/client/requests.rs +++ b/src/lsp/client/requests.rs @@ -269,6 +269,7 @@ fn language_id(path: &Path) -> &'static str { Some("rs") => "rust", Some("ts" | "mts" | "cts") => "typescript", Some("vim") => "vim", + Some("odin") => "odin", _ => "plaintext", } } diff --git a/tests/e2e/cases/odin.yaml b/tests/e2e/cases/odin.yaml index 6d7d791..75a0653 100644 --- a/tests/e2e/cases/odin.yaml +++ b/tests/e2e/cases/odin.yaml @@ -1,9 +1,30 @@ language: id: odin - kind: metadata + kind: source project: playground/odin + query-profile: + symbol-query: Order + callable-query: build_sample_order + format-file: main.odin + expected-names: + - Order + - OrderItem + - build_sample_order + - format_order pairs: - language: odin server: ols smoke: - status: capabilities + status: queries + exceptions: + - command: grep + outcome: empty-matches + reason: ols returns no workspace/symbol matches for this fixture + - command: build-index + outcome: failure + message: background-work progress + reason: ols exposes no background-work progress signal + lifecycle: + status: scenarios + direct-run: + status: exchange From 1ae5f9bba4ba1983d71efab5f44f4de3214a1f0d Mon Sep 17 00:00:00 2001 From: Vasiliy Kulikov Date: Thu, 10 Sep 2026 22:37:25 +0300 Subject: [PATCH 09/10] Promote clojure/clojure_lsp to full real-server E2E query coverage Give clojure a real fixture project (main.clj: OrderItem/Order records, build-sample-order/format-order/order-total fns) and promote its language kind from metadata to source with a query profile. Except grep and declaration (clojure-lsp returns no workspace/symbol or textDocument/declaration matches for this fixture), callers (build-sample-order is only called from top-level code, not from another named function, so it legitimately has no call-hierarchy callers), and build-index (no background-work progress signal, same pattern as several other servers). --- data | 2 +- playground/clojure/main.clj | 16 ++++++++++++++++ tests/e2e/cases/clojure.yaml | 31 +++++++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/data b/data index 4fb4539..65c311e 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 4fb453903beed464c803bb715b7739fb3b58bcb8 +Subproject commit 65c311e05c7938bf0771401eba70a8ae56498498 diff --git a/playground/clojure/main.clj b/playground/clojure/main.clj index e69de29..53f7bef 100644 --- a/playground/clojure/main.clj +++ b/playground/clojure/main.clj @@ -0,0 +1,16 @@ +(ns main) + +(defrecord OrderItem [name quantity price]) + +(defrecord Order [customer items]) + +(defn order-total [order] + (reduce + (map (fn [item] (* (:quantity item) (:price item))) (:items order)))) + +(defn build-sample-order [] + (->Order "Carol" [(->OrderItem "Mouse" 1 35.0) (->OrderItem "Pad" 1 12.5)])) + +(defn format-order [order] + (str (:customer order) " has " (count (:items order)) " items worth " (order-total order))) + +(println (format-order (build-sample-order))) diff --git a/tests/e2e/cases/clojure.yaml b/tests/e2e/cases/clojure.yaml index 8feb7d5..d300922 100644 --- a/tests/e2e/cases/clojure.yaml +++ b/tests/e2e/cases/clojure.yaml @@ -1,9 +1,36 @@ language: id: clojure - kind: metadata + kind: source project: playground/clojure + query-profile: + symbol-query: Order + callable-query: build-sample-order + format-file: main.clj + expected-names: + - Order + - OrderItem + - build-sample-order + - format-order pairs: - language: clojure server: clojure_lsp smoke: - status: capabilities + status: queries + exceptions: + - command: grep + outcome: empty-matches + reason: clojure-lsp returns no workspace/symbol matches for this fixture + - command: callers + outcome: empty-matches + reason: build-sample-order is only called from top-level code, not from another named function + - command: declaration + outcome: empty-matches + reason: clojure-lsp returns no textDocument/declaration matches for this fixture + - command: build-index + outcome: failure + message: background-work progress + reason: clojure-lsp exposes no background-work progress signal + lifecycle: + status: scenarios + direct-run: + status: exchange From 3a951976e2b0d4222a3fcfd8d583562495e5b1c3 Mon Sep 17 00:00:00 2001 From: Vasiliy Kulikov Date: Thu, 10 Sep 2026 23:05:07 +0300 Subject: [PATCH 10/10] up --- data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data b/data index 65c311e..ec28c6b 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 65c311e05c7938bf0771401eba70a8ae56498498 +Subproject commit ec28c6bf19fa27df7b4c9451ea299743a98e9bf0