Skip to content

Add mtlog-analyzer support for Zed editor #56

@willibrandon

Description

@willibrandon

Description:
Implement mtlog-analyzer support as a Zed extension. This would be the 4th editor integration alongside VS Code, GoLand, and Neovim.

Background:
Zed's extension system uses Rust compiled to WebAssembly. Extensions run sandboxed and communicate via LSP. Unlike other editors, Zed requires a persistent LSP server rather than one-shot command execution, so we'll create mtlog-lsp as an LSP wrapper with bundled analyzer.

Architecture

mtlog-lsp (Bundled LSP Server)

A unified component that provides LSP functionality with integrated analyzer:

  • Bundles mtlog-analyzer directly in the binary for zero subprocess overhead
  • Stays running as a persistent LSP server
  • Runs analyzer using packages.Load and analysis.Pass internally
  • Handles byte offset to line/character position conversion
  • Manages diagnostics and code actions separately to avoid conflicts with gopls

Installation (single binary):

go install github.com/willibrandon/mtlog/cmd/mtlog-lsp@latest

Implementation

Extension Structure

zed-extension/mtlog/
├── Cargo.toml
├── extension.toml
├── src/
│   └── lib.rs
└── README.md

Core Implementation

use zed_extension_api::{self as zed, Command, Extension, LanguageServerId, Result};

struct MtlogAnalyzerExtension {
    cached_binary_path: Option<String>,
}

impl Extension for MtlogAnalyzerExtension {
    fn new() -> Self {
        Self {
            cached_binary_path: None,
        }
    }

    fn language_server_command(
        &mut self,
        _id: &LanguageServerId,
        worktree: &zed::Worktree,
    ) -> Result<Command> {
        let binary_path = self.cached_binary_path.get_or_insert_with(|| {
            find_mtlog_lsp().unwrap_or_else(|| "mtlog-lsp".to_string())
        });

        Ok(Command {
            command: binary_path.clone(),
            args: vec![],  // mtlog-lsp doesn't need arguments
            env: Default::default(),
        })
    }
}

Binary Detection

fn find_mtlog_lsp() -> Option<String> {
    // First check if in PATH
    if let Some(path) = worktree.which("mtlog-lsp") {
        return Some(path);
    }
    
    let paths = vec![
        std::env::var("GOBIN").ok(),
        std::env::var("GOPATH").ok().map(|p| format!("{}/bin", p)),
        Some(format!("{}/go/bin", std::env::var("HOME").ok()?)),
    ];
    
    for path in paths.into_iter().flatten() {
        let binary = format!("{}/mtlog-lsp", path);
        if std::fs::metadata(&binary).is_ok() {
            return Some(binary);
        }
    }
    None
}

Configuration Files

extension.toml:

id = "mtlog-analyzer"
name = "mtlog-analyzer"
version = "0.1.0"
schema_version = 1
authors = ["Brandon Williams"]
description = "Static analysis for mtlog message templates in Go"
repository = "https://github.com/willibrandon/mtlog"

[language_servers.mtlog-analyzer]
name = "mtlog-analyzer"
languages = ["Go"]

Cargo.toml:

[package]
name = "zed-mtlog-analyzer"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
zed_extension_api = "0.2.0"

Features

  1. All MTLOG001-MTLOG013 diagnostics via LSP
  2. Quick fixes/Code Actions work properly through LSP code actions
  3. Automatic binary detection for mtlog-lsp in standard Go paths
  4. Installation instructions when mtlog-lsp not found
  5. Proper byte offset conversion for accurate text edit positions
  6. Configuration support for analyzer flags (passed through initialization_options)
  7. Diagnostic suppression via workspace configuration actions

Requirements

Only one component needed (bundled analyzer):

go install github.com/willibrandon/mtlog/cmd/mtlog-lsp@latest

The mtlog-lsp binary will include the analyzer bundled directly, eliminating:

  • Subprocess spawning overhead
  • Binary detection complexity for analyzer
  • Process communication latency
  • Dual installation requirements

Publishing

Initial Publishing

  1. Fork zed-industries/extensions
  2. Add extension as submodule
  3. Update extensions.toml with entry
  4. Submit PR to zed-industries/extensions

Automated Updates

Since mtlog releases all components together, we'll automate Zed extension updates:

Release Script (scripts/release-zed-extension.sh):

#!/bin/bash
# Automatically update Zed extension after mtlog release

set -e

VERSION=$1
COMMIT=$2
EXTENSIONS_DIR="$HOME/zed-extensions-fork"

if [ -z "$VERSION" ] || [ -z "$COMMIT" ]; then
    echo "Usage: $0 <version> <commit>"
    exit 1
fi

cd "$EXTENSIONS_DIR"
git checkout main
git pull upstream main

# Update submodule
cd extensions/mtlog-analyzer
git fetch
git checkout "$COMMIT"
cd ../..

# Update version
sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" extensions.toml
rm extensions.toml.bak

# Create branch and PR
BRANCH="update-mtlog-analyzer-$VERSION"
git checkout -b "$BRANCH"
git add .
git commit -m "Update mtlog-analyzer to $VERSION"
git push origin "$BRANCH"

# Create PR using GitHub CLI
gh pr create \
  --repo zed-industries/extensions \
  --title "Update mtlog-analyzer to $VERSION" \
  --body "Release: https://github.com/willibrandon/mtlog/releases/tag/$VERSION"

echo "✓ PR created"

GitHub Actions Integration (.github/workflows/release.yml):

- name: Update Zed Extension
  if: startsWith(github.ref, 'refs/tags/')
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    # Setup: ensure fork exists
    gh repo fork zed-industries/extensions --clone=false || true
    
    # Clone/update fork
    git clone https://github.com/${{ github.actor }}/extensions.git zed-extensions-fork
    cd zed-extensions-fork
    git remote add upstream https://github.com/zed-industries/extensions
    
    # Run update
    ../scripts/release-zed-extension.sh \
      "${{ github.event.release.tag_name }}" \
      "${{ github.sha }}"

This automation approach:

  • Runs automatically on release
  • Uses simple shell scripts (no complex dependencies)
  • Easy to test locally
  • Easy to debug if issues arise
  • ~30 lines of maintainable code

Testing Requirements

  • Verify diagnostics display correctly
  • Test quick fix/code action application
  • Validate binary detection on macOS/Linux
  • Check performance impact (<10ms added latency)
  • Test with go.mod projects and standalone files
  • Verify mtlog-lsp stays running and handles multiple requests
  • Test that code actions apply at correct positions
  • Verify bundled analyzer performance matches standalone

Implementation Notes

  • Zed requires a persistent LSP server (not one-shot execution)
  • mtlog-lsp will bundle the analyzer directly using packages.Load API
  • Extension only needs to provide command to launch mtlog-lsp
  • No custom UI needed - Zed's built-in diagnostics panel works
  • Quick fixes work through standard LSP code actions
  • Diagnostics are published without Data field to avoid gopls conflicts
  • Configuration passed via initialization_options from Zed settings

Comparison with Other Editor Integrations

Editor Language LOC Notes
VS Code TypeScript ~3,700 Custom diagnostics provider, quick fixes, configuration UI
GoLand Kotlin ~7,200 IntelliJ platform integration, external annotator, custom UI
Neovim Lua ~14,200 Complete LSP client implementation, caching, async handling
Zed Rust ~200 Simple LSP wrapper launching mtlog-lsp
mtlog-lsp Go ~750 LSP server with bundled analyzer (required for Zed)

Zed requires minimal extension code because it delegates entirely to its built-in LSP infrastructure. The bundled approach will eliminate subprocess overhead and simplify installation to a single binary.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestmtlog-analyzerStatic analysis tool for detecting mtlog usage issuesmtlog-lspLanguage Server Protocol implementation for mtlog-analyzertoolingDevelopment tools, build systems, and analyzerszedZed editor extension and integration

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions