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 super::{RPCModelContentChangeDTO::RPCModelContentChangeDTO, RPCRangeDTO::RPCRangeDTO};
29use crate::ApplicationState::Internal::AnalyzeTextLinesAndEOL;
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 = "PascalCase")]
43pub struct DocumentStateDTO {
44 #[serde(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 pub EOL:String,
60
61 pub IsDirty:bool,
63
64 pub Encoding:String,
66
67 pub VersionIdentifier:i64,
69}
70
71impl DocumentStateDTO {
72 pub fn Create(URI:Url, LanguageIdentifier:Option<String>, Content:String) -> Result<Self, CommonError> {
90 if URI.as_str().is_empty() {
92 return Err(CommonError::InvalidArgument {
93 ArgumentName:"URI".into(),
94 Reason:"URI cannot be empty".into(),
95 });
96 }
97
98 let LanguageID = LanguageIdentifier.unwrap_or_else(|| "plaintext".to_string());
99
100 if LanguageID.len() > MAX_LANGUAGE_ID_LENGTH {
102 return Err(CommonError::InvalidArgument {
103 ArgumentName:"LanguageIdentifier".into(),
104 Reason:format!("Language identifier exceeds maximum length of {} bytes", MAX_LANGUAGE_ID_LENGTH),
105 });
106 }
107
108 let (Lines, EOL) = AnalyzeTextLinesAndEOL(&Content);
109
110 if Lines.len() > MAX_DOCUMENT_LINES {
112 return Err(CommonError::InvalidArgument {
113 ArgumentName:"Content".into(),
114 Reason:format!("Document exceeds maximum line count of {}", MAX_DOCUMENT_LINES),
115 });
116 }
117
118 for (Index, Line) in Lines.iter().enumerate() {
120 if Line.len() > MAX_LINE_LENGTH {
121 return Err(CommonError::InvalidArgument {
122 ArgumentName:"Content".into(),
123 Reason:format!("Line {} exceeds maximum length of {} bytes", Index + 1, MAX_LINE_LENGTH),
124 });
125 }
126 }
127
128 let Encoding = "utf8".to_string();
129
130 Ok(Self {
131 URI,
132
133 LanguageIdentifier:LanguageID,
134
135 Version:1,
136
137 Lines,
138
139 EOL,
140
141 IsDirty:false,
142
143 Encoding,
144
145 VersionIdentifier:1,
146 })
147 }
148
149 pub fn CreateUnsafe(
152 URI:Url,
153 LanguageIdentifier:String,
154 Lines:Vec<String>,
155 EOL:String,
156 IsDirty:bool,
157 Encoding:String,
158 Version:i64,
159 VersionIdentifier:i64,
160 ) -> Self {
161 Self {
162 URI,
163 LanguageIdentifier,
164 Version,
165 Lines,
166 EOL,
167 IsDirty,
168 Encoding,
169 VersionIdentifier,
170 }
171 }
172
173 pub fn GetText(&self) -> String { self.Lines.join(&self.EOL) }
175
176 pub fn ToDTO(&self) -> Result<Value, CommonError> {
178 serde_json::to_value(self).map_err(|Error| CommonError::SerializationError { Description:Error.to_string() })
179 }
180
181 pub fn ApplyChanges(&mut self, NewVersion:i64, ChangesValue:&Value) -> Result<(), CommonError> {
184 if NewVersion <= self.Version {
186 return Ok(());
187 }
188
189 if let Ok(RPCChange) = serde_json::from_value::<Vec<RPCModelContentChangeDTO>>(ChangesValue.clone()) {
191 log::trace!("Applying {} delta change(s) to document {}", RPCChange.len(), self.URI);
192
193 self.Lines = ApplyDeltaChanges(&self.Lines, &self.EOL, &RPCChange);
194 } else if let Some(FullText) = ChangesValue.as_str() {
195 let (NewLines, NewEOL) = AnalyzeTextLinesAndEOL(FullText);
197
198 self.Lines = NewLines;
199
200 self.EOL = NewEOL;
201 } else {
202 return Err(CommonError::InvalidArgument {
203 ArgumentName:"ChangesValue".into(),
204
205 Reason:format!(
206 "Invalid change format for {}: expected string or RPCModelContentChangeDTO array.",
207 self.URI
208 ),
209 });
210 }
211
212 self.Version = NewVersion;
214
215 self.VersionIdentifier += 1;
216
217 self.IsDirty = true;
218
219 Ok(())
220 }
221}
222
223fn ApplyDeltaChanges(Lines:&[String], EOL:&str, RPCChange:&[RPCModelContentChangeDTO]) -> Vec<String> {
240 let mut ResultText = Lines.join(EOL);
242
243 if RPCChange.is_empty() {
245 return Lines.to_vec();
246 }
247
248 let mut SortedChanges:Vec<&RPCModelContentChangeDTO> = RPCChange.iter().collect();
252 SortedChanges.sort_by(|a, b| CMP_Range_Position(&b.Range, &a.Range));
253
254 for Change in SortedChanges {
256 let StartOffset = PositionToOffset(&ResultText, EOL, &Change.Range.StartLineNumber, &Change.Range.StartColumn);
258 let EndOffset = PositionToOffset(&ResultText, EOL, &Change.Range.EndLineNumber, &Change.Range.EndColumn);
259
260 if StartOffset > EndOffset {
262 log::error!(
263 "[ApplyDeltaChanges] Invalid range: start ({}) > end ({}) for text length {}",
264 StartOffset,
265 EndOffset,
266 ResultText.len()
267 );
268 continue;
269 }
270
271 let TextLength = ResultText.len();
272 if StartOffset > TextLength || EndOffset > TextLength {
273 log::error!(
274 "[ApplyDeltaChanges] Out of bounds: start ({}) or end ({}) exceeds text length {}",
275 StartOffset,
276 EndOffset,
277 TextLength
278 );
279 continue;
280 }
281
282 let OldText = ResultText.as_bytes();
285 ResultText =
286 String::from_utf8_lossy(&[&OldText[..StartOffset], Change.Text.as_bytes(), &OldText[EndOffset..]].concat())
287 .into_owned();
288 }
289
290 AnalyzeTextLinesAndEOL(&ResultText).0
292}
293
294fn PositionToOffset(Text:&str, EOL:&str, LineNumber:&usize, Column:&usize) -> usize {
299 let Lines:Vec<&str> = Text.split(EOL).collect();
300 let EOLLength = EOL.len();
301
302 let mut Offset = 0;
303
304 for LineIndex in 0..*LineNumber {
306 if LineIndex < Lines.len() {
307 Offset += Lines[LineIndex].len() + EOLLength;
308 }
309 }
310
311 if *LineNumber < Lines.len() {
313 let CurrentLine = Lines[*LineNumber];
315 let CharOffset = CurrentLine
316 .char_indices()
317 .nth(*Column)
318 .map_or(CurrentLine.len(), |(offset, _)| offset);
319 Offset += CharOffset;
320 }
321
322 Offset
323}
324
325fn CMP_Range_Position(A:&RPCRangeDTO, B:&RPCRangeDTO) -> std::cmp::Ordering {
329 A.StartLineNumber
330 .cmp(&B.StartLineNumber)
331 .then_with(|| A.StartColumn.cmp(&B.StartColumn))
332}