дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02: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.
Цей коміт міститься в:
1 джерело
bf06ca7832
коміт
a66f01ec87
17 files changed
+262
-31
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
Заблокувати користувача