1use std::collections::HashMap;
2
3use tower_lsp::lsp_types::{Diagnostic, Range, Url};
4
5use logicaffeine_base::Interner;
6use logicaffeine_compile::analysis::VarState;
7use logicaffeine_language::{
8 analysis::{TypeRegistry, PolicyRegistry},
9 token::Token,
10};
11
12use crate::index::SymbolIndex;
13use crate::line_index::LineIndex;
14use crate::pipeline;
15
16pub struct DocumentState {
18 pub source: String,
19 pub version: i32,
20 pub line_index: LineIndex,
21
22 pub tokens: Vec<Token>,
24 pub interner: Interner,
25 pub diagnostics: Vec<Diagnostic>,
26 pub symbol_index: SymbolIndex,
27 pub type_registry: TypeRegistry,
28 pub policy_registry: PolicyRegistry,
29 pub ownership_states: HashMap<String, VarState>,
30}
31
32impl DocumentState {
33 pub fn new(source: String, version: i32) -> Self {
37 Self::with_uri(source, version, None)
38 }
39
40 pub fn with_uri(source: String, version: i32, uri: Option<&Url>) -> Self {
42 let line_index = LineIndex::new(&source);
43
44 let analysis = pipeline::analyze(&source);
45 let diagnostics = build_diagnostics(&analysis, &line_index, uri);
46
47 DocumentState {
48 source,
49 version,
50 line_index,
51 tokens: analysis.tokens,
52 interner: analysis.interner,
53 diagnostics,
54 symbol_index: analysis.symbol_index,
55 type_registry: analysis.type_registry,
56 policy_registry: analysis.policy_registry,
57 ownership_states: analysis.ownership_states,
58 }
59 }
60
61}
62
63pub fn apply_content_change(text: &mut String, range: Option<Range>, new_text: &str) {
69 match range {
70 None => {
71 text.clear();
72 text.push_str(new_text);
73 }
74 Some(range) => {
75 let line_index = LineIndex::new(text);
76 let start = line_index.offset(range.start);
77 let end = line_index.offset(range.end);
78 text.replace_range(start..end, new_text);
79 }
80 }
81}
82
83fn build_diagnostics(
85 analysis: &pipeline::AnalysisResult,
86 line_index: &LineIndex,
87 uri: Option<&Url>,
88) -> Vec<Diagnostic> {
89 let mut diagnostics = crate::diagnostics::convert_errors(
90 &analysis.errors,
91 &analysis.tokens,
92 &analysis.interner,
93 line_index,
94 uri,
95 );
96 diagnostics.extend(crate::diagnostics::convert_analysis_errors(
97 &analysis.escape_errors,
98 &analysis.tokens,
99 &analysis.interner,
100 line_index,
101 uri,
102 ));
103 diagnostics.extend(crate::diagnostics::convert_analysis_errors(
104 &analysis.ownership_errors,
105 &analysis.tokens,
106 &analysis.interner,
107 line_index,
108 uri,
109 ));
110 diagnostics.extend(crate::diagnostics::unused_variable_hints(
111 &analysis.symbol_index,
112 line_index,
113 ));
114 diagnostics.extend(crate::diagnostics::shadowing_warnings(
115 &analysis.symbol_index,
116 line_index,
117 uri,
118 ));
119 diagnostics.extend(crate::diagnostics::unused_function_hints(
120 &analysis.symbol_index,
121 line_index,
122 ));
123 diagnostics
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn new_document_parses_source() {
132 let doc = DocumentState::new("## Main\n Let x be 5.\n Show x.\n".to_string(), 1);
133 assert_eq!(doc.version, 1);
134 assert!(doc.diagnostics.is_empty(), "Valid source should have no diagnostics: {:?}", doc.diagnostics);
135 assert!(!doc.tokens.is_empty());
136 assert!(!doc.symbol_index.definitions.is_empty());
137 }
138
139 #[test]
140 fn fresh_snapshot_replaces_analysis() {
141 let doc = DocumentState::new("## Main\n Let x be 5.\n".to_string(), 1);
142 assert_eq!(doc.symbol_index.definitions_of("x").len(), 1);
143 assert_eq!(doc.symbol_index.definitions_of("y").len(), 0);
144
145 let doc = DocumentState::new("## Main\n Let y be 10.\n".to_string(), 2);
147 assert_eq!(doc.version, 2);
148 assert_eq!(doc.symbol_index.definitions_of("y").len(), 1);
149 }
150
151 #[test]
152 fn empty_document() {
153 let doc = DocumentState::new("".to_string(), 0);
154 assert_eq!(doc.version, 0);
155 assert_eq!(doc.source, "");
156 }
157
158 #[test]
159 fn document_source_stored() {
160 let source = "## Main\n Let x be 5.\n";
161 let doc = DocumentState::new(source.to_string(), 1);
162 assert_eq!(doc.source, source);
163 }
164
165 #[test]
166 fn snapshot_of_broken_source_carries_diagnostics() {
167 let doc = DocumentState::new("## Main\n Let x be 5.\n Show x.\n".to_string(), 1);
168 assert!(doc.diagnostics.is_empty(), "Valid source should have no diagnostics");
169 let doc = DocumentState::new("## Main\n Let be.\n".to_string(), 2);
170 assert!(!doc.diagnostics.is_empty(), "Invalid source should produce diagnostics");
171 }
172
173 #[test]
174 fn line_index_matches_snapshot_source() {
175 let doc = DocumentState::new("line0\nline1\nline2\nline3\nline4\n".to_string(), 2);
176 let pos = doc.line_index.position(doc.source.len().saturating_sub(2));
177 assert_eq!(pos.line, 4, "In a 5-line doc, near-end should be line 4");
178 }
179
180 #[test]
181 fn document_with_move_error_has_diagnostics() {
182 let source = "## Main\n Let x be 5.\n Let y be 0.\n Give x to y.\n Show x.\n";
184 let doc = DocumentState::new(source.to_string(), 1);
185 let move_diags: Vec<_> = doc.diagnostics.iter()
186 .filter(|d| {
187 let is_move_code = d.code.as_ref().map_or(false, |c| {
188 matches!(c, tower_lsp::lsp_types::NumberOrString::String(s) if s == "use-after-move")
189 });
190 let is_move_msg = d.message.contains("after") && d.message.contains("move");
191 is_move_code || is_move_msg
192 })
193 .collect();
194 assert!(
195 !move_diags.is_empty(),
196 "Document with use-after-move should have ownership diagnostics. All diags: {:?}",
197 doc.diagnostics
198 );
199 }
200
201 #[test]
202 fn document_ownership_states_available() {
203 let source = "## Main\n Let x be 5.\n Show x.\n";
204 let doc = DocumentState::new(source.to_string(), 1);
205 assert!(
206 !doc.ownership_states.is_empty(),
207 "Document should have ownership states for variables"
208 );
209 }
210}