devscout_rs/query/read.rs
1use std::path::Path;
2
3use super::index::GraphIndex;
4use super::member::MemberCandidate;
5use super::refs::{build_refs_model_inner, RefsModel, RefsResult};
6use super::refs_tables::{DEFAULT_CAP, INBOUND_CAP, OUTBOUND_CAP};
7
8// ============================================================================
9// build_read_model -- the declaration span plus the same inbound answer.
10// ============================================================================
11
12/// The declaration span of a resolved def: the file, its 1-based start and
13/// end lines, and the VERBATIM source text of those lines (no trim, no clip
14///
15/// -- a span is quoted, not summarized). `end_line` is what tree-sitter
16/// delimited as the whole declaration node; when the file has changed since
17/// the map, `end_line` is clamped to the file's current last line rather
18/// than guessed upward, and a file that shrank below the span's start yields
19/// no span at all.
20#[derive(Debug, Clone, PartialEq)]
21pub struct ReadSpan {
22 /// The file value.
23 pub file: String,
24 /// The start line value.
25 pub start_line: usize,
26 /// The end line value.
27 pub end_line: usize,
28 /// The source value.
29 pub source: String,
30}
31
32/// The resolved `read` result for one symbol: everything `refs` answers,
33///
34/// plus the declaration span when one is on record. `span` is `None` for a
35/// def with no recorded end (TS defs, a graph written before end lines were
36/// extracted) and never faked from a start line alone.
37#[derive(Debug, Clone, PartialEq)]
38pub struct ReadModel {
39 /// The refs value.
40 pub refs: RefsModel,
41 /// The span value.
42 pub span: Option<ReadSpan>,
43}
44
45/// The outcome of a `read` query -- the `refs` outcome set unchanged, so the
46/// ambiguity and zero-hit discipline are literally the same code path's.
47#[derive(Debug, Clone, PartialEq)]
48pub enum ReadResult {
49 /// Represents `Resolved`.
50 Resolved(Box<ReadModel>),
51 /// A bare-member answer: one resolved-shaped model per declaring type.
52 Members(Vec<RefsModel>),
53 /// Represents `Ambiguous`.
54 Ambiguous(Vec<String>),
55 /// The `refs` outcome of the same name, unchanged (see
56 /// `RefsResult::MemberAmbiguous`).
57 MemberAmbiguous(Vec<MemberCandidate>),
58 /// Represents `NotFound`.
59 NotFound,
60}
61
62// Slices the mapped span out of the current file text. Fails closed: an
63// unreadable file or a span past EOF degrades to "no span", which the
64// renderer shows as a start-line-only answer, never as invented text.
65fn read_span(root: &Path, model: &RefsModel, end_line: usize) -> Option<ReadSpan> {
66 let site = model.sites.first()?;
67 if end_line < site.line {
68 return None;
69 }
70 let body = std::fs::read_to_string(root.join(&site.file)).ok()?;
71 let lines: Vec<&str> = body.split('\n').collect();
72 if site.line < 1 || site.line > lines.len() {
73 return None;
74 }
75 // The map may be behind the working tree; quoting past EOF would invent
76 // lines, so the end clamps to what the file actually has.
77 let end = end_line.min(lines.len());
78 let mut start_text = lines[site.line - 1];
79 if site.line == 1 {
80 start_text = start_text.strip_prefix('\u{feff}').unwrap_or(start_text);
81 }
82 let mut source = String::from(start_text);
83 for line in &lines[site.line..end] {
84 source.push('\n');
85 source.push_str(line);
86 }
87 Some(ReadSpan {
88 file: site.file.clone(),
89 start_line: site.line,
90 end_line: end,
91 source,
92 })
93}
94
95/// `read` = `refs` resolution + capped-and-ranked inbound machinery, reused
96///
97/// wholesale (`out` stays off -- the verb answers "what declares this and
98/// what points at it"), plus the one new fact: the declaration span.
99pub fn build_read_model(index: &GraphIndex, query: &str) -> ReadResult {
100 match build_refs_model_inner(
101 index,
102 query,
103 false,
104 DEFAULT_CAP,
105 INBOUND_CAP,
106 OUTBOUND_CAP,
107 false,
108 true,
109 ) {
110 RefsResult::Resolved(m) => {
111 let end_line = index.def(&m.id).map(|d| d.end_line).unwrap_or(0);
112 let span = if end_line > 0 {
113 read_span(&index.root, &m, end_line)
114 } else {
115 None
116 };
117 ReadResult::Resolved(Box::new(ReadModel { refs: m, span }))
118 }
119 RefsResult::Members(models) => ReadResult::Members(models),
120 RefsResult::Ambiguous(ids) => ReadResult::Ambiguous(ids),
121 RefsResult::MemberAmbiguous(candidates) => ReadResult::MemberAmbiguous(candidates),
122 RefsResult::NotFound => ReadResult::NotFound,
123 }
124}