Skip to content

Conversation

@reductor
Copy link
Contributor

@reductor reductor commented Aug 31, 2025

This should resolve #2141 which was a limitation for creating a Visual Studio extension for ast-grep using the builtin LSP client support, along with any LSP client which is prior to version 3.16.0 (14th Dec 2020)

I introduced some tests to support this however the existing tests and framework were a bit awkward for reusing data especially when things are interleaved with existing log messages and the did open ends up requiring "workspace/workspaceFolders" each time before it can get the published diagnostics (which might be a separate bug as it slows down getting diagnostics and make its it a multiple round trip request)

Summary by CodeRabbit

  • New Features

    • Per-document fix caching and improved code-action fallback so quick fixes are more reliable across clients.
  • Tests

    • In-process LSP test harness with Content-Length framed JSON-RPC, codec round-trip check, and code-action application tests.
  • Chores

    • Added development dependencies to support async testing and streaming utilities.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 31, 2025

Walkthrough

Adds a per-document fixes cache and routes code-action generation to use that cache when diagnostic.data is unavailable; updates utils types and signatures; wires fixes through diagnostics/republish/open/change flows; and adds a Content-Length framed LSP test harness and helpers.

Changes

Cohort / File(s) Summary
LSP backend: diagnostics & fixes cache
crates/lsp/src/lib.rs
Introduces per-document Fixes in VersionedAst, populates fixes during diagnostic computation, stores/propagates fixes in publish_diagnostics/republish flows, initializes fixes on open/change, and passes fixes to code-action handling.
Utilities: code-action fallback & types
crates/lsp/src/utils.rs
Adds type Fixes = HashMap<(Range, String), RewriteData>; derives Clone for OneFix; removes serde-based RewriteData::from_value; makes RewriteData::from_node_match pub; updates diagnostic_to_code_action signature to accept &Fixes; and changes convert_match_to_diagnostic to take &NodeMatch and leave diagnostic.data empty.
Tests: framed LSP harness and scenarios
crates/lsp/tests/basic.rs
Adds LspCodec Content-Length framed JSON-RPC codec, framing helpers (create_lsp_framed, send_did_open_framed, wait_for_diagnostics, etc.), harness utilities to apply code-action edits, and tests validating code-action and fix-all flows.
Dev dependencies
crates/lsp/Cargo.toml
Adds dev-dependencies: futures = "0.3.31", tokio-stream = "0.1.17", tokio-util = "0.7.16" for new async/framing tests.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Server as LSP Backend
  participant State as Backend State
  participant Utils as utils::diagnostic_to_code_action

  rect rgba(200,230,255,0.12)
  note over Client,Server: Initialize
  Client->>Server: initialize(capabilities)
  Server->>State: record client capabilities
  Server-->>Client: initializeResult
  end

  rect rgba(200,255,200,0.12)
  note over Client,Server: Open & Diagnostics
  Client->>Server: textDocument/didOpen
  Server->>Server: analyze -> diagnostics + compute Fixes
  Server->>State: store per-document Fixes
  Server-->>Client: textDocument/publishDiagnostics (data omitted)
  end

  rect rgba(255,240,200,0.12)
  note over Client,Server: Code Action Request
  Client->>Server: textDocument/codeAction
  alt diagnostic.data supported
    Server->>Utils: diagnostic_to_code_action(diagnostic, fixes_cache=None)
  else diagnostic.data not supported
    Server->>State: fetch Fixes for document
    Server->>Utils: diagnostic_to_code_action(diagnostic, fixes_cache=&Fixes)
  end
  Utils-->>Server: CodeAction[]
  Server-->>Client: CodeAction[] response
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Assessment against linked issues

Objective Addressed Explanation
Provide LSP fix support for clients lacking diagnostic.data support [#2141]

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Framed LSP test harness and codec added (crates/lsp/tests/basic.rs) Test infrastructure for Content-Length framed JSON-RPC; not required by the issue which targets runtime fix delivery for clients without diagnostic.data.
Dev-dependencies added (crates/lsp/Cargo.toml) Dev-only dependency additions to support the new framing tests; unrelated to runtime client fix support objective.

Possibly related PRs

Suggested reviewers

  • HerringtonDarkholme

Poem

I nibble bytes and patch a line,
When diagnostic data hides, I shine.
I stash small fixes, ready to steer,
Hop to the code, quick-fix cheer.
— a little rabbit with a carrot near 🥕

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/lsp/src/lib.rs (1)

721-725: Rebuild fixes on rules reload for clients without dataSupport.
After reload_rules, republish_all_diagnostics doesn't refresh versioned.fixes, breaking fallback until next edit/open.

Proposed change outside the modified hunk (for function republish_all_diagnostics):

// inside the loop, after `let Some(diagnostics) = self.get_diagnostics(&uri, versioned) else { ... };`
versioned.notes = self.build_notes(&diagnostics);
if !self.code_action_data_supported.load(std::sync::atomic::Ordering::Relaxed) {
  versioned.fixes = self.build_fixes(&diagnostics);
  let mut diagnostics = diagnostics;
  for d in &mut diagnostics {
    d.data = None;
  }
  self.client.publish_diagnostics(uri, diagnostics, Some(versioned.version)).await;
} else {
  self.client.publish_diagnostics(uri, diagnostics, Some(versioned.version)).await;
}
🧹 Nitpick comments (4)
crates/lsp/src/utils.rs (1)

76-99: Align CodeAction.kind with advertised kinds.
You advertise quickfix.ast-grep in capabilities but return kind=quickfix here. Consider returning the same sub-kind to avoid client-side filtering mismatches.

Apply this diff:

-      kind: Some(CodeActionKind::QUICKFIX),
+      // Align with advertised kind to survive client-side 'only' filtering
+      kind: Some(CodeActionKind::new("quickfix.ast-grep")),
crates/lsp/src/lib.rs (1)

331-345: Cache builder is correct but can skip non-fixable diagnostics.
Avoid inserting entries with empty fixers to reduce memory and lookups.

Apply this diff:

   if let (Some(data), Some(NumberOrString::String(id))) = (diagnostic.data.as_ref(), diagnostic.code.as_ref()) {
-    if let Some(rewrite_data) = RewriteData::from_value(data.clone()) {
-      fixes.insert((diagnostic.range, id.clone()), rewrite_data);
-    }
+    if let Some(rewrite_data) = RewriteData::from_value(data.clone()) {
+      if !rewrite_data.fixers.is_empty() {
+        fixes.insert((diagnostic.range, id.clone()), rewrite_data);
+      }
+    }
   }
crates/lsp/tests/basic.rs (2)

388-417: Robustness: handle additional headers in wait_for_diagnostics path.
Some servers include extra headers (e.g., Content-Type). Your custom codec currently assumes only Content-Length. Optional to extend decoder; fine for tests.


565-613: LspCodec: tolerate multiple headers or interleaved frames (optional).
Current decode searches a single "Content-Length: " and assumes no extra headers. Consider parsing header lines until CRLFCRLF to be more future-proof; fine as-is for internal tests.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7405f99 and 96c3dfe.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • crates/lsp/Cargo.toml (1 hunks)
  • crates/lsp/src/lib.rs (9 hunks)
  • crates/lsp/src/utils.rs (2 hunks)
  • crates/lsp/tests/basic.rs (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-09-29T21:59:50.675Z
Learnt from: HerringtonDarkholme
PR: ast-grep/ast-grep#1497
File: crates/cli/src/utils/mod.rs:174-175
Timestamp: 2024-09-29T21:59:50.675Z
Learning: ast-grep requires Rust 1.65 or newer. It's acceptable to use syntax and features introduced in Rust 1.65 or newer in ast-grep code.

Applied to files:

  • crates/lsp/Cargo.toml
🧬 Code graph analysis (2)
crates/lsp/src/lib.rs (1)
crates/lsp/src/utils.rs (2)
  • from_value (28-30)
  • diagnostic_to_code_action (60-100)
crates/lsp/tests/basic.rs (1)
crates/lsp/src/lib.rs (5)
  • did_open (167-173)
  • initialize (80-101)
  • initialized (103-125)
  • code_action (194-196)
  • new (222-235)
🔇 Additional comments (14)
crates/lsp/Cargo.toml (1)

31-40: Dev dependencies look appropriate for the new async framed test harness.
No issues spotted; versions are compatible with tokio 1.45.0.

crates/lsp/src/utils.rs (2)

15-22: Deriving Clone on OneFix and RewriteData is appropriate for caching.
Enables cheap cloning out of the fixes cache without unnecessary Arc wrapping.


59-74: Solid fallback to cached fixes when diagnostic.data is absent.
Keying by (Range, code) and normalizing NumberOrString to String is sensible.

crates/lsp/src/lib.rs (5)

34-35: Per-document fixes cache is a good fit.
The (Range, String) -> RewriteData map makes lookup unambiguous and avoids duplicating computation.


46-47: Capability flag stored atomically is fine.
Relaxed ordering is sufficient for a config bit read/written on init.


81-83: Correctly detect publishDiagnostics.dataSupport at initialize.
Stores once and used later to choose fallback paths.


223-235: Backend::new wiring LGTM.
Initializes the new state without changing constructor signature semantics.


237-244: Capability extraction path matches LSP shape.
Looks up textDocument.publishDiagnostics.dataSupport safely.

crates/lsp/tests/basic.rs (6)

366-386: Helper send_did_open_framed is clear and reusable.
Good factoring; reduces duplication across tests.


419-448: Code action request helper LGTM.
Encapsulates framing and response wait with timeout.


451-507: End-to-end test with dataSupport enabled looks good.
Covers initialize → didOpen → diagnostics → codeAction flow.


509-563: End-to-end test with dataSupport disabled validates the fallback.
Simulating removal of diagnostic.data from the client request is a good coverage point.


615-647: create_lsp_framed wiring is neat.
Single duplex + split keeps the harness compact.


649-663: Codec round-trip test is a nice sanity check.
Catches framing regressions early.

@codecov
Copy link

codecov bot commented Aug 31, 2025

Codecov Report

❌ Patch coverage is 87.83784% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.81%. Comparing base (9b94cc5) to head (f2a1008).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
crates/lsp/src/lib.rs 81.08% 7 Missing ⚠️
crates/lsp/src/utils.rs 94.59% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2209      +/-   ##
==========================================
+ Coverage   86.19%   87.81%   +1.62%     
==========================================
  Files         107      107              
  Lines       17035    17059      +24     
==========================================
+ Hits        14683    14981     +298     
+ Misses       2352     2078     -274     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@HerringtonDarkholme HerringtonDarkholme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • please fix linting and formatting errors
  • I am not a fan of maintaining two sets of implementatiions. I would prefer just keeping the fixCache code path.

Futher test cases to cover some cases of overlapping code fixes.

TODO: Add/Fix tests for fixAll as it's currently ignored and fairly limited.
@reductor
Copy link
Contributor Author

reductor commented Sep 1, 2025

I've done some changes to remove the older approach and solely use the fixes cache, I plan on doing a bit more on adding further tests for the fixAll LSP action along with some other possibly edge cases now that the older method of using the data field is no longer used.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/lsp/src/lib.rs (1)

687-705: Clear stale diagnostics when no rules match (bug).

republish_all_diagnostics skips publishing when get_diagnostics returns None, leaving old diagnostics on the client. Always publish an empty list and clear caches.

Apply this diff:

-      // Republish diagnostics for this file
-      let Some((diagnostics, fixes)) = self.get_diagnostics(&uri, versioned) else {
-        continue;
-      };
-      versioned.notes = self.build_notes(&diagnostics);
-      versioned.fixes = fixes;
-      self
-        .client
-        .publish_diagnostics(uri, diagnostics, Some(versioned.version))
-        .await;
+      // Republish diagnostics for this file (send empty to clear stale ones)
+      let (diagnostics, fixes) = match self.get_diagnostics(&uri, versioned) {
+        Some((d, f)) => (d, f),
+        None => (Vec::new(), Fixes::new()),
+      };
+      versioned.notes = self.build_notes(&diagnostics);
+      versioned.fixes = fixes;
+      self
+        .client
+        .publish_diagnostics(uri, diagnostics, Some(versioned.version))
+        .await;
🧹 Nitpick comments (9)
crates/lsp/src/lib.rs (3)

30-31: De-duplicate Fixes alias; import from utils instead.

Maintain a single source of truth to avoid drift.

Apply this diff:

-use utils::{convert_match_to_diagnostic, diagnostic_to_code_action, RewriteData};
+use utils::{convert_match_to_diagnostic, diagnostic_to_code_action, RewriteData, Fixes};
@@
-type Fixes = HashMap<(Range, String), RewriteData>;

And in crates/lsp/src/utils.rs, make Fixes public (see separate comment there).


439-459: Prefer non-overlapping, larger edits first; avoid extra iterator churn.

Sorting by end ascending keeps the shortest when ranges overlap. Prefer longest-first at the same start to reduce partial edits. Also consume the vector directly.

Apply this diff:

-    let mut fixes = Vec::from_iter(fixes.iter());
-    fixes.sort_by_key(|((range, _id), _rewrite_data)| (range.start, range.end));
+    let mut entries: Vec<_> = fixes.iter().collect();
+    // start ASC, end DESC to prefer larger ranges when overlapping
+    entries.sort_by(|((ra, _), _), ((rb, _), _)| {
+      ra.start.cmp(&rb.start).then(rb.end.cmp(&ra.end))
+    });
@@
-    let edits: Vec<TextEdit> = fixes
-      .iter()
+    let edits: Vec<TextEdit> = entries
+      .into_iter()
       .filter_map(|((range, _id), rewrite_data)| {
         if range.start < last {
           return None;
         }
         let fixed = rewrite_data.fixers.first()?.fixed.to_string();
         let edit = TextEdit::new(*range, fixed);
         last = range.end;
         Some(edit)
       })
       .collect();

396-397: Guard against empty content changes to avoid index panic.

LSP can legally send an empty array.

Apply this diff:

-    let text = &params.content_changes[0].text;
+    let change = params.content_changes.first()?;
+    let text = &change.text;
crates/lsp/src/utils.rs (3)

14-15: Export Fixes so lib.rs can reuse it.

Prevents duplicated aliases and keeps the key contract centralized.

Apply this diff:

-type Fixes = HashMap<(Range, String), RewriteData>;
+pub type Fixes = HashMap<(Range, String), RewriteData>;

28-53: Skip caching empty fixers.

Avoid storing RewriteData with no actionable edits.

Apply this diff:

   pub fn from_node_match<L: LanguageExt>(
@@
-    let fixers = rule
+    let fixers: Vec<OneFix> = rule
@@
-      .collect();
-    Some(Self { fixers })
+      .collect();
+    if fixers.is_empty() {
+      None
+    } else {
+      Some(Self { fixers })
+    }

67-85: Mark only the first quick fix as preferred.

Setting all as preferred is ambiguous for clients.

Apply this diff:

-  let actions = rewrite_data.fixers.clone().into_iter().map(|fixer| {
+  let actions = rewrite_data
+    .fixers
+    .clone()
+    .into_iter()
+    .enumerate()
+    .map(|(i, fixer)| {
@@
-    CodeAction {
+    CodeAction {
       title,
       command: None,
       diagnostics: None,
       edit: Some(edit),
       disabled: None,
       kind: Some(CodeActionKind::QUICKFIX),
-      is_preferred: Some(true),
+      is_preferred: Some(i == 0),
       data: None,
     }
   });
crates/lsp/tests/basic.rs (3)

555-557: Remove fixed sleep; rely on wait_for_diagnostics for robustness.

Avoids flaky timing.

Apply this diff:

-  tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;

687-714: Decoder should return Ok(None) on partial/incomplete UTF-8, not error.

Prevents spurious decode errors on chunk boundaries.

Apply this diff:

-    let src_str =
-      std::str::from_utf8(&src[..]).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
+    let src_str = match std::str::from_utf8(&src[..]) {
+      Ok(s) => s,
+      Err(_) => return Ok(None), // wait for more bytes
+    };

419-446: Consider unique request IDs to avoid collisions across requests.

Using a constant id can collide with prior in-flight requests; increment or randomize.

Example:

-  let code_action_request = serde_json::json!({
-    "jsonrpc": "2.0",
-    "id": 1,
+  static ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
+  let id = ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+  let code_action_request = serde_json::json!({
+    "jsonrpc": "2.0",
+    "id": id,
@@
-    match tokio::time::timeout(std::time::Duration::from_secs(2), sender.next()).await {
+    match tokio::time::timeout(std::time::Duration::from_secs(2), sender.next()).await {
       Ok(Some(Ok(msg))) => {
-        if msg.get("id") == Some(&serde_json::json!(1)) {
+        if msg.get("id") == Some(&serde_json::json!(id)) {
           return Some(msg);
         }
       }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 74c40d2 and e7b2211.

📒 Files selected for processing (3)
  • crates/lsp/src/lib.rs (11 hunks)
  • crates/lsp/src/utils.rs (5 hunks)
  • crates/lsp/tests/basic.rs (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: reductor
PR: ast-grep/ast-grep#2209
File: crates/lsp/src/lib.rs:355-357
Timestamp: 2025-08-31T10:52:39.398Z
Learning: In the ast-grep LSP server, diagnostic.data is kept intact even when clients don't advertise codeAction data support. This defensive programming approach ensures compatibility with clients that might work despite not properly advertising their capabilities, prioritizing robustness over strict LSP spec compliance.
📚 Learning: 2024-09-29T21:59:50.675Z
Learnt from: HerringtonDarkholme
PR: ast-grep/ast-grep#1497
File: crates/cli/src/utils/mod.rs:174-175
Timestamp: 2024-09-29T21:59:50.675Z
Learning: ast-grep requires Rust 1.65 or newer. It's acceptable to use syntax and features introduced in Rust 1.65 or newer in ast-grep code.

Applied to files:

  • crates/lsp/tests/basic.rs
  • crates/lsp/src/lib.rs
📚 Learning: 2025-08-31T10:52:39.398Z
Learnt from: reductor
PR: ast-grep/ast-grep#2209
File: crates/lsp/src/lib.rs:355-357
Timestamp: 2025-08-31T10:52:39.398Z
Learning: In the ast-grep LSP server, diagnostic.data is kept intact even when clients don't advertise codeAction data support. This defensive programming approach ensures compatibility with clients that might work despite not properly advertising their capabilities, prioritizing robustness over strict LSP spec compliance.

Applied to files:

  • crates/lsp/src/lib.rs
📚 Learning: 2024-10-22T14:04:15.734Z
Learnt from: HerringtonDarkholme
PR: ast-grep/ast-grep#1546
File: crates/cli/src/utils/rule_overwrite.rs:76-91
Timestamp: 2024-10-22T14:04:15.734Z
Learning: In the `ast-grep-cli` application code, avoid unnecessary abstractions; functions don't need to accept iterators instead of `Vec` if not required.

Applied to files:

  • crates/lsp/src/lib.rs
🧬 Code graph analysis (2)
crates/lsp/tests/basic.rs (1)
crates/lsp/src/lib.rs (5)
  • did_open (166-172)
  • new (221-233)
  • initialize (81-100)
  • initialized (102-124)
  • code_action (193-195)
crates/lsp/src/lib.rs (1)
crates/lsp/src/utils.rs (3)
  • convert_match_to_diagnostic (163-186)
  • from_node_match (28-52)
  • diagnostic_to_code_action (56-88)
🔇 Additional comments (2)
crates/lsp/src/lib.rs (1)

503-518: LGTM: using per-document fixes cache with a held DashMap guard.

Safe borrow of document.fixes and clean filtering for ast-grep diagnostics. This aligns with the fallback path for clients without diagnostic data support.

crates/lsp/src/utils.rs (1)

163-186: LGTM: diagnostics no longer carry data; message/labels handling intact.

The new flow relies on the fixes cache; conversion code remains correct.

@reductor
Copy link
Contributor Author

reductor commented Sep 6, 2025

  • please fix linting and formatting errors
  • I am not a fan of maintaining two sets of implementatiions. I would prefer just keeping the fixCache code path.

I've updated this again removing the data based implementation to now use just the fixCache and fixed the linting and formatting issues.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/lsp/src/lib.rs (1)

70-77: Don’t advertise resolveProvider without implementing resolve.

You set resolve_provider: Some(true) but don’t implement codeAction/resolve; some clients will call it and get “method not found”.

-    resolve_provider: Some(true),
+    resolve_provider: Some(false),

Or implement resolve and carry minimal data to round-trip.

🧹 Nitpick comments (2)
crates/lsp/src/utils.rs (1)

27-56: from_node_match: exact-range filter is safe but may drop valid fixers.

You currently skip fixers whose edit doesn’t exactly match the node range. That prevents misapplies, but also drops legit multi-node/multi-line edits. Consider supporting expanded ranges later.

crates/lsp/tests/basic.rs (1)

341-379: Minor: expose request_code_action for reuse in tests.

It’s handy for reuse across scenarios; consider pub(crate) if needed by other test modules.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e7b2211 and e0eb2c2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • crates/lsp/src/lib.rs (13 hunks)
  • crates/lsp/src/utils.rs (4 hunks)
  • crates/lsp/tests/basic.rs (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Rust code must be formatted with rustfmt (run cargo fmt --all -- --check before committing)
Rust code must pass Clippy with -D clippy::all (cargo clippy --all-targets --all-features --workspace --release --locked -- -D clippy::all)

Files:

  • crates/lsp/src/lib.rs
  • crates/lsp/src/utils.rs
  • crates/lsp/tests/basic.rs
🧠 Learnings (4)
📓 Common learnings
Learnt from: reductor
PR: ast-grep/ast-grep#2209
File: crates/lsp/src/lib.rs:355-357
Timestamp: 2025-08-31T10:52:39.435Z
Learning: In the ast-grep LSP server, diagnostic.data is kept intact even when clients don't advertise codeAction data support. This defensive programming approach ensures compatibility with clients that might work despite not properly advertising their capabilities, prioritizing robustness over strict LSP spec compliance.
📚 Learning: 2025-08-31T10:52:39.435Z
Learnt from: reductor
PR: ast-grep/ast-grep#2209
File: crates/lsp/src/lib.rs:355-357
Timestamp: 2025-08-31T10:52:39.435Z
Learning: In the ast-grep LSP server, diagnostic.data is kept intact even when clients don't advertise codeAction data support. This defensive programming approach ensures compatibility with clients that might work despite not properly advertising their capabilities, prioritizing robustness over strict LSP spec compliance.

Applied to files:

  • crates/lsp/src/lib.rs
📚 Learning: 2024-10-22T14:04:15.734Z
Learnt from: HerringtonDarkholme
PR: ast-grep/ast-grep#1546
File: crates/cli/src/utils/rule_overwrite.rs:76-91
Timestamp: 2024-10-22T14:04:15.734Z
Learning: In the `ast-grep-cli` application code, avoid unnecessary abstractions; functions don't need to accept iterators instead of `Vec` if not required.

Applied to files:

  • crates/lsp/src/lib.rs
📚 Learning: 2024-09-29T21:59:50.675Z
Learnt from: HerringtonDarkholme
PR: ast-grep/ast-grep#1497
File: crates/cli/src/utils/mod.rs:174-175
Timestamp: 2024-09-29T21:59:50.675Z
Learning: ast-grep requires Rust 1.65 or newer. It's acceptable to use syntax and features introduced in Rust 1.65 or newer in ast-grep code.

Applied to files:

  • crates/lsp/src/lib.rs
  • crates/lsp/tests/basic.rs
🧬 Code graph analysis (3)
crates/lsp/src/lib.rs (1)
crates/lsp/src/utils.rs (3)
  • convert_match_to_diagnostic (172-195)
  • diagnostic_to_code_action (60-97)
  • from_node_match (28-56)
crates/lsp/src/utils.rs (2)
crates/cli/src/print/mod.rs (2)
  • fixers (106-117)
  • from (169-183)
crates/config/src/fixer.rs (1)
  • title (163-165)
crates/lsp/tests/basic.rs (1)
crates/lsp/src/lib.rs (4)
  • new (220-232)
  • did_open (165-171)
  • code_action (192-194)
  • initialize (80-99)
🔇 Additional comments (6)
crates/lsp/src/utils.rs (2)

14-15: Key choice sanity-check: Range as HashMap key.

Using lsp_types::Range as a Hash key is fine if it implements Hash/Eq across your MSRV; just flagging that any future upstream change could break this alias.


172-195: Intentional: diagnostic.data removed.

Acknowledging the shift to cache-only flow; good for VS’s client compatibility. Please ensure lib.rs never relies on data downstream.

crates/lsp/tests/basic.rs (1)

441-480: Single-line test expectation depends on newline preservation.

This will pass once apply_all_code_actions preserves the trailing newline; otherwise it’s flaky.

crates/lsp/src/lib.rs (3)

270-301: Good: compute Fixes alongside diagnostics.

Caching fixes keyed by (range, rule.id) aligns with the new data-less flow.


507-531: Safe use of cached fixes with DashMap guard looks good.

Guard lifetime covers fixes_cache reference.


327-341: Publish: propagate empty diagnostics/fixes when none.

unwrap_or_default() path correctly clears prior state.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
crates/lsp/tests/basic.rs (1)

378-436: Fix: apply edits across multiple lines and preserve trailing newline.

Current implementation drops the final newline and ignores multi-line ranges, which can make “fix all” assertions inaccurate.

Apply:

 fn apply_all_code_actions(text: &str, actions: &Vec<Value>) -> String {
-  // As offsets are based on the original text, we need to track changes
-  let mut lines: Vec<String> = text.lines().map(|s| s.to_string()).collect();
+  // Work line-wise but preserve trailing newline and handle multi-line edits
+  let mut lines: Vec<String> = text.lines().map(|s| s.to_string()).collect();
+  let had_trailing_newline = text.ends_with('\n');
   let mut all_edits = Vec::new();
@@
-  // Apply edits
-  for (start_line, start_char, _end_line, end_char, new_text) in all_edits {
-    let line = &lines[start_line];
-    let prefix = &line[..start_char];
-    let suffix = &line[end_char..];
-    lines[start_line] = format!("{}{}{}", prefix, new_text, suffix);
-  }
+  // Apply edits (same-line and cross-line)
+  for (start_line, start_char, end_line, end_char, new_text) in all_edits {
+    if start_line == end_line {
+      let line = &lines[start_line];
+      let prefix = &line[..start_char];
+      let suffix = &line[end_char..];
+      lines[start_line] = format!("{}{}{}", prefix, new_text, suffix);
+    } else {
+      let first = &lines[start_line];
+      let last = &lines[end_line];
+      let prefix = &first[..start_char];
+      let suffix = &last[end_char..];
+      let merged = format!("{}{}{}", prefix, new_text, suffix);
+      lines.splice(start_line..=end_line, std::iter::once(merged));
+    }
+  }
@@
-  // Join lines back into a single string
-  lines.join("\n")
+  // Join lines back, preserving original trailing newline
+  let mut s = lines.join("\n");
+  if had_trailing_newline {
+    s.push('\n');
+  }
+  s
 }
🧹 Nitpick comments (2)
crates/lsp/tests/basic.rs (2)

310-338: Minor: shorten timeout loop to speed up tests.

Consider fewer iterations or a shorter timeout (e.g., 10 x 1s) to reduce test latency on failures.


538-590: Update expectation to keep trailing newline; prefer assert_eq!.

After fixing apply_all_code_actions, preserve the final '\n' and use assert_eq! for clarity.

-  assert!(actions.len() == 1, "Expected 1 code action for fix all");
+  assert_eq!(actions.len(), 1, "Expected 1 code action for fix all");
@@
-  // TODO: This fix ends up with \n being trimmed at the end, need to investigate
-  assert_eq!(fixed_text, "alert('Hello, world!')\nalert('Another log')");
+  assert_eq!(fixed_text, "alert('Hello, world!')\nalert('Another log')\n");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e0eb2c2 and f2a1008.

📒 Files selected for processing (1)
  • crates/lsp/tests/basic.rs (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Rust code must be formatted with rustfmt (run cargo fmt --all -- --check before committing)
Rust code must pass Clippy with -D clippy::all (cargo clippy --all-targets --all-features --workspace --release --locked -- -D clippy::all)

Files:

  • crates/lsp/tests/basic.rs
🧠 Learnings (2)
📓 Common learnings
Learnt from: reductor
PR: ast-grep/ast-grep#2209
File: crates/lsp/src/lib.rs:355-357
Timestamp: 2025-08-31T10:52:39.435Z
Learning: In the ast-grep LSP server, diagnostic.data is kept intact even when clients don't advertise codeAction data support. This defensive programming approach ensures compatibility with clients that might work despite not properly advertising their capabilities, prioritizing robustness over strict LSP spec compliance.
📚 Learning: 2024-09-29T21:59:50.675Z
Learnt from: HerringtonDarkholme
PR: ast-grep/ast-grep#1497
File: crates/cli/src/utils/mod.rs:174-175
Timestamp: 2024-09-29T21:59:50.675Z
Learning: ast-grep requires Rust 1.65 or newer. It's acceptable to use syntax and features introduced in Rust 1.65 or newer in ast-grep code.

Applied to files:

  • crates/lsp/tests/basic.rs
🧬 Code graph analysis (1)
crates/lsp/tests/basic.rs (1)
crates/lsp/src/lib.rs (4)
  • new (220-232)
  • did_open (165-171)
  • code_action (192-194)
  • initialize (80-99)
🔇 Additional comments (9)
crates/lsp/tests/basic.rs (9)

61-65: LGTM: simple, thread-safe ID allocator.

AtomicI32 with Relaxed is fine for tests.


288-309: LGTM: didOpen helper is correct.

Payload matches LSP shape; using framed send is appropriate.


340-354: LGTM: response waiter is fine.

Matches IDs and times out safely.


356-377: LGTM: codeAction requester is correct.

Carries the diagnostic through context as expected.


438-478: LGTM: single-line edit flow validates the cache-based actions.


480-536: LGTM: overlapping diagnostics each yield one action; assertions are correct.


631-642: LGTM: encoder framing is correct (byte-length).


644-686: LGTM: framed server wiring looks solid.

Single duplex stream + split on server side is fine; init/initialized handshake is correct.


688-702: LGTM: round-trip codec test.

Covers the basic encode/decode path.

@HerringtonDarkholme
Copy link
Member

thanks

@HerringtonDarkholme HerringtonDarkholme added this pull request to the merge queue Sep 6, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to invalid changes in the merge commit Sep 6, 2025
@HerringtonDarkholme HerringtonDarkholme added this pull request to the merge queue Sep 6, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to invalid changes in the merge commit Sep 6, 2025
@HerringtonDarkholme HerringtonDarkholme added this pull request to the merge queue Sep 6, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to invalid changes in the merge commit Sep 6, 2025
@HerringtonDarkholme HerringtonDarkholme merged commit 3a57441 into ast-grep:main Sep 6, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] LSP fix support for LSP clients without diagnostic data support

2 participants