Skip to main content

Mountain/Environment/DocumentProvider/
OpenDocument.rs

1//! Document opening and content resolution logic.
2//!
3//! Implements `OpenDocument` for `MountainEnvironment`. Resolution order:
4//!
5//! 1. **Already open** - if the URI is already in `OpenDocuments`, re-emits
6//!    `sky://documents/open` so the Sky workbench focuses the existing tab and
7//!    returns immediately without a disk read.
8//! 2. **Caller-supplied content** - if the `content` argument is `Some`, it is
9//!    used verbatim (used by untitled / virtual documents).
10//! 3. **`file://` URI** - content is read from disk via `ApplicationRunTime`.
11//! 4. **Custom scheme** - content is fetched from the Cocoon sidecar via
12//!    `$provideTextDocumentContent` RPC (10 s timeout). This covers schemes
13//!    like `git:`, `output:`, `vscode-notebook-cell:`, etc.
14//!
15//! On success, a new `DocumentStateDTO` is inserted into `OpenDocuments`,
16//! `sky://documents/open` is emitted to the Sky workbench, and
17//! `$acceptModelAdded` is sent to Cocoon.
18
19use std::sync::Arc;
20
21use CommonLibrary::{
22	Effect::ApplicationRunTime::ApplicationRunTime as _,
23	Environment::Requires::Requires,
24	Error::CommonError::CommonError,
25	FileSystem::ReadFile::ReadFile,
26	IPC::{IPCProvider::IPCProvider, SkyEvent::SkyEvent},
27};
28use serde_json::{Value, json};
29// `Emitter` was previously imported here for the now-replaced
30// direct `.emit(...)` calls; emit is now done via `LogSkyEmit`
31// which carries the trait import internally. `Manager` remains
32// because `.state::<…>()` below depends on it.
33use tauri::Manager;
34use url::Url;
35
36use crate::{
37	ApplicationState::DTO::DocumentStateDTO::DocumentStateDTO,
38	Environment::Utility,
39	IPC::SkyEmit::LogSkyEmit,
40	RunTime::ApplicationRunTime::ApplicationRunTime,
41	dev_log,
42};
43
44/// Opens a document. If the URI scheme is not native (`file`), it attempts to
45/// resolve the content from a registered sidecar provider
46/// (`TextDocumentContentProvider`).
47pub(super) async fn open_document(
48	environment:&crate::Environment::MountainEnvironment::MountainEnvironment,
49
50	uri_components_dto:Value,
51
52	language_identifier:Option<String>,
53
54	content:Option<String>,
55) -> Result<Url, CommonError> {
56	let uri = Utility::UriParsing::GetURLFromURIComponentsDTO(&uri_components_dto)?;
57
58	dev_log!("model", "[DocumentProvider] Opening document: {}", uri);
59
60	// First, check if the document is already open.
61	if let Some(existing_document) = environment
62		.ApplicationState
63		.Feature
64		.Documents
65		.OpenDocuments
66		.lock()
67		.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
68		.get(uri.as_str())
69	{
70		dev_log!("model", "[DocumentProvider] Document {} is already open.", uri);
71
72		match existing_document.ToDTO() {
73			Ok(dto) => {
74				if let Err(error) = LogSkyEmit(&environment.ApplicationHandle, SkyEvent::DocumentsOpen.AsStr(), dto) {
75					dev_log!(
76						"model",
77						"error: [DocumentProvider] Failed to emit document open event: {}",
78						error
79					);
80				}
81			},
82
83			Err(error) => {
84				dev_log!(
85					"model",
86					"error: [DocumentProvider] Failed to serialize existing document DTO: {}",
87					error
88				);
89			},
90		}
91
92		return Ok(existing_document.URI.clone());
93	}
94
95	// Resolve the content based on the URI scheme.
96	let file_content = if let Some(c) = content {
97		c
98	} else if uri.scheme() == "file" {
99		let file_path = uri.to_file_path().map_err(|_| {
100			CommonError::InvalidArgument {
101				ArgumentName:"URI".into(),
102				Reason:"Cannot convert non-file URI to path".into(),
103			}
104		})?;
105
106		let runtime = environment.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
107
108		let file_content_bytes = runtime.Run(ReadFile(file_path.clone())).await?;
109
110		String::from_utf8(file_content_bytes)
111			.map_err(|error| CommonError::FileSystemIO { Path:file_path, Description:error.to_string() })?
112	} else {
113		// Custom scheme: attempt to resolve from a sidecar provider.
114		dev_log!(
115			"model",
116			"[DocumentProvider] Non-native scheme '{}'. Attempting to resolve from sidecar.",
117			uri.scheme()
118		);
119
120		let ipc_provider:Arc<dyn IPCProvider> = environment.Require();
121
122		let rpc_result = ipc_provider
123			.SendRequestToSideCar(
124				// In a multi-host world, we'd look this up
125				"cocoon-main".to_string(),
126				"$provideTextDocumentContent".to_string(),
127				json!([uri_components_dto]),
128				10000,
129			)
130			.await?;
131
132		rpc_result.as_str().map(String::from).ok_or_else(|| {
133			CommonError::IPCError {
134				Description:format!("Failed to get valid string content for custom URI scheme '{}'", uri.scheme()),
135			}
136		})?
137	};
138
139	// The rest of the flow is the same for all schemes.
140	let new_document = DocumentStateDTO::Create(uri.clone(), language_identifier, file_content)?;
141
142	let dto_for_notification = new_document.ToDTO()?;
143
144	environment
145		.ApplicationState
146		.Feature
147		.Documents
148		.OpenDocuments
149		.lock()
150		.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
151		.insert(uri.to_string(), new_document);
152
153	if let Err(error) = LogSkyEmit(
154		&environment.ApplicationHandle,
155		SkyEvent::DocumentsOpen.AsStr(),
156		dto_for_notification.clone(),
157	) {
158		dev_log!(
159			"model",
160			"error: [DocumentProvider] Failed to emit document open event: {}",
161			error
162		);
163	}
164
165	crate::Environment::DocumentProvider::Notifications::notify_model_added(environment, &dto_for_notification).await;
166
167	Ok(uri)
168}