1use std::{
65 collections::HashMap,
66 path::PathBuf,
67 sync::{Arc, Mutex, OnceLock},
68 time::{Duration, Instant},
69};
70
71use CommonLibrary::{
72 DTO::WorkspaceEditDTO::WorkspaceEditDTO,
73 Error::CommonError::CommonError,
74 Workspace::{WorkspaceEditApplier::WorkspaceEditApplier, WorkspaceProvider::WorkspaceProvider},
75};
76use async_trait::async_trait;
77use globset::GlobBuilder;
78use ignore::WalkBuilder;
79use serde_json::Value;
80use tokio::sync::Notify;
81use url::Url;
82
83use super::{MountainEnvironment::MountainEnvironment, Utility};
84use crate::dev_log;
85
86const FIND_FILES_CACHE_TTL:Duration = Duration::from_millis(2500);
97const FIND_FILES_CACHE_CAPACITY:usize = 128;
98
99#[derive(Hash, Eq, PartialEq, Clone)]
100struct FindFilesCacheKey {
101 Folders:Vec<PathBuf>,
102 Include:String,
103 Exclude:Option<String>,
104 Cap:usize,
105 UseIgnoreFiles:bool,
106 FollowSymlinks:bool,
107 RestrictBase:Option<String>,
108}
109
110struct FindFilesCacheEntry {
111 Result:Vec<Url>,
112 StoredAt:Instant,
113}
114
115fn FindFilesCache() -> &'static Mutex<HashMap<FindFilesCacheKey, FindFilesCacheEntry>> {
116 static CACHE:OnceLock<Mutex<HashMap<FindFilesCacheKey, FindFilesCacheEntry>>> = OnceLock::new();
117 CACHE.get_or_init(|| Mutex::new(HashMap::with_capacity(FIND_FILES_CACHE_CAPACITY)))
118}
119
120fn FindFilesCachePut(Key:FindFilesCacheKey, Result:Vec<Url>) {
125 if let Ok(mut Guard) = FindFilesCache().lock() {
126 if Guard.len() >= FIND_FILES_CACHE_CAPACITY {
127 let Cutoff = Instant::now() - FIND_FILES_CACHE_TTL;
128 Guard.retain(|_, V| V.StoredAt > Cutoff);
129 if Guard.len() >= FIND_FILES_CACHE_CAPACITY {
130 let DropCount = Guard.len() / 2;
131 let StaleKeys:Vec<FindFilesCacheKey> = Guard.iter().take(DropCount).map(|(K, _)| K.clone()).collect();
132 for K in StaleKeys {
133 Guard.remove(&K);
134 }
135 }
136 }
137 Guard.insert(Key, FindFilesCacheEntry { Result, StoredAt:Instant::now() });
138 }
139}
140
141fn FindFilesCacheGet(Key:&FindFilesCacheKey) -> Option<Vec<Url>> {
142 let Guard = FindFilesCache().lock().ok()?;
143 let Entry = Guard.get(Key)?;
144 if Entry.StoredAt.elapsed() > FIND_FILES_CACHE_TTL {
145 return None;
146 }
147 Some(Entry.Result.clone())
148}
149
150pub fn ClearFindFilesCache() {
156 if let Ok(mut Guard) = FindFilesCache().lock() {
157 Guard.clear();
158 }
159}
160
161fn FindFilesInFlight() -> &'static Mutex<HashMap<FindFilesCacheKey, Arc<Notify>>> {
174 static IN_FLIGHT:OnceLock<Mutex<HashMap<FindFilesCacheKey, Arc<Notify>>>> = OnceLock::new();
175 IN_FLIGHT.get_or_init(|| Mutex::new(HashMap::new()))
176}
177
178#[async_trait]
179impl WorkspaceProvider for MountainEnvironment {
180 async fn GetWorkspaceFoldersInfo(&self) -> Result<Vec<(Url, String, usize)>, CommonError> {
182 dev_log!("workspaces", "[WorkspaceProvider] Getting workspace folders info.");
183 let FoldersGuard = self
184 .ApplicationState
185 .Workspace
186 .WorkspaceFolders
187 .lock()
188 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
189 Ok(FoldersGuard.iter().map(|f| (f.URI.clone(), f.Name.clone(), f.Index)).collect())
190 }
191
192 async fn GetWorkspaceFolderInfo(&self, URIToMatch:Url) -> Result<Option<(Url, String, usize)>, CommonError> {
195 let FoldersGuard = self
196 .ApplicationState
197 .Workspace
198 .WorkspaceFolders
199 .lock()
200 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
201 for Folder in FoldersGuard.iter() {
202 if URIToMatch.as_str().starts_with(Folder.URI.as_str()) {
203 return Ok(Some((Folder.URI.clone(), Folder.Name.clone(), Folder.Index)));
204 }
205 }
206
207 Ok(None)
208 }
209
210 async fn GetWorkspaceName(&self) -> Result<Option<String>, CommonError> {
212 self.ApplicationState.GetWorkspaceIdentifier().map(Some)
213 }
214
215 async fn GetWorkspaceConfigurationPath(&self) -> Result<Option<PathBuf>, CommonError> {
217 Ok(self
218 .ApplicationState
219 .Workspace
220 .WorkspaceConfigurationPath
221 .lock()
222 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
223 .clone())
224 }
225
226 async fn IsWorkspaceTrusted(&self) -> Result<bool, CommonError> {
228 Ok(self
229 .ApplicationState
230 .Workspace
231 .IsTrusted
232 .load(std::sync::atomic::Ordering::Relaxed))
233 }
234
235 async fn RequestWorkspaceTrust(&self, _Options:Option<Value>) -> Result<bool, CommonError> {
237 dev_log!(
238 "workspaces",
239 "warn: [WorkspaceProvider] RequestWorkspaceTrust is not implemented; defaulting to trusted."
240 );
241 Ok(true)
242 }
243
244 async fn FindFilesInWorkspace(
269 &self,
270 IncludePatternDTO:Value,
271 ExcludePatternDTO:Option<Value>,
272 MaxResults:Option<usize>,
273 UseIgnoreFiles:bool,
274 FollowSymlinks:bool,
275 ) -> Result<Vec<Url>, CommonError> {
276 dev_log!("workspaces", "[WorkspaceProvider] FindFilesInWorkspace called");
277
278 let IncludePattern = ExtractGlobPattern(&IncludePatternDTO);
279 let IncludePattern = match IncludePattern {
280 Some(P) if !P.is_empty() => P,
281 _ => {
282 dev_log!("workspaces", "[FindFilesInWorkspace] empty include pattern → []");
283 return Ok(Vec::new());
284 },
285 };
286 dev_log!(
297 "workspaces",
298 "[FindFilesInWorkspace] include={} dto_shape={}",
299 IncludePattern,
300 if IncludePatternDTO.is_string() {
301 "string"
302 } else if IncludePatternDTO.is_object() {
303 "object"
304 } else if IncludePatternDTO.is_null() {
305 "null"
306 } else {
307 "other"
308 }
309 );
310 let ExcludePattern = ExcludePatternDTO
311 .as_ref()
312 .and_then(ExtractGlobPattern)
313 .filter(|P| !P.is_empty());
314 let Cap = MaxResults.unwrap_or(10_000).max(1);
315
316 let IncludeMatcher = GlobBuilder::new(&IncludePattern)
317 .literal_separator(false)
318 .build()
319 .map(|G| G.compile_matcher())
320 .map_err(|Error| {
321 CommonError::InvalidArgument { ArgumentName:"IncludePattern".into(), Reason:Error.to_string() }
322 })?;
323 let ExcludeMatcher = match &ExcludePattern {
324 Some(P) => {
325 Some(
326 GlobBuilder::new(P)
327 .literal_separator(false)
328 .build()
329 .map(|G| G.compile_matcher())
330 .map_err(|Error| {
331 CommonError::InvalidArgument {
332 ArgumentName:"ExcludePattern".into(),
333 Reason:Error.to_string(),
334 }
335 })?,
336 )
337 },
338 None => None,
339 };
340
341 let RestrictBase = ExtractRelativeBase(&IncludePatternDTO);
346
347 let Folders:Vec<PathBuf> = self
348 .ApplicationState
349 .Workspace
350 .WorkspaceFolders
351 .lock()
352 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
353 .iter()
354 .filter_map(|Folder| Folder.URI.to_file_path().ok())
355 .collect();
356 if Folders.is_empty() {
357 dev_log!("workspaces", "[FindFilesInWorkspace] no workspace folders → []");
358 return Ok(Vec::new());
359 }
360
361 let WalkRoots:Vec<PathBuf> = match &RestrictBase {
362 Some(Base) => {
363 let BasePath = PathBuf::from(Base);
364 if Folders.iter().any(|F| BasePath.starts_with(F) || F.starts_with(&BasePath)) {
365 vec![BasePath]
366 } else {
367 Folders.clone()
368 }
369 },
370 None => Folders.clone(),
371 };
372
373 let CacheKey = FindFilesCacheKey {
380 Folders:WalkRoots.clone(),
381 Include:IncludePattern.clone(),
382 Exclude:ExcludePattern.clone(),
383 Cap,
384 UseIgnoreFiles,
385 FollowSymlinks,
386 RestrictBase:RestrictBase.clone(),
387 };
388 if let Some(Cached) = FindFilesCacheGet(&CacheKey) {
389 dev_log!("workspaces", "[FindFilesInWorkspace] cache hit → {} match(es)", Cached.len());
390 return Ok(Cached);
391 }
392
393 enum SingleFlightRole {
403 Follower(Arc<Notify>),
404 Leader(Arc<Notify>),
405 }
406 let RoleResolved:SingleFlightRole = {
407 let mut Guard = FindFilesInFlight()
408 .lock()
409 .map_err(|Error| CommonError::StateLockPoisoned { Context:Error.to_string() })?;
410 match Guard.get(&CacheKey) {
411 Some(Existing) => SingleFlightRole::Follower(Existing.clone()),
412 None => {
413 let LeaderNotify = Arc::new(Notify::new());
414 Guard.insert(CacheKey.clone(), LeaderNotify.clone());
415 SingleFlightRole::Leader(LeaderNotify)
416 },
417 }
418 };
419 let LeaderNotify:Arc<Notify> = match RoleResolved {
420 SingleFlightRole::Follower(WaitNotify) => {
421 dev_log!(
422 "workspaces",
423 "[FindFilesInWorkspace] singleflight wait - leader walk in progress for include={}",
424 IncludePattern
425 );
426 WaitNotify.notified().await;
427 return Ok(FindFilesCacheGet(&CacheKey).unwrap_or_default());
428 },
429 SingleFlightRole::Leader(N) => N,
430 };
431
432 struct LeaderGuard {
436 Key:FindFilesCacheKey,
437 Notify:Arc<Notify>,
438 Completed:bool,
439 }
440 impl Drop for LeaderGuard {
441 fn drop(&mut self) {
442 if !self.Completed {
443 if let Ok(mut Guard) = FindFilesInFlight().lock() {
444 Guard.remove(&self.Key);
445 }
446 self.Notify.notify_waiters();
447 }
448 }
449 }
450 let mut Leader = LeaderGuard { Key:CacheKey.clone(), Notify:LeaderNotify, Completed:false };
451
452 let Results:Arc<Mutex<Vec<Url>>> = Arc::new(Mutex::new(Vec::with_capacity(Cap.min(1024))));
453 let Cap = Cap;
454
455 for Root in WalkRoots {
456 if Results.lock().map(|G| G.len() >= Cap).unwrap_or(true) {
457 break;
458 }
459 let RootForRel = Root.clone();
460 let IncludeMatcher = IncludeMatcher.clone();
461 let ExcludeMatcher = ExcludeMatcher.clone();
462 let ResultsArc = Results.clone();
463
464 let mut Builder = WalkBuilder::new(&Root);
465 Builder
466 .standard_filters(UseIgnoreFiles)
467 .git_ignore(UseIgnoreFiles)
468 .git_global(UseIgnoreFiles)
469 .git_exclude(UseIgnoreFiles)
470 .ignore(UseIgnoreFiles)
471 .parents(UseIgnoreFiles)
472 .follow_links(FollowSymlinks)
473 .hidden(true);
474
475 Builder.build_parallel().run(|| {
476 let RootForRel = RootForRel.clone();
477 let IncludeMatcher = IncludeMatcher.clone();
478 let ExcludeMatcher = ExcludeMatcher.clone();
479 let ResultsArc = ResultsArc.clone();
480 Box::new(move |EntryResult| {
481 if ResultsArc.lock().map(|G| G.len() >= Cap).unwrap_or(true) {
482 return ignore::WalkState::Quit;
483 }
484 let Entry = match EntryResult {
485 Ok(E) => E,
486 Err(_) => return ignore::WalkState::Continue,
487 };
488 if !Entry.file_type().map(|T| T.is_file()).unwrap_or(false) {
489 return ignore::WalkState::Continue;
490 }
491 let Path = Entry.path();
492 let Relative = match Path.strip_prefix(&RootForRel) {
493 Ok(R) => R.to_string_lossy().replace('\\', "/"),
494 Err(_) => Path.to_string_lossy().to_string(),
495 };
496 if let Some(Excl) = &ExcludeMatcher {
497 if Excl.is_match(&Relative) {
498 return ignore::WalkState::Continue;
499 }
500 }
501 if !IncludeMatcher.is_match(&Relative) {
502 return ignore::WalkState::Continue;
503 }
504 if let Ok(FileUrl) = Url::from_file_path(Path) {
505 let mut Guard = match ResultsArc.lock() {
506 Ok(G) => G,
507 Err(_) => return ignore::WalkState::Quit,
508 };
509 if Guard.len() < Cap {
510 Guard.push(FileUrl);
511 }
512 if Guard.len() >= Cap {
513 return ignore::WalkState::Quit;
514 }
515 }
516 ignore::WalkState::Continue
517 })
518 });
519 }
520
521 let Final = Arc::try_unwrap(Results)
522 .map_err(|_| {
523 CommonError::Unknown { Description:"FindFilesInWorkspace: result Arc had outstanding refs".into() }
524 })?
525 .into_inner()
526 .map_err(|Error| CommonError::StateLockPoisoned { Context:Error.to_string() })?;
527 dev_log!(
528 "workspaces",
529 "[FindFilesInWorkspace] returned {} match(es) include={} exclude={:?} roots={}",
530 Final.len(),
531 IncludePattern,
532 ExcludePattern,
533 CacheKey.Folders.len()
534 );
535 FindFilesCachePut(CacheKey.clone(), Final.clone());
536
537 {
542 if let Ok(mut Guard) = FindFilesInFlight().lock() {
543 Guard.remove(&CacheKey);
544 }
545 Leader.Notify.notify_waiters();
546 Leader.Completed = true;
547 }
548
549 Ok(Final)
550 }
551
552 async fn OpenFile(&self, path:PathBuf) -> Result<(), CommonError> {
566 use tauri::Emitter;
567 dev_log!("workspaces", "[WorkspaceProvider] OpenFile called for: {:?}", path);
568
569 let UriString = match Url::from_file_path(&path) {
570 Ok(U) => U.to_string(),
571 Err(_) => format!("file://{}", path.to_string_lossy()),
572 };
573
574 self.ApplicationHandle
575 .emit(
576 "sky://editor/openDocument",
577 serde_json::json!({
578 "uri": UriString,
579 "viewColumn": null,
580 }),
581 )
582 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })?;
583
584 Ok(())
585 }
586}
587
588#[async_trait]
589impl WorkspaceEditApplier for MountainEnvironment {
590 async fn ApplyWorkspaceEdit(&self, Edit:WorkspaceEditDTO) -> Result<bool, CommonError> {
606 use tauri::Emitter;
607 dev_log!("workspaces", "[WorkspaceEditApplier] Applying workspace edit");
608
609 let WorkspaceEditDTO { Edits } = Edit;
610 let DocumentMirror = &self.ApplicationState.Feature.Documents;
611 let mut AnyFailure = false;
612
613 for (DocumentURIValue, TextEdits) in Edits {
614 let UriString = DocumentURIValue
615 .as_str()
616 .map(String::from)
617 .or_else(|| DocumentURIValue.get("value").and_then(Value::as_str).map(String::from))
618 .unwrap_or_default();
619 if UriString.is_empty() {
620 dev_log!("workspaces", "warn: [WorkspaceEditApplier] empty URI in edit; skipping");
621 continue;
622 }
623
624 let _ = self.ApplicationHandle.emit(
626 "sky://editor/applyEdits",
627 serde_json::json!({
628 "uri": UriString,
629 "edits": TextEdits,
630 }),
631 );
632
633 let IsOpen = DocumentMirror.Get(&UriString).is_some();
641 if !IsOpen {
642 if let Err(Error) = ApplyEditsToDisk(&UriString, &TextEdits).await {
643 AnyFailure = true;
644 dev_log!(
645 "workspaces",
646 "warn: [WorkspaceEditApplier] on-disk apply failed for {}: {}",
647 UriString,
648 Error
649 );
650 }
651 }
652 }
653
654 Ok(!AnyFailure)
655 }
656}
657
658async fn ApplyEditsToDisk(UriString:&str, TextEdits:&[Value]) -> Result<(), CommonError> {
664 use std::path::Path;
665 let RawPath = if let Some(Stripped) = UriString.strip_prefix("file://") {
666 percent_decode(Stripped)
667 } else if UriString.starts_with('/') {
668 UriString.to_string()
669 } else {
670 return Err(CommonError::InvalidArgument {
671 ArgumentName:"uri".into(),
672 Reason:format!("ApplyWorkspaceEdit: unsupported scheme in {}", UriString),
673 });
674 };
675 let Path = Path::new(&RawPath);
676
677 let Original = tokio::fs::read_to_string(Path)
678 .await
679 .map_err(|Error| CommonError::FromStandardIOError(Error, Path.to_path_buf(), "ApplyWorkspaceEdit.Read"))?;
680
681 let LineOffsets = ComputeLineOffsets(&Original);
686 let mut WithOffsets:Vec<(usize, usize, String)> = Vec::with_capacity(TextEdits.len());
687 for Edit in TextEdits {
688 let StartLine = Edit.pointer("/range/start/line").and_then(Value::as_u64).unwrap_or(0) as usize;
689 let StartChar = Edit.pointer("/range/start/character").and_then(Value::as_u64).unwrap_or(0) as usize;
690 let EndLine = Edit
691 .pointer("/range/end/line")
692 .and_then(Value::as_u64)
693 .unwrap_or(StartLine as u64) as usize;
694 let EndChar = Edit
695 .pointer("/range/end/character")
696 .and_then(Value::as_u64)
697 .unwrap_or(StartChar as u64) as usize;
698 let NewText = Edit.get("newText").and_then(Value::as_str).unwrap_or("").to_string();
699 let StartOffset = LinePosToOffset(&LineOffsets, &Original, StartLine, StartChar);
700 let EndOffset = LinePosToOffset(&LineOffsets, &Original, EndLine, EndChar);
701 WithOffsets.push((StartOffset, EndOffset, NewText));
702 }
703
704 WithOffsets.sort_by(|A, B| B.0.cmp(&A.0));
705
706 let mut Mutated = Original;
707 for (Start, End, NewText) in WithOffsets {
708 let SafeStart = Start.min(Mutated.len());
709 let SafeEnd = End.max(SafeStart).min(Mutated.len());
710 Mutated.replace_range(SafeStart..SafeEnd, &NewText);
711 }
712
713 let TempPath = Path.with_extension(format!(
716 "{}.land-tmp-{}",
717 Path.extension().and_then(|E| E.to_str()).unwrap_or("tmp"),
718 std::process::id()
719 ));
720 tokio::fs::write(&TempPath, Mutated.as_bytes())
721 .await
722 .map_err(|Error| CommonError::FromStandardIOError(Error, TempPath.clone(), "ApplyWorkspaceEdit.Write"))?;
723 tokio::fs::rename(&TempPath, Path)
724 .await
725 .map_err(|Error| CommonError::FromStandardIOError(Error, Path.to_path_buf(), "ApplyWorkspaceEdit.Rename"))?;
726 Ok(())
727}
728
729fn ComputeLineOffsets(Source:&str) -> Vec<usize> {
731 let mut Offsets = Vec::with_capacity(Source.len() / 40 + 1);
732 Offsets.push(0);
733 for (Index, Byte) in Source.bytes().enumerate() {
734 if Byte == b'\n' {
735 Offsets.push(Index + 1);
736 }
737 }
738 Offsets
739}
740
741fn LinePosToOffset(LineOffsets:&[usize], Source:&str, Line:usize, Character:usize) -> usize {
746 if Line >= LineOffsets.len() {
747 return Source.len();
748 }
749 let LineStart = LineOffsets[Line];
750 let LineEnd = if Line + 1 < LineOffsets.len() {
751 LineOffsets[Line + 1].saturating_sub(1)
752 } else {
753 Source.len()
754 };
755 let LineText = &Source[LineStart..LineEnd.min(Source.len())];
756 let mut Utf16Count:usize = 0;
757 for (ByteOffset, Char) in LineText.char_indices() {
758 if Utf16Count >= Character {
759 return LineStart + ByteOffset;
760 }
761 Utf16Count += Char.len_utf16();
762 }
763 LineStart + LineText.len()
764}
765
766fn percent_decode(Input:&str) -> String {
770 let mut Out = String::with_capacity(Input.len());
771 let mut Bytes = Input.as_bytes().iter().peekable();
772 while let Some(&Byte) = Bytes.next() {
773 if Byte == b'%' {
774 let H = Bytes.next().copied();
775 let L = Bytes.next().copied();
776 if let (Some(H), Some(L)) = (H, L) {
777 if let (Some(Hi), Some(Lo)) = (HexDigit(H), HexDigit(L)) {
778 Out.push((Hi * 16 + Lo) as char);
779 continue;
780 }
781 Out.push('%');
782 Out.push(H as char);
783 Out.push(L as char);
784 continue;
785 }
786 Out.push('%');
787 } else {
788 Out.push(Byte as char);
789 }
790 }
791 Out
792}
793
794fn HexDigit(Byte:u8) -> Option<u8> {
795 match Byte {
796 b'0'..=b'9' => Some(Byte - b'0'),
797 b'a'..=b'f' => Some(Byte - b'a' + 10),
798 b'A'..=b'F' => Some(Byte - b'A' + 10),
799 _ => None,
800 }
801}
802
803fn ExtractGlobPattern(Pattern:&Value) -> Option<String> {
809 if let Some(S) = Pattern.as_str() {
810 return Some(S.to_string());
811 }
812 if let Some(Obj) = Pattern.as_object() {
813 if let Some(P) = Obj.get("pattern").and_then(Value::as_str) {
814 return Some(P.to_string());
815 }
816 if let Some(P) = Obj.get("value").and_then(Value::as_str) {
817 return Some(P.to_string());
818 }
819 if let Some(P) = Obj.get("Pattern").and_then(Value::as_str) {
820 return Some(P.to_string());
821 }
822 }
823 None
824}
825
826fn ExtractRelativeBase(Pattern:&Value) -> Option<String> {
831 let Obj = Pattern.as_object()?;
832 if let Some(B) = Obj.get("base").and_then(Value::as_str) {
833 return Some(B.to_string());
834 }
835 if let Some(B) = Obj.get("baseUri") {
836 if let Some(S) = B.as_str() {
837 if let Some(Stripped) = S.strip_prefix("file://") {
838 return Some(Stripped.to_string());
839 }
840 return Some(S.to_string());
841 }
842 if let Some(P) = B.as_object().and_then(|O| O.get("path")).and_then(Value::as_str) {
843 return Some(P.to_string());
844 }
845 if let Some(P) = B.as_object().and_then(|O| O.get("fsPath")).and_then(Value::as_str) {
846 return Some(P.to_string());
847 }
848 }
849 None
850}