Skip to content

On Windows, textDocument/publishDiagnostics can silently come back empty due to a case-sensitivity mismatch between mem_docs and the project-derived vfs path for the same open file #248

Description

@Hanatarou

Environment

  • elp version: 1.1.0+build-2026-06-11
  • OS: Windows 10/11
  • Workspace path containing mixed-case, non-ASCII segments (e.g. G:\Arquivos Auxiliares\...), though we've since confirmed the underlying case-folding behavior is not limited to accented paths.

Summary

elp computes real diagnostics internally (visible via window/logMessage handle_event NativeDiagnostics/ErlangServiceDiagnostics with populated normal/labeled_syntax_errors content), but the actual textDocument/publishDiagnostics notification sent to the client comes back with an empty diagnostics: [] array for the same file, same revision.

Root cause (source-level, from a local build of main)

crates/elp/src/server.rs, in the diagnostics-publish path:

let uri = file_id_to_uri(&self.vfs.read(), *file_id);
...
let version = convert::vfs_path(&uri)
    .ok()
    .and_then(|path| self.mem_docs.read().get(&path).cloned())
    .map(|d| d.version);
if version.is_none() {
    // File is not in mem_docs, clear all but
    // eqwalizer project diagnostics
    Arc::make_mut(&mut self.diagnostics).clear(*file_id);
}
  • file_id_to_uri builds the URI from the vfs's own canonical path for file_id. On Windows, in our testing, this path comes back entirely lowercased (not just the drive letter - crates/elp/src/convert.rs's uri_from_abs_path only explicitly lowercases the drive letter, so the rest of the lowering must originate further upstream, in the paths/vfs crates pulled from the rust-analyzer repo per Cargo.toml, not in this repo's own source).
  • mem_docs is populated by the textDocument/didOpen handler using convert::abs_path(&params.text_document.uri) directly - i.e. whatever case the client sent, unmodified.
  • On Windows, if the client sends the file's real on-disk case (which is the natural, unmodified thing for an LSP client to do), these two representations of the same physical file diverge in case, mem_docs.get(&path) misses, version is None, and the diagnostics computed one line earlier get thrown away before ever reaching the client.

This reproduces reliably any time the workspace path's real case differs from what the internal vfs canonicalization normalizes it to (which, empirically, in our environment, was always fully lowercase - not just the drive letter).

Reproduction

  1. Open a file whose real on-disk path contains uppercase letters anywhere past the drive letter (this is the common case on Windows - most directory names aren't all-lowercase).
  2. Confirm via log: "info" that NativeDiagnostics/ErlangServiceDiagnostics internally compute non-empty diagnostics for the file.
  3. Observe the corresponding textDocument/publishDiagnostics notification: diagnostics: [].

Workaround (client-side, not a fix)

We worked around this in our LSP client by tracking the real-case URI at didOpen time and rewriting inbound file:// URIs from the server back to that real case before any client-side logic touches them. This does not address the actual inconsistency inside elp - mem_docs and the project-derived vfs path still disagree internally; we're just correcting for it after the fact on our end.

Suggested fix direction

Normalize case consistently at the single point where VfsPath/AbsPath values are constructed from client-provided URIs (didOpen, and anywhere else that ingests a client URI) so that they always match whatever canonical form vfs/file_id_to_uri produces internally - or, conversely, key mem_docs by FileId rather than by a re-derived path string, sidestepping the case question entirely.

Related, and possibly the same root cause surfacing differently: crates/ide/src/diagnostics/module_mismatch.rs compares -module() against path.name_and_extension() derived from the same internally-canonicalized path, which means on Windows this check can also produce false positives/negatives depending on which case convention the file's VfsPath happens to have been registered under.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions