Support absolute disabled_globs (#25755)

Closes: #25556

We were always comparing `disabled_globs` against the relative file
path, we'll now use the absolute path if the glob is also absolute.

Release Notes:

- Support absolute globs in `edit_predictions.disabled_globs`
This commit is contained in:
Agus Zubiaga 2025-02-27 15:29:32 -03:00 committed by GitHub
parent c5632f8c31
commit 6eb2ffe77a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 191 additions and 16 deletions

View file

@ -4477,6 +4477,7 @@ impl IndentSize {
pub struct TestFile {
pub path: Arc<Path>,
pub root_name: String,
pub local_root: Option<PathBuf>,
}
#[cfg(any(test, feature = "test-support"))]
@ -4490,7 +4491,11 @@ impl File for TestFile {
}
fn as_local(&self) -> Option<&dyn LocalFile> {
None
if self.local_root.is_some() {
Some(self)
} else {
None
}
}
fn disk_state(&self) -> DiskState {
@ -4518,6 +4523,23 @@ impl File for TestFile {
}
}
#[cfg(any(test, feature = "test-support"))]
impl LocalFile for TestFile {
fn abs_path(&self, _cx: &App) -> PathBuf {
PathBuf::from(self.local_root.as_ref().unwrap())
.join(&self.root_name)
.join(self.path.as_ref())
}
fn load(&self, _cx: &App) -> Task<Result<String>> {
unimplemented!()
}
fn load_bytes(&self, _cx: &App) -> Task<Result<Vec<u8>>> {
unimplemented!()
}
}
pub(crate) fn contiguous_ranges(
values: impl Iterator<Item = u32>,
max_len: usize,

View file

@ -254,6 +254,7 @@ fn file(path: &str) -> Arc<dyn File> {
Arc::new(TestFile {
path: Path::new(path).into(),
root_name: "zed".into(),
local_root: None,
})
}

View file

@ -231,13 +231,33 @@ pub struct EditPredictionSettings {
/// A list of globs representing files that edit predictions should be disabled for.
/// This list adds to a pre-existing, sensible default set of globs.
/// Any additional ones you add are combined with them.
pub disabled_globs: Vec<GlobMatcher>,
pub disabled_globs: Vec<DisabledGlob>,
/// Configures how edit predictions are displayed in the buffer.
pub mode: EditPredictionsMode,
/// Settings specific to GitHub Copilot.
pub copilot: CopilotSettings,
}
impl EditPredictionSettings {
/// Returns whether edit predictions are enabled for the given path.
pub fn enabled_for_file(&self, file: &Arc<dyn File>, cx: &App) -> bool {
!self.disabled_globs.iter().any(|glob| {
if glob.is_absolute {
file.as_local()
.map_or(false, |local| glob.matcher.is_match(local.abs_path(cx)))
} else {
glob.matcher.is_match(file.path())
}
})
}
}
#[derive(Clone, Debug)]
pub struct DisabledGlob {
matcher: GlobMatcher,
is_absolute: bool,
}
/// The mode in which edit predictions should be displayed.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
@ -965,16 +985,12 @@ impl AllLanguageSettings {
}
/// Returns whether edit predictions are enabled for the given path.
pub fn inline_completions_enabled_for_path(&self, path: &Path) -> bool {
!self
.edit_predictions
.disabled_globs
.iter()
.any(|glob| glob.is_match(path))
pub fn edit_predictions_enabled_for_file(&self, file: &Arc<dyn File>, cx: &App) -> bool {
self.edit_predictions.enabled_for_file(file, cx)
}
/// Returns whether edit predictions are enabled for the given language and path.
pub fn show_inline_completions(&self, language: Option<&Arc<Language>>, cx: &App) -> bool {
pub fn show_edit_predictions(&self, language: Option<&Arc<Language>>, cx: &App) -> bool {
self.language(None, language.map(|l| l.name()).as_ref(), cx)
.show_edit_predictions
}
@ -1199,7 +1215,12 @@ impl settings::Settings for AllLanguageSettings {
},
disabled_globs: completion_globs
.iter()
.filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
.filter_map(|g| {
Some(DisabledGlob {
matcher: globset::Glob::new(g).ok()?.compile_matcher(),
is_absolute: Path::new(g).is_absolute(),
})
})
.collect(),
mode: edit_predictions_mode,
copilot: copilot_settings,
@ -1357,6 +1378,8 @@ pub struct PrettierSettings {
#[cfg(test)]
mod tests {
use gpui::TestAppContext;
use super::*;
#[test]
@ -1401,6 +1424,132 @@ mod tests {
assert!(result.is_err());
}
#[gpui::test]
fn test_edit_predictions_enabled_for_file(cx: &mut TestAppContext) {
use crate::TestFile;
use std::path::PathBuf;
let cx = cx.app.borrow_mut();
let build_settings = |globs: &[&str]| -> EditPredictionSettings {
EditPredictionSettings {
disabled_globs: globs
.iter()
.map(|glob_str| {
#[cfg(windows)]
let glob_str = {
let mut g = String::new();
if glob_str.starts_with('/') {
g.push_str("C:");
}
g.push_str(&glob_str.replace('/', "\\"));
g
};
#[cfg(windows)]
let glob_str = glob_str.as_str();
DisabledGlob {
matcher: globset::Glob::new(glob_str).unwrap().compile_matcher(),
is_absolute: Path::new(glob_str).is_absolute(),
}
})
.collect(),
..Default::default()
}
};
const WORKTREE_NAME: &str = "project";
let make_test_file = |segments: &[&str]| -> Arc<dyn File> {
let mut path_buf = PathBuf::new();
path_buf.extend(segments);
Arc::new(TestFile {
path: path_buf.as_path().into(),
root_name: WORKTREE_NAME.to_string(),
local_root: Some(PathBuf::from(if cfg!(windows) {
"C:\\absolute\\"
} else {
"/absolute/"
})),
})
};
let test_file = make_test_file(&["src", "test", "file.rs"]);
// Test relative globs
let settings = build_settings(&["*.rs"]);
assert!(!settings.enabled_for_file(&test_file, &cx));
let settings = build_settings(&["*.txt"]);
assert!(settings.enabled_for_file(&test_file, &cx));
// Test absolute globs
let settings = build_settings(&["/absolute/**/*.rs"]);
assert!(!settings.enabled_for_file(&test_file, &cx));
let settings = build_settings(&["/other/**/*.rs"]);
assert!(settings.enabled_for_file(&test_file, &cx));
// Test exact path match relative
let settings = build_settings(&["src/test/file.rs"]);
assert!(!settings.enabled_for_file(&test_file, &cx));
let settings = build_settings(&["src/test/otherfile.rs"]);
assert!(settings.enabled_for_file(&test_file, &cx));
// Test exact path match absolute
let settings = build_settings(&[&format!("/absolute/{}/src/test/file.rs", WORKTREE_NAME)]);
assert!(!settings.enabled_for_file(&test_file, &cx));
let settings = build_settings(&["/other/test/otherfile.rs"]);
assert!(settings.enabled_for_file(&test_file, &cx));
// Test * glob
let settings = build_settings(&["*"]);
assert!(!settings.enabled_for_file(&test_file, &cx));
let settings = build_settings(&["*.txt"]);
assert!(settings.enabled_for_file(&test_file, &cx));
// Test **/* glob
let settings = build_settings(&["**/*"]);
assert!(!settings.enabled_for_file(&test_file, &cx));
let settings = build_settings(&["other/**/*"]);
assert!(settings.enabled_for_file(&test_file, &cx));
// Test directory/** glob
let settings = build_settings(&["src/**"]);
assert!(!settings.enabled_for_file(&test_file, &cx));
let test_file_root: Arc<dyn File> = Arc::new(TestFile {
path: PathBuf::from("file.rs").as_path().into(),
root_name: WORKTREE_NAME.to_string(),
local_root: Some(PathBuf::from("/absolute/")),
});
assert!(settings.enabled_for_file(&test_file_root, &cx));
let settings = build_settings(&["other/**"]);
assert!(settings.enabled_for_file(&test_file, &cx));
// Test **/directory/* glob
let settings = build_settings(&["**/test/*"]);
assert!(!settings.enabled_for_file(&test_file, &cx));
let settings = build_settings(&["**/other/*"]);
assert!(settings.enabled_for_file(&test_file, &cx));
// Test multiple globs
let settings = build_settings(&["*.rs", "*.txt", "src/**"]);
assert!(!settings.enabled_for_file(&test_file, &cx));
let settings = build_settings(&["*.txt", "*.md", "other/**"]);
assert!(settings.enabled_for_file(&test_file, &cx));
// Test dot files
let dot_file = make_test_file(&[".config", "settings.json"]);
let settings = build_settings(&[".*/**"]);
assert!(!settings.enabled_for_file(&dot_file, &cx));
let dot_env_file = make_test_file(&[".env"]);
let settings = build_settings(&[".env"]);
assert!(!settings.enabled_for_file(&dot_env_file, &cx));
}
#[test]
pub fn test_resolve_language_servers() {
fn language_server_names(names: &[&str]) -> Vec<LanguageServerName> {