Dotfiles, and distinct number color !53
9 files changed, +85 −23
Layout: unified · split
Sources/OrgApp/AgendaModel.swift +2 −1
| @@ -35,7 +35,8 @@ public final class AgendaModel { | |||
| 35 | /// Emacs lists a directory in `org-agenda-files`. | 35 | /// Emacs lists a directory in `org-agenda-files`. |
| 36 | public var files: [String] { | 36 | public var files: [String] { |
| 37 | workspace.files | 37 | workspace.files |
| 38 | .filter { $0.kind == .org && (includeSubfolders || !$0.relativePath.contains("/")) } | 38 | // Dotfiles stay out, as `org-agenda-file-regexp` leaves them out. |
| 39 | .filter { $0.kind == .org && !$0.name.hasPrefix(".") && (includeSubfolders || !$0.relativePath.contains("/")) } | ||
| 39 | .map(\.path) | 40 | .map(\.path) |
| 40 | .sorted() | 41 | .sorted() |
| 41 | } | 42 | } |
Sources/OrgApp/UserConfig.swift +6
| @@ -1,6 +1,7 @@ | |||
| 1 | import Foundation | 1 | import Foundation |
| 2 | import OrgCore | 2 | import OrgCore |
| 3 | import OrgPresentation | 3 | import OrgPresentation |
| 4 | import OrgWorkspace | ||
| 4 | 5 | ||
| 5 | /// The configuration folder (`$XDG_CONFIG_HOME/orgstar`, else `~/.config/orgstar`) and | 6 | /// The configuration folder (`$XDG_CONFIG_HOME/orgstar`, else `~/.config/orgstar`) and |
| 6 | /// `config.toml` in it: every setting of the Settings window as text, to edit, keep in a | 7 | /// `config.toml` in it: every setting of the Settings window as text, to edit, keep in a |
| @@ -100,6 +101,11 @@ public enum UserConfig { | |||
| 100 | comment: "left, right, both or none", legacy: "general.option-as-meta"), | 101 | comment: "left, right, both or none", legacy: "general.option-as-meta"), |
| 101 | Setting(section: "orgstar", key: "show-markup", defaultsKey: "showMarkup", kind: .bool, fallback: .bool(false), | 102 | Setting(section: "orgstar", key: "show-markup", defaultsKey: "showMarkup", kind: .bool, fallback: .bool(false), |
| 102 | comment: "show link brackets and emphasis markers (View ▸ Show Markup)", legacy: "editor.show-markup"), | 103 | comment: "show link brackets and emphasis markers (View ▸ Show Markup)", legacy: "editor.show-markup"), |
| 104 | Setting(section: "orgstar", key: "show-hidden-files", defaultsKey: "showHiddenFiles", kind: .bool, fallback: .bool(true), | ||
| 105 | comment: "list dotfiles and dot folders in your folders", legacy: ""), | ||
| 106 | Setting(section: "orgstar", key: "ignored-folders", defaultsKey: "ignoredFolders", kind: .string, | ||
| 107 | fallback: .string(ScanRules.defaultIgnoredFolders.sorted().joined(separator: " ")), | ||
| 108 | comment: "folder names never listed or searched, separated by spaces", legacy: ""), | ||
| 103 | Setting(section: "orgstar", key: "agenda-include-subfolders", defaultsKey: "agendaSubfolders", kind: .bool, fallback: .bool(false), | 109 | Setting(section: "orgstar", key: "agenda-include-subfolders", defaultsKey: "agendaSubfolders", kind: .bool, fallback: .bool(false), |
| 104 | comment: "the agenda reads org files in subfolders too", legacy: "agenda.include-subfolders"), | 110 | comment: "the agenda reads org files in subfolders too", legacy: "agenda.include-subfolders"), |
| 105 | Setting(section: "orgstar", key: "reminders", defaultsKey: "reminders", kind: .bool, fallback: .bool(true), | 111 | Setting(section: "orgstar", key: "reminders", defaultsKey: "reminders", kind: .bool, fallback: .bool(true), |
Sources/OrgApp/WorkspaceModel.swift +10 −2
| @@ -104,11 +104,19 @@ public final class WorkspaceModel { | |||
| 104 | 104 | ||
| 105 | // MARK: - Indexing | 105 | // MARK: - Indexing |
| 106 | 106 | ||
| 107 | /// Which files the folders show; a change rescans them. | ||
| 108 | public var scanRules = ScanRules() { | ||
| 109 | didSet { | ||
| 110 | guard scanRules != oldValue else { return } | ||
| 111 | Task { await refresh() } | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 107 | /// Reconciles every root off the main thread, then reloads the file list. | 115 | /// Reconciles every root off the main thread, then reloads the file list. |
| 108 | public func refresh() async { | 116 | public func refresh() async { |
| 109 | isIndexing = true | 117 | isIndexing = true |
| 110 | defer { isIndexing = false } | 118 | defer { isIndexing = false } |
| 111 | let reconciler = Reconciler(index: index, settings: settings) | 119 | let reconciler = Reconciler(index: index, settings: settings, rules: scanRules) |
| 112 | for url in resolvedRoots.values { | 120 | for url in resolvedRoots.values { |
| 113 | do { | 121 | do { |
| 114 | _ = try await Task.detached { try reconciler.reconcile(root: url) }.value | 122 | _ = try await Task.detached { try reconciler.reconcile(root: url) }.value |
| @@ -129,7 +137,7 @@ public final class WorkspaceModel { | |||
| 129 | 137 | ||
| 130 | /// Reconciles the paths a batch of file events named, or whole roots when events were lost. | 138 | /// Reconciles the paths a batch of file events named, or whole roots when events were lost. |
| 131 | public func filesChanged(_ paths: [URL], rescan roots: [URL]) async { | 139 | public func filesChanged(_ paths: [URL], rescan roots: [URL]) async { |
| 132 | let reconciler = Reconciler(index: index, settings: settings) | 140 | let reconciler = Reconciler(index: index, settings: settings, rules: scanRules) |
| 133 | for rootURL in roots { | 141 | for rootURL in roots { |
| 134 | _ = try? await Task.detached { try reconciler.reconcile(root: rootURL) }.value | 142 | _ = try? await Task.detached { try reconciler.reconcile(root: rootURL) }.value |
| 135 | } | 143 | } |
Sources/OrgPresentation/ThemeSpec.swift +2 −2
| @@ -143,7 +143,7 @@ public struct ThemeSpec: Equatable, Sendable { | |||
| 143 | "block-background": "#f6f8fa", "block-delimiter": "#8c959f", "table": "#1f2328", | 143 | "block-background": "#f6f8fa", "block-delimiter": "#8c959f", "table": "#1f2328", |
| 144 | "line-number": "#8c959f", "line-number-current": "#1f2328", | 144 | "line-number": "#8c959f", "line-number-current": "#1f2328", |
| 145 | "syntax-keyword": "#cf222e", "syntax-string": "#0a3069", "syntax-comment": "#6e7781", "syntax-function": "#8250df", | 145 | "syntax-keyword": "#cf222e", "syntax-string": "#0a3069", "syntax-comment": "#6e7781", "syntax-function": "#8250df", |
| 146 | "syntax-type": "#953800", "syntax-number": "#0550ae", "syntax-property": "#0550ae", "syntax-label": "#8250df", | 146 | "syntax-type": "#953800", "syntax-number": "#1b7c83", "syntax-property": "#0550ae", "syntax-label": "#8250df", |
| 147 | "modeline-highlight": "#1f2328", "state-normal": "#0969da", "state-insert": "#1a7f37", "state-visual": "#bc4c00", | 147 | "modeline-highlight": "#1f2328", "state-normal": "#0969da", "state-insert": "#1a7f37", "state-visual": "#bc4c00", |
| 148 | "agenda-date": "#1f2328", "agenda-today": "#0969da", "agenda-time": "#6e7781", "agenda-category": "#6e7781", | 148 | "agenda-date": "#1f2328", "agenda-today": "#0969da", "agenda-time": "#6e7781", "agenda-category": "#6e7781", |
| 149 | "agenda-deadline": "#cf222e", "agenda-upcoming": "#bc4c00", "agenda-scheduled": "#6e7781", "agenda-scheduled-past": "#953800", | 149 | "agenda-deadline": "#cf222e", "agenda-upcoming": "#bc4c00", "agenda-scheduled": "#6e7781", "agenda-scheduled-past": "#953800", |
| @@ -159,7 +159,7 @@ public struct ThemeSpec: Equatable, Sendable { | |||
| 159 | "block-background": "#161b22", "block-delimiter": "#6e7681", "table": "#e6edf3", | 159 | "block-background": "#161b22", "block-delimiter": "#6e7681", "table": "#e6edf3", |
| 160 | "line-number": "#6e7681", "line-number-current": "#e6edf3", | 160 | "line-number": "#6e7681", "line-number-current": "#e6edf3", |
| 161 | "syntax-keyword": "#ff7b72", "syntax-string": "#a5d6ff", "syntax-comment": "#8b949e", "syntax-function": "#d2a8ff", | 161 | "syntax-keyword": "#ff7b72", "syntax-string": "#a5d6ff", "syntax-comment": "#8b949e", "syntax-function": "#d2a8ff", |
| 162 | "syntax-type": "#ffa657", "syntax-number": "#79c0ff", "syntax-property": "#79c0ff", "syntax-label": "#d2a8ff", | 162 | "syntax-type": "#ffa657", "syntax-number": "#56d4dd", "syntax-property": "#79c0ff", "syntax-label": "#d2a8ff", |
| 163 | "modeline-highlight": "#e6edf3", "state-normal": "#1f6feb", "state-insert": "#238636", "state-visual": "#9e6a03", | 163 | "modeline-highlight": "#e6edf3", "state-normal": "#1f6feb", "state-insert": "#238636", "state-visual": "#9e6a03", |
| 164 | "agenda-date": "#e6edf3", "agenda-today": "#58a6ff", "agenda-time": "#8b949e", "agenda-category": "#8b949e", | 164 | "agenda-date": "#e6edf3", "agenda-today": "#58a6ff", "agenda-time": "#8b949e", "agenda-category": "#8b949e", |
| 165 | "agenda-deadline": "#ff7b72", "agenda-upcoming": "#ffa657", "agenda-scheduled": "#8b949e", "agenda-scheduled-past": "#d29922", | 165 | "agenda-deadline": "#ff7b72", "agenda-upcoming": "#ffa657", "agenda-scheduled": "#8b949e", "agenda-scheduled-past": "#d29922", |
Sources/OrgWorkspace/Discovery.swift +37 −10
| @@ -41,17 +41,43 @@ public enum FileClassification: Sendable, Equatable { | |||
| 41 | case ignored | 41 | case ignored |
| 42 | } | 42 | } |
| 43 | 43 | ||
| 44 | /// Which files and folders a scan takes in. | ||
| 45 | public struct ScanRules: Sendable, Equatable { | ||
| 46 | /// Dotfiles and dot folders, other than ignored folders. | ||
| 47 | public var hiddenFiles: Bool | ||
| 48 | /// Folder names never looked into: version control, caches, build output. | ||
| 49 | public var ignoredFolders: Set<String> | ||
| 50 | |||
| 51 | public static let defaultIgnoredFolders: Set<String> = [ | ||
| 52 | ".git", ".hg", ".svn", ".jj", ".bzr", ".stfolder", ".stversions", ".Trash", ".Spotlight-V100", ".fseventsd", | ||
| 53 | ".build", ".venv", ".cache", ".tox", ".mypy_cache", ".pytest_cache", ".gradle", ".next", ".terraform", "node_modules", | ||
| 54 | ] | ||
| 55 | |||
| 56 | public init(hiddenFiles: Bool = true, ignoredFolders: Set<String> = defaultIgnoredFolders) { | ||
| 57 | self.hiddenFiles = hiddenFiles | ||
| 58 | self.ignoredFolders = ignoredFolders | ||
| 59 | } | ||
| 60 | |||
| 61 | public func skipsFolder(_ name: String) -> Bool { | ||
| 62 | ignoredFolders.contains(name) || (!hiddenFiles && name.hasPrefix(".")) | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 44 | public enum WorkspaceScanner { | 66 | public enum WorkspaceScanner { |
| 45 | /// What a file name means to the workspace. Syncthing temp files, our own temp and backup | 67 | /// What a file name means to the workspace. Syncthing temp files, our own temp and backup |
| 46 | /// files, Emacs backups and auto-saves, and other hidden files are ignored; files that | 68 | /// files, Emacs backups, auto-saves and lock files, and Finder's files are ignored, and |
| 47 | /// aren't org files are `.other`. | 69 | /// other dotfiles unless `rules` takes them in; files that aren't org files are `.other`. |
| 48 | public static func classify(_ name: String) -> FileClassification { | 70 | public static func classify(_ name: String, rules: ScanRules = ScanRules()) -> FileClassification { |
| 49 | if name.hasPrefix(".") { | 71 | if name.hasPrefix(".") { |
| 50 | if name.hasSuffix(".icloud") { | 72 | if name.hasSuffix(".icloud") { |
| 51 | let inner = String(name.dropFirst().dropLast(".icloud".count)) | 73 | let inner = String(name.dropFirst().dropLast(".icloud".count)) |
| 52 | if case .file = classify(inner) { return .placeholder(inner) } | 74 | if case .file = classify(inner, rules: rules) { return .placeholder(inner) } |
| 75 | return .ignored | ||
| 76 | } | ||
| 77 | if !rules.hiddenFiles || name.hasPrefix(".#") || name.hasPrefix(".syncthing.") || name.contains(".orgstar-") | ||
| 78 | || name == ".DS_Store" || name == ".localized" { | ||
| 79 | return .ignored | ||
| 53 | } | 80 | } |
| 54 | return .ignored | ||
| 55 | } | 81 | } |
| 56 | if name.hasSuffix("~") || name.hasPrefix("#") { return .ignored } | 82 | if name.hasSuffix("~") || name.hasPrefix("#") { return .ignored } |
| 57 | let conflict = name.contains(".sync-conflict-") | 83 | let conflict = name.contains(".sync-conflict-") |
| @@ -60,8 +86,8 @@ public enum WorkspaceScanner { | |||
| 60 | return .file(.other) | 86 | return .file(.other) |
| 61 | } | 87 | } |
| 62 | 88 | ||
| 63 | /// Every file under `root`, skipping hidden folders (`.git`, `.stfolder`, `.stversions`). | 89 | /// Every file under `root`, skipping the folders `rules` ignores. |
| 64 | public static func scan(_ root: URL) throws -> Discovery { | 90 | public static func scan(_ root: URL, rules: ScanRules = ScanRules()) throws -> Discovery { |
| 65 | let keys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey, .contentModificationDateKey] | 91 | let keys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey, .contentModificationDateKey] |
| 66 | guard let enumerator = FileManager.default.enumerator(at: root, includingPropertiesForKeys: keys) else { | 92 | guard let enumerator = FileManager.default.enumerator(at: root, includingPropertiesForKeys: keys) else { |
| 67 | return Discovery() | 93 | return Discovery() |
| @@ -71,10 +97,10 @@ public enum WorkspaceScanner { | |||
| 71 | let values = try url.resourceValues(forKeys: Set(keys)) | 97 | let values = try url.resourceValues(forKeys: Set(keys)) |
| 72 | let name = url.lastPathComponent | 98 | let name = url.lastPathComponent |
| 73 | if values.isDirectory == true { | 99 | if values.isDirectory == true { |
| 74 | if name.hasPrefix(".") { enumerator.skipDescendants() } | 100 | if rules.skipsFolder(name) { enumerator.skipDescendants() } |
| 75 | continue | 101 | continue |
| 76 | } | 102 | } |
| 77 | switch classify(name) { | 103 | switch classify(name, rules: rules) { |
| 78 | case .file(let kind): | 104 | case .file(let kind): |
| 79 | discovery.files.append(DiscoveredFile( | 105 | discovery.files.append(DiscoveredFile( |
| 80 | url: url.standardizedFileURL, | 106 | url: url.standardizedFileURL, |
| @@ -103,7 +129,8 @@ public struct AgendaScope: Sendable, Equatable { | |||
| 103 | } | 129 | } |
| 104 | 130 | ||
| 105 | public func contains(relativePath: String, kind: FileKind) -> Bool { | 131 | public func contains(relativePath: String, kind: FileKind) -> Bool { |
| 106 | guard kind == .org else { return false } | 132 | // `org-agenda-file-regexp` leaves out dotfiles. |
| 133 | guard kind == .org, !(relativePath as NSString).lastPathComponent.hasPrefix(".") else { return false } | ||
| 107 | guard !globs.isEmpty else { return true } | 134 | guard !globs.isEmpty else { return true } |
| 108 | // Without FNM_PATHNAME, `*` also matches `/`, so `work/*` covers nested folders. | 135 | // Without FNM_PATHNAME, `*` also matches `/`, so `work/*` covers nested folders. |
| 109 | return globs.contains { fnmatch($0, relativePath, 0) == 0 } | 136 | return globs.contains { fnmatch($0, relativePath, 0) == 0 } |
Sources/OrgWorkspace/Reconciler.swift +7 −5
| @@ -16,15 +16,17 @@ public struct ReconcileReport: Sendable, Equatable { | |||
| 16 | public struct Reconciler: Sendable { | 16 | public struct Reconciler: Sendable { |
| 17 | public let index: IndexStore | 17 | public let index: IndexStore |
| 18 | public let settings: IndexSettings | 18 | public let settings: IndexSettings |
| 19 | public let rules: ScanRules | ||
| 19 | 20 | ||
| 20 | public init(index: IndexStore, settings: IndexSettings) { | 21 | public init(index: IndexStore, settings: IndexSettings, rules: ScanRules = ScanRules()) { |
| 21 | self.index = index | 22 | self.index = index |
| 22 | self.settings = settings | 23 | self.settings = settings |
| 24 | self.rules = rules | ||
| 23 | } | 25 | } |
| 24 | 26 | ||
| 25 | /// Reconciles the whole root. | 27 | /// Reconciles the whole root. |
| 26 | public func reconcile(root: URL) throws -> ReconcileReport { | 28 | public func reconcile(root: URL) throws -> ReconcileReport { |
| 27 | let discovery = try WorkspaceScanner.scan(root) | 29 | let discovery = try WorkspaceScanner.scan(root, rules: rules) |
| 28 | for placeholder in discovery.placeholders { | 30 | for placeholder in discovery.placeholders { |
| 29 | try? FileManager.default.startDownloadingUbiquitousItem(at: placeholder) | 31 | try? FileManager.default.startDownloadingUbiquitousItem(at: placeholder) |
| 30 | } | 32 | } |
| @@ -40,9 +42,9 @@ public struct Reconciler: Sendable { | |||
| 40 | var found: [DiscoveredFile] = [] | 42 | var found: [DiscoveredFile] = [] |
| 41 | let rootComponents = root.standardizedFileURL.pathComponents.count | 43 | let rootComponents = root.standardizedFileURL.pathComponents.count |
| 42 | for url in paths.map(\.standardizedFileURL) { | 44 | for url in paths.map(\.standardizedFileURL) { |
| 43 | // Files in hidden folders (`.git`) aren't part of the workspace, as in `scan`. | 45 | // Files in ignored folders (`.git`) aren't part of the workspace, as in `scan`. |
| 44 | if url.pathComponents.dropFirst(rootComponents).dropLast().contains(where: { $0.hasPrefix(".") }) { continue } | 46 | if url.pathComponents.dropFirst(rootComponents).dropLast().contains(where: rules.skipsFolder) { continue } |
| 45 | guard case .file(let kind) = WorkspaceScanner.classify(url.lastPathComponent), | 47 | guard case .file(let kind) = WorkspaceScanner.classify(url.lastPathComponent, rules: rules), |
| 46 | let values = try? url.resourceValues(forKeys: [.fileSizeKey, .contentModificationDateKey, .isDirectoryKey]), | 48 | let values = try? url.resourceValues(forKeys: [.fileSizeKey, .contentModificationDateKey, .isDirectoryKey]), |
| 47 | values.isDirectory != true else { continue } | 49 | values.isDirectory != true else { continue } |
| 48 | found.append(DiscoveredFile( | 50 | found.append(DiscoveredFile( |
Sources/Orgstar/ContentView.swift +6
| @@ -3,6 +3,7 @@ import OrgApp | |||
| 3 | import OrgCore | 3 | import OrgCore |
| 4 | import OrgEditorAppKit | 4 | import OrgEditorAppKit |
| 5 | import OrgPresentation | 5 | import OrgPresentation |
| 6 | import OrgWorkspace | ||
| 6 | import OrgIndex | 7 | import OrgIndex |
| 7 | import SwiftUI | 8 | import SwiftUI |
| 8 | import UniformTypeIdentifiers | 9 | import UniformTypeIdentifiers |
| @@ -47,6 +48,8 @@ struct ContentView: View { | |||
| 47 | @AppStorage("outlineWidth") private var outlineWidth = 220.0 | 48 | @AppStorage("outlineWidth") private var outlineWidth = 220.0 |
| 48 | @AppStorage("showMarkup") private var showMarkup = false | 49 | @AppStorage("showMarkup") private var showMarkup = false |
| 49 | @AppStorage("showLineNumbers") private var showLineNumbers = true | 50 | @AppStorage("showLineNumbers") private var showLineNumbers = true |
| 51 | @AppStorage("showHiddenFiles") private var showHiddenFiles = true | ||
| 52 | @AppStorage("ignoredFolders") private var ignoredFolders = ScanRules.defaultIgnoredFolders.sorted().joined(separator: " ") | ||
| 50 | @AppStorage("themeFont") private var themeFont = "" | 53 | @AppStorage("themeFont") private var themeFont = "" |
| 51 | @AppStorage("themeFontSize") private var themeFontSize = 13 | 54 | @AppStorage("themeFontSize") private var themeFontSize = 13 |
| 52 | @AppStorage("themeLineSpacing") private var themeLineSpacing = 2 | 55 | @AppStorage("themeLineSpacing") private var themeLineSpacing = 2 |
| @@ -163,6 +166,9 @@ struct ContentView: View { | |||
| 163 | .searchFocused($searchFocused) | 166 | .searchFocused($searchFocused) |
| 164 | .toolbar { toolbarItems } | 167 | .toolbar { toolbarItems } |
| 165 | .onChange(of: session.path) { plainOutlineShown = false } | 168 | .onChange(of: session.path) { plainOutlineShown = false } |
| 169 | .onChange(of: ScanRules(hiddenFiles: showHiddenFiles, ignoredFolders: Set(ignoredFolders.split(separator: " ").map(String.init))), initial: true) { _, rules in | ||
| 170 | workspace.scanRules = rules | ||
| 171 | } | ||
| 166 | .onChange(of: selectedFile) { old, path in | 172 | .onChange(of: selectedFile) { old, path in |
| 167 | guard let path else { return } | 173 | guard let path else { return } |
| 168 | if !DocumentSession.isOrgPath(path), !FileTypes.isText(path) { | 174 | if !DocumentSession.isOrgPath(path), !FileTypes.isText(path) { |
Sources/Orgstar/OrgstarApp.swift +2
| @@ -346,6 +346,7 @@ struct SettingsView: View { | |||
| 346 | @AppStorage("reminderLead") private var reminderLead = 12 | 346 | @AppStorage("reminderLead") private var reminderLead = 12 |
| 347 | @AppStorage("globalCapture") private var globalCapture = true | 347 | @AppStorage("globalCapture") private var globalCapture = true |
| 348 | @AppStorage("settingsTab") private var tab = "general" | 348 | @AppStorage("settingsTab") private var tab = "general" |
| 349 | @AppStorage("showHiddenFiles") private var showHiddenFiles = true | ||
| 349 | @AppStorage("themeFont") private var themeFont = "" | 350 | @AppStorage("themeFont") private var themeFont = "" |
| 350 | @AppStorage("themeFontSize") private var themeFontSize = 13 | 351 | @AppStorage("themeFontSize") private var themeFontSize = 13 |
| 351 | @AppStorage("themeLineSpacing") private var themeLineSpacing = 2 | 352 | @AppStorage("themeLineSpacing") private var themeLineSpacing = 2 |
| @@ -401,6 +402,7 @@ struct SettingsView: View { | |||
| 401 | Picker("Keys", selection: $preset) { | 402 | Picker("Keys", selection: $preset) { |
| 402 | ForEach(KeymapPreset.allCases, id: \.self) { Text($0.title).tag($0) } | 403 | ForEach(KeymapPreset.allCases, id: \.self) { Text($0.title).tag($0) } |
| 403 | } | 404 | } |
| 405 | Toggle("Show hidden files and folders", isOn: $showHiddenFiles) | ||
| 404 | Picker("Option as Meta", selection: $optionAsMeta) { | 406 | Picker("Option as Meta", selection: $optionAsMeta) { |
| 405 | Text("Left Option").tag(OptionAsMeta.left.rawValue) | 407 | Text("Left Option").tag(OptionAsMeta.left.rawValue) |
| 406 | Text("Right Option").tag(OptionAsMeta.right.rawValue) | 408 | Text("Right Option").tag(OptionAsMeta.right.rawValue) |
Tests/OrgWorkspaceTests/DiscoveryTests.swift +13 −3
| @@ -15,7 +15,11 @@ struct ClassificationTests { | |||
| 15 | ("#a.org#", .ignored), | 15 | ("#a.org#", .ignored), |
| 16 | ("a.md", .file(.other)), | 16 | ("a.md", .file(.other)), |
| 17 | ("photo.jpg", .file(.other)), | 17 | ("photo.jpg", .file(.other)), |
| 18 | (".hidden.org", .ignored), | 18 | (".hidden.org", .file(.org)), |
| 19 | (".zshrc", .file(.other)), | ||
| 20 | (".#a.org", .ignored), | ||
| 21 | (".DS_Store", .ignored), | ||
| 22 | (".a.org.orgstar-backup-1", .ignored), | ||
| 19 | ]) | 23 | ]) |
| 20 | func classify(name: String, expected: FileClassification) { | 24 | func classify(name: String, expected: FileClassification) { |
| 21 | #expect(WorkspaceScanner.classify(name) == expected) | 25 | #expect(WorkspaceScanner.classify(name) == expected) |
| @@ -25,6 +29,7 @@ struct ClassificationTests { | |||
| 25 | #expect(AgendaScope().contains(relativePath: "a.org", kind: .org)) | 29 | #expect(AgendaScope().contains(relativePath: "a.org", kind: .org)) |
| 26 | #expect(!AgendaScope().contains(relativePath: "a.org_archive", kind: .archive)) | 30 | #expect(!AgendaScope().contains(relativePath: "a.org_archive", kind: .archive)) |
| 27 | #expect(!AgendaScope().contains(relativePath: "a.org", kind: .conflict)) | 31 | #expect(!AgendaScope().contains(relativePath: "a.org", kind: .conflict)) |
| 32 | #expect(!AgendaScope().contains(relativePath: "sub/.draft.org", kind: .org)) | ||
| 28 | let work = AgendaScope(globs: ["work/*"]) | 33 | let work = AgendaScope(globs: ["work/*"]) |
| 29 | #expect(work.contains(relativePath: "work/deep/a.org", kind: .org)) | 34 | #expect(work.contains(relativePath: "work/deep/a.org", kind: .org)) |
| 30 | #expect(!work.contains(relativePath: "home/a.org", kind: .org)) | 35 | #expect(!work.contains(relativePath: "home/a.org", kind: .org)) |
| @@ -40,9 +45,14 @@ struct ScanTests { | |||
| 40 | try folder.write(".stversions/d.org", "* d\n") | 45 | try folder.write(".stversions/d.org", "* d\n") |
| 41 | try folder.write("sub/.e.org.icloud", "") | 46 | try folder.write("sub/.e.org.icloud", "") |
| 42 | try folder.write("notes.txt", "") | 47 | try folder.write("notes.txt", "") |
| 48 | try folder.write(".config/app.toml", "") | ||
| 49 | try folder.write(".notes.org", "* n\n") | ||
| 50 | try folder.write("node_modules/x/index.js", "") | ||
| 43 | let discovery = try WorkspaceScanner.scan(folder.url) | 51 | let discovery = try WorkspaceScanner.scan(folder.url) |
| 44 | #expect(discovery.files.map(\.url.lastPathComponent) == ["a.org", "notes.txt", "b.org_archive"]) | 52 | #expect(discovery.files.map(\.url.lastPathComponent) == ["app.toml", ".notes.org", "a.org", "notes.txt", "b.org_archive"]) |
| 45 | #expect(discovery.files.map(\.kind) == [.org, .other, .archive]) | 53 | #expect(discovery.files.map(\.kind) == [.other, .org, .org, .other, .archive]) |
| 54 | let visible = try WorkspaceScanner.scan(folder.url, rules: ScanRules(hiddenFiles: false)) | ||
| 55 | #expect(visible.files.map(\.url.lastPathComponent) == ["a.org", "notes.txt", "b.org_archive"]) | ||
| 46 | #expect(discovery.placeholders.map(\.lastPathComponent) == ["e.org"]) | 56 | #expect(discovery.placeholders.map(\.lastPathComponent) == ["e.org"]) |
| 47 | } | 57 | } |
| 48 | } | 58 | } |