Skip to content

[RFC] Query diagnostics for the current project #57

Description

@lumpsoid

When working with Dart LSP, it populates all diagnostics on startup. However, with the current lspce behavior, flymake only shows diagnostics per open buffer — meaning you either pay the overhead of opening the whole project in buffers, or you query the Rust server directly for the current project diagnostics state.

As I understand it, flymake is philosophically buffer-based, so there's no way around this limitation from that side.

Digging around, I found that the current lspce Rust implementation already collects all diagnostics in a per-project internal state. So the idea is to extend the client with a public API to query that state and return all diagnostics for a specified project. Users could then use familiar tools to build a UI for conveniently navigating this state.

I've implemented this extension along with a consult-based UI presenter.

I'm interested in your opinion, do you see this extension as a desirable way to provide such functionality?

extension patch:

diff --git a/src/lib.rs b/src/lib.rs
index 8ccf1cb..d92f3d7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -978,6 +978,31 @@ fn read_file_diagnostics(
     Ok(None)
 }
 
+#[defun]
+fn read_all_diagnostics(env: &Env, root_uri: String, file_type: String) -> Result<Option<String>> {
+    let projects = projects().lock().unwrap();
+    if let Some(p) = projects.get(&root_uri) {
+        if let Some(server) = p.servers.get(&file_type) {
+            let server_data = server.server_data.lock().unwrap();
+            let map: HashMap<&String, &Vec<lsp_types::Diagnostic>> = server_data
+                .file_infos
+                .iter()
+                .filter(|(_, fi)| !fi.diagnostics.is_empty())
+                .map(|(uri, fi)| (uri, &fi.diagnostics))
+                .collect();
+            if map.is_empty() {
+                return Ok(None);
+            }
+            return Ok(Some(serde_json::to_string(&map)?));
+        } else {
+            env.message(&format!("No server for {}", &file_type));
+        }
+    } else {
+        env.message(&format!("No project for {} {}", &root_uri, &file_type));
+    }
+    Ok(None)
+}
+
 #[defun]
 fn read_latest_response_id(
     env: &Env,

as an example, diagnostics viewer based on consult:

Details
;;; lspce-consult-diagnostics.el --- consult picker for lspce project diagnostics

(require 'consult)

(defface lspce-diag-error-face
  '((t :foreground "#ff5f5f" :weight bold))
  "Face for error diagnostics.")

(defface lspce-diag-warn-face
  '((t :foreground "#e5c07b" :weight bold))
  "Face for warning diagnostics.")

(defface lspce-diag-info-face
  '((t :foreground "#61afef"))
  "Face for info diagnostics.")

(defface lspce-diag-hint-face
  '((t :foreground "#98c379"))
  "Face for hint diagnostics.")

(defun lspce--diag-severity-label (sev)
  (pcase sev
    (1 (propertize "ERR " 'face 'lspce-diag-error-face))
    (2 (propertize "WARN" 'face 'lspce-diag-warn-face))
    (3 (propertize "INFO" 'face 'lspce-diag-info-face))
    (_ (propertize "HINT" 'face 'lspce-diag-hint-face))))

(defun lspce--diag-severity-order (candidate)
  (pcase (get-text-property 0 'lspce-severity candidate)
    (1 0) (2 1) (3 2) (_ 3)))

(defun lspce--diag-candidates ()
  "Build a flat candidate list from project diagnostics.
Each string carries text properties: lspce-uri, lspce-line, lspce-col, lspce-severity."
  (let ((table (lspce-get-project-diagnostics))
        candidates)
    (when table
      (maphash
       (lambda (uri diags)
         (seq-do
          (lambda (d)
            (let* ((range    (gethash "range"    d))
                   (start    (gethash "start"    range))
                   (line     (gethash "line"      start))
                   (col      (gethash "character" start))
                   (severity (gethash "severity"  d))
                   (msg      (gethash "message"   d))
                   (path     (string-remove-prefix "file://" uri))
                   (display-path
                    (if (and lspce--root-uri
                             (string-prefix-p
                              (string-remove-prefix "file://" lspce--root-uri)
                              path))
                        (file-relative-name
                         path
                         (string-remove-prefix "file://" lspce--root-uri))
                      path))
                   (candidate
                    (format "%s %-40s %4d:%-4d %s"
                            (lspce--diag-severity-label severity)
                            (propertize display-path 'face 'font-lock-keyword-face)
                            (1+ line)
                            (1+ col)
                            (propertize msg 'face 'font-lock-comment-face))))
              (add-text-properties 0 1
                                   (list 'lspce-uri      uri
                                         'lspce-line     line
                                         'lspce-col      col
                                         'lspce-severity severity)
                                   candidate)
              (push candidate candidates)))
          diags))
       table))
    ;; Sort: errors first, then warn, info, hint
    (sort (nreverse candidates)
          (lambda (a b)
            (< (lspce--diag-severity-order a)
               (lspce--diag-severity-order b))))))

(defun lspce--diag-jump (candidate)
  "Open file and jump to the location encoded in CANDIDATE."
  (when candidate
    (let* ((uri  (get-text-property 0 'lspce-uri  candidate))
           (line (get-text-property 0 'lspce-line candidate))
           (col  (get-text-property 0 'lspce-col  candidate))
           (path (string-remove-prefix "file://" uri)))
      (find-file path)
      (goto-char (point-min))
      (forward-line line)
      (move-to-column col))))

;;;###autoload
(defun lspce-consult-diagnostics ()
  "Browse project-wide LSP diagnostics with consult.
Errors are shown first, then warnings, info, hints.
Selecting a candidate opens the file at the diagnostic location."
  (interactive)
  (unless lspce-mode
    (user-error "lspce-mode is not active in this buffer"))
  (let ((candidates (lspce--diag-candidates)))
    (unless candidates
      (user-error "lspce: no project diagnostics"))
    (let ((selected
           (consult--read
            candidates
            :prompt "LSP diagnostics: "
            :require-match t
            :sort nil
            :category 'lspce-diagnostic
            :lookup (lambda (selected candidates _input _narrow)
                      (car (member selected candidates)))
            :state (lambda (action candidate)
                     (when (and candidate (eq action 'preview))
                       (lspce--diag-jump candidate))))))
      (lspce--diag-jump selected))))

(provide 'lspce-consult-diagnostics)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions