Mountain/ApplicationState/DTO/
DocumentStateDTO.rs1use CommonLibrary::{Error::CommonError::CommonError, Utility::Serialization::URLSerializationHelper};
24use serde::{Deserialize, Serialize};
25use serde_json::Value;
26use url::Url;
27
28use crate::{ApplicationState::Internal::TextProcessing::AnalyzeTextLinesAndEOL::AnalyzeTextLinesAndEOL, dev_log};
29use super::{RPCModelContentChangeDTO::RPCModelContentChangeDTO, RPCRangeDTO::RPCRangeDTO};
30
31const MAX_DOCUMENT_LINES:usize = 1_000_000;
33
34const MAX_LINE_LENGTH:usize = 100_000;
36
37const MAX_LANGUAGE_ID_LENGTH:usize = 128;
39
40#[derive(Serialize, Deserialize, Clone, Debug)]
42#[serde(rename_all = "camelCase")]
43pub struct DocumentStateDTO {
44 #[serde(rename = "uri", with = "URLSerializationHelper")]
46 pub URI:Url,
47
48 #[serde(skip_serializing_if = "String::is_empty")]
50 pub LanguageIdentifier:String,
51
52 pub Version:i64,
54
55 pub Lines:Vec<String>,
57
58 #[serde(rename = "eol")]
60 pub EOL:String,
61
62 pub IsDirty:bool,
64
65 pub Encoding:String,
67
68 pub VersionIdentifier:i64,
70}
71
72impl DocumentStateDTO {
73 pub fn Create(URI:Url, LanguageIdentifier:Option<String>, Content:String) -> Result<Self, CommonError> {
91 if URI.as_str().is_empty() {
93 return Err(CommonError::InvalidArgument {
94 ArgumentName:"URI".into(),
95 Reason:"URI cannot be empty".into(),
96 });
97 }
98
99 let LanguageID = LanguageIdentifier.unwrap_or_else(|| "plaintext".to_string());
100
101 if LanguageID.len() > MAX_LANGUAGE_ID_LENGTH {
103 return Err(CommonError::InvalidArgument {
104 ArgumentName:"LanguageIdentifier".into(),
105 Reason:format!("Language identifier exceeds maximum length of {} bytes", MAX_LANGUAGE_ID_LENGTH),
106 });
107 }
108
109 let (Lines, EOL) = AnalyzeTextLinesAndEOL(&Content);
110
111 if Lines.len() > MAX_DOCUMENT_LINES {
113 return Err(CommonError::InvalidArgument {
114 ArgumentName:"Content".into(),
115 Reason:format!("Document exceeds maximum line count of {}", MAX_DOCUMENT_LINES),
116 });
117 }
118
119 for (Index, Line) in Lines.iter().enumerate() {
121 if Line.len() > MAX_LINE_LENGTH {
122 return Err(CommonError::InvalidArgument {
123 ArgumentName:"Content".into(),
124 Reason:format!("Line {} exceeds maximum length of {} bytes", Index + 1, MAX_LINE_LENGTH),
125 });
126 }
127 }
128
129 let Encoding = "utf8".to_string();
130
131 Ok(Self {
132 URI,
133
134 LanguageIdentifier:LanguageID,
135
136 Version:1,
137
138 Lines,
139
140 EOL,
141
142 IsDirty:false,
143
144 Encoding,
145
146 VersionIdentifier:1,
147 })
148 }
149
150 pub fn CreateUnsafe(
153 URI:Url,
154
155 LanguageIdentifier:String,
156
157 Lines:Vec<String>,
158
159 EOL:String,
160
161 IsDirty:bool,
162
163 Encoding:String,
164
165 Version:i64,
166
167 VersionIdentifier:i64,
168 ) -> Self {
169 Self {
170 URI,
171
172 LanguageIdentifier,
173
174 Version,
175
176 Lines,
177
178 EOL,
179
180 IsDirty,
181
182 Encoding,
183
184 VersionIdentifier,
185 }
186 }
187
188 pub fn GetText(&self) -> String { self.Lines.join(&self.EOL) }
190
191 pub fn ToDTO(&self) -> Result<Value, CommonError> {
193 serde_json::to_value(self).map_err(|Error| CommonError::SerializationError { Description:Error.to_string() })
194 }
195
196 pub fn ApplyChanges(&mut self, NewVersion:i64, ChangesValue:&Value) -> Result<(), CommonError> {
199 if NewVersion <= self.Version {
201 return Ok(());
202 }
203
204 if let Ok(RPCChange) = serde_json::from_value::<Vec<RPCModelContentChangeDTO>>(ChangesValue.clone()) {
206 dev_log!("model", "applying {} delta change(s) to document {}", RPCChange.len(), self.URI);
207
208 self.Lines = ApplyDeltaChanges(&self.Lines, &self.EOL, &RPCChange);
209 } else if let Some(FullText) = ChangesValue.as_str() {
210 let (NewLines, NewEOL) = AnalyzeTextLinesAndEOL(FullText);
212
213 self.Lines = NewLines;
214
215 self.EOL = NewEOL;
216 } else {
217 return Err(CommonError::InvalidArgument {
218 ArgumentName:"ChangesValue".into(),
219
220 Reason:format!(
221 "Invalid change format for {}: expected string or RPCModelContentChangeDTO array.",
222 self.URI
223 ),
224 });
225 }
226
227 self.Version = NewVersion;
229
230 self.VersionIdentifier += 1;
231
232 self.IsDirty = true;
233
234 Ok(())
235 }
236}
237
238fn ApplyDeltaChanges(Lines:&[String], EOL:&str, RPCChange:&[RPCModelContentChangeDTO]) -> Vec<String> {
255 let mut ResultText = Lines.join(EOL);
257
258 if RPCChange.is_empty() {
260 return Lines.to_vec();
261 }
262
263 let mut SortedChanges:Vec<&RPCModelContentChangeDTO> = RPCChange.iter().collect();
267
268 SortedChanges.sort_by(|a, b| CMP_Range_Position(&b.Range, &a.Range));
269
270 for Change in SortedChanges {
272 let StartOffset = PositionToOffset(&ResultText, EOL, &Change.Range.StartLineNumber, &Change.Range.StartColumn);
274
275 let EndOffset = PositionToOffset(&ResultText, EOL, &Change.Range.EndLineNumber, &Change.Range.EndColumn);
276
277 if StartOffset > EndOffset {
279 dev_log!(
280 "model",
281 "error: invalid range: start ({}) > end ({}) for text length {}",
282 StartOffset,
283 EndOffset,
284 ResultText.len()
285 );
286
287 continue;
288 }
289
290 let TextLength = ResultText.len();
291
292 if StartOffset > TextLength || EndOffset > TextLength {
293 dev_log!(
294 "model",
295 "error: out of bounds: start ({}) or end ({}) exceeds text length {}",
296 StartOffset,
297 EndOffset,
298 TextLength
299 );
300
301 continue;
302 }
303
304 let OldText = ResultText.as_bytes();
307
308 ResultText =
309 String::from_utf8_lossy(&[&OldText[..StartOffset], Change.Text.as_bytes(), &OldText[EndOffset..]].concat())
310 .into_owned();
311 }
312
313 AnalyzeTextLinesAndEOL(&ResultText).0
315}
316
317fn PositionToOffset(Text:&str, EOL:&str, LineNumber:&usize, Column:&usize) -> usize {
322 let Lines:Vec<&str> = Text.split(EOL).collect();
323
324 let EOLLength = EOL.len();
325
326 let mut Offset = 0;
327
328 for LineIndex in 0..*LineNumber {
330 if LineIndex < Lines.len() {
331 Offset += Lines[LineIndex].len() + EOLLength;
332 }
333 }
334
335 if *LineNumber < Lines.len() {
337 let CurrentLine = Lines[*LineNumber];
339
340 let CharOffset = CurrentLine
341 .char_indices()
342 .nth(*Column)
343 .map_or(CurrentLine.len(), |(offset, _)| offset);
344
345 Offset += CharOffset;
346 }
347
348 Offset
349}
350
351fn CMP_Range_Position(A:&RPCRangeDTO, B:&RPCRangeDTO) -> std::cmp::Ordering {
355 A.StartLineNumber
356 .cmp(&B.StartLineNumber)
357 .then_with(|| A.StartColumn.cmp(&B.StartColumn))
358}