Skip to content

LSP Phase 3: Completion, find references, formatting integration #120

@gregwinn

Description

@gregwinn

Summary

Add code completion, find-all-references, and formatting integration to the Winn LSP server. This is the final phase that makes the IDE experience production-grade.

Prerequisites

  • LSP Phase 1 (diagnostics) — must be complete
  • LSP Phase 2 (navigation) — must be complete (symbol table required)

LSP Methods to Implement

textDocument/completion

Trigger on typing. Provide contextual completions:

Context Candidates Source
After Mod. Functions in that module Symbol table + stdlib registry
After |> All visible functions Current module + imports
Start of expression Local functions + variables in scope Symbol table + scope analysis
After import Known module names Stdlib list + project modules
After def Nothing (new function name)
Inside function args Variables in scope Scope tracking

Completion Item Structure

{
  "label": "puts",
  "kind": 3,            // Function
  "detail": "IO.puts/1",
  "documentation": "Print a string to stdout",
  "insertText": "puts(${1:value})",
  "insertTextFormat": 2  // Snippet
}

Stdlib Completion Registry

Build a static registry of all stdlib functions for completion:

stdlib_completions() -> #{
  'IO' => [
    #{name => puts, arity => 1, doc => <<"Print to stdout">>},
    #{name => gets, arity => 0, doc => <<"Read line from stdin">>},
    #{name => inspect, arity => 1, doc => <<"Debug print">>}
  ],
  'String' => [
    #{name => upcase, arity => 1, doc => <<"Uppercase a string">>},
    #{name => downcase, arity => 1, doc => <<"Lowercase a string">>},
    ...
  ],
  ...
}.

Source this from winn_runtime.erl exported functions + doc comments.

textDocument/references

Find all uses of a symbol across the workspace.

Strategy:

  1. Identify the symbol at cursor position (function name, module name, variable)
  2. For functions: search all files' ASTs for {call, _, Name, _} or {dot_call, _, Mod, Name, _} matching
  3. For modules: search for {dot_call, _, Mod, _, _}, {import_directive, _, Mod}, {alias_directive, _, Mod, _}
  4. For variables: search within current function scope only

Workspace indexing:

  • On startup: parse all .winn files in src/ and subdirectories
  • On file change: re-index that file only
  • Store cross-reference map in ETS: {SymbolKey, [{File, Line, Col}]}

textDocument/formatting

Integrate the existing winn_formatter with LSP:

handle_request(<<"textDocument/formatting">>, Params) ->
    Uri = maps:get(<<"textDocument">>, Params),
    Source = get_document(Uri),
    case winn_formatter:format_string(Source) of
        {ok, Formatted} ->
            Edits = compute_full_text_edit(Source, Formatted),
            {ok, Edits};
        {error, _} ->
            {ok, []}  %% Don't format if parse fails
    end.

This replaces the need for a separate format-on-save extension. Users get format-on-save via VS Code's built-in editor.formatOnSave setting.

textDocument/rangeFormatting

Format only a selection. Use winn_formatter:format_string/1 on the selected range — may need to extract the containing function/module for context.

textDocument/rename

Rename a symbol across the workspace:

  1. Find all references (reuse textDocument/references)
  2. Generate TextEdit for each occurrence
  3. Return WorkspaceEdit with edits grouped by file

Scope:

  • Variable rename: within current function only
  • Function rename: across all files that call it
  • Module rename: across all files + rename source file

textDocument/signatureHelp

Show function signature while typing arguments:

IO.puts( | )
─────────
IO.puts(value) — Print a string to stdout

Trigger on ( and ,. Look up the function being called in the symbol table, return parameter names and doc.

New Files

File Purpose ~Lines
apps/winn/src/winn_lsp_completion.erl Completion provider + stdlib registry 250
apps/winn/src/winn_lsp_references.erl Find-all-references engine 200
apps/winn/src/winn_lsp_formatting.erl Formatter integration 60
apps/winn/src/winn_lsp_rename.erl Rename refactoring 150
apps/winn/src/winn_lsp_workspace.erl Workspace-wide indexing + file watching 200
apps/winn/test/winn_lsp_completion_tests.erl Completion tests 150
apps/winn/test/winn_lsp_references_tests.erl References tests 100

Modified Files

File Change
winn_lsp_handler.erl Add handlers for completion, references, formatting, rename, signatureHelp
winn_lsp.erl Register new capabilities in initialize response
winn_lsp_documents.erl Workspace-wide document tracking

Scope Tracking Enhancement

For completion and rename to work well, need basic scope tracking:

%% At cursor position, what variables are in scope?
%% Walk function body up to cursor line, collect assignments
scope_at(AST, Line) ->
    %% Returns #{VarName => {DefinedAtLine, Type}}

This doesn't need full type inference — just track what names are bound and where.

Acceptance Criteria

  • Typing IO. shows completion list of IO functions
  • Typing inside a function shows local variables + functions
  • Completion includes snippet placeholders for function args
  • Find References highlights all call sites of a function
  • Format Document uses winn fmt under the hood
  • Format on Save works via VS Code setting
  • Rename variable updates all occurrences in function
  • Rename function updates all call sites across files
  • Signature help shows params while typing func(

Part of v0.9.0 — Developer Tooling

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions