dir.rs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. use crate::fs::feature::git::GitCache;
  2. use crate::fs::fields::GitStatus;
  3. use std::io;
  4. use std::fs;
  5. use std::path::{Path, PathBuf};
  6. use std::slice::Iter as SliceIter;
  7. use log::*;
  8. use crate::fs::File;
  9. /// A **Dir** provides a cached list of the file paths in a directory that’s
  10. /// being listed.
  11. ///
  12. /// This object gets passed to the Files themselves, in order for them to
  13. /// check the existence of surrounding files, then highlight themselves
  14. /// accordingly. (See `File#get_source_files`)
  15. pub struct Dir {
  16. /// A vector of the files that have been read from this directory.
  17. contents: Vec<PathBuf>,
  18. /// The path that was read.
  19. pub path: PathBuf,
  20. }
  21. impl Dir {
  22. /// Create a new Dir object filled with all the files in the directory
  23. /// pointed to by the given path. Fails if the directory can’t be read, or
  24. /// isn’t actually a directory, or if there’s an IO error that occurs at
  25. /// any point.
  26. ///
  27. /// The `read_dir` iterator doesn’t actually yield the `.` and `..`
  28. /// entries, so if the user wants to see them, we’ll have to add them
  29. /// ourselves after the files have been read.
  30. pub fn read_dir(path: PathBuf) -> io::Result<Self> {
  31. info!("Reading directory {:?}", &path);
  32. let contents = fs::read_dir(&path)?
  33. .map(|result| result.map(|entry| entry.path()))
  34. .collect::<Result<_, _>>()?;
  35. Ok(Self { contents, path })
  36. }
  37. /// Produce an iterator of IO results of trying to read all the files in
  38. /// this directory.
  39. pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, git: Option<&'ig GitCache>, git_ignoring: bool, deref_links: bool) -> Files<'dir, 'ig> {
  40. Files {
  41. inner: self.contents.iter(),
  42. dir: self,
  43. dotfiles: dots.shows_dotfiles(),
  44. dots: dots.dots(),
  45. git,
  46. git_ignoring,
  47. deref_links,
  48. }
  49. }
  50. /// Whether this directory contains a file with the given path.
  51. pub fn contains(&self, path: &Path) -> bool {
  52. self.contents.iter().any(|p| p.as_path() == path)
  53. }
  54. /// Append a path onto the path specified by this directory.
  55. pub fn join(&self, child: &Path) -> PathBuf {
  56. self.path.join(child)
  57. }
  58. }
  59. /// Iterator over reading the contents of a directory as `File` objects.
  60. pub struct Files<'dir, 'ig> {
  61. /// The internal iterator over the paths that have been read already.
  62. inner: SliceIter<'dir, PathBuf>,
  63. /// The directory that begat those paths.
  64. dir: &'dir Dir,
  65. /// Whether to include dotfiles in the list.
  66. dotfiles: bool,
  67. /// Whether the `.` or `..` directories should be produced first, before
  68. /// any files have been listed.
  69. dots: DotsNext,
  70. git: Option<&'ig GitCache>,
  71. git_ignoring: bool,
  72. /// Whether symbolic links should be dereferenced when querying information.
  73. deref_links: bool,
  74. }
  75. impl<'dir, 'ig> Files<'dir, 'ig> {
  76. fn parent(&self) -> PathBuf {
  77. // We can’t use `Path#parent` here because all it does is remove the
  78. // last path component, which is no good for us if the path is
  79. // relative. For example, while the parent of `/testcases/files` is
  80. // `/testcases`, the parent of `.` is an empty path. Adding `..` on
  81. // the end is the only way to get to the *actual* parent directory.
  82. self.dir.path.join("..")
  83. }
  84. /// Go through the directory until we encounter a file we can list (which
  85. /// varies depending on the dotfile visibility flag)
  86. fn next_visible_file(&mut self) -> Option<Result<File<'dir>, (PathBuf, io::Error)>> {
  87. loop {
  88. if let Some(path) = self.inner.next() {
  89. let filename = File::filename(path);
  90. if ! self.dotfiles && filename.starts_with('.') {
  91. continue;
  92. }
  93. // Also hide _prefix files on Windows because it's used by old applications
  94. // as an alternative to dot-prefix files.
  95. #[cfg(windows)]
  96. if ! self.dotfiles && filename.starts_with('_') {
  97. continue;
  98. }
  99. if self.git_ignoring {
  100. let git_status = self.git.map(|g| g.get(path, false)).unwrap_or_default();
  101. if git_status.unstaged == GitStatus::Ignored {
  102. continue;
  103. }
  104. }
  105. let file = File::from_args(
  106. path.clone(),
  107. self.dir,
  108. filename,
  109. self.deref_links
  110. ).map_err(|e| (path.clone(), e));
  111. // Windows has its own concept of hidden files, with a flag
  112. // stored in the file's attributes
  113. #[cfg(windows)]
  114. if !self.dotfiles && file.as_ref().is_ok_and(|f| f.attributes().hidden) {
  115. continue;
  116. }
  117. return Some(file);
  118. }
  119. return None;
  120. }
  121. }
  122. }
  123. /// The dot directories that need to be listed before actual files, if any.
  124. /// If these aren’t being printed, then `FilesNext` is used to skip them.
  125. enum DotsNext {
  126. /// List the `.` directory next.
  127. Dot,
  128. /// List the `..` directory next.
  129. DotDot,
  130. /// Forget about the dot directories and just list files.
  131. Files,
  132. }
  133. impl<'dir, 'ig> Iterator for Files<'dir, 'ig> {
  134. type Item = Result<File<'dir>, (PathBuf, io::Error)>;
  135. fn next(&mut self) -> Option<Self::Item> {
  136. match self.dots {
  137. DotsNext::Dot => {
  138. self.dots = DotsNext::DotDot;
  139. Some(File::new_aa_current(self.dir)
  140. .map_err(|e| (Path::new(".").to_path_buf(), e)))
  141. }
  142. DotsNext::DotDot => {
  143. self.dots = DotsNext::Files;
  144. Some(File::new_aa_parent(self.parent(), self.dir)
  145. .map_err(|e| (self.parent(), e)))
  146. }
  147. DotsNext::Files => {
  148. self.next_visible_file()
  149. }
  150. }
  151. }
  152. }
  153. /// Usually files in Unix use a leading dot to be hidden or visible, but two
  154. /// entries in particular are “extra-hidden”: `.` and `..`, which only become
  155. /// visible after an extra `-a` option.
  156. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  157. pub enum DotFilter {
  158. /// Shows files, dotfiles, and `.` and `..`.
  159. DotfilesAndDots,
  160. /// Show files and dotfiles, but hide `.` and `..`.
  161. Dotfiles,
  162. /// Just show files, hiding anything beginning with a dot.
  163. JustFiles,
  164. }
  165. impl Default for DotFilter {
  166. fn default() -> Self {
  167. Self::JustFiles
  168. }
  169. }
  170. impl DotFilter {
  171. /// Whether this filter should show dotfiles in a listing.
  172. fn shows_dotfiles(self) -> bool {
  173. match self {
  174. Self::JustFiles => false,
  175. Self::Dotfiles => true,
  176. Self::DotfilesAndDots => true,
  177. }
  178. }
  179. /// Whether this filter should add dot directories to a listing.
  180. fn dots(self) -> DotsNext {
  181. match self {
  182. Self::JustFiles => DotsNext::Files,
  183. Self::Dotfiles => DotsNext::Files,
  184. Self::DotfilesAndDots => DotsNext::Dot,
  185. }
  186. }
  187. }