1use std::collections::HashMap;
18use std::ffi::OsString;
19use std::fmt;
20use std::fs::{self, File};
21use std::io::{self, Read, Write};
22use std::path::{Path, PathBuf};
23use std::str;
24use std::time::SystemTime;
25
26use xi_rope::Rope;
27use xi_rpc::RemoteError;
28
29use crate::tabs::BufferId;
30
31#[cfg(feature = "notify")]
32use crate::tabs::OPEN_FILE_EVENT_TOKEN;
33#[cfg(feature = "notify")]
34use crate::watcher::FileWatcher;
35#[cfg(target_family = "unix")]
36use std::{fs::Permissions, os::unix::fs::PermissionsExt};
37
38const UTF8_BOM: &str = "\u{feff}";
39
40pub struct FileManager {
42 open_files: HashMap<PathBuf, BufferId>,
43 file_info: HashMap<BufferId, FileInfo>,
44 #[cfg(feature = "notify")]
46 watcher: FileWatcher,
47}
48
49#[derive(Debug)]
50pub struct FileInfo {
51 pub encoding: CharacterEncoding,
52 pub path: PathBuf,
53 pub mod_time: Option<SystemTime>,
54 pub has_changed: bool,
55 #[cfg(target_family = "unix")]
56 pub permissions: Option<u32>,
57}
58
59pub enum FileError {
60 Io(io::Error, PathBuf),
61 UnknownEncoding(PathBuf),
62 HasChanged(PathBuf),
63}
64
65#[derive(Debug, Clone, Copy)]
66pub enum CharacterEncoding {
67 Utf8,
68 Utf8WithBom,
69}
70
71impl FileManager {
72 #[cfg(feature = "notify")]
73 pub fn new(watcher: FileWatcher) -> Self {
74 FileManager { open_files: HashMap::new(), file_info: HashMap::new(), watcher }
75 }
76
77 #[cfg(not(feature = "notify"))]
78 pub fn new() -> Self {
79 FileManager { open_files: HashMap::new(), file_info: HashMap::new() }
80 }
81
82 #[cfg(feature = "notify")]
83 pub fn watcher(&mut self) -> &mut FileWatcher {
84 &mut self.watcher
85 }
86
87 pub fn get_info(&self, id: BufferId) -> Option<&FileInfo> {
88 self.file_info.get(&id)
89 }
90
91 pub fn get_editor(&self, path: &Path) -> Option<BufferId> {
92 self.open_files.get(path).cloned()
93 }
94
95 pub fn check_file(&mut self, path: &Path, id: BufferId) -> bool {
98 if let Some(info) = self.file_info.get_mut(&id) {
99 let mod_t = get_mod_time(path);
100 if mod_t != info.mod_time {
101 info.has_changed = true
102 }
103 return info.has_changed;
104 }
105 false
106 }
107
108 pub fn open(&mut self, path: &Path, id: BufferId) -> Result<Rope, FileError> {
109 if !path.exists() {
110 let _ = File::create(path).map_err(|e| FileError::Io(e, path.to_owned()))?;
111 }
112
113 let (rope, info) = try_load_file(path)?;
114
115 self.open_files.insert(path.to_owned(), id);
116 if self.file_info.insert(id, info).is_none() {
117 #[cfg(feature = "notify")]
118 self.watcher.watch(path, false, OPEN_FILE_EVENT_TOKEN);
119 }
120 Ok(rope)
121 }
122
123 pub fn close(&mut self, id: BufferId) {
124 if let Some(info) = self.file_info.remove(&id) {
125 self.open_files.remove(&info.path);
126 #[cfg(feature = "notify")]
127 self.watcher.unwatch(&info.path, OPEN_FILE_EVENT_TOKEN);
128 }
129 }
130
131 pub fn save(&mut self, path: &Path, text: &Rope, id: BufferId) -> Result<(), FileError> {
132 let is_existing = self.file_info.contains_key(&id);
133 if is_existing {
134 self.save_existing(path, text, id)
135 } else {
136 self.save_new(path, text, id)
137 }
138 }
139
140 fn save_new(&mut self, path: &Path, text: &Rope, id: BufferId) -> Result<(), FileError> {
141 try_save(path, text, CharacterEncoding::Utf8, self.get_info(id))
142 .map_err(|e| FileError::Io(e, path.to_owned()))?;
143 let info = FileInfo {
144 encoding: CharacterEncoding::Utf8,
145 path: path.to_owned(),
146 mod_time: get_mod_time(path),
147 has_changed: false,
148 #[cfg(target_family = "unix")]
149 permissions: get_permissions(path),
150 };
151 self.open_files.insert(path.to_owned(), id);
152 self.file_info.insert(id, info);
153 #[cfg(feature = "notify")]
154 self.watcher.watch(path, false, OPEN_FILE_EVENT_TOKEN);
155 Ok(())
156 }
157
158 fn save_existing(&mut self, path: &Path, text: &Rope, id: BufferId) -> Result<(), FileError> {
159 let prev_path = self.file_info[&id].path.clone();
160 if prev_path != path {
161 self.save_new(path, text, id)?;
162 self.open_files.remove(&prev_path);
163 #[cfg(feature = "notify")]
164 self.watcher.unwatch(&prev_path, OPEN_FILE_EVENT_TOKEN);
165 } else if self.file_info[&id].has_changed {
166 return Err(FileError::HasChanged(path.to_owned()));
167 } else {
168 let encoding = self.file_info[&id].encoding;
169 try_save(path, text, encoding, self.get_info(id))
170 .map_err(|e| FileError::Io(e, path.to_owned()))?;
171 self.file_info.get_mut(&id).unwrap().mod_time = get_mod_time(path);
172 }
173 Ok(())
174 }
175}
176
177fn try_load_file<P>(path: P) -> Result<(Rope, FileInfo), FileError>
178where
179 P: AsRef<Path>,
180{
181 let mut f =
184 File::open(path.as_ref()).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?;
185 let mut bytes = Vec::new();
186 f.read_to_end(&mut bytes).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?;
187
188 let encoding = CharacterEncoding::guess(&bytes);
189 let rope = try_decode(bytes, encoding, path.as_ref())?;
190 let info = FileInfo {
191 encoding,
192 mod_time: get_mod_time(&path),
193 #[cfg(target_family = "unix")]
194 permissions: get_permissions(&path),
195 path: path.as_ref().to_owned(),
196 has_changed: false,
197 };
198 Ok((rope, info))
199}
200
201#[allow(unused)]
202fn try_save(
203 path: &Path,
204 text: &Rope,
205 encoding: CharacterEncoding,
206 file_info: Option<&FileInfo>,
207) -> io::Result<()> {
208 let tmp_extension = path.extension().map_or_else(
209 || OsString::from("swp"),
210 |ext| {
211 let mut ext = ext.to_os_string();
212 ext.push(".swp");
213 ext
214 },
215 );
216 let tmp_path = &path.with_extension(tmp_extension);
217
218 let mut f = File::create(tmp_path)?;
219 match encoding {
220 CharacterEncoding::Utf8WithBom => f.write_all(UTF8_BOM.as_bytes())?,
221 CharacterEncoding::Utf8 => (),
222 }
223
224 for chunk in text.iter_chunks(..text.len()) {
225 f.write_all(chunk.as_bytes())?;
226 }
227
228 fs::rename(tmp_path, path)?;
229
230 #[cfg(target_family = "unix")]
231 {
232 if let Some(info) = file_info {
233 fs::set_permissions(path, Permissions::from_mode(info.permissions.unwrap_or(0o644)))?;
234 }
235 }
236
237 Ok(())
238}
239
240fn try_decode(bytes: Vec<u8>, encoding: CharacterEncoding, path: &Path) -> Result<Rope, FileError> {
241 match encoding {
242 CharacterEncoding::Utf8 => Ok(Rope::from(
243 str::from_utf8(&bytes).map_err(|_e| FileError::UnknownEncoding(path.to_owned()))?,
244 )),
245 CharacterEncoding::Utf8WithBom => {
246 let s = String::from_utf8(bytes)
247 .map_err(|_e| FileError::UnknownEncoding(path.to_owned()))?;
248 Ok(Rope::from(&s[UTF8_BOM.len()..]))
249 }
250 }
251}
252
253impl CharacterEncoding {
254 fn guess(s: &[u8]) -> Self {
255 if s.starts_with(UTF8_BOM.as_bytes()) {
256 CharacterEncoding::Utf8WithBom
257 } else {
258 CharacterEncoding::Utf8
259 }
260 }
261}
262
263fn get_mod_time<P: AsRef<Path>>(path: P) -> Option<SystemTime> {
266 File::open(path).and_then(|f| f.metadata()).and_then(|meta| meta.modified()).ok()
267}
268
269#[cfg(target_family = "unix")]
272fn get_permissions<P: AsRef<Path>>(path: P) -> Option<u32> {
273 File::open(path).and_then(|f| f.metadata()).map(|meta| meta.permissions().mode()).ok()
274}
275
276impl From<FileError> for RemoteError {
277 fn from(src: FileError) -> RemoteError {
278 let code = src.error_code();
281 let message = src.to_string();
282 RemoteError::custom(code, message, None)
283 }
284}
285
286impl FileError {
287 fn error_code(&self) -> i64 {
288 match self {
289 FileError::Io(_, _) => 5,
290 FileError::UnknownEncoding(_) => 6,
291 FileError::HasChanged(_) => 7,
292 }
293 }
294}
295
296impl fmt::Display for FileError {
297 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
298 match self {
299 FileError::Io(ref e, ref p) => write!(f, "{}. File path: {:?}", e, p),
300 FileError::UnknownEncoding(ref p) => write!(f, "Error decoding file: {:?}", p),
301 FileError::HasChanged(ref p) => write!(
302 f,
303 "File has changed on disk. \
304 Please save elsewhere and reload the file. File path: {:?}",
305 p
306 ),
307 }
308 }
309}