-
-
Notifications
You must be signed in to change notification settings - Fork 301
Allowing LSP clients without publish diagnostics data support to support code fixes #2209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. Comment |
There was a problem hiding this 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.
⛔ Files ignored due to path filters (1)
Cargo.lockis 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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
HerringtonDarkholme
left a comment
There was a problem hiding this 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
fixCachecode 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.
|
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. |
There was a problem hiding this 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 = ¶ms.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.
📒 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.rscrates/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.
I've updated this again removing the data based implementation to now use just the fixCache and fixed the linting and formatting issues. |
There was a problem hiding this 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.
⛔ Files ignored due to path filters (1)
Cargo.lockis 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 (runcargo fmt --all -- --checkbefore 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.rscrates/lsp/src/utils.rscrates/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.rscrates/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.
There was a problem hiding this 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.
📒 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 (runcargo fmt --all -- --checkbefore 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.
|
thanks |
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
Tests
Chores