Skip to main content

Mountain/IPC/WindServiceHandlers/FileSystem/Native/
FileWriteNative.rs

1//! Wire method `file:write` / `file:writeFile`. Accepts either a plain
2//! string body or a `{ buffer: number[] | base64 }` VSBuffer. Parent
3//! directory is created best-effort. After a successful write, fires
4//! `$acceptModelSaved` to Cocoon so `onDidSaveTextDocument` reaches
5//! extensions (T1.4 save notification).
6
7use serde_json::{Value, json};
8
9use crate::{IPC::WindServiceHandlers::Utilities::PathExtraction::Fn as extract_path_from_arg, dev_log};
10
11pub async fn Fn(Arguments:Vec<Value>) -> Result<Value, String> {
12	let ResourceArg = Arguments.get(0).ok_or("Missing file path")?;
13
14	// Capture the `external` field (full file:// URI) from the URI object
15	// for the $acceptModelSaved notification before we consume the path.
16	let ExternalUri = ResourceArg
17		.as_object()
18		.and_then(|O| O.get("external"))
19		.and_then(|V| V.as_str())
20		.map(|S| S.to_string());
21
22	let Path = extract_path_from_arg(ResourceArg)?;
23
24	let Content = Arguments.get(1).ok_or("Missing file content")?;
25
26	let Bytes = if let Some(S) = Content.as_str() {
27		S.as_bytes().to_vec()
28	} else if let Some(Obj) = Content.as_object() {
29		if let Some(Buf) = Obj.get("buffer") {
30			if let Some(Arr) = Buf.as_array() {
31				Arr.iter().filter_map(|V| V.as_u64().map(|N| N as u8)).collect()
32			} else if let Some(S) = Buf.as_str() {
33				// base64-encoded buffer (sent by some VS Code serialisation paths).
34				// Decode it; fall back to raw UTF-8 bytes if not valid base64.
35				use base64::Engine as _;
36
37				base64::engine::general_purpose::STANDARD
38					.decode(S)
39					.unwrap_or_else(|_| S.as_bytes().to_vec())
40			} else {
41				return Err("Unsupported buffer format".to_string());
42			}
43		} else {
44			serde_json::to_string(Content).unwrap_or_default().into_bytes()
45		}
46	} else {
47		return Err("File content must be a string or VSBuffer".to_string());
48	};
49
50	if let Some(Parent) = std::path::Path::new(&Path).parent() {
51		tokio::fs::create_dir_all(Parent).await.ok();
52	}
53
54	let Start = std::time::Instant::now();
55
56	tokio::fs::write(&Path, &Bytes)
57		.await
58		.map_err(|E| format!("Failed to write file: {} (path: {})", E, Path))?;
59
60	let ElapsedMs = Start.elapsed().as_millis();
61
62	dev_log!("vfs", "file:write ok path={} bytes={} ms={}", Path, Bytes.len(), ElapsedMs);
63
64	// T1.4 - notify Cocoon that the model on disk now matches the editor
65	// buffer so `onDidSaveTextDocument` fires for subscribed extensions.
66	// Build a file:// URI from `external` (preferred) or the path string.
67	let FileUri = ExternalUri.unwrap_or_else(|| format!("file://{}", Path));
68
69	tokio::spawn(async move {
70		if let Err(Error) = ::Vine::Client::SendNotification::Fn(
71			"cocoon-main".to_string(),
72			"$acceptModelSaved".to_string(),
73			json!({ "uri": FileUri }),
74		)
75		.await
76		{
77			let ErrStr = format!("{:?}", Error);
78
79			if ErrStr.contains("ClientNotConnected") {
80				dev_log!(
81					"vfs-verbose",
82					"[FileWriteNative] $acceptModelSaved skipped (Cocoon not yet connected)"
83				);
84			} else {
85				dev_log!("vfs", "warn: [FileWriteNative] $acceptModelSaved notify failed: {:?}", Error);
86			}
87		}
88	});
89
90	// Return mtime/size so VS Code's DiskFileSystemProvider can update its
91	// FileStatWithMetadata cache - prevents a spurious "file changed on disk"
92	// conflict caused by the pre-write etag being stale after the write.
93	match tokio::fs::metadata(&Path).await {
94		Ok(Meta) => Ok(crate::IPC::WindServiceHandlers::Utilities::MetadataEncoding::Fn(&Meta)),
95
96		// Write succeeded but post-write stat failed (e.g. NFS race, EPERM).
97		// Returning null here causes DiskFileSystemProvider to receive null.mtime
98		// → TypeError → document flips to conflict state even though the write
99		// was fine. Propagate the error so the caller retries the stat instead.
100		Err(E) => Err(format!("file:write post-stat failed for {}: {}", Path, E)),
101	}
102}