mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
feat(files): Add Ignored Files and Folders setting, honoured everywhere
Name patterns the IDE treats as nonexistent (.DS_Store, __pycache__, *.pyc, VCS dirs, ...) were hard-coded in five places, and Git changes ignored none of them, so .DS_Store showed up as unversioned. files.ignoredNames (IDEA's default list) now drives the project view, navigation bar, index, watcher, Find in Files, inspections and the Git status.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
//! "Ignored Files and Folders" (IDEA: Settings › File Types): file names the IDE
|
||||
//! treats as nonexistent — hidden from the project view and navigation, never
|
||||
//! indexed or searched, never offered as unversioned changes.
|
||||
|
||||
/// IDEA's default list, plus `.jj` and the Windows shell files.
|
||||
pub const DEFAULT_IGNORED_NAMES: &[&str] = &[
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.rbc",
|
||||
"*.yarb",
|
||||
"*~",
|
||||
".DS_Store",
|
||||
".git",
|
||||
".hg",
|
||||
".jj",
|
||||
".svn",
|
||||
"CVS",
|
||||
"__pycache__",
|
||||
"_svn",
|
||||
"vssver.scc",
|
||||
"vssver2.scc",
|
||||
"Thumbs.db",
|
||||
"desktop.ini",
|
||||
];
|
||||
|
||||
/// Matches single path segments against name patterns (`*` and `?` wildcards).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct IgnoredNames {
|
||||
exact: Vec<String>,
|
||||
wildcards: Vec<String>,
|
||||
}
|
||||
|
||||
impl IgnoredNames {
|
||||
pub fn new(patterns: &[String]) -> Self {
|
||||
let mut out = Self::default();
|
||||
for p in patterns.iter().map(|p| p.trim()).filter(|p| !p.is_empty() && !p.contains('/')) {
|
||||
if p.contains(['*', '?']) {
|
||||
out.wildcards.push(p.to_owned());
|
||||
} else {
|
||||
out.exact.push(p.to_owned());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn is_ignored_name(&self, name: &str) -> bool {
|
||||
self.exact.iter().any(|e| e == name) || self.wildcards.iter().any(|w| wildcard(w.as_bytes(), name.as_bytes()))
|
||||
}
|
||||
|
||||
/// Whether any segment of a `/`-separated relative path is ignored.
|
||||
pub fn is_ignored_path(&self, rel: &str) -> bool {
|
||||
rel.split('/').any(|seg| !seg.is_empty() && self.is_ignored_name(seg))
|
||||
}
|
||||
}
|
||||
|
||||
fn wildcard(p: &[u8], s: &[u8]) -> bool {
|
||||
let (mut pi, mut si, mut star, mut mark) = (0, 0, None, 0);
|
||||
while si < s.len() {
|
||||
if pi < p.len() && (p[pi] == b'?' || p[pi] == s[si]) {
|
||||
pi += 1;
|
||||
si += 1;
|
||||
} else if pi < p.len() && p[pi] == b'*' {
|
||||
star = Some(pi);
|
||||
pi += 1;
|
||||
mark = si;
|
||||
} else if let Some(st) = star {
|
||||
pi = st + 1;
|
||||
mark += 1;
|
||||
si = mark;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
p[pi..].iter().all(|&c| c == b'*')
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn defaults() -> IgnoredNames {
|
||||
IgnoredNames::new(&DEFAULT_IGNORED_NAMES.iter().map(|s| s.to_string()).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_names_and_wildcards() {
|
||||
let m = defaults();
|
||||
for name in [".DS_Store", "__pycache__", "a.pyc", "notes.txt~", "Thumbs.db"] {
|
||||
assert!(m.is_ignored_name(name), "{name}");
|
||||
}
|
||||
for name in ["DS_Store", "a.py", "pyc", "src", ".gitignore"] {
|
||||
assert!(!m.is_ignored_name(name), "{name}");
|
||||
}
|
||||
assert!(wildcard(b"a?c*", b"abcdef"));
|
||||
assert!(!wildcard(b"a?c", b"abcd"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_any_path_segment() {
|
||||
let m = defaults();
|
||||
assert!(m.is_ignored_path("src/.DS_Store"));
|
||||
assert!(m.is_ignored_path("pkg/__pycache__/"));
|
||||
assert!(m.is_ignored_path("pkg/__pycache__/mod.cpython-312.pyc"));
|
||||
assert!(!m.is_ignored_path("src/main.rs"));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod editor_settings;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod ids;
|
||||
pub mod ignored;
|
||||
pub mod ipc_path;
|
||||
pub mod project_dir;
|
||||
pub mod settings;
|
||||
|
||||
@@ -303,6 +303,8 @@ pub struct FileSettings {
|
||||
/// Save on focus loss / tab switch, like IDEA.
|
||||
pub auto_save: bool,
|
||||
pub exclude: Vec<String>,
|
||||
/// Ignored Files and Folders: name patterns hidden everywhere; see [`crate::ignored`].
|
||||
pub ignored_names: Vec<String>,
|
||||
pub trim_trailing_whitespace: bool,
|
||||
pub ensure_final_newline: bool,
|
||||
/// Local History keeps changes from this many working days (days with changes), like IDEA.
|
||||
@@ -316,6 +318,7 @@ impl Default for FileSettings {
|
||||
Self {
|
||||
auto_save: true,
|
||||
exclude: vec!["**/.git".into(), "**/node_modules".into(), "**/target".into()],
|
||||
ignored_names: crate::ignored::DEFAULT_IGNORED_NAMES.iter().map(|s| (*s).into()).collect(),
|
||||
trim_trailing_whitespace: true,
|
||||
ensure_final_newline: true,
|
||||
local_history_days: 5,
|
||||
@@ -423,9 +426,9 @@ pub struct ProjectStructureSettings {
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// `files.exclude` plus the Project Structure excluded folders as globs.
|
||||
/// `files.exclude`, the ignored names and the Project Structure excluded folders as globs.
|
||||
pub fn exclude_globs(&self) -> Vec<String> {
|
||||
let mut out = self.files.exclude.clone();
|
||||
let mut out = self.file_excludes();
|
||||
for dir in &self.project_structure.excluded {
|
||||
let dir = dir.trim().trim_matches('/');
|
||||
if !dir.is_empty() {
|
||||
@@ -434,6 +437,17 @@ impl Settings {
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `files.exclude` plus `files.ignoredNames` (a bare name matches at any depth).
|
||||
pub fn file_excludes(&self) -> Vec<String> {
|
||||
let mut out = self.files.exclude.clone();
|
||||
out.extend(self.files.ignored_names.iter().filter(|n| !n.trim().is_empty()).cloned());
|
||||
out
|
||||
}
|
||||
|
||||
pub fn ignored_names(&self) -> crate::ignored::IgnoredNames {
|
||||
crate::ignored::IgnoredNames::new(&self.files.ignored_names)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -9,8 +9,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
/// VCS metadata directories are never indexed.
|
||||
const ALWAYS_SKIP: &[&str] = &[".git", ".hg", ".svn", ".jj", ".DS_Store"];
|
||||
/// VCS metadata directories are never indexed, whatever the ignored names say.
|
||||
const ALWAYS_SKIP: &[&str] = &[".git", ".hg", ".svn", ".jj"];
|
||||
|
||||
pub(crate) struct PathFilter {
|
||||
root: PathBuf,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! writes (commit, checkout, push/pull, stash, merge) shell out to `git` so
|
||||
//! hooks, credentials and signing behave exactly as on the command line.
|
||||
|
||||
use ide_core::ignored::IgnoredNames;
|
||||
use ide_core::{CancellationToken, IdeResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
@@ -283,6 +284,7 @@ pub struct Repo {
|
||||
root: PathBuf,
|
||||
git_dir: PathBuf,
|
||||
inner: std::sync::Mutex<repo::Cache>,
|
||||
ignored: std::sync::RwLock<IgnoredNames>,
|
||||
}
|
||||
|
||||
impl Repo {
|
||||
@@ -294,6 +296,10 @@ impl Repo {
|
||||
pub fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
/// Untracked files with one of these names (or under such a folder) are not reported.
|
||||
pub fn set_ignored_names(&self, names: IgnoredNames) {
|
||||
*self.ignored.write().unwrap_or_else(|p| p.into_inner()) = names;
|
||||
}
|
||||
/// Staged (HEAD↔index) and unstaged (index↔worktree, incl. untracked) changes,
|
||||
/// sorted by path. Conflicted files report `unstaged: Conflicted` and no staged kind.
|
||||
pub fn status(&self) -> IdeResult<Vec<FileStatus>> {
|
||||
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn discover(path: &Path) -> IdeResult<Option<Repo>> {
|
||||
let common_dir = absolute(repo.common_dir())?;
|
||||
let files = config_files(&git_dir, &common_dir);
|
||||
let cache = Cache { config_stamp: stamp(&files), config_files: files, repo: ts };
|
||||
Ok(Some(Repo { root, git_dir, inner: std::sync::Mutex::new(cache) }))
|
||||
Ok(Some(Repo { root, git_dir, inner: std::sync::Mutex::new(cache), ignored: Default::default() }))
|
||||
}
|
||||
|
||||
fn absolute(p: &Path) -> IdeResult<PathBuf> {
|
||||
|
||||
@@ -18,6 +18,7 @@ pub(crate) fn status(r: &Repo) -> IdeResult<Vec<FileStatus>> {
|
||||
let repo = r.gix()?;
|
||||
let mut map: BTreeMap<String, Entry> = BTreeMap::new();
|
||||
let mut conflicts: HashSet<String> = HashSet::new();
|
||||
let ignored = r.ignored.read().unwrap_or_else(|p| p.into_inner()).clone();
|
||||
|
||||
let platform = repo
|
||||
.status(gix::progress::Discard)
|
||||
@@ -50,6 +51,9 @@ pub(crate) fn status(r: &Repo) -> IdeResult<Vec<FileStatus>> {
|
||||
continue;
|
||||
}
|
||||
let mut path = to_string(entry.rela_path.as_ref());
|
||||
if ignored.is_ignored_path(&path) {
|
||||
continue;
|
||||
}
|
||||
if entry.disk_kind.is_some_and(|k| k.is_dir()) && !path.ends_with('/') {
|
||||
path.push('/');
|
||||
}
|
||||
|
||||
@@ -192,8 +192,12 @@ fn status_matrix() {
|
||||
fx.write("dir/nested/untracked2.txt", "u\n");
|
||||
fx.write("debug.log", "ignored\n");
|
||||
fx.write("build/out.bin", "ignored\n");
|
||||
fx.write(".DS_Store", "junk");
|
||||
fx.write("dir/.DS_Store", "junk");
|
||||
fx.write("pkg/__pycache__/m.pyc", "junk");
|
||||
|
||||
let repo = fx.repo();
|
||||
repo.set_ignored_names(ide_core::settings::Settings::default().ignored_names());
|
||||
let st = repo.status().unwrap();
|
||||
let check = |rel: &str, staged: Option<ChangeKind>, unstaged: Option<ChangeKind>| {
|
||||
let s = status_of(&st, rel);
|
||||
@@ -213,6 +217,7 @@ fn status_matrix() {
|
||||
assert_eq!(status_of(&st, "renamed.txt").renamed_from.as_deref(), Some("rename_me.txt"));
|
||||
assert!(!st.iter().any(|s| s.rel_path == "rename_me.txt"), "rename source listed: {st:#?}");
|
||||
assert!(!st.iter().any(|s| s.rel_path.contains("debug.log") || s.rel_path.contains("build")));
|
||||
assert!(!st.iter().any(|s| s.rel_path.contains(".DS_Store") || s.rel_path.contains("pkg")), "{st:#?}");
|
||||
// Sorted by path.
|
||||
let paths: Vec<_> = st.iter().map(|s| s.rel_path.clone()).collect();
|
||||
let mut sorted = paths.clone();
|
||||
|
||||
@@ -23,11 +23,9 @@ use std::time::Instant;
|
||||
use tauri::ipc::Channel;
|
||||
use tauri::{State, Window};
|
||||
|
||||
/// Files of `scope`, minus excluded roots.
|
||||
/// Files of `scope`, minus excluded roots and ignored names.
|
||||
fn files_in_scope(project: &Project, scope: &SearchScope, cancel: &CancellationToken) -> IdeResult<Vec<SourceFile>> {
|
||||
let excluded: Vec<String> =
|
||||
project.roots().excluded_globs().iter().map(|d| format!("{}/**", d.trim_start_matches('/'))).collect();
|
||||
let mask = FileMask::new(&[], &excluded)?;
|
||||
let mask = FileMask::new(&[], &project.search_excludes())?;
|
||||
Ok(ide_search::scope_files(&project.root, scope, &mask, None, cancel)?
|
||||
.into_iter()
|
||||
.map(|f| SourceFile { path: f.path, rel_path: f.rel_path })
|
||||
|
||||
@@ -29,9 +29,10 @@ pub async fn search_start(
|
||||
on_event: Channel<SearchEvent>,
|
||||
) -> IdeResult<()> {
|
||||
let project = state.project(window.label())?;
|
||||
// Excluded roots (Project Structure marks and detected `target`, `node_modules`, …) are never searched.
|
||||
// Excluded roots (Project Structure marks and detected `target`, `node_modules`, …) and
|
||||
// ignored names are never searched.
|
||||
query.exclude.extend(project.search_excludes());
|
||||
let roots = project.roots();
|
||||
query.exclude.extend(roots.excluded_globs().iter().map(|d| format!("{}/**", d.trim_start_matches('/'))));
|
||||
let set = match query.scope {
|
||||
ide_search::SearchScope::FileSet { set } => Some(set),
|
||||
_ => None,
|
||||
|
||||
+23
-4
@@ -376,9 +376,13 @@ impl AppState {
|
||||
impl Project {
|
||||
/// Runs `f` with the git repository, or fails when the project is not under git.
|
||||
pub fn with_repo<T>(&self, f: impl FnOnce(&Repo) -> IdeResult<T>) -> IdeResult<T> {
|
||||
crate::commands::vcs::apply_git_executable(&self.settings.effective());
|
||||
let settings = self.settings.effective();
|
||||
crate::commands::vcs::apply_git_executable(&settings);
|
||||
match self.repo.lock().as_ref() {
|
||||
Some(repo) => f(repo),
|
||||
Some(repo) => {
|
||||
repo.set_ignored_names(settings.ignored_names());
|
||||
f(repo)
|
||||
}
|
||||
None => Err(IdeError::unavailable("project is not a git repository")),
|
||||
}
|
||||
}
|
||||
@@ -436,7 +440,22 @@ impl Project {
|
||||
self.cache_dir.join("build")
|
||||
}
|
||||
|
||||
/// `files.exclude` plus the effective excluded roots (user-marked and detected).
|
||||
/// File masks (`ide_search::FileMask`) for Find in Files and inspections: the
|
||||
/// excluded roots and the ignored names (files, and everything under such folders).
|
||||
pub fn search_excludes(&self) -> Vec<String> {
|
||||
let mut out: Vec<String> =
|
||||
self.roots().excluded_globs().iter().map(|d| format!("{}/**", d.trim_start_matches('/'))).collect();
|
||||
for name in &self.settings.effective().files.ignored_names {
|
||||
let name = name.trim();
|
||||
if !name.is_empty() && !name.contains(['/', ',']) {
|
||||
out.push(name.to_owned());
|
||||
out.push(format!("**/{name}/**"));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `files.exclude`, the ignored names and the effective excluded roots (user-marked and detected).
|
||||
pub fn exclude_globs(&self) -> Vec<String> {
|
||||
exclude_globs(&self.settings.effective(), &self.detected_roots.read())
|
||||
}
|
||||
@@ -509,7 +528,7 @@ pub fn history_retention(settings: &ide_core::settings::Settings) -> ide_workspa
|
||||
}
|
||||
|
||||
fn exclude_globs(settings: &ide_core::settings::Settings, detected: &[SourceFolder]) -> Vec<String> {
|
||||
let mut globs = settings.files.exclude.clone();
|
||||
let mut globs = settings.file_excludes();
|
||||
globs.extend(ProjectRoots::compute(detected.to_vec(), &settings.project_structure).excluded_globs());
|
||||
globs
|
||||
}
|
||||
|
||||
@@ -1854,6 +1854,8 @@ export type FileSettings = {
|
||||
/** Save on focus loss / tab switch, like IDEA. */
|
||||
autoSave?: boolean,
|
||||
exclude?: string[],
|
||||
/** Ignored Files and Folders: name patterns hidden everywhere; see [`crate::ignored`]. */
|
||||
ignoredNames?: string[],
|
||||
trimTrailingWhitespace?: boolean,
|
||||
ensureFinalNewline?: boolean,
|
||||
/** Local History keeps changes from this many working days (days with changes), like IDEA. */
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Navigation bar popup (⌘↑) model: the path of the active file as segments
|
||||
* (project › dirs › file › symbols at the caret) and the child list of a segment.
|
||||
*/
|
||||
import { isIgnoredName } from "@/platform/ignoredNames";
|
||||
import type { FileEntry, OutlineItem } from "@/platform/ipc";
|
||||
import { badgeKind, fileIconName } from "@/ui";
|
||||
|
||||
@@ -19,7 +20,11 @@ const parentOf = (p: string) => p.slice(0, Math.max(p.lastIndexOf("/"), 0)) || "
|
||||
const nameOf = (p: string) => p.slice(p.lastIndexOf("/") + 1);
|
||||
|
||||
/** Segments for `path`; directories from the project root, or just the parent folder outside it. */
|
||||
export function navSegments(project: { root: string; name: string } | null, path: string, chain: readonly OutlineItem[]): NavSegment[] {
|
||||
export function navSegments(
|
||||
project: { root: string; name: string } | null,
|
||||
path: string,
|
||||
chain: readonly OutlineItem[],
|
||||
): NavSegment[] {
|
||||
const out: NavSegment[] = [];
|
||||
const parent = parentOf(path);
|
||||
if (project && (parent === project.root || parent.startsWith(project.root + "/"))) {
|
||||
@@ -42,12 +47,17 @@ export function navSegments(project: { root: string; name: string } | null, path
|
||||
return out;
|
||||
}
|
||||
|
||||
const HIDDEN = new Set([".git", ".hg", ".svn", ".DS_Store", "__pycache__"]);
|
||||
|
||||
export function dirChildren(entries: readonly FileEntry[]): NavChild[] {
|
||||
return entries
|
||||
.filter((e) => !HIDDEN.has(e.name))
|
||||
.map((e) => ({ kind: e.kind === "dir" || (e.kind === "symlink" && e.size === 0) ? "dir" : "file", path: e.path, label: e.name }) as NavChild);
|
||||
.filter((e) => !isIgnoredName(e.name))
|
||||
.map(
|
||||
(e) =>
|
||||
({
|
||||
kind: e.kind === "dir" || (e.kind === "symlink" && e.size === 0) ? "dir" : "file",
|
||||
path: e.path,
|
||||
label: e.name,
|
||||
}) as NavChild,
|
||||
);
|
||||
}
|
||||
|
||||
/** Children of a file or symbol segment: its outline items (one level). */
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* incremental refresh for `fsChanged` events. No React, no IPC.
|
||||
*/
|
||||
import type { FileEntry } from "@/bindings";
|
||||
import { isIgnoredName } from "@/platform/ignoredNames";
|
||||
import { isSameOrInside, nameOf, parentOf, relativeTo } from "./paths";
|
||||
|
||||
export interface TreeOptions {
|
||||
/** Show IDE-hidden names (`.git`, `.DS_Store`, …). Dotfiles are always shown. */
|
||||
/** Show ignored names (`files.ignoredNames`: `.git`, `.DS_Store`, …). Dotfiles are always shown. */
|
||||
showHidden: boolean;
|
||||
/** Show entries matched by .gitignore / exclude settings (dimmed). */
|
||||
showExcluded: boolean;
|
||||
@@ -53,16 +54,9 @@ export interface TreeModel {
|
||||
|
||||
export type Listings = ReadonlyMap<string, readonly FileEntry[]>;
|
||||
|
||||
/** Names IDEA hides by default ("Ignored Files and Folders"). */
|
||||
const IDE_HIDDEN = new Set([".git", ".hg", ".svn", "CVS", "_svn", ".DS_Store", "__pycache__", "vssver.scc", "vssver2.scc"]);
|
||||
const IDE_HIDDEN_EXT = /\.(pyc|pyo|rbc|yarb)$|~$/;
|
||||
|
||||
export function isIdeHidden(name: string): boolean {
|
||||
return IDE_HIDDEN.has(name) || IDE_HIDDEN_EXT.test(name);
|
||||
}
|
||||
|
||||
export function isVisible(entry: FileEntry, opts: TreeOptions): boolean {
|
||||
if (!opts.showHidden && isIdeHidden(entry.name)) return false;
|
||||
// Ignored names are also excluded on the backend; only "Show Hidden" brings them back.
|
||||
if (isIgnoredName(entry.name)) return opts.showHidden;
|
||||
if (!opts.showExcluded && entry.ignored) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* so `validateSettings` never throws — anything unknown or malformed falls
|
||||
* back to the default (mirrors `ide-core::settings` defaults).
|
||||
*/
|
||||
import { DEFAULT_IGNORED_NAMES } from "@/platform/ignoredNames";
|
||||
import type { Settings } from "@/platform/ipc";
|
||||
import {
|
||||
DEFAULT_LANGUAGES,
|
||||
@@ -58,6 +59,7 @@ export interface ValidSettings extends WorkflowSettings {
|
||||
files: {
|
||||
autoSave: boolean;
|
||||
exclude: string[];
|
||||
ignoredNames: string[];
|
||||
trimTrailingWhitespace: boolean;
|
||||
ensureFinalNewline: boolean;
|
||||
/** Local History keeps this many working days (days with changes). */
|
||||
@@ -95,6 +97,7 @@ export const DEFAULT_SETTINGS: ValidSettings = {
|
||||
files: {
|
||||
autoSave: true,
|
||||
exclude: ["**/.git", "**/node_modules", "**/target"],
|
||||
ignoredNames: [...DEFAULT_IGNORED_NAMES],
|
||||
trimTrailingWhitespace: true,
|
||||
ensureFinalNewline: true,
|
||||
localHistoryDays: 5,
|
||||
@@ -188,6 +191,9 @@ export function validateSettings(raw: Settings | unknown): ValidSettings {
|
||||
exclude: Array.isArray(f.exclude)
|
||||
? f.exclude.filter((x): x is string => typeof x === "string" && !!x.trim())
|
||||
: [...d.files.exclude],
|
||||
ignoredNames: Array.isArray(f.ignoredNames)
|
||||
? f.ignoredNames.filter((x): x is string => typeof x === "string" && !!x.trim())
|
||||
: [...d.files.ignoredNames],
|
||||
trimTrailingWhitespace: bool(f.trimTrailingWhitespace, d.files.trimTrailingWhitespace),
|
||||
ensureFinalNewline: bool(f.ensureFinalNewline, d.files.ensureFinalNewline),
|
||||
localHistoryDays: num(f.localHistoryDays, d.files.localHistoryDays, LIMITS.localHistoryDays, true),
|
||||
|
||||
@@ -20,6 +20,19 @@ function ExcludeRow() {
|
||||
);
|
||||
}
|
||||
|
||||
function IgnoredNamesRow() {
|
||||
const [names, setNames] = useSetting<string[]>("files.ignoredNames");
|
||||
return (
|
||||
<Row
|
||||
label="Ignore files and folders"
|
||||
stacked
|
||||
hint="Names (e.g. .DS_Store, *.pyc) hidden from the project tree, navigation bar, search, indexing and Git changes."
|
||||
>
|
||||
<GlobListEditor value={names} onChange={setNames} placeholder="*.ext" aria-label="Ignored names" />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export const BUILTIN_PAGES: PageSpec[] = [
|
||||
{
|
||||
id: "keymap",
|
||||
@@ -39,7 +52,10 @@ export const BUILTIN_PAGES: PageSpec[] = [
|
||||
sections: [
|
||||
{
|
||||
title: "Ignored Files and Folders",
|
||||
options: [{ kind: "custom", label: "Exclude patterns", component: ExcludeRow }],
|
||||
options: [
|
||||
{ kind: "custom", label: "Ignore files and folders", component: IgnoredNamesRow },
|
||||
{ kind: "custom", label: "Exclude patterns", component: ExcludeRow },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/** "Ignored Files and Folders" (`files.ignoredNames`): names hidden everywhere; mirrors `ide_core::ignored`. */
|
||||
import { useProject } from "./project";
|
||||
|
||||
/** Defaults of `ide_core::ignored::DEFAULT_IGNORED_NAMES`; used until settings load. */
|
||||
export const DEFAULT_IGNORED_NAMES: readonly string[] = [
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.rbc",
|
||||
"*.yarb",
|
||||
"*~",
|
||||
".DS_Store",
|
||||
".git",
|
||||
".hg",
|
||||
".jj",
|
||||
".svn",
|
||||
"CVS",
|
||||
"__pycache__",
|
||||
"_svn",
|
||||
"vssver.scc",
|
||||
"vssver2.scc",
|
||||
"Thumbs.db",
|
||||
"desktop.ini",
|
||||
];
|
||||
|
||||
let cached: { names: readonly string[]; exact: Set<string>; wildcards: RegExp[] } | null = null;
|
||||
|
||||
function compile(names: readonly string[]) {
|
||||
const exact = new Set<string>();
|
||||
const wildcards: RegExp[] = [];
|
||||
for (const raw of names) {
|
||||
const n = raw.trim();
|
||||
if (!n || n.includes("/")) continue;
|
||||
if (!/[*?]/.test(n)) exact.add(n);
|
||||
else {
|
||||
const body = n
|
||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
||||
.replace(/\*/g, ".*")
|
||||
.replace(/\?/g, ".");
|
||||
wildcards.push(new RegExp(`^${body}$`, "s"));
|
||||
}
|
||||
}
|
||||
return { names, exact, wildcards };
|
||||
}
|
||||
|
||||
export function isIgnoredName(name: string): boolean {
|
||||
const names = useProject.getState().settings?.files?.ignoredNames ?? DEFAULT_IGNORED_NAMES;
|
||||
const m = cached?.names === names ? cached : (cached = compile(names));
|
||||
return m.exact.has(name) || m.wildcards.some((re) => re.test(name));
|
||||
}
|
||||
Reference in New Issue
Block a user