返回 CodeWhale
lsp_hooks.rs
根目录 / crates / tui / src / core / engine / lsp_hooks.rs
1 //! Post-edit LSP diagnostics hooks for engine tool execution.
2 //!
3 //! The turn loop only needs to ask "did a successful edit produce diagnostics?"
4 //! This module owns the tool-input path extraction and the synthetic diagnostic
5 //! message injection so the top-level engine module stays focused on session
6 //! orchestration.
7
8 use std::path::PathBuf;
9
10 use crate::tools::apply_patch::preflight_apply_patch;
11
12 use super::*;
13
14 /// #136: derive the file path(s) edited by a tool call. Returns the empty
15 /// vec for tools that don't modify files. We intentionally only handle the
16 /// three known edit tools — adding more (e.g. specialized refactor tools)
17 /// is a one-line change here.
18 pub(super) fn edited_paths_for_tool(tool_name: &str, input: &serde_json::Value) -> Vec<PathBuf> {
19 let semantic = crate::tools::canonical_action::canonical_action_alias(tool_name, input);
20 match semantic {
21 "edit_file" | "write_file" => {
22 if let Some(path) = input.get("path").and_then(|v| v.as_str()) {
23 vec![PathBuf::from(path)]
24 } else {
25 Vec::new()
26 }
27 }
28 "apply_patch" => preflight_apply_patch(input)
29 .map(|preflight| {
30 preflight
31 .touched_files
32 .into_iter()
33 .map(PathBuf::from)
34 .collect()
35 })
36 .unwrap_or_default(),
37 _ => Vec::new(),
38 }
39 }
40
41 impl Engine {
42 /// #136: post-edit hook. Inspects the tool name + input, derives the
43 /// edited file path, and asks the LSP manager for diagnostics. The
44 /// rendered block is queued in `pending_lsp_blocks` and flushed to the
45 /// session message stream just before the next API request. Failure is
46 /// silent by design — a missing/crashing LSP server must never block
47 /// the agent.
48 pub(super) async fn run_post_edit_lsp_hook(
49 &mut self,
50 tool_name: &str,
51 tool_input: &serde_json::Value,
52 ) {
53 if !self.lsp_manager.config().enabled {
54 return;
55 }
56 let paths = edited_paths_for_tool(tool_name, tool_input);
57 let mut found = 0usize;
58 let mut files = 0usize;
59 for path in paths {
60 let absolute = if path.is_absolute() {
61 path.clone()
62 } else {
63 self.session.workspace.join(&path)
64 };
65 // Use a short edit-sequence based on the existing turn counter so
66 // log output stays correlated even though we do not currently
67 // batch by sequence.
68 let seq = self.turn_counter;
69 if let Some(block) = self.lsp_manager.diagnostics_for(&absolute, seq).await {
70 found = found.saturating_add(block.items.len());
71 files = files.saturating_add(1);
72 self.pending_lsp_blocks.push(block);
73 }
74 }
75 if found > 0 {
76 let _ = self
77 .tx_event
78 .send(Event::LspRepairUpdate {
79 diagnostics_found: found,
80 files,
81 injected: false,
82 })
83 .await;
84 }
85 }
86
87 /// Drain `pending_lsp_blocks` into a single synthetic user message so the
88 /// model sees the diagnostics on its next request. Skips when nothing is
89 /// pending. The message uses the standard `text` content block shape
90 /// (the same shape as the post-tool steer messages) so we don't need to
91 /// invent a new envelope.
92 pub(super) async fn flush_pending_lsp_diagnostics(&mut self) {
93 if self.pending_lsp_blocks.is_empty() {
94 return;
95 }
96 let blocks = std::mem::take(&mut self.pending_lsp_blocks);
97 let found: usize = blocks.iter().map(|b| b.items.len()).sum();
98 let files = blocks.len();
99 let rendered = crate::lsp::render_blocks(&blocks);
100 if rendered.is_empty() {
101 return;
102 }
103 self.add_session_message(self.runtime_text_message_with_turn_metadata(
104 rendered,
105 crate::core::ops::UserInputProvenance::Runtime,
106 ))
107 .await;
108 let _ = self
109 .tx_event
110 .send(Event::LspRepairUpdate {
111 diagnostics_found: found,
112 files,
113 injected: true,
114 })
115 .await;
116 }
117 }
118
119 #[cfg(test)]
120 mod primitive_name_tests {
121 use super::*;
122 use serde_json::json;
123
124 #[test]
125 fn lowercase_file_mutations_reach_the_lsp_hook() {
126 for name in ["write", "edit", "write_file", "edit_file"] {
127 assert_eq!(
128 edited_paths_for_tool(name, &json!({"path": "src/lib.rs"})),
129 vec![PathBuf::from("src/lib.rs")]
130 );
131 }
132 }
133 }
134
134 lines RUST