diff options
Diffstat (limited to 'Scripts/main.js')
| -rw-r--r-- | Scripts/main.js | 915 |
1 files changed, 915 insertions, 0 deletions
diff --git a/Scripts/main.js b/Scripts/main.js new file mode 100644 index 0000000..76bd816 --- /dev/null +++ b/Scripts/main.js | |||
| @@ -0,0 +1,915 @@ | |||
| 1 | "use strict"; | ||
| 2 | |||
| 3 | const EXTENSION_ID = "at.dcz.nova-zig"; | ||
| 4 | const TASK_ASSISTANT_ID = `${EXTENSION_ID}.tasks`; | ||
| 5 | const LANGUAGE_CLIENT_ID = `${EXTENSION_ID}.zls`; | ||
| 6 | const ISSUE_MATCHER = "zig.compiler"; | ||
| 7 | |||
| 8 | const CONFIG_KEYS = { | ||
| 9 | zigPath: `${EXTENSION_ID}.toolchain.zig-path`, | ||
| 10 | zlsPath: `${EXTENSION_ID}.toolchain.zls-path`, | ||
| 11 | lldbDapPath: `${EXTENSION_ID}.toolchain.lldb-dap-path`, | ||
| 12 | zlsEnabled: `${EXTENSION_ID}.zls.enabled`, | ||
| 13 | zlsBuildOnSave: `${EXTENSION_ID}.zls.build-on-save`, | ||
| 14 | zlsDebug: `${EXTENSION_ID}.zls.debug`, | ||
| 15 | }; | ||
| 16 | |||
| 17 | let languageServer = null; | ||
| 18 | let taskAssistant = null; | ||
| 19 | let issueAssistant = null; | ||
| 20 | let commandRegistrations = []; | ||
| 21 | |||
| 22 | exports.activate = function activate() { | ||
| 23 | registerCommands(); | ||
| 24 | taskAssistant = new ZigTaskAssistant(); | ||
| 25 | languageServer = new ZigLanguageServer(); | ||
| 26 | issueAssistant = new ZigIssueAssistant(); | ||
| 27 | }; | ||
| 28 | |||
| 29 | exports.deactivate = function deactivate() { | ||
| 30 | if (languageServer) { | ||
| 31 | languageServer.dispose(); | ||
| 32 | languageServer = null; | ||
| 33 | } | ||
| 34 | |||
| 35 | if (taskAssistant) { | ||
| 36 | taskAssistant.dispose(); | ||
| 37 | taskAssistant = null; | ||
| 38 | } | ||
| 39 | |||
| 40 | if (issueAssistant) { | ||
| 41 | issueAssistant.dispose(); | ||
| 42 | issueAssistant = null; | ||
| 43 | } | ||
| 44 | |||
| 45 | commandRegistrations.forEach((disposable) => { | ||
| 46 | if (disposable && typeof disposable.dispose === "function") { | ||
| 47 | disposable.dispose(); | ||
| 48 | } | ||
| 49 | }); | ||
| 50 | commandRegistrations = []; | ||
| 51 | }; | ||
| 52 | |||
| 53 | function getConfigValue(key) { | ||
| 54 | const workspaceValue = nova.workspace.config.get(key); | ||
| 55 | if (workspaceValue !== undefined && workspaceValue !== null && workspaceValue !== "") { | ||
| 56 | return workspaceValue; | ||
| 57 | } | ||
| 58 | |||
| 59 | const globalValue = nova.config.get(key); | ||
| 60 | if (globalValue !== undefined && globalValue !== null && globalValue !== "") { | ||
| 61 | return globalValue; | ||
| 62 | } | ||
| 63 | |||
| 64 | return null; | ||
| 65 | } | ||
| 66 | |||
| 67 | function getBooleanConfigValue(key, fallback) { | ||
| 68 | const workspaceValue = nova.workspace.config.get(key); | ||
| 69 | if (typeof workspaceValue === "boolean") { | ||
| 70 | return workspaceValue; | ||
| 71 | } | ||
| 72 | |||
| 73 | const globalValue = nova.config.get(key); | ||
| 74 | if (typeof globalValue === "boolean") { | ||
| 75 | return globalValue; | ||
| 76 | } | ||
| 77 | |||
| 78 | return fallback; | ||
| 79 | } | ||
| 80 | |||
| 81 | function normalizeArray(value) { | ||
| 82 | if (!Array.isArray(value)) { | ||
| 83 | return []; | ||
| 84 | } | ||
| 85 | |||
| 86 | return value | ||
| 87 | .map((entry) => (entry === null || entry === undefined ? "" : String(entry).trim())) | ||
| 88 | .filter((entry) => entry.length > 0); | ||
| 89 | } | ||
| 90 | |||
| 91 | function resolveWorkspaceRelativePath(path) { | ||
| 92 | if (!path) { | ||
| 93 | return null; | ||
| 94 | } | ||
| 95 | |||
| 96 | if (path.startsWith("/")) { | ||
| 97 | return path; | ||
| 98 | } | ||
| 99 | |||
| 100 | if (nova.workspace.path) { | ||
| 101 | return nova.path.join(nova.workspace.path, path); | ||
| 102 | } | ||
| 103 | |||
| 104 | return path; | ||
| 105 | } | ||
| 106 | |||
| 107 | function resolvePathAgainstBase(path, base) { | ||
| 108 | if (!path) { | ||
| 109 | return null; | ||
| 110 | } | ||
| 111 | |||
| 112 | if (path.startsWith("/")) { | ||
| 113 | return path; | ||
| 114 | } | ||
| 115 | |||
| 116 | if (base) { | ||
| 117 | return nova.path.join(base, path); | ||
| 118 | } | ||
| 119 | |||
| 120 | return resolveWorkspaceRelativePath(path); | ||
| 121 | } | ||
| 122 | |||
| 123 | function getTaskConfigValue(config, key) { | ||
| 124 | if (!config) { | ||
| 125 | return null; | ||
| 126 | } | ||
| 127 | |||
| 128 | const value = config.get(key); | ||
| 129 | return value === undefined || value === null || value === "" ? null : value; | ||
| 130 | } | ||
| 131 | |||
| 132 | function getTaskArgs(config, key) { | ||
| 133 | return normalizeArray(getTaskConfigValue(config, key)); | ||
| 134 | } | ||
| 135 | |||
| 136 | function getTaskCwd(config) { | ||
| 137 | const configured = getTaskConfigValue(config, "cwd"); | ||
| 138 | if (configured) { | ||
| 139 | return resolveWorkspaceRelativePath(configured); | ||
| 140 | } | ||
| 141 | |||
| 142 | return nova.workspace.path || null; | ||
| 143 | } | ||
| 144 | |||
| 145 | function activeZigFilePath() { | ||
| 146 | const editor = nova.workspace.activeTextEditor; | ||
| 147 | if (!editor || !editor.document || !editor.document.path) { | ||
| 148 | return null; | ||
| 149 | } | ||
| 150 | |||
| 151 | if (editor.document.syntax !== "zig") { | ||
| 152 | return null; | ||
| 153 | } | ||
| 154 | |||
| 155 | return editor.document.path; | ||
| 156 | } | ||
| 157 | |||
| 158 | function activeZigFileDirectory() { | ||
| 159 | const filePath = activeZigFilePath(); | ||
| 160 | if (!filePath) { | ||
| 161 | return null; | ||
| 162 | } | ||
| 163 | |||
| 164 | return nova.path.dirname(filePath); | ||
| 165 | } | ||
| 166 | |||
| 167 | function localizedText(key, fallback, variables) { | ||
| 168 | let text = nova.localize(key, fallback); | ||
| 169 | |||
| 170 | if (!variables || typeof variables !== "object") { | ||
| 171 | return text; | ||
| 172 | } | ||
| 173 | |||
| 174 | for (const [name, value] of Object.entries(variables)) { | ||
| 175 | text = text.split(`{${name}}`).join(String(value)); | ||
| 176 | } | ||
| 177 | |||
| 178 | return text; | ||
| 179 | } | ||
| 180 | |||
| 181 | function runProcess(command, options) { | ||
| 182 | return new Promise((resolve) => { | ||
| 183 | const stdout = []; | ||
| 184 | const stderr = []; | ||
| 185 | const process = new Process(command, options || {}); | ||
| 186 | |||
| 187 | process.onStdout((line) => stdout.push(line)); | ||
| 188 | process.onStderr((line) => stderr.push(line)); | ||
| 189 | process.onDidExit((status) => { | ||
| 190 | resolve({ | ||
| 191 | status, | ||
| 192 | stdout: stdout.join(""), | ||
| 193 | stderr: stderr.join(""), | ||
| 194 | }); | ||
| 195 | }); | ||
| 196 | |||
| 197 | process.start(); | ||
| 198 | }); | ||
| 199 | } | ||
| 200 | |||
| 201 | async function findExecutableOnPath(commandName) { | ||
| 202 | const result = await runProcess("/usr/bin/env", { | ||
| 203 | args: ["which", commandName], | ||
| 204 | }); | ||
| 205 | |||
| 206 | if (result.status !== 0) { | ||
| 207 | return null; | ||
| 208 | } | ||
| 209 | |||
| 210 | const path = result.stdout.trim(); | ||
| 211 | return path.length > 0 ? path : null; | ||
| 212 | } | ||
| 213 | |||
| 214 | async function resolveExecutable(configKey, defaultCommand) { | ||
| 215 | const configuredPath = getConfigValue(configKey); | ||
| 216 | if (configuredPath) { | ||
| 217 | return configuredPath; | ||
| 218 | } | ||
| 219 | |||
| 220 | return findExecutableOnPath(defaultCommand); | ||
| 221 | } | ||
| 222 | |||
| 223 | async function findExecutableWithXcode(commandName) { | ||
| 224 | const result = await runProcess("/usr/bin/xcrun", { | ||
| 225 | args: ["--find", commandName], | ||
| 226 | }); | ||
| 227 | |||
| 228 | if (result.status !== 0) { | ||
| 229 | return null; | ||
| 230 | } | ||
| 231 | |||
| 232 | const path = result.stdout.trim(); | ||
| 233 | return path.length > 0 ? path : null; | ||
| 234 | } | ||
| 235 | |||
| 236 | async function resolveLLDBDAPExecutable() { | ||
| 237 | const configuredPath = getConfigValue(CONFIG_KEYS.lldbDapPath); | ||
| 238 | if (configuredPath) { | ||
| 239 | return configuredPath; | ||
| 240 | } | ||
| 241 | |||
| 242 | const xcodePath = await findExecutableWithXcode("lldb-dap"); | ||
| 243 | if (xcodePath) { | ||
| 244 | return xcodePath; | ||
| 245 | } | ||
| 246 | |||
| 247 | return findExecutableOnPath("lldb-dap"); | ||
| 248 | } | ||
| 249 | |||
| 250 | function executableDisplayName(path, fallback) { | ||
| 251 | return path || fallback; | ||
| 252 | } | ||
| 253 | |||
| 254 | function lldbFrameworkPaths() { | ||
| 255 | return [ | ||
| 256 | "/Applications/Xcode-beta.app/Contents/SharedFrameworks/", | ||
| 257 | "/Applications/Xcode.app/Contents/SharedFrameworks/", | ||
| 258 | "/Library/Developer/CommandLineTools/Library/PrivateFrameworks/", | ||
| 259 | ]; | ||
| 260 | } | ||
| 261 | |||
| 262 | function lldbDapProxyPath() { | ||
| 263 | return nova.path.join(nova.extension.path, "Scripts", "lldb-dap-proxy.pl"); | ||
| 264 | } | ||
| 265 | |||
| 266 | function debugAdapterLogPath() { | ||
| 267 | return "/tmp/zig-lldb-dap-proxy.log"; | ||
| 268 | } | ||
| 269 | |||
| 270 | function issueNormalizerScriptPath() { | ||
| 271 | return nova.path.join(nova.extension.path, "Scripts", "normalize-zig-issues.pl"); | ||
| 272 | } | ||
| 273 | |||
| 274 | function quoteShellArgument(value) { | ||
| 275 | const text = value === null || value === undefined ? "" : String(value); | ||
| 276 | return `'${text.replace(/'/g, `'\\''`)}'`; | ||
| 277 | } | ||
| 278 | |||
| 279 | function escapeAppleScriptString(value) { | ||
| 280 | return String(value) | ||
| 281 | .replace(/\\/g, "\\\\") | ||
| 282 | .replace(/"/g, '\\"'); | ||
| 283 | } | ||
| 284 | |||
| 285 | function buildShellCommand(command, args, cwd) { | ||
| 286 | const segments = []; | ||
| 287 | if (cwd) { | ||
| 288 | segments.push(`cd ${quoteShellArgument(cwd)}`); | ||
| 289 | } | ||
| 290 | |||
| 291 | segments.push([quoteShellArgument(command), ...(args || []).map(quoteShellArgument)].join(" ")); | ||
| 292 | return segments.join("; "); | ||
| 293 | } | ||
| 294 | |||
| 295 | function buildIssueNormalizedCommand(command, args) { | ||
| 296 | const commandLine = [ | ||
| 297 | quoteShellArgument(command), | ||
| 298 | ...(args || []).map(quoteShellArgument), | ||
| 299 | ].join(" "); | ||
| 300 | const rewriter = `/usr/bin/perl ${quoteShellArgument(issueNormalizerScriptPath())}`; | ||
| 301 | return `setopt pipefail; ${commandLine} 2>&1 | ${rewriter}`; | ||
| 302 | } | ||
| 303 | |||
| 304 | function launchInTerminal(commandLine) { | ||
| 305 | return new Promise((resolve) => { | ||
| 306 | const script = `tell application "Terminal" | ||
| 307 | activate | ||
| 308 | do script "${escapeAppleScriptString(commandLine)}" | ||
| 309 | end tell`; | ||
| 310 | const process = new Process("/usr/bin/osascript", { | ||
| 311 | args: ["-e", script], | ||
| 312 | }); | ||
| 313 | |||
| 314 | let stderr = ""; | ||
| 315 | process.onStderr((line) => { | ||
| 316 | stderr += line; | ||
| 317 | }); | ||
| 318 | process.onDidExit((status) => { | ||
| 319 | resolve({ status, stderr: stderr.trim() }); | ||
| 320 | }); | ||
| 321 | process.start(); | ||
| 322 | }); | ||
| 323 | } | ||
| 324 | |||
| 325 | function registerCommands() { | ||
| 326 | commandRegistrations.push( | ||
| 327 | nova.commands.register(`${EXTENSION_ID}.runInTerminal`, async (workspace, payload) => { | ||
| 328 | const command = payload && payload.command; | ||
| 329 | const args = (payload && payload.args) || []; | ||
| 330 | const cwd = (payload && payload.cwd) || workspace.path || null; | ||
| 331 | |||
| 332 | if (!command) { | ||
| 333 | workspace.showWarningMessage( | ||
| 334 | localizedText( | ||
| 335 | "warning.terminal.launch_failed", | ||
| 336 | "Unable to launch the Zig task in Terminal." | ||
| 337 | ) | ||
| 338 | ); | ||
| 339 | return; | ||
| 340 | } | ||
| 341 | |||
| 342 | const result = await launchInTerminal(buildShellCommand(command, args, cwd)); | ||
| 343 | if (result.status !== 0) { | ||
| 344 | const prefix = localizedText( | ||
| 345 | "warning.terminal.open_failed", | ||
| 346 | "Unable to open Terminal for the Zig task." | ||
| 347 | ); | ||
| 348 | const suffix = result.stderr ? ` ${result.stderr}` : ""; | ||
| 349 | workspace.showWarningMessage(`${prefix}${suffix}`); | ||
| 350 | } | ||
| 351 | }) | ||
| 352 | ); | ||
| 353 | } | ||
| 354 | |||
| 355 | function syncWorkspaceZlsConfiguration(settings) { | ||
| 356 | const bridge = { | ||
| 357 | "zls.zig_exe_path": settings.zig_exe_path, | ||
| 358 | "zls.enable_build_on_save": settings.enable_build_on_save, | ||
| 359 | }; | ||
| 360 | |||
| 361 | Object.entries(bridge).forEach(([key, value]) => { | ||
| 362 | if (value === undefined || value === null || value === "") { | ||
| 363 | nova.workspace.config.remove(key); | ||
| 364 | } else { | ||
| 365 | nova.workspace.config.set(key, value); | ||
| 366 | } | ||
| 367 | }); | ||
| 368 | } | ||
| 369 | |||
| 370 | class ZigLanguageServer { | ||
| 371 | constructor() { | ||
| 372 | this.client = null; | ||
| 373 | this.clientStopDisposable = null; | ||
| 374 | this.restartGeneration = 0; | ||
| 375 | this.warnedMissing = new Set(); | ||
| 376 | this.disposables = []; | ||
| 377 | |||
| 378 | this.observeConfig(CONFIG_KEYS.zigPath, true); | ||
| 379 | this.observeConfig(CONFIG_KEYS.zlsPath, true); | ||
| 380 | this.observeConfig(CONFIG_KEYS.zlsEnabled, true); | ||
| 381 | this.observeConfig(CONFIG_KEYS.zlsBuildOnSave, true); | ||
| 382 | this.observeConfig(CONFIG_KEYS.zlsDebug, true); | ||
| 383 | this.disposables.push( | ||
| 384 | nova.workspace.onDidChangePath(() => { | ||
| 385 | this.start(); | ||
| 386 | }) | ||
| 387 | ); | ||
| 388 | |||
| 389 | this.start(); | ||
| 390 | } | ||
| 391 | |||
| 392 | observeConfig(key, restart) { | ||
| 393 | this.disposables.push( | ||
| 394 | nova.config.onDidChange(key, () => { | ||
| 395 | if (restart) { | ||
| 396 | this.start(); | ||
| 397 | } else { | ||
| 398 | void this.pushConfiguration(); | ||
| 399 | } | ||
| 400 | }) | ||
| 401 | ); | ||
| 402 | this.disposables.push( | ||
| 403 | nova.workspace.config.onDidChange(key, () => { | ||
| 404 | if (restart) { | ||
| 405 | this.start(); | ||
| 406 | } else { | ||
| 407 | void this.pushConfiguration(); | ||
| 408 | } | ||
| 409 | }) | ||
| 410 | ); | ||
| 411 | } | ||
| 412 | |||
| 413 | dispose() { | ||
| 414 | this.stop(); | ||
| 415 | this.disposables.forEach((disposable) => { | ||
| 416 | if (disposable && typeof disposable.dispose === "function") { | ||
| 417 | disposable.dispose(); | ||
| 418 | } | ||
| 419 | }); | ||
| 420 | this.disposables = []; | ||
| 421 | } | ||
| 422 | |||
| 423 | async start() { | ||
| 424 | // Guard against stale async continuations: if a config change triggers | ||
| 425 | // another start() while we are awaiting, the generation check below bails out. | ||
| 426 | const generation = ++this.restartGeneration; | ||
| 427 | this.stop(); | ||
| 428 | |||
| 429 | if (!getBooleanConfigValue(CONFIG_KEYS.zlsEnabled, true)) { | ||
| 430 | return; | ||
| 431 | } | ||
| 432 | |||
| 433 | const zlsPath = await resolveExecutable(CONFIG_KEYS.zlsPath, "zls"); | ||
| 434 | if (generation !== this.restartGeneration) { | ||
| 435 | return; | ||
| 436 | } | ||
| 437 | |||
| 438 | if (!zlsPath) { | ||
| 439 | this.warnMissingTool( | ||
| 440 | "zls", | ||
| 441 | localizedText( | ||
| 442 | "warning.zls.not_found", | ||
| 443 | "ZLS was not found. Install it or set a ZLS executable path in Zig extension settings." | ||
| 444 | ) | ||
| 445 | ); | ||
| 446 | return; | ||
| 447 | } | ||
| 448 | |||
| 449 | const { settings, zigPath } = await this.resolveSettings(); | ||
| 450 | if (generation !== this.restartGeneration) { | ||
| 451 | return; | ||
| 452 | } | ||
| 453 | |||
| 454 | syncWorkspaceZlsConfiguration(settings); | ||
| 455 | |||
| 456 | const debugLogs = getBooleanConfigValue(CONFIG_KEYS.zlsDebug, false); | ||
| 457 | |||
| 458 | const serverOptions = { | ||
| 459 | path: zlsPath, | ||
| 460 | args: debugLogs ? [] : ["--disable-lsp-logs"], | ||
| 461 | }; | ||
| 462 | |||
| 463 | const clientOptions = { | ||
| 464 | syntaxes: [{ syntax: "zig", languageId: "zig" }], | ||
| 465 | debug: debugLogs, | ||
| 466 | initializationOptions: { | ||
| 467 | zls: settings, | ||
| 468 | }, | ||
| 469 | }; | ||
| 470 | |||
| 471 | const client = new LanguageClient( | ||
| 472 | LANGUAGE_CLIENT_ID, | ||
| 473 | localizedText("name.language_server", "Zig Language Server"), | ||
| 474 | serverOptions, | ||
| 475 | clientOptions | ||
| 476 | ); | ||
| 477 | |||
| 478 | this.clientStopDisposable = client.onDidStop((error) => { | ||
| 479 | if (error) { | ||
| 480 | console.error(`[${LANGUAGE_CLIENT_ID}] ${error.message}`); | ||
| 481 | nova.workspace.showWarningMessage( | ||
| 482 | localizedText( | ||
| 483 | "warning.zls.stopped_unexpectedly", | ||
| 484 | "The Zig language server stopped unexpectedly ({executable}).", | ||
| 485 | { executable: executableDisplayName(zlsPath, "zls") } | ||
| 486 | ) | ||
| 487 | ); | ||
| 488 | } | ||
| 489 | }); | ||
| 490 | |||
| 491 | try { | ||
| 492 | client.start(); | ||
| 493 | this.client = client; | ||
| 494 | nova.subscriptions.add(client); | ||
| 495 | void this.pushConfiguration(); | ||
| 496 | this.warnedMissing.delete("zls"); | ||
| 497 | if (zigPath) { | ||
| 498 | this.warnedMissing.delete("zig"); | ||
| 499 | } | ||
| 500 | } catch (error) { | ||
| 501 | console.error(`[${LANGUAGE_CLIENT_ID}] Failed to start ZLS`, error); | ||
| 502 | nova.workspace.showWarningMessage( | ||
| 503 | localizedText( | ||
| 504 | "warning.zls.start_failed", | ||
| 505 | "Unable to start the Zig language server at {path}.", | ||
| 506 | { path: zlsPath } | ||
| 507 | ) | ||
| 508 | ); | ||
| 509 | this.stop(); | ||
| 510 | } | ||
| 511 | } | ||
| 512 | |||
| 513 | async resolveSettings() { | ||
| 514 | const settings = { | ||
| 515 | enable_build_on_save: getBooleanConfigValue(CONFIG_KEYS.zlsBuildOnSave, false), | ||
| 516 | }; | ||
| 517 | const zigPath = await resolveExecutable(CONFIG_KEYS.zigPath, "zig"); | ||
| 518 | if (zigPath) { | ||
| 519 | settings.zig_exe_path = zigPath; | ||
| 520 | } | ||
| 521 | |||
| 522 | return { settings, zigPath }; | ||
| 523 | } | ||
| 524 | |||
| 525 | async pushConfiguration() { | ||
| 526 | const generation = this.restartGeneration; | ||
| 527 | const { settings } = await this.resolveSettings(); | ||
| 528 | if (generation !== this.restartGeneration || !this.client || !this.client.running) { | ||
| 529 | return; | ||
| 530 | } | ||
| 531 | |||
| 532 | syncWorkspaceZlsConfiguration(settings); | ||
| 533 | |||
| 534 | this.client.sendNotification("workspace/didChangeConfiguration", { | ||
| 535 | settings: { | ||
| 536 | zls: settings, | ||
| 537 | }, | ||
| 538 | }); | ||
| 539 | } | ||
| 540 | |||
| 541 | stop() { | ||
| 542 | if (this.clientStopDisposable && typeof this.clientStopDisposable.dispose === "function") { | ||
| 543 | this.clientStopDisposable.dispose(); | ||
| 544 | this.clientStopDisposable = null; | ||
| 545 | } | ||
| 546 | |||
| 547 | if (this.client) { | ||
| 548 | this.client.stop(); | ||
| 549 | nova.subscriptions.remove(this.client); | ||
| 550 | this.client = null; | ||
| 551 | } | ||
| 552 | } | ||
| 553 | |||
| 554 | warnMissingTool(tool, message) { | ||
| 555 | if (this.warnedMissing.has(tool)) { | ||
| 556 | return; | ||
| 557 | } | ||
| 558 | |||
| 559 | this.warnedMissing.add(tool); | ||
| 560 | nova.workspace.showWarningMessage(message); | ||
| 561 | } | ||
| 562 | } | ||
| 563 | |||
| 564 | class ZigTaskAssistant { | ||
| 565 | constructor() { | ||
| 566 | this.disposable = nova.assistants.registerTaskAssistant(this, { | ||
| 567 | identifier: TASK_ASSISTANT_ID, | ||
| 568 | name: localizedText("name.extension", "Zig"), | ||
| 569 | }); | ||
| 570 | } | ||
| 571 | |||
| 572 | dispose() { | ||
| 573 | if (this.disposable && typeof this.disposable.dispose === "function") { | ||
| 574 | this.disposable.dispose(); | ||
| 575 | this.disposable = null; | ||
| 576 | } | ||
| 577 | } | ||
| 578 | |||
| 579 | provideTasks() { | ||
| 580 | const task = new Task(localizedText("task.current_file.name", "Current Zig File")); | ||
| 581 | task.setAction(Task.Run, new TaskResolvableAction({ | ||
| 582 | data: { | ||
| 583 | type: "current-file-run", | ||
| 584 | }, | ||
| 585 | })); | ||
| 586 | task.setAction(Task.Clean, new TaskResolvableAction({ | ||
| 587 | data: { | ||
| 588 | type: "current-file-clean", | ||
| 589 | }, | ||
| 590 | })); | ||
| 591 | return [task]; | ||
| 592 | } | ||
| 593 | |||
| 594 | async resolveTaskAction(context) { | ||
| 595 | const type = context.data && context.data.type; | ||
| 596 | const config = context.config; | ||
| 597 | const cwd = getTaskCwd(config); | ||
| 598 | |||
| 599 | if (type === "clean") { | ||
| 600 | return this.resolveCleanAction(cwd); | ||
| 601 | } | ||
| 602 | |||
| 603 | switch (type) { | ||
| 604 | case "build": | ||
| 605 | return this.resolveBuildAction(config, cwd); | ||
| 606 | case "build-debug": | ||
| 607 | return this.resolveDebugBuildAction(config, cwd); | ||
| 608 | case "build-run": | ||
| 609 | return this.resolveBuildRunAction(config, cwd); | ||
| 610 | case "build-run-terminal": | ||
| 611 | return this.resolveBuildRunTerminalAction(config, cwd); | ||
| 612 | case "build-test": | ||
| 613 | return this.resolveBuildTestAction(config, cwd); | ||
| 614 | case "current-file-run": | ||
| 615 | return this.resolveCurrentFileRunAction(); | ||
| 616 | case "current-file-clean": | ||
| 617 | return this.resolveCurrentFileCleanAction(); | ||
| 618 | case "debug": | ||
| 619 | return this.resolveDebugAction(config, cwd); | ||
| 620 | case "file-test": | ||
| 621 | return this.resolveFileTestAction(config, cwd); | ||
| 622 | default: | ||
| 623 | return null; | ||
| 624 | } | ||
| 625 | } | ||
| 626 | |||
| 627 | createAction(command, args, cwd) { | ||
| 628 | return new TaskProcessAction("/bin/zsh", { | ||
| 629 | args: ["-lc", buildIssueNormalizedCommand(command, args)], | ||
| 630 | cwd, | ||
| 631 | env: { | ||
| 632 | NOVA_ZIG_TASK_CWD: cwd || "", | ||
| 633 | }, | ||
| 634 | matchers: [ISSUE_MATCHER], | ||
| 635 | }); | ||
| 636 | } | ||
| 637 | |||
| 638 | resolveCleanAction(cwd) { | ||
| 639 | if (!cwd) { | ||
| 640 | nova.workspace.showWarningMessage( | ||
| 641 | localizedText( | ||
| 642 | "warning.clean.missing_cwd", | ||
| 643 | "Choose a workspace or working directory before cleaning Zig build artifacts." | ||
| 644 | ) | ||
| 645 | ); | ||
| 646 | return null; | ||
| 647 | } | ||
| 648 | |||
| 649 | return new TaskProcessAction("/bin/rm", { | ||
| 650 | args: ["-rf", ".zig-cache", "zig-cache", "zig-out"], | ||
| 651 | cwd, | ||
| 652 | }); | ||
| 653 | } | ||
| 654 | |||
| 655 | async resolveBuildAction(config, cwd) { | ||
| 656 | const zigPath = await resolveExecutable(CONFIG_KEYS.zigPath, "zig"); | ||
| 657 | if (!zigPath) { | ||
| 658 | nova.workspace.showWarningMessage( | ||
| 659 | localizedText( | ||
| 660 | "warning.zig.not_found", | ||
| 661 | "Zig was not found. Install it or set a Zig executable path in Zig extension settings." | ||
| 662 | ) | ||
| 663 | ); | ||
| 664 | return null; | ||
| 665 | } | ||
| 666 | |||
| 667 | return this.createAction(zigPath, ["build", ...getTaskArgs(config, "buildArgs")], cwd); | ||
| 668 | } | ||
| 669 | |||
| 670 | async resolveDebugBuildAction(config, cwd) { | ||
| 671 | const zigPath = await resolveExecutable(CONFIG_KEYS.zigPath, "zig"); | ||
| 672 | if (!zigPath) { | ||
| 673 | nova.workspace.showWarningMessage( | ||
| 674 | localizedText( | ||
| 675 | "warning.zig.not_found", | ||
| 676 | "Zig was not found. Install it or set a Zig executable path in Zig extension settings." | ||
| 677 | ) | ||
| 678 | ); | ||
| 679 | return null; | ||
| 680 | } | ||
| 681 | |||
| 682 | return this.createAction(zigPath, ["build", "-Doptimize=Debug", ...getTaskArgs(config, "buildArgs")], cwd); | ||
| 683 | } | ||
| 684 | |||
| 685 | async resolveBuildRunAction(config, cwd) { | ||
| 686 | const zigPath = await resolveExecutable(CONFIG_KEYS.zigPath, "zig"); | ||
| 687 | if (!zigPath) { | ||
| 688 | nova.workspace.showWarningMessage( | ||
| 689 | localizedText( | ||
| 690 | "warning.zig.not_found", | ||
| 691 | "Zig was not found. Install it or set a Zig executable path in Zig extension settings." | ||
| 692 | ) | ||
| 693 | ); | ||
| 694 | return null; | ||
| 695 | } | ||
| 696 | |||
| 697 | const step = | ||
| 698 | getTaskConfigValue(config, "runStep") || getTaskConfigValue(config, "step") || "run"; | ||
| 699 | const args = ["build", ...getTaskArgs(config, "buildArgs"), step]; | ||
| 700 | const runArgs = getTaskArgs(config, "runArgs"); | ||
| 701 | |||
| 702 | if (runArgs.length > 0) { | ||
| 703 | args.push("--", ...runArgs); | ||
| 704 | } | ||
| 705 | |||
| 706 | return this.createAction(zigPath, args, cwd); | ||
| 707 | } | ||
| 708 | |||
| 709 | async resolveBuildRunTerminalAction(config, cwd) { | ||
| 710 | const zigPath = await resolveExecutable(CONFIG_KEYS.zigPath, "zig"); | ||
| 711 | if (!zigPath) { | ||
| 712 | nova.workspace.showWarningMessage( | ||
| 713 | localizedText( | ||
| 714 | "warning.zig.not_found", | ||
| 715 | "Zig was not found. Install it or set a Zig executable path in Zig extension settings." | ||
| 716 | ) | ||
| 717 | ); | ||
| 718 | return null; | ||
| 719 | } | ||
| 720 | |||
| 721 | const step = | ||
| 722 | getTaskConfigValue(config, "runStep") || getTaskConfigValue(config, "step") || "run"; | ||
| 723 | const args = ["build", ...getTaskArgs(config, "buildArgs"), step]; | ||
| 724 | const runArgs = getTaskArgs(config, "runArgs"); | ||
| 725 | |||
| 726 | if (runArgs.length > 0) { | ||
| 727 | args.push("--", ...runArgs); | ||
| 728 | } | ||
| 729 | |||
| 730 | return new TaskCommandAction(`${EXTENSION_ID}.runInTerminal`, { | ||
| 731 | args: [ | ||
| 732 | { | ||
| 733 | command: zigPath, | ||
| 734 | args, | ||
| 735 | cwd, | ||
| 736 | }, | ||
| 737 | ], | ||
| 738 | }); | ||
| 739 | } | ||
| 740 | |||
| 741 | async resolveBuildTestAction(config, cwd) { | ||
| 742 | const zigPath = await resolveExecutable(CONFIG_KEYS.zigPath, "zig"); | ||
| 743 | if (!zigPath) { | ||
| 744 | nova.workspace.showWarningMessage( | ||
| 745 | localizedText( | ||
| 746 | "warning.zig.not_found", | ||
| 747 | "Zig was not found. Install it or set a Zig executable path in Zig extension settings." | ||
| 748 | ) | ||
| 749 | ); | ||
| 750 | return null; | ||
| 751 | } | ||
| 752 | |||
| 753 | const step = | ||
| 754 | getTaskConfigValue(config, "testStep") || getTaskConfigValue(config, "step") || "test"; | ||
| 755 | const args = ["build", ...getTaskArgs(config, "buildArgs"), step]; | ||
| 756 | const testArgs = getTaskArgs(config, "testArgs"); | ||
| 757 | |||
| 758 | if (testArgs.length > 0) { | ||
| 759 | args.push("--", ...testArgs); | ||
| 760 | } | ||
| 761 | |||
| 762 | return this.createAction(zigPath, args, cwd); | ||
| 763 | } | ||
| 764 | |||
| 765 | async resolveCurrentFileRunAction() { | ||
| 766 | const zigPath = await resolveExecutable(CONFIG_KEYS.zigPath, "zig"); | ||
| 767 | if (!zigPath) { | ||
| 768 | nova.workspace.showWarningMessage( | ||
| 769 | localizedText( | ||
| 770 | "warning.zig.not_found", | ||
| 771 | "Zig was not found. Install it or set a Zig executable path in Zig extension settings." | ||
| 772 | ) | ||
| 773 | ); | ||
| 774 | return null; | ||
| 775 | } | ||
| 776 | |||
| 777 | const filePath = activeZigFilePath(); | ||
| 778 | const cwd = filePath ? nova.path.dirname(filePath) : null; | ||
| 779 | if (!filePath || !cwd) { | ||
| 780 | nova.workspace.showWarningMessage( | ||
| 781 | localizedText( | ||
| 782 | "warning.current_file.focus_editor_for_run", | ||
| 783 | "Focus a Zig editor before running Current Zig File." | ||
| 784 | ) | ||
| 785 | ); | ||
| 786 | return null; | ||
| 787 | } | ||
| 788 | |||
| 789 | return this.createAction(zigPath, ["run", filePath], cwd); | ||
| 790 | } | ||
| 791 | |||
| 792 | resolveCurrentFileCleanAction() { | ||
| 793 | const cwd = activeZigFileDirectory(); | ||
| 794 | if (!cwd) { | ||
| 795 | nova.workspace.showWarningMessage( | ||
| 796 | localizedText( | ||
| 797 | "warning.current_file.focus_editor_for_clean", | ||
| 798 | "Focus a Zig editor before cleaning Current Zig File artifacts." | ||
| 799 | ) | ||
| 800 | ); | ||
| 801 | return null; | ||
| 802 | } | ||
| 803 | |||
| 804 | return this.resolveCleanAction(cwd); | ||
| 805 | } | ||
| 806 | |||
| 807 | async resolveDebugAction(config, cwd) { | ||
| 808 | const lldbDapPath = await resolveLLDBDAPExecutable(); | ||
| 809 | if (!lldbDapPath) { | ||
| 810 | nova.workspace.showWarningMessage( | ||
| 811 | localizedText( | ||
| 812 | "warning.lldb_dap.not_found", | ||
| 813 | "lldb-dap was not found. Install Xcode Command Line Tools or set an LLDB DAP executable path in Zig extension settings." | ||
| 814 | ) | ||
| 815 | ); | ||
| 816 | return null; | ||
| 817 | } | ||
| 818 | |||
| 819 | const configuredProgramPath = getTaskConfigValue(config, "programPath"); | ||
| 820 | const programPath = resolvePathAgainstBase(configuredProgramPath, cwd); | ||
| 821 | if (!programPath) { | ||
| 822 | nova.workspace.showWarningMessage( | ||
| 823 | localizedText( | ||
| 824 | "warning.debug.choose_program", | ||
| 825 | "Choose a program path before running Zig Debug." | ||
| 826 | ) | ||
| 827 | ); | ||
| 828 | return null; | ||
| 829 | } | ||
| 830 | |||
| 831 | const consoleMode = getTaskConfigValue(config, "console") || "internalConsole"; | ||
| 832 | const stopOnEntry = Boolean(config && config.get("stopOnEntry")); | ||
| 833 | |||
| 834 | const action = new TaskDebugAdapterAction("zig-lldb-dap"); | ||
| 835 | action.transport = "stdio"; | ||
| 836 | action.command = "/usr/bin/perl"; | ||
| 837 | action.args = [lldbDapProxyPath(), lldbDapPath, debugAdapterLogPath()]; | ||
| 838 | action.debugRequest = "launch"; | ||
| 839 | action.env = { | ||
| 840 | DYLD_FRAMEWORK_PATH: lldbFrameworkPaths().join(":"), | ||
| 841 | NOVA_ZIG_LLDB_DAP_PATH: lldbDapPath, | ||
| 842 | NOVA_ZIG_DEBUG_LOG: debugAdapterLogPath(), | ||
| 843 | }; | ||
| 844 | action.debugArgs = { | ||
| 845 | program: programPath, | ||
| 846 | cwd, | ||
| 847 | args: getTaskArgs(config, "runArgs"), | ||
| 848 | stopOnEntry, | ||
| 849 | }; | ||
| 850 | |||
| 851 | if (consoleMode !== "internalConsole") { | ||
| 852 | action.debugArgs.console = consoleMode; | ||
| 853 | } | ||
| 854 | |||
| 855 | return action; | ||
| 856 | } | ||
| 857 | |||
| 858 | async resolveFileTestAction(config, cwd) { | ||
| 859 | const zigPath = await resolveExecutable(CONFIG_KEYS.zigPath, "zig"); | ||
| 860 | if (!zigPath) { | ||
| 861 | nova.workspace.showWarningMessage( | ||
| 862 | localizedText( | ||
| 863 | "warning.zig.not_found", | ||
| 864 | "Zig was not found. Install it or set a Zig executable path in Zig extension settings." | ||
| 865 | ) | ||
| 866 | ); | ||
| 867 | return null; | ||
| 868 | } | ||
| 869 | |||
| 870 | const configuredPath = getTaskConfigValue(config, "filePath"); | ||
| 871 | const filePath = configuredPath | ||
| 872 | ? resolveWorkspaceRelativePath(configuredPath) | ||
| 873 | : activeZigFilePath(); | ||
| 874 | |||
| 875 | if (!filePath) { | ||
| 876 | nova.workspace.showWarningMessage( | ||
| 877 | localizedText( | ||
| 878 | "warning.file_test.choose_file", | ||
| 879 | "Choose a Zig file for this task or focus a Zig editor before running Zig File Test." | ||
| 880 | ) | ||
| 881 | ); | ||
| 882 | return null; | ||
| 883 | } | ||
| 884 | |||
| 885 | return this.createAction(zigPath, ["test", filePath, ...getTaskArgs(config, "zigArgs")], cwd); | ||
| 886 | } | ||
| 887 | } | ||
| 888 | |||
| 889 | class ZigIssueAssistant { | ||
| 890 | constructor() { | ||
| 891 | this.disposable = nova.assistants.registerIssueAssistant( | ||
| 892 | [{ syntax: "zig" }, { syntax: "zig-package" }], | ||
| 893 | this, | ||
| 894 | { event: "onChange" } | ||
| 895 | ); | ||
| 896 | } | ||
| 897 | |||
| 898 | dispose() { | ||
| 899 | if (this.disposable && typeof this.disposable.dispose === "function") { | ||
| 900 | this.disposable.dispose(); | ||
| 901 | this.disposable = null; | ||
| 902 | } | ||
| 903 | } | ||
| 904 | |||
| 905 | provideIssues(editor) { | ||
| 906 | if (!editor || !editor.document) { | ||
| 907 | return []; | ||
| 908 | } | ||
| 909 | |||
| 910 | // Nova's LanguageClient already owns core LSP publishDiagnostics handling. | ||
| 911 | // Registering an issue assistant here tells Nova that Zig supports live | ||
| 912 | // checking, so the Problems UI doesn't show a misleading empty-state banner. | ||
| 913 | return []; | ||
| 914 | } | ||
| 915 | } | ||
