WIP: Add a setting to visually redact enviroment variables (#7124)
Release Notes: - Added bash syntax highlighting to `.env` files. - Added a `private_files` setting for configuring which files should be considered to contain environment variables or other sensitive information. - Added a `redact_private_values` setting to add or remove censor bars over variable values in files matching the `private_files` patterns. -(internal) added a new `redactions.scm` query to our language support, allowing different config file formats to indicate where environment variable values can be identified in the syntax tree, added this query to `bash`, `json`, `toml`, and `yaml` files. --------- Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
parent
5333eff0e4
commit
f98d636203
23 changed files with 330 additions and 32 deletions
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"tab_size": 4
|
"tab_size": 4
|
||||||
},
|
},
|
||||||
"formatter": "auto"
|
"formatter": "auto"
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,17 @@
|
||||||
"show_wrap_guides": true,
|
"show_wrap_guides": true,
|
||||||
// Character counts at which to show wrap guides in the editor.
|
// Character counts at which to show wrap guides in the editor.
|
||||||
"wrap_guides": [],
|
"wrap_guides": [],
|
||||||
|
// Hide the values of in variables from visual display in private files
|
||||||
|
"redact_private_values": false,
|
||||||
|
// Globs to match against file paths to determine if a file is private.
|
||||||
|
"private_files": [
|
||||||
|
"**/.env*",
|
||||||
|
"**/*.pem",
|
||||||
|
"**/*.key",
|
||||||
|
"**/*.cert",
|
||||||
|
"**/*.crt",
|
||||||
|
"**/secrets.yml"
|
||||||
|
],
|
||||||
// Whether to use additional LSP queries to format (and amend) the code after
|
// Whether to use additional LSP queries to format (and amend) the code after
|
||||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||||
"use_on_type_format": true,
|
"use_on_type_format": true,
|
||||||
|
|
|
@ -1251,6 +1251,10 @@ mod tests {
|
||||||
fn worktree_id(&self) -> usize {
|
fn worktree_id(&self) -> usize {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_private(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl language::LocalFile for File {
|
impl language::LocalFile for File {
|
||||||
|
|
|
@ -225,8 +225,9 @@ impl CopilotButton {
|
||||||
let file = snapshot.file_at(suggestion_anchor).cloned();
|
let file = snapshot.file_at(suggestion_anchor).cloned();
|
||||||
|
|
||||||
self.editor_enabled = Some(
|
self.editor_enabled = Some(
|
||||||
all_language_settings(self.file.as_ref(), cx)
|
file.as_ref().map(|file| !file.is_private()).unwrap_or(true)
|
||||||
.copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
|
&& all_language_settings(self.file.as_ref(), cx)
|
||||||
|
.copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
|
||||||
);
|
);
|
||||||
self.language = language.cloned();
|
self.language = language.cloned();
|
||||||
self.file = file;
|
self.file = file;
|
||||||
|
|
|
@ -8493,6 +8493,31 @@ impl Editor {
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the text ranges corresponding to the redaction query
|
||||||
|
pub fn redacted_ranges(
|
||||||
|
&self,
|
||||||
|
search_range: Range<Anchor>,
|
||||||
|
display_snapshot: &DisplaySnapshot,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Vec<Range<DisplayPoint>> {
|
||||||
|
display_snapshot
|
||||||
|
.buffer_snapshot
|
||||||
|
.redacted_ranges(search_range, |file| {
|
||||||
|
if let Some(file) = file {
|
||||||
|
file.is_private()
|
||||||
|
&& EditorSettings::get(Some((file.worktree_id(), file.path())), cx)
|
||||||
|
.redact_private_values
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|range| {
|
||||||
|
range.start.to_display_point(display_snapshot)
|
||||||
|
..range.end.to_display_point(display_snapshot)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn highlight_text<T: 'static>(
|
pub fn highlight_text<T: 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
ranges: Vec<Range<Anchor>>,
|
ranges: Vec<Range<Anchor>>,
|
||||||
|
|
|
@ -13,6 +13,7 @@ pub struct EditorSettings {
|
||||||
pub scrollbar: Scrollbar,
|
pub scrollbar: Scrollbar,
|
||||||
pub relative_line_numbers: bool,
|
pub relative_line_numbers: bool,
|
||||||
pub seed_search_query_from_cursor: SeedQuerySetting,
|
pub seed_search_query_from_cursor: SeedQuerySetting,
|
||||||
|
pub redact_private_values: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When to populate a new search's query based on the text under the cursor.
|
/// When to populate a new search's query based on the text under the cursor.
|
||||||
|
@ -93,6 +94,13 @@ pub struct EditorSettingsContent {
|
||||||
///
|
///
|
||||||
/// Default: always
|
/// Default: always
|
||||||
pub seed_search_query_from_cursor: Option<SeedQuerySetting>,
|
pub seed_search_query_from_cursor: Option<SeedQuerySetting>,
|
||||||
|
|
||||||
|
/// Hide the values of variables in `private` files, as defined by the
|
||||||
|
/// private_files setting. This only changes the visual representation,
|
||||||
|
/// the values are still present in the file and can be selected / copied / pasted
|
||||||
|
///
|
||||||
|
/// Default: false
|
||||||
|
pub redact_private_values: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrollbar related settings
|
/// Scrollbar related settings
|
||||||
|
|
|
@ -1153,7 +1153,9 @@ impl EditorElement {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.with_z_index(0, |cx| {
|
cx.with_z_index(0, |cx| self.paint_redactions(text_bounds, &layout, cx));
|
||||||
|
|
||||||
|
cx.with_z_index(1, |cx| {
|
||||||
for cursor in cursors {
|
for cursor in cursors {
|
||||||
cursor.paint(content_origin, cx);
|
cursor.paint(content_origin, cx);
|
||||||
}
|
}
|
||||||
|
@ -1162,6 +1164,32 @@ impl EditorElement {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paint_redactions(
|
||||||
|
&mut self,
|
||||||
|
text_bounds: Bounds<Pixels>,
|
||||||
|
layout: &LayoutState,
|
||||||
|
cx: &mut ElementContext,
|
||||||
|
) {
|
||||||
|
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
|
||||||
|
let line_end_overshoot = layout.line_end_overshoot();
|
||||||
|
|
||||||
|
// A softer than perfect black
|
||||||
|
let redaction_color = gpui::rgb(0x0e1111);
|
||||||
|
|
||||||
|
for range in layout.redacted_ranges.iter() {
|
||||||
|
self.paint_highlighted_range(
|
||||||
|
range.clone(),
|
||||||
|
redaction_color.into(),
|
||||||
|
Pixels::ZERO,
|
||||||
|
line_end_overshoot,
|
||||||
|
layout,
|
||||||
|
content_origin,
|
||||||
|
text_bounds,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn paint_overlays(
|
fn paint_overlays(
|
||||||
&mut self,
|
&mut self,
|
||||||
text_bounds: Bounds<Pixels>,
|
text_bounds: Bounds<Pixels>,
|
||||||
|
@ -1957,6 +1985,8 @@ impl EditorElement {
|
||||||
cx.theme().colors(),
|
cx.theme().colors(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let redacted_ranges = editor.redacted_ranges(start_anchor..end_anchor, &snapshot.display_snapshot, cx);
|
||||||
|
|
||||||
let mut newest_selection_head = None;
|
let mut newest_selection_head = None;
|
||||||
|
|
||||||
if editor.show_local_selections {
|
if editor.show_local_selections {
|
||||||
|
@ -2298,6 +2328,7 @@ impl EditorElement {
|
||||||
active_rows,
|
active_rows,
|
||||||
highlighted_rows,
|
highlighted_rows,
|
||||||
highlighted_ranges,
|
highlighted_ranges,
|
||||||
|
redacted_ranges,
|
||||||
line_numbers,
|
line_numbers,
|
||||||
display_hunks,
|
display_hunks,
|
||||||
blocks,
|
blocks,
|
||||||
|
@ -3082,6 +3113,7 @@ pub struct LayoutState {
|
||||||
display_hunks: Vec<DisplayDiffHunk>,
|
display_hunks: Vec<DisplayDiffHunk>,
|
||||||
blocks: Vec<BlockLayout>,
|
blocks: Vec<BlockLayout>,
|
||||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||||||
|
redacted_ranges: Vec<Range<DisplayPoint>>,
|
||||||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||||
scrollbar_row_range: Range<f32>,
|
scrollbar_row_range: Range<f32>,
|
||||||
show_scrollbars: bool,
|
show_scrollbars: bool,
|
||||||
|
@ -3095,6 +3127,12 @@ pub struct LayoutState {
|
||||||
space_invisible: ShapedLine,
|
space_invisible: ShapedLine,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LayoutState {
|
||||||
|
fn line_end_overshoot(&self) -> Pixels {
|
||||||
|
0.15 * self.position_map.line_height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct CodeActionsIndicator {
|
struct CodeActionsIndicator {
|
||||||
row: u32,
|
row: u32,
|
||||||
button: IconButton,
|
button: IconButton,
|
||||||
|
|
|
@ -1364,5 +1364,9 @@ mod tests {
|
||||||
fn to_proto(&self) -> rpc::proto::File {
|
fn to_proto(&self) -> rpc::proto::File {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_private(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -383,6 +383,9 @@ pub trait File: Send + Sync {
|
||||||
|
|
||||||
/// Converts this file into a protobuf message.
|
/// Converts this file into a protobuf message.
|
||||||
fn to_proto(&self) -> rpc::proto::File;
|
fn to_proto(&self) -> rpc::proto::File;
|
||||||
|
|
||||||
|
/// Return whether Zed considers this to be a dotenv file.
|
||||||
|
fn is_private(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The file associated with a buffer, in the case where the file is on the local disk.
|
/// The file associated with a buffer, in the case where the file is on the local disk.
|
||||||
|
@ -2877,6 +2880,43 @@ impl BufferSnapshot {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns anchor ranges for any matches of the redaction query.
|
||||||
|
/// The buffer can be associated with multiple languages, and the redaction query associated with each
|
||||||
|
/// will be run on the relevant section of the buffer.
|
||||||
|
pub fn redacted_ranges<'a, T: ToOffset>(
|
||||||
|
&'a self,
|
||||||
|
range: Range<T>,
|
||||||
|
) -> impl Iterator<Item = Range<usize>> + 'a {
|
||||||
|
let offset_range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
let mut syntax_matches = self.syntax.matches(offset_range, self, |grammar| {
|
||||||
|
grammar
|
||||||
|
.redactions_config
|
||||||
|
.as_ref()
|
||||||
|
.map(|config| &config.query)
|
||||||
|
});
|
||||||
|
|
||||||
|
let configs = syntax_matches
|
||||||
|
.grammars()
|
||||||
|
.iter()
|
||||||
|
.map(|grammar| grammar.redactions_config.as_ref())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
iter::from_fn(move || {
|
||||||
|
let redacted_range = syntax_matches
|
||||||
|
.peek()
|
||||||
|
.and_then(|mat| {
|
||||||
|
configs[mat.grammar_index].and_then(|config| {
|
||||||
|
mat.captures
|
||||||
|
.iter()
|
||||||
|
.find(|capture| capture.index == config.redaction_capture_ix)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(|mat| mat.node.byte_range());
|
||||||
|
syntax_matches.advance();
|
||||||
|
redacted_range
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns selections for remote peers intersecting the given range.
|
/// Returns selections for remote peers intersecting the given range.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub fn remote_selections_in_range(
|
pub fn remote_selections_in_range(
|
||||||
|
|
|
@ -453,6 +453,7 @@ pub struct LanguageQueries {
|
||||||
pub embedding: Option<Cow<'static, str>>,
|
pub embedding: Option<Cow<'static, str>>,
|
||||||
pub injections: Option<Cow<'static, str>>,
|
pub injections: Option<Cow<'static, str>>,
|
||||||
pub overrides: Option<Cow<'static, str>>,
|
pub overrides: Option<Cow<'static, str>>,
|
||||||
|
pub redactions: Option<Cow<'static, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a language for the given range. Some languages (e.g. HTML)
|
/// Represents a language for the given range. Some languages (e.g. HTML)
|
||||||
|
@ -623,6 +624,7 @@ pub struct Grammar {
|
||||||
pub(crate) error_query: Query,
|
pub(crate) error_query: Query,
|
||||||
pub(crate) highlights_query: Option<Query>,
|
pub(crate) highlights_query: Option<Query>,
|
||||||
pub(crate) brackets_config: Option<BracketConfig>,
|
pub(crate) brackets_config: Option<BracketConfig>,
|
||||||
|
pub(crate) redactions_config: Option<RedactionConfig>,
|
||||||
pub(crate) indents_config: Option<IndentConfig>,
|
pub(crate) indents_config: Option<IndentConfig>,
|
||||||
pub outline_config: Option<OutlineConfig>,
|
pub outline_config: Option<OutlineConfig>,
|
||||||
pub embedding_config: Option<EmbeddingConfig>,
|
pub embedding_config: Option<EmbeddingConfig>,
|
||||||
|
@ -664,6 +666,11 @@ struct InjectionConfig {
|
||||||
patterns: Vec<InjectionPatternConfig>,
|
patterns: Vec<InjectionPatternConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct RedactionConfig {
|
||||||
|
pub query: Query,
|
||||||
|
pub redaction_capture_ix: u32,
|
||||||
|
}
|
||||||
|
|
||||||
struct OverrideConfig {
|
struct OverrideConfig {
|
||||||
query: Query,
|
query: Query,
|
||||||
values: HashMap<u32, (String, LanguageConfigOverride)>,
|
values: HashMap<u32, (String, LanguageConfigOverride)>,
|
||||||
|
@ -1303,6 +1310,7 @@ impl Language {
|
||||||
indents_config: None,
|
indents_config: None,
|
||||||
injection_config: None,
|
injection_config: None,
|
||||||
override_config: None,
|
override_config: None,
|
||||||
|
redactions_config: None,
|
||||||
error_query: Query::new(&ts_language, "(ERROR) @error").unwrap(),
|
error_query: Query::new(&ts_language, "(ERROR) @error").unwrap(),
|
||||||
ts_language,
|
ts_language,
|
||||||
highlight_map: Default::default(),
|
highlight_map: Default::default(),
|
||||||
|
@ -1359,6 +1367,11 @@ impl Language {
|
||||||
.with_override_query(query.as_ref())
|
.with_override_query(query.as_ref())
|
||||||
.context("Error loading override query")?;
|
.context("Error loading override query")?;
|
||||||
}
|
}
|
||||||
|
if let Some(query) = queries.redactions {
|
||||||
|
self = self
|
||||||
|
.with_redaction_query(query.as_ref())
|
||||||
|
.context("Error loading redaction query")?;
|
||||||
|
}
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1589,6 +1602,22 @@ impl Language {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_redaction_query(mut self, source: &str) -> anyhow::Result<Self> {
|
||||||
|
let grammar = self.grammar_mut();
|
||||||
|
let query = Query::new(&grammar.ts_language, source)?;
|
||||||
|
let mut redaction_capture_ix = None;
|
||||||
|
get_capture_indices(&query, &mut [("redact", &mut redaction_capture_ix)]);
|
||||||
|
|
||||||
|
if let Some(redaction_capture_ix) = redaction_capture_ix {
|
||||||
|
grammar.redactions_config = Some(RedactionConfig {
|
||||||
|
query,
|
||||||
|
redaction_capture_ix,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
fn grammar_mut(&mut self) -> &mut Grammar {
|
fn grammar_mut(&mut self) -> &mut Grammar {
|
||||||
Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
|
Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1059,7 +1059,7 @@ impl<'a> SyntaxMapMatches<'a> {
|
||||||
.position(|later_layer| key < later_layer.sort_key())
|
.position(|later_layer| key < later_layer.sort_key())
|
||||||
.unwrap_or(self.active_layer_count - 1);
|
.unwrap_or(self.active_layer_count - 1);
|
||||||
self.layers[0..i].rotate_left(1);
|
self.layers[0..i].rotate_left(1);
|
||||||
} else {
|
} else if self.active_layer_count != 0 {
|
||||||
self.layers[0..self.active_layer_count].rotate_left(1);
|
self.layers[0..self.active_layer_count].rotate_left(1);
|
||||||
self.active_layer_count -= 1;
|
self.active_layer_count -= 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ use text::{
|
||||||
BufferId, Edit, TextSummary,
|
BufferId, Edit, TextSummary,
|
||||||
};
|
};
|
||||||
use theme::SyntaxTheme;
|
use theme::SyntaxTheme;
|
||||||
|
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
@ -2784,6 +2785,26 @@ impl MultiBufferSnapshot {
|
||||||
.map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone()))
|
.map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn excerpts_for_range<'a, T: ToOffset>(
|
||||||
|
&'a self,
|
||||||
|
range: Range<T>,
|
||||||
|
) -> impl Iterator<Item = (&'a Excerpt, usize)> + 'a {
|
||||||
|
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
|
||||||
|
let mut cursor = self.excerpts.cursor::<usize>();
|
||||||
|
cursor.seek(&range.start, Bias::Right, &());
|
||||||
|
cursor.prev(&());
|
||||||
|
|
||||||
|
iter::from_fn(move || {
|
||||||
|
cursor.next(&());
|
||||||
|
if cursor.start() < &range.end {
|
||||||
|
cursor.item().map(|item| (item, *cursor.start()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn excerpt_boundaries_in_range<R, T>(
|
pub fn excerpt_boundaries_in_range<R, T>(
|
||||||
&self,
|
&self,
|
||||||
range: R,
|
range: R,
|
||||||
|
@ -2942,6 +2963,37 @@ impl MultiBufferSnapshot {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn redacted_ranges<'a, T: ToOffset>(
|
||||||
|
&'a self,
|
||||||
|
range: Range<T>,
|
||||||
|
redaction_enabled: impl Fn(Option<&Arc<dyn File>>) -> bool + 'a,
|
||||||
|
) -> impl Iterator<Item = Range<usize>> + 'a {
|
||||||
|
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
self.excerpts_for_range(range.clone())
|
||||||
|
.filter_map(move |(excerpt, excerpt_offset)| {
|
||||||
|
redaction_enabled(excerpt.buffer.file()).then(move || {
|
||||||
|
let excerpt_buffer_start =
|
||||||
|
excerpt.range.context.start.to_offset(&excerpt.buffer);
|
||||||
|
|
||||||
|
excerpt
|
||||||
|
.buffer
|
||||||
|
.redacted_ranges(excerpt.range.context.clone())
|
||||||
|
.map(move |mut redacted_range| {
|
||||||
|
// Re-base onto the excerpts coordinates in the multibuffer
|
||||||
|
redacted_range.start =
|
||||||
|
excerpt_offset + (redacted_range.start - excerpt_buffer_start);
|
||||||
|
redacted_range.end =
|
||||||
|
excerpt_offset + (redacted_range.end - excerpt_buffer_start);
|
||||||
|
|
||||||
|
redacted_range
|
||||||
|
})
|
||||||
|
.skip_while(move |redacted_range| redacted_range.end < range.start)
|
||||||
|
.take_while(move |redacted_range| redacted_range.start < range.end)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn diagnostics_update_count(&self) -> usize {
|
pub fn diagnostics_update_count(&self) -> usize {
|
||||||
self.diagnostics_update_count
|
self.diagnostics_update_count
|
||||||
}
|
}
|
||||||
|
|
|
@ -6470,6 +6470,7 @@ impl Project {
|
||||||
path: entry.path.clone(),
|
path: entry.path.clone(),
|
||||||
worktree: worktree_handle.clone(),
|
worktree: worktree_handle.clone(),
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
|
is_private: entry.is_private,
|
||||||
}
|
}
|
||||||
} else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
|
} else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
|
||||||
File {
|
File {
|
||||||
|
@ -6479,6 +6480,7 @@ impl Project {
|
||||||
path: entry.path.clone(),
|
path: entry.path.clone(),
|
||||||
worktree: worktree_handle.clone(),
|
worktree: worktree_handle.clone(),
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
|
is_private: entry.is_private,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
File {
|
File {
|
||||||
|
@ -6488,6 +6490,7 @@ impl Project {
|
||||||
mtime: old_file.mtime(),
|
mtime: old_file.mtime(),
|
||||||
worktree: worktree_handle.clone(),
|
worktree: worktree_handle.clone(),
|
||||||
is_deleted: true,
|
is_deleted: true,
|
||||||
|
is_private: old_file.is_private,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ pub struct ProjectSettings {
|
||||||
/// Configuration for Git-related features
|
/// Configuration for Git-related features
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub git: GitSettings,
|
pub git: GitSettings,
|
||||||
|
|
||||||
/// Completely ignore files matching globs from `file_scan_exclusions`
|
/// Completely ignore files matching globs from `file_scan_exclusions`
|
||||||
///
|
///
|
||||||
/// Default: [
|
/// Default: [
|
||||||
|
@ -34,6 +35,10 @@ pub struct ProjectSettings {
|
||||||
/// ]
|
/// ]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub file_scan_exclusions: Option<Vec<String>>,
|
pub file_scan_exclusions: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Treat the files matching these globs as `.env` files.
|
||||||
|
/// Default: [ "**/.env*" ]
|
||||||
|
pub private_files: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
|
|
|
@ -230,6 +230,7 @@ pub struct LocalSnapshot {
|
||||||
/// id of their parent directory.
|
/// id of their parent directory.
|
||||||
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
|
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
|
||||||
file_scan_exclusions: Vec<PathMatcher>,
|
file_scan_exclusions: Vec<PathMatcher>,
|
||||||
|
private_files: Vec<PathMatcher>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BackgroundScannerState {
|
struct BackgroundScannerState {
|
||||||
|
@ -319,16 +320,34 @@ impl Worktree {
|
||||||
cx.new_model(move |cx: &mut ModelContext<Worktree>| {
|
cx.new_model(move |cx: &mut ModelContext<Worktree>| {
|
||||||
cx.observe_global::<SettingsStore>(move |this, cx| {
|
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||||
if let Self::Local(this) = this {
|
if let Self::Local(this) = this {
|
||||||
let new_file_scan_exclusions =
|
let new_file_scan_exclusions = path_matchers(
|
||||||
file_scan_exclusions(ProjectSettings::get_global(cx));
|
ProjectSettings::get_global(cx)
|
||||||
if new_file_scan_exclusions != this.snapshot.file_scan_exclusions {
|
.file_scan_exclusions
|
||||||
|
.as_deref(),
|
||||||
|
"file_scan_exclusions",
|
||||||
|
);
|
||||||
|
let new_private_files = path_matchers(
|
||||||
|
ProjectSettings::get(Some((cx.handle().entity_id().as_u64() as usize, &Path::new(""))), cx).private_files.as_deref(),
|
||||||
|
"private_files",
|
||||||
|
);
|
||||||
|
|
||||||
|
if new_file_scan_exclusions != this.snapshot.file_scan_exclusions
|
||||||
|
|| new_private_files != this.snapshot.private_files
|
||||||
|
{
|
||||||
this.snapshot.file_scan_exclusions = new_file_scan_exclusions;
|
this.snapshot.file_scan_exclusions = new_file_scan_exclusions;
|
||||||
|
this.snapshot.private_files = new_private_files;
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Re-scanning directories, new scan exclude files: {:?}",
|
"Re-scanning directories, new scan exclude files: {:?}, new dotenv files: {:?}",
|
||||||
this.snapshot
|
this.snapshot
|
||||||
.file_scan_exclusions
|
.file_scan_exclusions
|
||||||
.iter()
|
.iter()
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
this.snapshot
|
||||||
|
.private_files
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -357,7 +376,16 @@ impl Worktree {
|
||||||
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
||||||
|
|
||||||
let mut snapshot = LocalSnapshot {
|
let mut snapshot = LocalSnapshot {
|
||||||
file_scan_exclusions: file_scan_exclusions(ProjectSettings::get_global(cx)),
|
file_scan_exclusions: path_matchers(
|
||||||
|
ProjectSettings::get_global(cx)
|
||||||
|
.file_scan_exclusions
|
||||||
|
.as_deref(),
|
||||||
|
"file_scan_exclusions",
|
||||||
|
),
|
||||||
|
private_files: path_matchers(
|
||||||
|
ProjectSettings::get(Some((cx.handle().entity_id().as_u64() as usize, &Path::new(""))), cx).private_files.as_deref(),
|
||||||
|
"private_files",
|
||||||
|
),
|
||||||
ignores_by_parent_abs_path: Default::default(),
|
ignores_by_parent_abs_path: Default::default(),
|
||||||
git_repositories: Default::default(),
|
git_repositories: Default::default(),
|
||||||
snapshot: Snapshot {
|
snapshot: Snapshot {
|
||||||
|
@ -650,20 +678,22 @@ fn start_background_scan_tasks(
|
||||||
vec![background_scanner, scan_state_updater]
|
vec![background_scanner, scan_state_updater]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_scan_exclusions(project_settings: &ProjectSettings) -> Vec<PathMatcher> {
|
fn path_matchers(values: Option<&[String]>, context: &'static str) -> Vec<PathMatcher> {
|
||||||
project_settings.file_scan_exclusions.as_deref().unwrap_or(&[]).iter()
|
values
|
||||||
.sorted()
|
.unwrap_or(&[])
|
||||||
.filter_map(|pattern| {
|
.iter()
|
||||||
PathMatcher::new(pattern)
|
.sorted()
|
||||||
.map(Some)
|
.filter_map(|pattern| {
|
||||||
.unwrap_or_else(|e| {
|
PathMatcher::new(pattern)
|
||||||
log::error!(
|
.map(Some)
|
||||||
"Skipping pattern {pattern} in `file_scan_exclusions` project settings due to parsing error: {e:#}"
|
.unwrap_or_else(|e| {
|
||||||
);
|
log::error!(
|
||||||
None
|
"Skipping pattern {pattern} in `{}` project settings due to parsing error: {e:#}", context
|
||||||
})
|
);
|
||||||
})
|
None
|
||||||
.collect()
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalWorktree {
|
impl LocalWorktree {
|
||||||
|
@ -1003,6 +1033,7 @@ impl LocalWorktree {
|
||||||
mtime: entry.mtime,
|
mtime: entry.mtime,
|
||||||
is_local: true,
|
is_local: true,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
|
is_private: entry.is_private,
|
||||||
},
|
},
|
||||||
text,
|
text,
|
||||||
diff_base,
|
diff_base,
|
||||||
|
@ -1017,6 +1048,7 @@ impl LocalWorktree {
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("Excluded file {abs_path:?} got removed during loading")
|
format!("Excluded file {abs_path:?} got removed during loading")
|
||||||
})?;
|
})?;
|
||||||
|
let is_private = snapshot.is_path_private(path.as_ref());
|
||||||
Ok((
|
Ok((
|
||||||
File {
|
File {
|
||||||
entry_id: None,
|
entry_id: None,
|
||||||
|
@ -1025,6 +1057,7 @@ impl LocalWorktree {
|
||||||
mtime: metadata.mtime,
|
mtime: metadata.mtime,
|
||||||
is_local: true,
|
is_local: true,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
|
is_private,
|
||||||
},
|
},
|
||||||
text,
|
text,
|
||||||
diff_base,
|
diff_base,
|
||||||
|
@ -1053,14 +1086,15 @@ impl LocalWorktree {
|
||||||
let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx);
|
let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx);
|
||||||
let fs = Arc::clone(&self.fs);
|
let fs = Arc::clone(&self.fs);
|
||||||
let abs_path = self.absolutize(&path);
|
let abs_path = self.absolutize(&path);
|
||||||
|
let is_private = self.snapshot.is_path_private(&path);
|
||||||
|
|
||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
let entry = save.await?;
|
let entry = save.await?;
|
||||||
let abs_path = abs_path?;
|
let abs_path = abs_path?;
|
||||||
let this = this.upgrade().context("worktree dropped")?;
|
let this = this.upgrade().context("worktree dropped")?;
|
||||||
|
|
||||||
let (entry_id, mtime, path) = match entry {
|
let (entry_id, mtime, path, is_dotenv) = match entry {
|
||||||
Some(entry) => (Some(entry.id), entry.mtime, entry.path),
|
Some(entry) => (Some(entry.id), entry.mtime, entry.path, entry.is_private),
|
||||||
None => {
|
None => {
|
||||||
let metadata = fs
|
let metadata = fs
|
||||||
.metadata(&abs_path)
|
.metadata(&abs_path)
|
||||||
|
@ -1073,7 +1107,7 @@ impl LocalWorktree {
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("Excluded buffer {path:?} got removed during saving")
|
format!("Excluded buffer {path:?} got removed during saving")
|
||||||
})?;
|
})?;
|
||||||
(None, metadata.mtime, path)
|
(None, metadata.mtime, path, is_private)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1085,6 +1119,7 @@ impl LocalWorktree {
|
||||||
mtime,
|
mtime,
|
||||||
is_local: true,
|
is_local: true,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
|
is_private: is_dotenv,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(project_id) = project_id {
|
if let Some(project_id) = project_id {
|
||||||
|
@ -2295,6 +2330,14 @@ impl LocalSnapshot {
|
||||||
paths
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_path_private(&self, path: &Path) -> bool {
|
||||||
|
path.ancestors().any(|ancestor| {
|
||||||
|
self.private_files
|
||||||
|
.iter()
|
||||||
|
.any(|exclude_matcher| exclude_matcher.is_match(&ancestor))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_path_excluded(&self, mut path: PathBuf) -> bool {
|
pub fn is_path_excluded(&self, mut path: PathBuf) -> bool {
|
||||||
loop {
|
loop {
|
||||||
if self
|
if self
|
||||||
|
@ -2747,6 +2790,7 @@ pub struct File {
|
||||||
pub(crate) entry_id: Option<ProjectEntryId>,
|
pub(crate) entry_id: Option<ProjectEntryId>,
|
||||||
pub(crate) is_local: bool,
|
pub(crate) is_local: bool,
|
||||||
pub(crate) is_deleted: bool,
|
pub(crate) is_deleted: bool,
|
||||||
|
pub(crate) is_private: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl language::File for File {
|
impl language::File for File {
|
||||||
|
@ -2819,6 +2863,10 @@ impl language::File for File {
|
||||||
is_deleted: self.is_deleted,
|
is_deleted: self.is_deleted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_private(&self) -> bool {
|
||||||
|
self.is_private
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl language::LocalFile for File {
|
impl language::LocalFile for File {
|
||||||
|
@ -2874,6 +2922,7 @@ impl File {
|
||||||
entry_id: Some(entry.id),
|
entry_id: Some(entry.id),
|
||||||
is_local: true,
|
is_local: true,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
|
is_private: entry.is_private,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2899,6 +2948,7 @@ impl File {
|
||||||
entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
|
entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
|
||||||
is_local: false,
|
is_local: false,
|
||||||
is_deleted: proto.is_deleted,
|
is_deleted: proto.is_deleted,
|
||||||
|
is_private: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2943,6 +2993,8 @@ pub struct Entry {
|
||||||
/// entries in that they are not included in searches.
|
/// entries in that they are not included in searches.
|
||||||
pub is_external: bool,
|
pub is_external: bool,
|
||||||
pub git_status: Option<GitFileStatus>,
|
pub git_status: Option<GitFileStatus>,
|
||||||
|
/// Whether this entry is considered to be a `.env` file.
|
||||||
|
pub is_private: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
@ -2997,6 +3049,7 @@ impl Entry {
|
||||||
is_symlink: metadata.is_symlink,
|
is_symlink: metadata.is_symlink,
|
||||||
is_ignored: false,
|
is_ignored: false,
|
||||||
is_external: false,
|
is_external: false,
|
||||||
|
is_private: false,
|
||||||
git_status: None,
|
git_status: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3732,7 +3785,7 @@ impl BackgroundScanner {
|
||||||
ancestor_inodes.insert(child_entry.inode);
|
ancestor_inodes.insert(child_entry.inode);
|
||||||
|
|
||||||
new_jobs.push(Some(ScanJob {
|
new_jobs.push(Some(ScanJob {
|
||||||
abs_path: child_abs_path,
|
abs_path: child_abs_path.clone(),
|
||||||
path: child_path,
|
path: child_path,
|
||||||
is_external: child_entry.is_external,
|
is_external: child_entry.is_external,
|
||||||
ignore_stack: if child_entry.is_ignored {
|
ignore_stack: if child_entry.is_ignored {
|
||||||
|
@ -3766,6 +3819,16 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let relative_path = job.path.join(child_name);
|
||||||
|
let state = self.state.lock();
|
||||||
|
if state.snapshot.is_path_private(&relative_path) {
|
||||||
|
log::debug!("detected private file: {relative_path:?}");
|
||||||
|
child_entry.is_private = true;
|
||||||
|
}
|
||||||
|
drop(state)
|
||||||
|
}
|
||||||
|
|
||||||
new_entries.push(child_entry);
|
new_entries.push(child_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3866,6 +3929,7 @@ impl BackgroundScanner {
|
||||||
let is_dir = fs_entry.is_dir();
|
let is_dir = fs_entry.is_dir();
|
||||||
fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
|
fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
|
||||||
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
|
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
|
||||||
|
fs_entry.is_private = state.snapshot.is_path_private(path);
|
||||||
|
|
||||||
if !is_dir && !fs_entry.is_ignored && !fs_entry.is_external {
|
if !is_dir && !fs_entry.is_ignored && !fs_entry.is_external {
|
||||||
if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(path) {
|
if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(path) {
|
||||||
|
@ -4548,6 +4612,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
|
||||||
is_ignored: entry.is_ignored,
|
is_ignored: entry.is_ignored,
|
||||||
is_external: entry.is_external,
|
is_external: entry.is_external,
|
||||||
git_status: git_status_from_proto(entry.git_status),
|
git_status: git_status_from_proto(entry.git_status),
|
||||||
|
is_private: false,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
|
|
|
@ -101,6 +101,7 @@ pub struct EntryDetails {
|
||||||
is_processing: bool,
|
is_processing: bool,
|
||||||
is_cut: bool,
|
is_cut: bool,
|
||||||
git_status: Option<GitFileStatus>,
|
git_status: Option<GitFileStatus>,
|
||||||
|
is_dotenv: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
|
@ -1137,6 +1138,7 @@ impl ProjectPanel {
|
||||||
is_symlink: false,
|
is_symlink: false,
|
||||||
is_ignored: false,
|
is_ignored: false,
|
||||||
is_external: false,
|
is_external: false,
|
||||||
|
is_private: false,
|
||||||
git_status: entry.git_status,
|
git_status: entry.git_status,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1298,6 +1300,7 @@ impl ProjectPanel {
|
||||||
.clipboard_entry
|
.clipboard_entry
|
||||||
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
|
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
|
||||||
git_status: status,
|
git_status: status,
|
||||||
|
is_dotenv: entry.is_private,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(edit_state) = &self.edit_state {
|
if let Some(edit_state) = &self.edit_state {
|
||||||
|
|
|
@ -86,6 +86,7 @@ pub trait Settings: 'static + Send + Sync {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// path is a (worktree ID, Path)
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn get<'a>(path: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a Self
|
fn get<'a>(path: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a Self
|
||||||
where
|
where
|
||||||
|
|
|
@ -357,6 +357,7 @@ const QUERY_FILENAME_PREFIXES: &[(
|
||||||
("embedding", |q| &mut q.embedding),
|
("embedding", |q| &mut q.embedding),
|
||||||
("injections", |q| &mut q.injections),
|
("injections", |q| &mut q.injections),
|
||||||
("overrides", |q| &mut q.overrides),
|
("overrides", |q| &mut q.overrides),
|
||||||
|
("redactions", |q| &mut q.redactions),
|
||||||
];
|
];
|
||||||
|
|
||||||
fn load_queries(name: &str) -> LanguageQueries {
|
fn load_queries(name: &str) -> LanguageQueries {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name = "Shell Script"
|
name = "Shell Script"
|
||||||
path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile"]
|
path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile", ".env"]
|
||||||
line_comments = ["# "]
|
line_comments = ["# "]
|
||||||
first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b"
|
first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b"
|
||||||
brackets = [
|
brackets = [
|
||||||
|
|
2
crates/zed/src/languages/bash/redactions.scm
Normal file
2
crates/zed/src/languages/bash/redactions.scm
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
(variable_assignment
|
||||||
|
value: (_) @redact)
|
4
crates/zed/src/languages/json/redactions.scm
Normal file
4
crates/zed/src/languages/json/redactions.scm
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
(pair value: (number) @redact)
|
||||||
|
(pair value: (string) @redact)
|
||||||
|
(array (number) @redact)
|
||||||
|
(array (string) @redact)
|
1
crates/zed/src/languages/toml/redactions.scm
Normal file
1
crates/zed/src/languages/toml/redactions.scm
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(pair (bare_key) "=" (_) @redact)
|
1
crates/zed/src/languages/yaml/redactions.scm
Normal file
1
crates/zed/src/languages/yaml/redactions.scm
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(block_mapping_pair value: (flow_node) @redact)
|
Loading…
Add table
Add a link
Reference in a new issue