aboutsummaryrefslogtreecommitdiff
path: root/Scripts
diff options
context:
space:
mode:
Diffstat (limited to 'Scripts')
-rwxr-xr-xScripts/build-parser.sh53
-rw-r--r--Scripts/lldb-dap-proxy.pl224
-rw-r--r--Scripts/main.js1583
-rw-r--r--Scripts/normalize-zig-issues.pl93
-rwxr-xr-xScripts/update-parser.sh64
5 files changed, 0 insertions, 2017 deletions
diff --git a/Scripts/build-parser.sh b/Scripts/build-parser.sh
deleted file mode 100755
index 5cd7039..0000000
--- a/Scripts/build-parser.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/sh
-#
-# build-parser.sh — Compile the Nova-compatible tree-sitter-zig parser.
-#
-# Purpose:
-# Build vendor/tree-sitter-zig/src/parser.c into a universal macOS dylib
-# that Nova loads via its private SyntaxKit framework.
-#
-# What it does:
-# - clang -dynamiclib for arm64 + x86_64
-# - links against Nova’s SyntaxKit framework (from /Applications/Nova.app)
-# - sets rpath @loader_path/../Frameworks so the dylib finds SyntaxKit
-# when Nova loads it from the bundled extension
-# - ad-hoc codesigns the dylib (Gatekeeper requirement)
-# - writes the result to Syntaxes/libtree-sitter-zig.dylib
-#
-# Usage:
-# ./Scripts/build-parser.sh
-#
-# Environment overrides:
-# NOVA_APP Path to Nova.app (default: /Applications/Nova.app)
-# SDKROOT macOS SDK path (default: `xcrun --show-sdk-path`)
-#
-# Requirements:
-# macOS, Xcode Command Line Tools (clang + xcrun), Nova installed.
-
-set -eu
-
-ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
-VENDOR_DIR="$ROOT/vendor/tree-sitter-zig"
-BUILD_DIR="$ROOT/build"
-OUTPUT="$ROOT/Syntaxes/libtree-sitter-zig.dylib"
-NOVA_APP="${NOVA_APP:-/Applications/Nova.app}"
-SDKROOT="${SDKROOT:-$(xcrun --show-sdk-path)}"
-
-mkdir -p "$BUILD_DIR"
-
-clang \
- -dynamiclib \
- -O2 \
- -fPIC \
- -arch arm64 \
- -arch x86_64 \
- -isysroot "$SDKROOT" \
- -I"$VENDOR_DIR/src" \
- -F"$NOVA_APP/Contents/Frameworks" \
- -framework SyntaxKit \
- -Wl,-rpath,@loader_path/../Frameworks \
- -o "$BUILD_DIR/libtree-sitter-zig.dylib" \
- "$VENDOR_DIR/src/parser.c"
-
-codesign --force --sign - "$BUILD_DIR/libtree-sitter-zig.dylib"
-cp "$BUILD_DIR/libtree-sitter-zig.dylib" "$OUTPUT"
diff --git a/Scripts/lldb-dap-proxy.pl b/Scripts/lldb-dap-proxy.pl
deleted file mode 100644
index beb6eaa..0000000
--- a/Scripts/lldb-dap-proxy.pl
+++ /dev/null
@@ -1,224 +0,0 @@
-#!/usr/bin/perl
-use strict;
-use warnings;
-use Fcntl qw(O_WRONLY O_APPEND O_CREAT O_NOFOLLOW);
-use IPC::Open3;
-use IO::Select;
-use JSON::PP;
-use Symbol 'gensym';
-
-my $adapter_path = $ARGV[0] // $ENV{NOVA_ZIG_LLDB_DAP_PATH};
-my $log_path = $ARGV[1] // $ENV{NOVA_ZIG_DEBUG_LOG};
-my @adapter_args = @ARGV > 2 ? @ARGV[2 .. $#ARGV] : ();
-
-sub log_msg {
- return unless defined $log_path;
- my ($msg) = @_;
- sysopen(my $fh, $log_path, O_WRONLY | O_APPEND | O_CREAT | O_NOFOLLOW, 0600) or return;
- my @t = gmtime(time);
- printf $fh "[%04d-%02d-%02dT%02d:%02d:%02dZ] %s\n",
- $t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0], $msg;
- close($fh);
-}
-
-unless (defined $adapter_path) {
- log_msg('missing adapter path');
- print STDERR "lldb-dap proxy requires a target adapter path.\n";
- exit(1);
-}
-
-log_msg("proxy start adapter=$adapter_path");
-
-# open3 treats an undef handle as "inherit parent STDIN/STDOUT", so pre-allocate
-# anonymous typeglobs to force pipe creation for all three streams.
-my ($child_in, $child_out, $child_err) = (gensym(), gensym(), gensym());
-my $pid = eval { open3($child_in, $child_out, $child_err, $adapter_path, @adapter_args) };
-if ($@) {
- (my $err = $@) =~ s/\s+$//;
- log_msg("adapter error $err");
- print STDERR "$err\n";
- exit(1);
-}
-
-binmode($_, ':raw') for \*STDIN, \*STDOUT, $child_in, $child_out, $child_err;
-
-$SIG{INT} = sub { log_msg('received SIGINT'); kill 'INT', $pid };
-$SIG{TERM} = sub { log_msg('received SIGTERM'); kill 'TERM', $pid };
-
-my $json = JSON::PP->new->utf8;
-
-my ($next_client_seq, $next_adapter_seq) = (1, 1);
-my (%client_request_seq_map, %adapter_request_seq_map, %request_args_by_client_seq);
-my ($stdin_buf, $child_buf) = ('', '');
-
-my $stdin_fn = fileno(\*STDIN);
-my $child_out_fn = fileno($child_out);
-
-my $sel = IO::Select->new(\*STDIN, $child_out, $child_err);
-
-LOOP: while ($sel->count > 0) {
- my @ready = $sel->can_read;
- for my $fh (@ready) {
- my ($chunk, $n);
- $n = sysread($fh, $chunk, 65536);
- unless (defined $n && $n > 0) {
- $sel->remove($fh);
- my $fn = fileno($fh);
- if ($fn == $stdin_fn) {
- log_msg('stdin closed');
- close($child_in);
- } elsif ($fn == $child_out_fn) {
- log_msg('adapter stdout closed');
- last LOOP;
- } else {
- log_msg('adapter stderr closed');
- }
- next;
- }
- my $fn = fileno($fh);
- if ($fn == $stdin_fn) {
- $stdin_buf .= $chunk;
- flush_dap(\$stdin_buf, $child_in, \&rewrite_client_message);
- } elsif ($fn == $child_out_fn) {
- $child_buf .= $chunk;
- flush_dap(\$child_buf, \*STDOUT, \&rewrite_adapter_message);
- } else {
- log_msg('stderr ' . $chunk);
- print STDERR $chunk;
- }
- }
-}
-
-waitpid($pid, 0);
-log_msg(sprintf 'adapter exited status=%d signal=%d', $? >> 8, $? & 127);
-# $? encodes exit code in the high byte and terminating signal in the low 7 bits.
-# Re-raise the signal so the parent sees a signal-killed exit rather than exit(0).
-my $signal = $? & 127;
-if ($signal) {
- $SIG{$_} = 'DEFAULT' for qw(INT TERM);
- kill $signal, $$;
- sleep 1;
-}
-log_msg(sprintf 'proxy exit code=%d', $? >> 8);
-exit($? >> 8);
-
-sub flush_dap {
- my ($buf_ref, $dest, $rewrite_fn) = @_;
- while (1) {
- my $header_end = index($$buf_ref, "\r\n\r\n");
- last if $header_end == -1;
-
- my $header = substr($$buf_ref, 0, $header_end);
- unless ($header =~ /Content-Length:\s*(\d+)/i) {
- syswrite($dest, $$buf_ref);
- $$buf_ref = '';
- return;
- }
-
- my $body_len = 0 + $1;
- my $frame_len = $header_end + 4 + $body_len;
- last if length($$buf_ref) < $frame_len;
-
- my $body = substr($$buf_ref, $header_end + 4, $body_len);
- $$buf_ref = substr($$buf_ref, $frame_len);
-
- my $msg = eval { $json->decode($body) };
- if (!$@ && ref($msg) eq 'HASH') {
- $rewrite_fn->($msg);
- my $out = $json->encode($msg);
- syswrite($dest, 'Content-Length: ' . length($out) . "\r\n\r\n");
- syswrite($dest, $out);
- } else {
- log_msg(sprintf 'non-json message forwarded%s', $@ ? " error=$@" : ' reason=not-a-hash');
- syswrite($dest, 'Content-Length: ' . $body_len . "\r\n\r\n");
- syswrite($dest, $body);
- }
- }
-}
-
-sub rewrite_client_message {
- my ($msg) = @_;
- my $type = $msg->{type} // '';
- my $command = $msg->{command} // '';
- log_msg(sprintf 'client message type=%s command=%s seq_in=%s request_seq_in=%s',
- $type, $command, $msg->{seq} // '', $msg->{request_seq} // '');
-
- # Nova omits the filters array on setExceptionBreakpoints requests, which
- # lldb-dap then rejects. Ensure the field is always present before forwarding.
- if ($type eq 'request' && $command eq 'setExceptionBreakpoints') {
- $msg->{arguments} = {} unless ref($msg->{arguments}) eq 'HASH';
- $msg->{arguments}{filters} = [] unless ref($msg->{arguments}{filters}) eq 'ARRAY';
- }
-
- if ($type eq 'response' && defined $msg->{request_seq}) {
- my $rseq = $msg->{request_seq};
- if (exists $adapter_request_seq_map{$rseq}) {
- $msg->{request_seq} = $adapter_request_seq_map{$rseq};
- delete $adapter_request_seq_map{$rseq};
- }
- }
-
- if (defined $msg->{seq}) {
- my $orig = $msg->{seq};
- my $rewrite = $next_client_seq++;
- if ($type eq 'request') {
- $client_request_seq_map{$rewrite} = $orig;
- $request_args_by_client_seq{$rewrite} = {
- command => $command,
- arguments => $msg->{arguments},
- };
- log_msg(sprintf 'client seq map %s=>%s', $orig, $rewrite);
- }
- $msg->{seq} = $rewrite;
- }
-
- log_msg(sprintf 'client message seq_out=%s request_seq_out=%s',
- $msg->{seq} // '', $msg->{request_seq} // '');
-}
-
-sub rewrite_adapter_message {
- my ($msg) = @_;
- my $type = $msg->{type} // '';
- my $command = $msg->{command} // '';
- log_msg(sprintf 'adapter message type=%s command=%s seq_in=%s request_seq_in=%s',
- $type, $command, $msg->{seq} // '', $msg->{request_seq} // '');
-
- if ($type eq 'response' && defined $msg->{request_seq}) {
- my $rseq = $msg->{request_seq};
- my $orig_req = $request_args_by_client_seq{$rseq};
- my $orig_seq = $client_request_seq_map{$rseq};
-
- if (defined $orig_seq) {
- $msg->{request_seq} = $orig_seq;
- delete $client_request_seq_map{$rseq};
- }
-
- # lldb-dap returns a successful setExceptionBreakpoints response without
- # the breakpoints array the DAP spec requires. Synthesise one verified
- # entry per filter so Nova's client confirms each breakpoint as registered.
- if (defined $orig_req
- && ($orig_req->{command} // '') eq 'setExceptionBreakpoints'
- && $msg->{success})
- {
- my $filters = ref($orig_req->{arguments}{filters}) eq 'ARRAY'
- ? $orig_req->{arguments}{filters} : [];
- $msg->{body} = {} unless ref($msg->{body}) eq 'HASH';
- unless (ref($msg->{body}{breakpoints}) eq 'ARRAY') {
- $msg->{body}{breakpoints} = [ map { +{ verified => JSON::PP::true } } @$filters ];
- }
- }
-
- delete $request_args_by_client_seq{$rseq};
- }
-
- if (defined $msg->{seq}) {
- my $orig = $msg->{seq};
- my $rewrite = $next_adapter_seq++;
- $adapter_request_seq_map{$rewrite} = $orig if $type eq 'request';
- $msg->{seq} = $rewrite;
- log_msg("adapter seq map $orig=>$rewrite");
- }
-
- log_msg(sprintf 'adapter message seq_out=%s request_seq_out=%s',
- $msg->{seq} // '', $msg->{request_seq} // '');
-}
diff --git a/Scripts/main.js b/Scripts/main.js
deleted file mode 100644
index 7477993..0000000
--- a/Scripts/main.js
+++ /dev/null
@@ -1,1583 +0,0 @@
-"use strict";
-
-// --- CONSTANTS ---------------------------------------------------------------
-
-const EXTENSION_ID = "at.dcz.nova-zig";
-const TASK_ASSISTANT_ID = `${EXTENSION_ID}.tasks`;
-const LANGUAGE_CLIENT_ID = `${EXTENSION_ID}.zls`;
-const ISSUE_MATCHER = "zig.compiler";
-const USER_OPTION_REGEX = /^[A-Za-z_][A-Za-z0-9_-]*(=.*)?$/;
-const STEP_CACHE_TTL_MS = 5 * 60 * 1000;
-
-const CONFIG_KEYS = {
- zigPath: `${EXTENSION_ID}.toolchain.zig-path`,
- zlsPath: `${EXTENSION_ID}.toolchain.zls-path`,
- lldbDapPath: `${EXTENSION_ID}.toolchain.lldb-dap-path`,
- zlsEnabled: `${EXTENSION_ID}.zls.enabled`,
- zlsBuildOnSave: `${EXTENSION_ID}.zls.build-on-save`,
- zlsDebug: `${EXTENSION_ID}.zls.debug`,
- lldbDapDebug: `${EXTENSION_ID}.debug-adapter.debug`,
- discoverSteps: `${EXTENSION_ID}.tasks.discover-steps`,
-};
-
-// --- LIFECYCLE ---------------------------------------------------------------
-
-let languageServer = null;
-let taskAssistant = null;
-let commandRegistrations = [];
-// Tracks when the last ZLS client was stopped. Used by start() to wait for
-// the old ZLS process to fully exit before spawning a new one — survives
-// across instances when Nova auto-restarts the extension.
-let lastZlsStopAt = 0;
-const ZLS_RESTART_GRACE_MS = 500;
-
-exports.activate = function activate() {
- // Nova may call activate() again without first calling deactivate() when a
- // language server crashes and the extension is auto-restarted. Dispose any
- // existing instances so their config observers and ZLS processes don't leak.
- if (languageServer) {
- languageServer.dispose();
- languageServer = null;
- }
- if (taskAssistant) {
- taskAssistant.dispose();
- taskAssistant = null;
- }
- commandRegistrations.forEach((disposable) => {
- if (disposable && typeof disposable.dispose === "function") {
- try {
- disposable.dispose();
- } catch (error) {
- console.error(
- `[${EXTENSION_ID}] Failed to dispose registration`,
- error,
- );
- }
- }
- });
- commandRegistrations = [];
-
- registerCommands();
- taskAssistant = new ZigTaskAssistant();
- languageServer = new ZigLanguageServer();
- console.log(`[${EXTENSION_ID}] activated`);
-};
-
-exports.deactivate = function deactivate() {
- if (languageServer) {
- languageServer.dispose();
- languageServer = null;
- }
-
- if (taskAssistant) {
- taskAssistant.dispose();
- taskAssistant = null;
- }
-
- commandRegistrations.forEach((disposable) => {
- if (disposable && typeof disposable.dispose === "function") {
- try {
- disposable.dispose();
- } catch (error) {
- console.error(
- `[${EXTENSION_ID}] Failed to dispose registration`,
- error,
- );
- }
- }
- });
- commandRegistrations = [];
-
- console.log(`[${EXTENSION_ID}] deactivated`);
-};
-
-// --- CONFIG HELPERS ----------------------------------------------------------
-
-/**
- * Resolve a configuration value respecting precedence
- *
- * @param {string} key - Configuration key
- */
-function getConfigValue(key) {
- const workspaceValue = nova.workspace.config.get(key);
- if (
- workspaceValue !== undefined &&
- workspaceValue !== null &&
- workspaceValue !== ""
- ) {
- return workspaceValue;
- }
-
- const globalValue = nova.config.get(key);
- if (globalValue !== undefined && globalValue !== null && globalValue !== "") {
- return globalValue;
- }
-
- return null;
-}
-
-/**
- * Resolve a configuration boolean value respecting precedence
- *
- * @param {string} key - Configuration key
- * @param {boolean} fallback - Fallback value
- */
-function getBooleanConfigValue(key, fallback) {
- const workspaceValue = nova.workspace.config.get(key);
- if (workspaceValue === "enabled") return true;
- if (workspaceValue === "disabled") return false;
- if (typeof workspaceValue === "boolean") return workspaceValue;
-
- const globalValue = nova.config.get(key);
- if (typeof globalValue === "boolean") return globalValue;
-
- return fallback;
-}
-
-/**
- * Normalize a config array value by converting to array, filtering nulls,
- * trimming whitespace, and removing empty entries
- *
- * @param {*} value - Any value
- * @returns {string[]} - Clean array of non-empty strings
- */
-function normalizeArray(value) {
- if (!Array.isArray(value)) {
- return [];
- }
-
- return value
- .map((entry) =>
- entry === null || entry === undefined ? "" : String(entry).trim(),
- )
- .filter((entry) => entry.length > 0);
-}
-
-/**
- * Resolve a relative path to an absolute path, using a provided base directory
- * or workspace root as fallback
- *
- * @param {string} path - Relative path
- * @param {string|null} base - Base path or null to use the workspace as base
- */
-function resolvePathAgainstBase(path, base) {
- if (!path) {
- return null;
- }
-
- if (path.startsWith("/")) {
- return path;
- }
-
- if (base) {
- return nova.path.join(base, path);
- }
-
- if (nova.workspace.path) {
- return nova.path.join(nova.workspace.path, path);
- }
-
- return path;
-}
-
-/**
- * Safely retrieve a task config value
- *
- * @param {Object} config - Nova task configuration object
- * @param {string} key - Key
- * @returns {string} - Value
- */
-function getTaskConfigValue(config, key) {
- if (!config) {
- return null;
- }
-
- const value = config.get(key);
- return value === undefined || value === null || value === "" ? null : value;
-}
-
-/**
- * Safely retrieve a task config argument list
- *
- * Specialized version of {@link getTaskConfigValue}
- *
- * @param {Object} config - Nova task configuration object
- * @param {string} key - Key
- * @returns {string[]} - Argument list
- */
-function getTaskArgs(config, key) {
- return normalizeArray(getTaskConfigValue(config, key));
-}
-
-/**
- * Resolves the task working directory from config, falling back to workspace root.
- *
- * @param {Object} config - Nova task configuration object
- * @returns {string|null} - Absolute working directory path or null if no workspace is open
- */
-function getTaskCwd(config) {
- const configured = getTaskConfigValue(config, "cwd");
- if (configured) {
- return resolvePathAgainstBase(configured, null);
- }
-
- return nova.workspace.path || null;
-}
-
-// --- TASK ARG HELPERS --------------------------------------------------------
-
-// Returns the configured step string, or null if the step should be omitted.
-// An explicit empty string means "no step argument" — `zig build` then runs the
-// default install step (Ziglings-style projects rely on this). Missing/null
-// returns the caller-supplied fallback.
-function resolveStep(config, key, fallback) {
- const raw = config ? config.get(key) : undefined;
- if (raw === undefined || raw === null) {
- return fallback === undefined ? null : fallback;
- }
- const trimmed = String(raw).trim();
- return trimmed.length > 0 ? trimmed : null;
-}
-
-// Backwards-compatible wrapper: zigBuildRun's `runStep` field. The legacy
-// `step` key is still honored for users on pre-0.1 task configs.
-function resolveRunStep(config) {
- const raw = config ? config.get("runStep") : undefined;
- if (raw === undefined || raw === null) {
- const legacy = getTaskConfigValue(config, "step");
- return legacy ? legacy : null;
- }
- const trimmed = String(raw).trim();
- return trimmed.length > 0 ? trimmed : null;
-}
-
-// Builds the `zig build` argv prefix shared by Build/Run/Test/Watch resolvers.
-// Order: ["build", -Doptimize, -Dtarget, -D<userOptions>, ...buildArgs, step?].
-// `runArgs` are the caller's responsibility — they go after `--`.
-function buildZigArgv(config, options) {
- const opts = options || {};
- const argv = ["build"];
-
- const optimize =
- getTaskConfigValue(config, "optimize") || opts.defaultOptimize || null;
- if (optimize) argv.push(`-Doptimize=${optimize}`);
-
- const target = getTaskConfigValue(config, "target");
- if (target) argv.push(`-Dtarget=${target}`);
-
- for (const entry of getTaskArgs(config, "userOptions")) {
- if (USER_OPTION_REGEX.test(entry)) {
- argv.push(`-D${entry}`);
- } else {
- console.warn(`[zig-task] Skipping invalid userOptions entry: ${entry}`);
- }
- }
-
- argv.push(...getTaskArgs(config, "buildArgs"));
-
- if (opts.step) argv.push(opts.step);
- return argv;
-}
-
-// --- PATH & WORKSPACE HELPERS ------------------------------------------------
-
-/**
- * Validate cwd is safe to clean (absolute, inside workspace, not root or home),
- * returning directories or null
- *
- * @param {string} cwd - Directory to clean
- * @returns {string[]|null} - Directories to remove
- */
-function resolveCleanPaths(cwd) {
- const showWarning = () => {
- nova.workspace.showWarningMessage(
- localizeText(
- "warning.clean.unsafe_cwd",
- "Refusing to clean: the working directory must be inside this workspace.",
- ),
- );
- };
-
- if (!cwd || typeof cwd !== "string" || !cwd.startsWith("/")) {
- showWarning();
- return null;
- }
-
- const normalized = nova.path.normalize(cwd);
- if (normalized === "/" || normalized === "") {
- showWarning();
- return null;
- }
-
- const home = nova.environment ? nova.environment.HOME : null;
- if (
- home &&
- (normalized === home || normalized === nova.path.normalize(home))
- ) {
- showWarning();
- return null;
- }
-
- const workspacePath = nova.workspace.path;
- if (!workspacePath) {
- showWarning();
- return null;
- }
-
- const workspaceNormalized = nova.path.normalize(workspacePath);
- if (
- normalized !== workspaceNormalized &&
- !normalized.startsWith(workspaceNormalized + "/")
- ) {
- showWarning();
- return null;
- }
-
- return [".zig-cache", "zig-cache", "zig-out"];
-}
-
-/**
- * Walk up from startDir to find the nearest ancestor containing build.zig,
- * stopping at workspace root
- *
- * @param {string} startDir - Start directory
- */
-function findNearestZigBuildDir(startDir) {
- const workspacePath = nova.workspace.path
- ? nova.path.normalize(nova.workspace.path)
- : null;
-
- if (!startDir) return workspacePath || null;
-
- let current = nova.path.normalize(startDir);
- for (let i = 0; i < 64; i++) {
- if (nova.fs.stat(nova.path.join(current, "build.zig"))) {
- return current;
- }
- if (current === "/" || (workspacePath && current === workspacePath)) {
- return workspacePath || null;
- }
- const parent = nova.path.dirname(current);
- if (!parent || parent === current) break;
- current = parent;
- }
- return workspacePath || null;
-}
-
-/**
- * Extract the package name from `build.zig.zon` by regex,
- * returning null if absent or unreadable
- *
- * @param {string} cwd - Directory in which to search for build.zig.zon
- * @returns {string|null} - Package name or null if parsing fails
- */
-function parseProjectName(cwd) {
- if (!cwd) return null;
- const zonPath = nova.path.join(cwd, "build.zig.zon");
- if (!nova.fs.stat(zonPath)) return null;
-
- let content = "";
- try {
- const file = nova.fs.open(zonPath, "r");
- try {
- content = file.read() || "";
- } finally {
- file.close();
- }
- } catch (error) {
- console.warn(`[zig] Failed to read ${zonPath}: ${error}`);
- return null;
- }
- if (typeof content !== "string" || content.length === 0) return null;
-
- const stripped = content.replace(/\/\/[^\n]*/g, "");
- const match = stripped.match(/\.name\s*=\s*(?:"([^"]+)"|\.([A-Za-z_][\w]*))/);
- if (!match) return null;
- return match[1] || match[2] || null;
-}
-
-/**
- * Resolve the path of the active zig file
- *
- * @returns {string|null} - Path or null if active editor is not a zig file
- */
-function activeZigFilePath() {
- const editor = nova.workspace.activeTextEditor;
- if (!editor || !editor.document || !editor.document.path) {
- return null;
- }
-
- if (editor.document.syntax !== "zig") {
- return null;
- }
-
- return editor.document.path;
-}
-
-/**
- * Resolve the directory of the active zig file
- *
- * @returns {string|null} - Directory or null if active editor is not a zig file
- */
-function activeZigFileDirectory() {
- const filePath = activeZigFilePath();
- if (!filePath) {
- return null;
- }
-
- return nova.path.dirname(filePath);
-}
-
-// --- STEP CACHE --------------------------------------------------------------
-
-// Per-cwd cache for `zig build --list-steps`. Invalidated by mtime changes on
-// build.zig / build.zig.zon and a soft 5-minute TTL. `getOrFetch` returns
-// cached steps synchronously and kicks off a background refresh when stale,
-// nudging Nova to reload tasks once the refresh lands.
-const STEP_DISCOVERY_WARN_THROTTLE_MS = 5 * 60 * 1000;
-
-const stepCache = {
- entries: new Map(),
- pending: new Map(),
- lastWarnedAt: new Map(),
-
- getOrFetch(cwd) {
- if (!cwd) return null;
-
- const buildZigPath = nova.path.join(cwd, "build.zig");
- const buildZigStat = nova.fs.stat(buildZigPath);
- if (!buildZigStat) return null;
-
- const buildZonStat = nova.fs.stat(nova.path.join(cwd, "build.zig.zon"));
- const buildZigMtimeMs = buildZigStat.mtime
- ? buildZigStat.mtime.getTime()
- : 0;
- const buildZonMtimeMs =
- buildZonStat && buildZonStat.mtime ? buildZonStat.mtime.getTime() : 0;
-
- const cached = this.entries.get(cwd);
- const fresh =
- cached &&
- cached.buildZigMtimeMs === buildZigMtimeMs &&
- cached.buildZonMtimeMs === buildZonMtimeMs &&
- Date.now() - cached.fetchedAt < STEP_CACHE_TTL_MS;
-
- if (fresh) return cached.steps;
-
- if (!this.pending.has(cwd)) {
- const promise = this.fetch(cwd, buildZigMtimeMs, buildZonMtimeMs).finally(
- () => {
- this.pending.delete(cwd);
- if (typeof nova.workspace.reloadTasks === "function") {
- try {
- nova.workspace.reloadTasks(TASK_ASSISTANT_ID);
- } catch (_) {}
- }
- },
- );
- this.pending.set(cwd, promise);
- }
-
- return cached ? cached.steps : null;
- },
-
- async fetch(cwd, buildZigMtimeMs, buildZonMtimeMs) {
- console.log(`[${TASK_ASSISTANT_ID}] stepCache.fetch: cwd=${cwd}`);
- const zigPath = await resolveZigExecutable();
- if (!zigPath) return null;
-
- const result = await runProcess(zigPath, {
- args: ["build", "--list-steps"],
- cwd,
- timeoutMs: 60000,
- });
- if (result.status !== 0) {
- const last = this.lastWarnedAt.get(cwd) || 0;
- if (Date.now() - last >= STEP_DISCOVERY_WARN_THROTTLE_MS) {
- this.lastWarnedAt.set(cwd, Date.now());
- const detail = (result.stderr || "").trim().slice(0, 500);
- console.warn(
- `[zig-task] Step discovery failed in ${cwd} (status ${result.status})${detail ? ": " + detail : ""}`,
- );
- }
- return null;
- }
-
- const steps = result.stdout
- .split("\n")
- .map((line) => line.trim())
- .filter((line) => line.length > 0)
- .map((line) => line.split(/\s+/, 1)[0])
- .filter((name) => /^[A-Za-z_][\w-]*$/.test(name));
-
- this.entries.set(cwd, {
- steps,
- buildZigMtimeMs,
- buildZonMtimeMs,
- fetchedAt: Date.now(),
- });
- return steps;
- },
-
- invalidate(cwd) {
- if (cwd) {
- this.entries.delete(cwd);
- this.lastWarnedAt.delete(cwd);
- } else {
- this.entries.clear();
- this.lastWarnedAt.clear();
- }
- },
-};
-
-// --- LOCALIZATION -------------------------------------------------------------
-
-/**
- * Resolve a localized string, substituting `{variable}` placeholders
- *
- * @param {string} key - Key
- * @param {string} fallback - Fallback text
- * @param {*} variables - Dictionary of variable names and values
- */
-function localizeText(key, fallback, variables) {
- let text = nova.localize(key, null);
-
- if (key === text) {
- return `Localization missing for ${key}`;
- }
-
- if (!variables || typeof variables !== "object") {
- return text;
- }
-
- for (const [name, value] of Object.entries(variables)) {
- text = text.split(`{${name}}`).join(String(value));
- }
-
- return text;
-}
-
-function localize(key, variables) {
- let text = nova.localize(key, null);
-
- if (key === text) {
- console.warn(`[locales] Missing localization for ${key}`);
- return text;
- }
-
- if (!variables || typeof variables !== "object") {
- return text;
- }
-
- for (const [name, value] of Object.entries(variables)) {
- text = text.split(`{${name}}`).join(String(value));
- }
-
- return text;
-}
-
-// --- PROCESS -----------------------------------------------------------------
-
-/**
- * Wraps Nova's Process API in a Promise, resolving with stdout, stderr, and exit status.
- *
- * @param {string} command - Absolute path to the executable
- * @param {Object} options - Options passed to Nova's Process constructor (args, cwd, env, …);
- * the optional `timeoutMs` is consumed here and removed before construction
- * @returns {Promise<{status: number, stdout: string, stderr: string}>}
- */
-function runProcess(command, options) {
- return new Promise((resolve, reject) => {
- const stdout = [];
- const stderr = [];
- const { timeoutMs, ...processOptions } = options || {};
- const process = new Process(command, processOptions);
-
- let settled = false;
- let timer = null;
- const settle = (result) => {
- if (settled) return;
- settled = true;
- if (timer) clearTimeout(timer);
- resolve(result);
- };
-
- process.onStdout((line) => stdout.push(line));
- process.onStderr((line) => stderr.push(line));
- process.onDidExit((status) => {
- settle({
- status,
- stdout: stdout.join(""),
- stderr: stderr.join(""),
- });
- });
-
- try {
- process.start();
- } catch (error) {
- if (timer) clearTimeout(timer);
- reject(error);
- return;
- }
-
- if (typeof timeoutMs === "number" && timeoutMs > 0) {
- timer = setTimeout(() => {
- try {
- if (typeof process.signal === "function") {
- process.signal("SIGTERM");
- }
- } catch (_) {}
- settle({
- status: -1,
- stdout: stdout.join(""),
- stderr: `${stderr.join("")}\n[timeout after ${timeoutMs}ms]`,
- });
- }, timeoutMs);
- }
- });
-}
-
-// --- EXECUTABLE RESOLVERS ----------------------------------------------------
-
-/**
- * Find executable on PATH using `which`
- *
- * @param {string} executableName - Executable name
- * @returns {Promise<string|null>} - Path to the executable or null if not found
- */
-async function findOnPath(executableName) {
- // Pass Nova's own PATH explicitly — child processes do not inherit the parent
- // environment automatically, so without this the subprocess sees a stripped PATH
- // that misses entries like /opt/homebrew/bin.
- const novaPath = (nova.environment && nova.environment.PATH) || "";
-
- // Augment Nova's PATH with well-known prefixes that Homebrew and common
- // package managers use but that may be absent when Nova is launched from the
- // Dock (where launchd provides a narrower system PATH than a login shell).
- const fallbackPrefixes = [
- "/opt/homebrew/bin", // Homebrew – Apple Silicon
- "/usr/local/bin", // Homebrew – Intel / manual installs
- `${nova.environment && nova.environment.HOME}/.local/bin`, // mise, cargo, etc.
- ];
- const augmentedPath = [
- ...new Set([...novaPath.split(":").filter(Boolean), ...fallbackPrefixes]),
- ].join(":");
-
- const result = await runProcess("/usr/bin/env", {
- args: ["which", executableName],
- env: { PATH: augmentedPath },
- });
- const found = result.stdout.trim();
- console.log(
- `[${EXTENSION_ID}] findOnPath: ${executableName} → ${result.status === 0 ? found : "not found"}`,
- );
-
- if (result.status !== 0) {
- return null;
- }
-
- return found.length > 0 ? found : null;
-}
-
-/**
- * Resolves an executable path from config first, falling back to PATH search
- *
- * @param {string} configKey - Configuration key
- * @param {string} defaultCommand - Default command to be searched on PATH
- * @returns {Promise<string|null>} - Path to the executable or null if not found
- */
-async function resolveExecutable(configKey, defaultCommand) {
- const configuredPath = getConfigValue(configKey);
- if (configuredPath) {
- console.log(
- `[${EXTENSION_ID}] findOnPath: ${defaultCommand} → config: ${configuredPath}`,
- );
- return configuredPath;
- }
-
- return await findOnPath(defaultCommand);
-}
-
-/**
- * Resolve the zig executable, show warning if not found
- *
- * @returns {Promise<string|null>} - Path to the zig executable or null if not found
- */
-async function resolveZigExecutable() {
- const zigPath = await resolveExecutable(CONFIG_KEYS.zigPath, "zig");
- if (!zigPath) {
- nova.workspace.showWarningMessage(
- localizeText(
- "warning.zig.not-found",
- "Zig was not found. Install it or set a Zig executable path in Zig extension settings.",
- ),
- );
- return null;
- }
- return zigPath;
-}
-
-/**
- * Resolve executable using Xcode `xcrun`
- *
- * @param {string} executableName - Executable name
- * @returns {Promise<string|null>} - Path to the executable or null if not found
- */
-async function findWithXcode(executableName) {
- const result = await runProcess("/usr/bin/xcrun", {
- args: ["--find", executableName],
- });
-
- if (result.status !== 0) {
- return null;
- }
-
- const path = result.stdout.trim();
- return path.length > 0 ? path : null;
-}
-
-/**
- * Resolve the lldb-dap executable
- *
- * @returns {Promise<string|null>} - Path to the lldb-dap executable or null if not found
- */
-async function resolveLldbDapExecutable() {
- const configuredPath = getConfigValue(CONFIG_KEYS.lldbDapPath);
- if (configuredPath) {
- return configuredPath;
- }
-
- const xcodePath = await findWithXcode("lldb-dap");
- if (xcodePath) {
- return xcodePath;
- }
-
- return await findOnPath("lldb-dap");
-}
-
-/**
- * Return Dynamic Linker (DYLD) framework search paths for LLDB
- *
- * @returns {string[]} - Search paths
- */
-function lldbFrameworkPaths() {
- return [
- "/Applications/Xcode-beta.app/Contents/SharedFrameworks/",
- "/Applications/Xcode.app/Contents/SharedFrameworks/",
- "/Library/Developer/CommandLineTools/Library/PrivateFrameworks/",
- ];
-}
-
-/**
- * Return the absolute path to the lldb-dap proxy Perl script bundled
- * with the extension
- *
- * @returns {string} - Path
- */
-function lldbDapProxyPath() {
- return nova.path.join(nova.extension.path, "Scripts", "lldb-dap-proxy.pl");
-}
-
-/**
- * Return the path to the debug adapter log file in extension global storage
- *
- * @returns {string} - Path
- */
-function debugAdapterLogPath() {
- return nova.path.join(nova.extension.globalStoragePath, "lldb-dap-proxy.log");
-}
-
-/**
- * Return the absolute path to the issue normalizer Perl script bundled
- * with the extension
- *
- * @returns {string} - Path
- */
-function issueNormalizerScriptPath() {
- return nova.path.join(
- nova.extension.path,
- "Scripts",
- "normalize-zig-issues.pl",
- );
-}
-
-// --- SHELL UTILITIES ---------------------------------------------------------
-
-/**
- * Wrap a value in single quotes with embedded single quotes escaped,
- * safe for POSIX shell
- *
- * @param {string} value - String
- * @returns {string} - Quoted string
- */
-function quoteShellArgument(value) {
- const text = value === null || value === undefined ? "" : String(value);
- return `'${text.replace(/'/g, `'\\''`)}'`;
-}
-
-/**
- * Escape backslashes and double quotes for safe embedding in AppleScript
- *
- * @param {string} value - String
- * @returns {string} - Escaped string
- */
-function escapeAppleScriptString(value) {
- const text = value === null || value === undefined ? "" : String(value);
- return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
-}
-
-/**
- * Build a safe shell command string with cd preamble
- *
- * @param {string} command - Command
- * @param {string[]} args - Arguments
- * @param {string} cwd - Working directory
- * @returns {string} - Safe shell command
- */
-function buildShellCommand(command, args, cwd) {
- const segments = [];
- if (cwd) {
- segments.push(`cd ${quoteShellArgument(cwd)}`);
- }
-
- segments.push(
- [quoteShellArgument(command), ...(args || []).map(quoteShellArgument)].join(
- " ",
- ),
- );
- return segments.join("; ");
-}
-
-/**
- * Build a shell command that pipes zig output through the issue normalizer
- *
- * @param {string} command - Command
- * @param {string[]} args - Arguments
- * @returns {string} - Piped command
- */
-function buildIssueNormalizedCommand(command, args) {
- const commandLine = [
- quoteShellArgument(command),
- ...(args || []).map(quoteShellArgument),
- ].join(" ");
- const rewriter = `/usr/bin/perl ${quoteShellArgument(issueNormalizerScriptPath())}`;
- return `setopt pipefail; ${commandLine} 2>&1 | ${rewriter}`;
-}
-
-/**
- * Runs a shell command string in a new macOS Terminal window.
- *
- * @param {string} commandLine - Command to run
- * @returns {Promise<{status: number, stderr: string}>}
- */
-function launchInTerminal(commandLine) {
- return new Promise((resolve) => {
- const script = `tell application "Terminal"
-activate
-do script "${escapeAppleScriptString(commandLine)}"
-end tell`;
- const process = new Process("/usr/bin/osascript", {
- args: ["-e", script],
- });
-
- let stderr = "";
- process.onStderr((line) => {
- stderr += line;
- });
- process.onDidExit((status) => {
- resolve({ status, stderr: stderr.trim() });
- });
-
- try {
- process.start();
- } catch (error) {
- resolve({ status: -1, stderr: String(error) });
- }
- });
-}
-
-// --- COMMANDS ----------------------------------------------------------------
-
-/** Registers the internal runInTerminal command used by external-terminal task actions. */
-function registerCommands() {
- commandRegistrations.push(
- nova.commands.register(
- `${EXTENSION_ID}.runInTerminal`,
- async (workspace, payload) => {
- const command = payload && payload.command;
- const args = (payload && payload.args) || [];
- const cwd = (payload && payload.cwd) || workspace.path || null;
- console.log(
- `[${EXTENSION_ID}] runInTerminal: command=${command} cwd=${cwd}`,
- );
-
- if (!command) {
- workspace.showWarningMessage(
- localizeText(
- "warning.terminal.launch_failed",
- "Unable to launch the Zig task in Terminal.",
- ),
- );
- return;
- }
-
- const result = await launchInTerminal(
- buildShellCommand(command, args, cwd),
- );
- if (result.status !== 0) {
- const prefix = localizeText(
- "warning.terminal.open_failed",
- "Unable to open Terminal for the Zig task.",
- );
- const suffix = result.stderr ? ` ${result.stderr}` : "";
- workspace.showWarningMessage(`${prefix}${suffix}`);
- }
- },
- ),
- );
-}
-
-// --- LANGUAGE SERVER ---------------------------------------------------------
-
-function syncWorkspaceZlsConfiguration(settings) {
- const bridge = {
- "zls.zig_exe_path": settings.zig_exe_path,
- "zls.enable_build_on_save": settings.enable_build_on_save,
- };
-
- Object.entries(bridge).forEach(([key, value]) => {
- if (value === undefined || value === null || value === "") {
- nova.workspace.config.remove(key);
- } else {
- nova.workspace.config.set(key, value);
- }
- });
-}
-
-class ZigLanguageServer {
- constructor() {
- this.client = null;
- this.clientStopDisposable = null;
- this.restartGeneration = 0;
- this.warnedMissing = new Set();
- this.disposables = [];
-
- this.observeConfig(CONFIG_KEYS.zigPath, true);
- this.observeConfig(CONFIG_KEYS.zlsPath, true);
- this.observeConfig(CONFIG_KEYS.zlsEnabled, true);
- this.observeConfig(CONFIG_KEYS.zlsBuildOnSave, true);
- this.observeConfig(CONFIG_KEYS.zlsDebug, true);
- this.disposables.push(
- nova.workspace.onDidChangePath(() => {
- this.start();
- }),
- );
-
- this.start();
- }
-
- observeConfig(key, restart) {
- const onChange = () => {
- if (restart) {
- console.log(
- `[${LANGUAGE_CLIENT_ID}] config changed (${key}) → restart`,
- );
- this.start();
- } else {
- console.log(
- `[${LANGUAGE_CLIENT_ID}] config changed (${key}) → push configuration`,
- );
- this.pushConfiguration().catch((error) => {
- console.error(
- `[${LANGUAGE_CLIENT_ID}] pushConfiguration failed`,
- error,
- );
- });
- }
- };
- this.disposables.push(nova.config.onDidChange(key, onChange));
- this.disposables.push(nova.workspace.config.onDidChange(key, onChange));
- }
-
- dispose() {
- this.stop();
- // Remove observers first so the workspace config cleanup below does not
- // accidentally fire any remaining callbacks.
- this.disposables.forEach((disposable) => {
- if (disposable && typeof disposable.dispose === "function") {
- try {
- disposable.dispose();
- } catch (error) {
- console.error(
- `[${LANGUAGE_CLIENT_ID}] Failed to dispose subscription`,
- error,
- );
- }
- }
- });
- this.disposables = [];
- }
-
- async start() {
- // Guard against stale async continuations: if a config change triggers
- // another start() while we are awaiting, the generation check below bails out.
- const generation = ++this.restartGeneration;
- console.log(`[${LANGUAGE_CLIENT_ID}] start (generation ${generation})`);
- this.stop();
-
- // If a ZLS process was recently stopped (either on this instance or on a
- // previous instance after Nova auto-restarted the extension), wait for it
- // to fully exit before spawning a new one. Without this pause the two
- // processes overlap, Nova sees two clients with the same ID, and kills
- // the new one before the LSP handshake completes.
- const sinceLastStop = Date.now() - lastZlsStopAt;
- if (lastZlsStopAt > 0 && sinceLastStop < ZLS_RESTART_GRACE_MS) {
- const wait = ZLS_RESTART_GRACE_MS - sinceLastStop;
- console.log(
- `[${LANGUAGE_CLIENT_ID}] waiting ${wait}ms for previous ZLS to exit`,
- );
- await new Promise((resolve) => setTimeout(resolve, wait));
- if (generation !== this.restartGeneration) {
- return;
- }
- }
-
- if (!getBooleanConfigValue(CONFIG_KEYS.zlsEnabled, true)) {
- console.log(`[${LANGUAGE_CLIENT_ID}] start: ZLS disabled, skipping`);
- return;
- }
-
- const zlsPath = await resolveExecutable(CONFIG_KEYS.zlsPath, "zls");
- if (generation !== this.restartGeneration) {
- return;
- }
-
- if (!zlsPath) {
- this.warnMissingTool(
- "zls",
- localizeText(
- "warning.zls.not-found",
- "ZLS was not found. Install it or set a ZLS executable path in Zig extension settings.",
- ),
- );
- return;
- }
-
- const { settings, zigPath } = await this.resolveSettings();
- if (generation !== this.restartGeneration) {
- return;
- }
-
- const debugLogs = getBooleanConfigValue(CONFIG_KEYS.zlsDebug, false);
-
- const serverOptions = {
- path: zlsPath,
- args: debugLogs ? [] : ["--disable-lsp-logs"],
- };
-
- const clientOptions = {
- syntaxes: [{ syntax: "zig", languageId: "zig" }],
- debug: debugLogs,
- initializationOptions: settings,
- };
-
- const client = new LanguageClient(
- LANGUAGE_CLIENT_ID,
- localizeText("name.language_server", "Zig Language Server"),
- serverOptions,
- clientOptions,
- );
-
- client.onNotification("window/logMessage", ({ type, message }) => {
- // type: 1=Error, 2=Warning, 3=Info, 4=Log
- const enriched =
- message === "ParseError"
- ? "ParseError — ZLS could not fully parse the Zig source (normal while editing)"
- : message;
- if (type === 1) {
- console.error(`[ZLS] ${enriched}`);
- } else if (type === 2) {
- console.warn(`[ZLS] ${enriched}`);
- } else if (debugLogs) {
- console.log(`[ZLS] ${enriched}`);
- }
- });
-
- this.clientStopDisposable = client.onDidStop((error) => {
- if (error) {
- console.error(`[${LANGUAGE_CLIENT_ID}] ${error.message}`);
- nova.workspace.showWarningMessage(
- localizeText(
- "warning.zls.stopped_unexpectedly",
- "The Zig Language Server stopped unexpectedly ({executable}).",
- { executable: zlsPath || "zls" },
- ),
- );
- }
- });
-
- console.log(
- `[${LANGUAGE_CLIENT_ID}] starting client: zls=${zlsPath} zig=${zigPath || "not found"}`,
- );
- try {
- client.start();
- this.client = client;
- nova.subscriptions.add(client);
- this.pushConfiguration(settings).catch((error) => {
- console.error(
- `[${LANGUAGE_CLIENT_ID}] pushConfiguration failed`,
- error,
- );
- });
- this.warnedMissing.delete("zls");
- if (zigPath) {
- this.warnedMissing.delete("zig");
- }
- } catch (error) {
- console.error(`[${LANGUAGE_CLIENT_ID}] Failed to start ZLS`, error);
- nova.workspace.showWarningMessage(
- localizeText(
- "warning.zls.start_failed",
- "Unable to start the Zig language server at {path}.",
- { path: zlsPath },
- ),
- );
- this.stop();
- }
- }
-
- async resolveSettings() {
- const settings = {
- enable_build_on_save: getBooleanConfigValue(
- CONFIG_KEYS.zlsBuildOnSave,
- false,
- ),
- };
- const zigPath = await resolveExecutable(CONFIG_KEYS.zigPath, "zig");
- if (zigPath) {
- settings.zig_exe_path = zigPath;
- }
-
- return { settings, zigPath };
- }
-
- async pushConfiguration(preResolvedSettings) {
- console.log(`[${LANGUAGE_CLIENT_ID}] pushConfiguration`);
- const generation = this.restartGeneration;
- const { settings } = preResolvedSettings
- ? { settings: preResolvedSettings }
- : await this.resolveSettings();
- if (
- generation !== this.restartGeneration ||
- !this.client ||
- !this.client.running
- ) {
- return;
- }
-
- this.client.sendNotification("workspace/didChangeConfiguration", {
- settings,
- });
- }
-
- stop() {
- if (this.client) {
- console.log(`[${LANGUAGE_CLIENT_ID}] stop`);
- }
- if (
- this.clientStopDisposable &&
- typeof this.clientStopDisposable.dispose === "function"
- ) {
- this.clientStopDisposable.dispose();
- this.clientStopDisposable = null;
- }
-
- if (this.client) {
- this.client.stop();
- nova.subscriptions.remove(this.client);
- this.client = null;
- lastZlsStopAt = Date.now();
- }
- }
-
- warnMissingTool(tool, message) {
- if (this.warnedMissing.has(tool)) {
- return;
- }
-
- this.warnedMissing.add(tool);
- nova.workspace.showWarningMessage(message);
- }
-}
-
-// --- TASK ASSISTANT ----------------------------------------------------------
-
-class ZigTaskAssistant {
- constructor() {
- this.disposable = nova.assistants.registerTaskAssistant(this, {
- identifier: TASK_ASSISTANT_ID,
- name: localize("autotasks.title"),
- });
- }
-
- dispose() {
- if (this.disposable && typeof this.disposable.dispose === "function") {
- this.disposable.dispose();
- this.disposable = null;
- }
- }
-
- provideTasks() {
- const tasks = [];
-
- const currentFile = new Task(localize("autotasks.current-file.name"));
- currentFile.image = "zig-script";
- currentFile.setAction(
- Task.Run,
- new TaskResolvableAction({
- data: {
- type: "current-file-run",
- },
- }),
- );
- currentFile.setAction(
- Task.Clean,
- new TaskResolvableAction({
- data: {
- type: "current-file-clean",
- },
- }),
- );
- tasks.push(currentFile);
-
- const workspacePath = nova.workspace.path;
- if (
- workspacePath &&
- getBooleanConfigValue(CONFIG_KEYS.discoverSteps, true)
- ) {
- const steps = stepCache.getOrFetch(workspacePath);
- if (steps && steps.length > 0) {
- for (const step of steps) {
- const task = new Task(localize("autotasks.buildstep.name", { step }));
- task.image = "zig-hex";
- task.setAction(
- Task.Run,
- new TaskResolvableAction({
- data: {
- type: "build-step",
- step,
- },
- }),
- );
- tasks.push(task);
- }
- }
- }
-
- return tasks;
- }
-
- async resolveTaskAction(context) {
- const type = context.data && context.data.type;
- const config = context.config;
- const cwd = getTaskCwd(config);
- console.log(
- `[${TASK_ASSISTANT_ID}] resolveTaskAction: type=${type} cwd=${cwd}`,
- );
-
- if (type === "clean") {
- return this.resolveCleanAction(cwd);
- }
-
- switch (type) {
- case "build":
- return this.resolveBuildAction(config, cwd);
- case "build-debug":
- return this.resolveDebugBuildAction(config, cwd);
- case "build-run":
- return this.resolveBuildRunAction(config, cwd);
- case "build-step":
- return this.resolveBuildStepAction(context.data && context.data.step);
- case "current-file-run":
- return this.resolveCurrentFileRunAction();
- case "current-file-clean":
- return this.resolveCurrentFileCleanAction();
- case "debug":
- return this.resolveDebugAction(config, cwd);
- case "test-build":
- case "test-run":
- return this.resolveTestAction(config, cwd);
- case "watch":
- return this.resolveWatchAction(config, cwd);
- default:
- return null;
- }
- }
-
- createAction(command, args, cwd) {
- return new TaskProcessAction("/bin/zsh", {
- args: ["-lc", buildIssueNormalizedCommand(command, args)],
- cwd,
- env: {
- NOVA_ZIG_TASK_CWD: cwd || "",
- },
- matchers: [ISSUE_MATCHER],
- });
- }
-
- async resolveCleanAction(cwd) {
- console.log(`[${TASK_ASSISTANT_ID}] resolveCleanAction: cwd=${cwd}`);
- if (!cwd) {
- nova.workspace.showWarningMessage(
- localizeText(
- "warning.clean.missing_cwd",
- "Choose a workspace or working directory before cleaning Zig build artifacts.",
- ),
- );
- return null;
- }
-
- const paths = resolveCleanPaths(cwd);
- if (!paths) return null;
-
- // If the project exposes an `uninstall` step, prefer running it before
- // wiping the cache directories. Use only what's already cached — don't
- // block clean on a fresh `--list-steps` invocation.
- const cached = stepCache.entries.get(cwd);
- const args = ["-rf", ...paths];
-
- if (
- cached &&
- Array.isArray(cached.steps) &&
- cached.steps.includes("uninstall")
- ) {
- const zigPath = await resolveZigExecutable();
- if (zigPath) {
- // `;` (not `&&`) is intentional: rm should run even if uninstall fails.
- const uninstall = buildShellCommand(zigPath, ["build", "uninstall"]);
- const remove = buildShellCommand("/bin/rm", args);
- return new TaskProcessAction("/bin/zsh", {
- args: ["-lc", `${uninstall}; ${remove}`],
- cwd,
- matchers: [ISSUE_MATCHER],
- });
- }
- }
-
- return new TaskProcessAction("/bin/rm", { args, cwd });
- }
-
- async resolveBuildAction(config, cwd) {
- console.log(`[${TASK_ASSISTANT_ID}] resolveBuildAction: cwd=${cwd}`);
- const zigPath = await resolveZigExecutable();
- if (!zigPath) return null;
-
- return this.createAction(zigPath, buildZigArgv(config), cwd);
- }
-
- async resolveDebugBuildAction(config, cwd) {
- console.log(`[${TASK_ASSISTANT_ID}] resolveDebugBuildAction: cwd=${cwd}`);
- const zigPath = await resolveZigExecutable();
- if (!zigPath) return null;
-
- return this.createAction(
- zigPath,
- buildZigArgv(config, { defaultOptimize: "Debug" }),
- cwd,
- );
- }
-
- async resolveBuildRunAction(config, cwd, forceConsole) {
- console.log(`[${TASK_ASSISTANT_ID}] resolveBuildRunAction: cwd=${cwd}`);
- const zigPath = await resolveZigExecutable();
- if (!zigPath) return null;
-
- const step = resolveRunStep(config);
- const argv = buildZigArgv(config, { step });
- const runArgs = getTaskArgs(config, "runArgs");
- if (runArgs.length > 0) argv.push("--", ...runArgs);
-
- const consoleMode =
- forceConsole ||
- getTaskConfigValue(config, "console") ||
- "internalConsole";
-
- if (consoleMode === "externalTerminal") {
- return new TaskCommandAction(`${EXTENSION_ID}.runInTerminal`, {
- args: [
- {
- command: zigPath,
- args: argv,
- cwd,
- },
- ],
- });
- }
-
- return this.createAction(zigPath, argv, cwd);
- }
-
- async resolveBuildStepAction(step) {
- console.log(`[${TASK_ASSISTANT_ID}] resolveBuildStepAction: step=${step}`);
- const zigPath = await resolveZigExecutable();
- if (!zigPath) return null;
-
- const cwd = nova.workspace.path || null;
- if (!cwd || !step || !/^[A-Za-z_][\w-]*$/.test(step)) return null;
-
- return this.createAction(zigPath, ["build", step], cwd);
- }
-
- async resolveTestAction(config, cwd) {
- console.log(`[${TASK_ASSISTANT_ID}] resolveTestAction: cwd=${cwd}`);
- const zigPath = await resolveZigExecutable();
- if (!zigPath) return null;
-
- const argv = buildZigArgv(config, { step: "test" });
-
- const summary = getTaskConfigValue(config, "summary");
- if (summary) argv.push(`--summary=${summary}`);
-
- const testFilter = getTaskConfigValue(config, "testFilter");
- if (testFilter) argv.push("--test-filter", testFilter);
-
- const runArgs = getTaskArgs(config, "runArgs");
- if (runArgs.length > 0) argv.push("--", ...runArgs);
-
- return this.createAction(zigPath, argv, cwd);
- }
-
- async resolveWatchAction(config, cwd) {
- console.log(`[${TASK_ASSISTANT_ID}] resolveWatchAction: cwd=${cwd}`);
- const zigPath = await resolveZigExecutable();
- if (!zigPath) return null;
-
- const step = resolveStep(config, "step", null);
- const argv = buildZigArgv(config, { step });
- argv.push("--watch");
-
- const debounce = getTaskConfigValue(config, "debounceMs");
- if (debounce !== null && debounce !== undefined && debounce !== "") {
- const n = Number(debounce);
- if (Number.isFinite(n) && n >= 0)
- argv.push("--debounce", String(Math.floor(n)));
- }
-
- const incremental = getTaskConfigValue(config, "incremental");
- if (incremental === "on") argv.push("-fincremental");
- else if (incremental === "off") argv.push("-fno-incremental");
-
- return this.createAction(zigPath, argv, cwd);
- }
-
- async resolveCurrentFileRunAction() {
- console.log(`[${TASK_ASSISTANT_ID}] resolveCurrentFileRunAction`);
- const zigPath = await resolveZigExecutable();
- if (!zigPath) return null;
-
- const filePath = activeZigFilePath();
- const cwd = filePath ? nova.path.dirname(filePath) : null;
- if (!filePath || !cwd) {
- nova.workspace.showWarningMessage(
- localizeText(
- "warning.current_file.focus_editor_for_run",
- "Focus a Zig editor before running Current Zig File.",
- ),
- );
- return null;
- }
-
- return this.createAction(zigPath, ["run", filePath], cwd);
- }
-
- async resolveCurrentFileCleanAction() {
- console.log(`[${TASK_ASSISTANT_ID}] resolveCurrentFileCleanAction`);
- const startDir = activeZigFileDirectory();
- if (!startDir) {
- nova.workspace.showWarningMessage(
- localizeText(
- "warning.current_file.focus_editor_for_clean",
- "Focus a Zig editor before cleaning Current Zig File artifacts.",
- ),
- );
- return null;
- }
-
- const cwd = findNearestZigBuildDir(startDir);
- return this.resolveCleanAction(cwd);
- }
-
- async resolveDebugAction(config, cwd) {
- console.log(`[${TASK_ASSISTANT_ID}] resolveDebugAction: cwd=${cwd}`);
- const lldbDapPath = await resolveLldbDapExecutable();
- if (!lldbDapPath) {
- nova.workspace.showWarningMessage(
- localizeText(
- "warning.lldb_dap.not-found",
- "lldb-dap was not found. Install Xcode Command Line Tools or set an LLDB DAP executable path in Zig extension settings.",
- ),
- );
- return null;
- }
-
- const configuredProgramPath = getTaskConfigValue(config, "programPath");
- let programPath = resolvePathAgainstBase(configuredProgramPath, cwd);
- if (!programPath) {
- const projectName = parseProjectName(cwd);
- if (projectName) {
- const candidate = nova.path.join(cwd, "zig-out", "bin", projectName);
- if (nova.fs.stat(candidate)) {
- programPath = candidate;
- }
- }
- }
- if (!programPath) {
- nova.workspace.showWarningMessage(
- localizeText(
- "warning.debug.choose_program",
- "Choose a program path before running Zig Debug.",
- ),
- );
- return null;
- }
-
- const consoleMode =
- getTaskConfigValue(config, "console") || "internalConsole";
- const stopOnEntry = Boolean(config && config.get("stopOnEntry"));
-
- const action = new TaskDebugAdapterAction("zig-lldb-dap");
- action.transport = "stdio";
- action.command = "/usr/bin/perl";
- const debugLog = getBooleanConfigValue(CONFIG_KEYS.lldbDapDebug, false);
- let logPath;
- if (debugLog) {
- try {
- const p = debugAdapterLogPath();
- const dir = nova.path.dirname(p);
- if (!nova.fs.stat(dir)) nova.fs.mkdir(dir);
- logPath = p;
- } catch (_) {}
- }
- action.args = logPath
- ? [lldbDapProxyPath(), lldbDapPath, logPath]
- : [lldbDapProxyPath(), lldbDapPath];
- action.debugRequest = "launch";
- action.env = {
- DYLD_FRAMEWORK_PATH: lldbFrameworkPaths().join(":"),
- NOVA_ZIG_LLDB_DAP_PATH: lldbDapPath,
- ...(logPath ? { NOVA_ZIG_DEBUG_LOG: logPath } : {}),
- };
- action.debugArgs = {
- program: programPath,
- cwd,
- args: getTaskArgs(config, "runArgs"),
- stopOnEntry,
- };
-
- if (consoleMode !== "internalConsole") {
- action.debugArgs.console = consoleMode;
- }
-
- return action;
- }
-}
diff --git a/Scripts/normalize-zig-issues.pl b/Scripts/normalize-zig-issues.pl
deleted file mode 100644
index b6ff328..0000000
--- a/Scripts/normalize-zig-issues.pl
+++ /dev/null
@@ -1,93 +0,0 @@
-use strict;
-use warnings;
-$| = 1;
-
-my $base = $ENV{NOVA_ZIG_TASK_CWD} || $ENV{PWD} || "";
-my %file_cache = ();
-
-sub normalize_path {
- my ($path) = @_;
- return $path if $path =~ m{^/} || $path =~ m{^[A-Za-z]:[\/\\]};
-
- # The Zig compiler occasionally emits paths without a leading slash (e.g.
- # "Users/..." instead of "/Users/...") when traversing relative directories.
- # Detect known top-level directory names and restore the leading slash.
- if ($path =~ m{^(?:Users|private|opt|Library|System|usr|var|tmp|etc|home)/}) {
- return "/$path";
- }
-
- return $base eq "" ? $path : "$base/$path";
-}
-
-sub source_line_for_path {
- my ($path, $line_number) = @_;
- return undef if $line_number < 1;
-
- if (!exists $file_cache{$path}) {
- if (open my $fh, "<", $path) {
- my @lines = <$fh>;
- close $fh;
- $file_cache{$path} = \@lines;
- } else {
- $file_cache{$path} = undef;
- }
- }
-
- my $lines = $file_cache{$path};
- return undef if !defined $lines;
- return undef if $line_number > scalar(@$lines);
-
- my $line = $lines->[$line_number - 1];
- $line =~ s/\r?\n$//;
- return $line;
-}
-
-# Zig reports argument-count errors at the column of the opening parenthesis,
-# which causes Nova to underline the entire argument list. For method-call
-# expressions (receiver.method(...)), shift the column back to the start of
-# the callee identifier so only the call site itself is highlighted.
-sub adjusted_call_column {
- my ($source_line, $column) = @_;
- return $column if !defined $source_line || $column < 1;
-
- my $column_index = $column - 1;
- my $open_paren = index($source_line, "(", $column_index);
- return $column if $open_paren < 0;
-
- my $prefix = substr($source_line, 0, $open_paren);
- return $column if $prefix !~ /([A-Za-z_][A-Za-z0-9_]*)\s*$/;
-
- my $callee = $1;
- my $callee_index = rindex($prefix, $callee);
- return $column if $callee_index < 0;
-
- my $target_column = $callee_index + 1;
- return $column if $target_column <= $column;
-
- my $between = substr($source_line, $column_index, $target_column - $column);
- return $column if $between !~ /\./;
-
- return $target_column;
-}
-
-while (my $line = <STDIN>) {
- my $newline = $line =~ s/\r?\n$// ? "\n" : "";
-
- if ($line =~ m{^([^:\n]+\.zig):(\d+):(\d+):\s*(error|warning|note):\s*(.*)$}) {
- my ($path, $line_number, $column, $severity, $message) = ($1, $2, $3, $4, $5);
- my $normalized = normalize_path($path);
-
- if ($message =~ /\bexpected\b.*argument\(s\).*\bfound\b/i) {
- my $source_line = source_line_for_path($normalized, $line_number);
- $column = adjusted_call_column($source_line, $column);
- }
-
- $line = "$normalized:$line_number:$column: $severity: $message";
- } elsif ($line =~ m{^([^:\n]+\.zig):(\d+):(\d+):}) {
- my ($path, $line_number, $column) = ($1, $2, $3);
- my $normalized = normalize_path($path);
- $line =~ s{^[^:\n]+\.zig:\d+:\d+:}{$normalized:$line_number:$column:};
- }
-
- print $line, $newline;
-}
diff --git a/Scripts/update-parser.sh b/Scripts/update-parser.sh
deleted file mode 100755
index 6e1fd63..0000000
--- a/Scripts/update-parser.sh
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/bin/sh
-#
-# update-parser.sh — Bump vendored tree-sitter-zig and rebuild the dylib.
-#
-# Purpose:
-# Refresh the vendored snapshot under vendor/tree-sitter-zig/ to a newer
-# upstream commit, update VENDORING.md, and rebuild the parser dylib.
-#
-# What it does:
-# - clones tree-sitter-grammars/tree-sitter-zig into a temp dir
-# - checks out the requested ref (default: HEAD of the default branch)
-# - exits early if the upstream SHA matches the pinned SHA
-# - replaces src/, queries/, grammar.js, tree-sitter.json, LICENSE
-# with the upstream copies; leaves VENDORING.md and README.upstream.md
-# untouched
-# - rewrites the "Pinned commit:" line in VENDORING.md
-# - invokes build-parser.sh to rebuild Syntaxes/libtree-sitter-zig.dylib
-# - prints a GitHub compare link for the diff
-#
-# Usage:
-# ./Scripts/update-parser.sh # bump to upstream HEAD
-# ./Scripts/update-parser.sh <ref> # bump to a tag, branch, or SHA
-#
-# Caveats:
-# - any local edits inside the listed paths are overwritten — review
-# `git diff vendor/` afterwards before committing
-# - parses VENDORING.md by an exact "Pinned commit: <sha>" line prefix;
-# keep that line format intact
-#
-# Requirements:
-# git, plus everything build-parser.sh needs.
-
-set -eu
-
-ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
-VENDOR_DIR="$ROOT/vendor/tree-sitter-zig"
-UPSTREAM="https://github.com/tree-sitter-grammars/tree-sitter-zig.git"
-REF="${1:-HEAD}"
-
-TMP="$(mktemp -d)"
-trap 'rm -rf "$TMP"' EXIT
-
-git clone --quiet "$UPSTREAM" "$TMP/repo"
-git -C "$TMP/repo" checkout --quiet "$REF"
-SHA="$(git -C "$TMP/repo" rev-parse HEAD)"
-
-OLD_SHA="$(awk '/^Pinned commit:/ {print $3}' "$VENDOR_DIR/VENDORING.md")"
-if [ "$SHA" = "$OLD_SHA" ]; then
- echo "Already at $SHA — nothing to do."
- exit 0
-fi
-
-for path in src queries grammar.js tree-sitter.json LICENSE; do
- rm -rf "$VENDOR_DIR/$path"
- cp -R "$TMP/repo/$path" "$VENDOR_DIR/$path"
-done
-
-sed -i.bak "s/^Pinned commit: .*/Pinned commit: $SHA/" "$VENDOR_DIR/VENDORING.md"
-rm "$VENDOR_DIR/VENDORING.md.bak"
-
-"$ROOT/Scripts/build-parser.sh"
-
-echo "Updated $OLD_SHA -> $SHA"
-echo "Compare: https://github.com/tree-sitter-grammars/tree-sitter-zig/compare/$OLD_SHA...$SHA"