返回 DeepSeek-TUI-2026
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 super::*;
11
12 /// #136: derive the file path(s) edited by a tool call. Returns the empty
13 /// vec for tools that don't modify files. We intentionally only handle the
14 /// three known edit tools — adding more (e.g. specialized refactor tools)
15 /// is a one-line change here.
16 pub(super) fn edited_paths_for_tool(tool_name: &str, input: &serde_json::Value) -> Vec<PathBuf> {
17 match tool_name {
18 "edit_file" | "write_file" => {
19 if let Some(path) = input.get("path").and_then(|v| v.as_str()) {
20 vec![PathBuf::from(path)]
21 } else {
22 Vec::new()
23 }
24 }
25 "apply_patch" => {
26 // `apply_patch` accepts either a `path` override or a list of
27 // `files` (each `{path, content}`). We try both shapes.
28 let mut out = Vec::new();
29 if let Some(path) = input.get("path").and_then(|v| v.as_str()) {
30 out.push(PathBuf::from(path));
31 }
32 if let Some(files) = input.get("files").and_then(|v| v.as_array()) {
33 for entry in files {
34 if let Some(path) = entry.get("path").and_then(|v| v.as_str()) {
35 out.push(PathBuf::from(path));
36 }
37 }
38 }
39 // Fallback: parse `---`/`+++` headers from a unified diff payload.
40 if out.is_empty()
41 && let Some(patch) = input.get("patch").and_then(|v| v.as_str())
42 {
43 out.extend(parse_patch_paths(patch));
44 }
45 out
46 }
47 _ => Vec::new(),
48 }
49 }
50
51 /// Lightweight parser for `+++ b/<path>` lines in a unified diff. Used as a
52 /// fallback when `apply_patch` is invoked with raw `patch` text and no
53 /// `path`/`files` override. We deliberately keep this dumb — the real
54 /// `apply_patch` tool already validates the patch shape; we only need a
55 /// best-effort hint for the LSP hook.
56 pub(super) fn parse_patch_paths(patch: &str) -> Vec<PathBuf> {
57 let mut out = Vec::new();
58 for line in patch.lines() {
59 if let Some(rest) = line.strip_prefix("+++ ") {
60 let trimmed = rest.trim();
61 // Strip leading `b/` per git diff conventions.
62 let path = trimmed.strip_prefix("b/").unwrap_or(trimmed);
63 // Skip `/dev/null` (deletion).
64 if path == "/dev/null" {
65 continue;
66 }
67 out.push(PathBuf::from(path));
68 }
69 }
70 out
71 }
72
73 impl Engine {
74 /// #136: post-edit hook. Inspects the tool name + input, derives the
75 /// edited file path, and asks the LSP manager for diagnostics. The
76 /// rendered block is queued in `pending_lsp_blocks` and flushed to the
77 /// session message stream just before the next API request. Failure is
78 /// silent by design — a missing/crashing LSP server must never block
79 /// the agent.
80 pub(super) async fn run_post_edit_lsp_hook(
81 &mut self,
82 tool_name: &str,
83 tool_input: &serde_json::Value,
84 ) {
85 if !self.lsp_manager.config().enabled {
86 return;
87 }
88 let paths = edited_paths_for_tool(tool_name, tool_input);
89 for path in paths {
90 let absolute = if path.is_absolute() {
91 path.clone()
92 } else {
93 self.session.workspace.join(&path)
94 };
95 // Use a short edit-sequence based on the existing turn counter so
96 // log output stays correlated even though we do not currently
97 // batch by sequence.
98 let seq = self.turn_counter;
99 if let Some(block) = self.lsp_manager.diagnostics_for(&absolute, seq).await {
100 self.pending_lsp_blocks.push(block);
101 }
102 }
103 }
104
105 /// Drain `pending_lsp_blocks` into a single synthetic user message so the
106 /// model sees the diagnostics on its next request. Skips when nothing is
107 /// pending. The message uses the standard `text` content block shape
108 /// (the same shape as the post-tool steer messages) so we don't need to
109 /// invent a new envelope.
110 pub(super) async fn flush_pending_lsp_diagnostics(&mut self) {
111 if self.pending_lsp_blocks.is_empty() {
112 return;
113 }
114 let blocks = std::mem::take(&mut self.pending_lsp_blocks);
115 let rendered = crate::lsp::render_blocks(&blocks);
116 if rendered.is_empty() {
117 return;
118 }
119 self.add_session_message(Message {
120 role: "user".to_string(),
121 content: vec![ContentBlock::Text {
122 text: rendered,
123 cache_control: None,
124 }],
125 })
126 .await;
127 }
128 }
129
129 lines RUST