1use std::path::Path;
2
3use anyhow::Context;
4use gpui::AppContext;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use settings::{Settings, SettingsSources};
8use util::paths::PathMatcher;
9
10#[derive(Clone, PartialEq, Eq)]
11pub struct WorktreeSettings {
12 pub file_scan_inclusions: PathMatcher,
13 pub file_scan_exclusions: PathMatcher,
14 pub private_files: PathMatcher,
15}
16
17impl WorktreeSettings {
18 pub fn is_path_private(&self, path: &Path) -> bool {
19 path.ancestors()
20 .any(|ancestor| self.private_files.is_match(ancestor))
21 }
22
23 pub fn is_path_excluded(&self, path: &Path) -> bool {
24 path.ancestors()
25 .any(|ancestor| self.file_scan_exclusions.is_match(&ancestor))
26 }
27
28 pub fn is_path_always_included(&self, path: &Path) -> bool {
29 path.ancestors()
30 .any(|ancestor| self.file_scan_inclusions.is_match(&ancestor))
31 }
32}
33
34#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
35pub struct WorktreeSettingsContent {
36 /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides
37 /// `file_scan_inclusions`.
38 ///
39 /// Default: [
40 /// "**/.git",
41 /// "**/.svn",
42 /// "**/.hg",
43 /// "**/CVS",
44 /// "**/.DS_Store",
45 /// "**/Thumbs.db",
46 /// "**/.classpath",
47 /// "**/.settings"
48 /// ]
49 #[serde(default)]
50 pub file_scan_exclusions: Option<Vec<String>>,
51
52 /// Always include files that match these globs when scanning for files, even if they're
53 /// ignored by git. This setting is overridden by `file_scan_exclusions`.
54 /// Default: [
55 /// ".env*",
56 /// "docker-compose.*.yml",
57 /// ]
58 #[serde(default)]
59 pub file_scan_inclusions: Option<Vec<String>>,
60
61 /// Treat the files matching these globs as `.env` files.
62 /// Default: [ "**/.env*" ]
63 pub private_files: Option<Vec<String>>,
64}
65
66impl Settings for WorktreeSettings {
67 const KEY: Option<&'static str> = None;
68
69 type FileContent = WorktreeSettingsContent;
70
71 fn load(
72 sources: SettingsSources<Self::FileContent>,
73 _: &mut AppContext,
74 ) -> anyhow::Result<Self> {
75 let result: WorktreeSettingsContent = sources.json_merge()?;
76 let mut file_scan_exclusions = result.file_scan_exclusions.unwrap_or_default();
77 let mut private_files = result.private_files.unwrap_or_default();
78 let mut parsed_file_scan_inclusions: Vec<String> = result
79 .file_scan_inclusions
80 .unwrap_or_default()
81 .iter()
82 .flat_map(|glob| {
83 Path::new(glob)
84 .ancestors()
85 .map(|a| a.to_string_lossy().into())
86 })
87 .filter(|p| p != "")
88 .collect();
89 file_scan_exclusions.sort();
90 private_files.sort();
91 parsed_file_scan_inclusions.sort();
92 Ok(Self {
93 file_scan_exclusions: path_matchers(&file_scan_exclusions, "file_scan_exclusions")?,
94 private_files: path_matchers(&private_files, "private_files")?,
95 file_scan_inclusions: path_matchers(
96 &parsed_file_scan_inclusions,
97 "file_scan_inclusions",
98 )?,
99 })
100 }
101}
102
103fn path_matchers(values: &[String], context: &'static str) -> anyhow::Result<PathMatcher> {
104 PathMatcher::new(values).with_context(|| format!("Failed to parse globs from {}", context))
105}