Skip to content

Commit 71ee6c6

Browse files
committed
Ignored files can be changed while preserving comments and white space in the users .gitignore_global file. Users can now reorder ignored files. Buttons were added in settings to open .gitconfig and .gitignore_global in an editor.
1 parent 9635fe5 commit 71ee6c6

File tree

7 files changed

+267
-51
lines changed

7 files changed

+267
-51
lines changed

CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettingsModel.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ final class SearchSettingsModel: ObservableObject {
4848
}
4949

5050
/// Selected patterns
51-
@Published var selection: Set<GlobPattern> = []
51+
@Published var selection: Set<UUID> = []
5252

5353
/// Stores the new values from the Search Settings Model into the settings.json whenever
5454
/// `ignoreGlobPatterns` is updated
@@ -60,12 +60,16 @@ final class SearchSettingsModel: ObservableObject {
6060
}
6161
}
6262

63+
func getPattern(for id: UUID) -> GlobPattern? {
64+
return ignoreGlobPatterns.first(where: { $0.id == id })
65+
}
66+
6367
func addPattern() {
6468
ignoreGlobPatterns.append(GlobPattern(value: ""))
6569
}
6670

67-
func removePatterns(_ selection: Set<GlobPattern>? = nil) {
68-
let patternsToRemove = selection ?? self.selection
71+
func removePatterns(_ selection: Set<UUID>? = nil) {
72+
let patternsToRemove = selection?.compactMap { getPattern(for: $0) } ?? []
6973
ignoreGlobPatterns.removeAll { patternsToRemove.contains($0) }
7074
self.selection.removeAll()
7175
}

CodeEdit/Features/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import SwiftUI
99

1010
struct IgnoredFilesListView: View {
11-
@ObservedObject private var model = IgnorePatternModel()
11+
@StateObject private var model = IgnorePatternModel()
1212

1313
var body: some View {
1414
GlobPatternList(

CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift

Lines changed: 181 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,64 @@
88
import Foundation
99

1010
class IgnorePatternModel: ObservableObject {
11-
@Published var patterns: [GlobPattern] = []
12-
@Published var selection: Set<GlobPattern> = []
13-
14-
let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient)
11+
@Published var loadingPatterns: Bool = false
12+
@Published var patterns: [GlobPattern] = [] {
13+
didSet {
14+
if !loadingPatterns {
15+
savePatterns()
16+
} else {
17+
loadingPatterns = false
18+
}
19+
}
20+
}
21+
@Published var selection: Set<UUID> = []
1522

16-
let fileURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".gitignore_global")
23+
private let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient)
24+
private let fileURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".gitignore_global")
25+
private var fileMonitor: DispatchSourceFileSystemObject?
1726

1827
init() {
1928
loadPatterns()
29+
startFileMonitor()
30+
}
31+
32+
deinit {
33+
stopFileMonitor()
34+
}
35+
36+
private func startFileMonitor() {
37+
let fileDescriptor = open(fileURL.path, O_EVTONLY)
38+
guard fileDescriptor != -1 else {
39+
return
40+
}
41+
42+
let source = DispatchSource.makeFileSystemObjectSource(
43+
fileDescriptor: fileDescriptor,
44+
eventMask: .write,
45+
queue: DispatchQueue.main
46+
)
47+
48+
source.setEventHandler { [weak self] in
49+
self?.loadPatterns()
50+
}
51+
52+
source.setCancelHandler {
53+
close(fileDescriptor)
54+
}
55+
56+
fileMonitor?.cancel() // Cancel any existing monitor
57+
fileMonitor = source
58+
source.resume()
59+
}
60+
61+
private func stopFileMonitor() {
62+
fileMonitor?.cancel()
63+
fileMonitor = nil
2064
}
2165

2266
func loadPatterns() {
67+
loadingPatterns = true
68+
2369
guard FileManager.default.fileExists(atPath: fileURL.path) else {
2470
patterns = []
2571
return
@@ -33,11 +79,139 @@ class IgnorePatternModel: ObservableObject {
3379
}
3480
}
3581

82+
// Map to track the line numbers of patterns.
83+
var patternLineMapping: [String: Int] = [:]
84+
85+
func getPattern(for id: UUID) -> GlobPattern? {
86+
return patterns.first(where: { $0.id == id })
87+
}
88+
3689
func savePatterns() {
90+
// Suspend the file monitor to avoid self-triggered updates
91+
stopFileMonitor()
92+
93+
defer {
94+
startFileMonitor()
95+
}
96+
97+
// Get the file contents; if the file doesn't exist, create it with the patterns
98+
guard let fileContent = try? String(contentsOf: fileURL) else {
99+
writeAllPatterns()
100+
return
101+
}
102+
103+
let lines = fileContent.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
104+
var patternToLineIndex: [String: Int] = [:] // Map patterns to their line indices
105+
var reorderedLines: [String] = [] // Store the final reordered lines
106+
107+
// Map existing patterns in the file
108+
for (index, line) in lines.enumerated() {
109+
let trimmedLine = line.trimmingCharacters(in: .whitespaces)
110+
if !trimmedLine.isEmpty && !trimmedLine.hasPrefix("#") {
111+
patternToLineIndex[trimmedLine] = index
112+
}
113+
}
114+
115+
// Add patterns in the new order specified by the `patterns` array
116+
for pattern in patterns {
117+
let value = pattern.value
118+
if let index = patternToLineIndex[value] {
119+
// Keep the original line if it matches a pattern
120+
reorderedLines.append(lines[index])
121+
patternToLineIndex.removeValue(forKey: value)
122+
} else {
123+
// Add new patterns that don't exist in the file
124+
reorderedLines.append(value)
125+
}
126+
}
127+
128+
// Add remaining non-pattern lines (comments, whitespace)
129+
for (index, line) in lines.enumerated() {
130+
let trimmedLine = line.trimmingCharacters(in: .whitespaces)
131+
if trimmedLine.isEmpty || trimmedLine.hasPrefix("#") {
132+
reorderedLines.insert(line, at: index)
133+
}
134+
}
135+
136+
// Ensure single blank line at the end
137+
reorderedLines = cleanUpWhitespace(in: reorderedLines)
138+
139+
// Write the updated content back to the file
140+
let updatedContent = reorderedLines.joined(separator: "\n")
141+
try? updatedContent.write(to: fileURL, atomically: true, encoding: .utf8)
142+
}
143+
144+
private func writeAllPatterns() {
37145
let content = patterns.map(\.value).joined(separator: "\n")
38146
try? content.write(to: fileURL, atomically: true, encoding: .utf8)
39147
}
40148

149+
private func handlePatterns(
150+
_ lines: inout [String],
151+
existingPatterns: inout Set<String>,
152+
patternLineMap: inout [String: Int]
153+
) {
154+
var handledPatterns = Set<String>()
155+
156+
// Update or preserve existing patterns
157+
for pattern in patterns {
158+
let value = pattern.value
159+
if let lineIndex = patternLineMap[value] {
160+
// Pattern already exists, update it in place
161+
lines[lineIndex] = value
162+
handledPatterns.insert(value)
163+
} else {
164+
// Check if the pattern has been edited and corresponds to a previous pattern
165+
if let oldPattern = existingPatterns.first(where: { !handledPatterns.contains($0) && $0 != value }),
166+
let lineIndex = patternLineMap[oldPattern] {
167+
lines[lineIndex] = value
168+
existingPatterns.remove(oldPattern)
169+
patternLineMap[value] = lineIndex
170+
handledPatterns.insert(value)
171+
} else {
172+
// Append new patterns at the end
173+
if let lastLine = lines.last, lastLine.trimmingCharacters(in: .whitespaces).isEmpty {
174+
lines.removeLast() // Remove trailing blank line before appending
175+
}
176+
lines.append(value)
177+
}
178+
}
179+
}
180+
181+
// Remove patterns no longer in the list
182+
let currentPatterns = Set(patterns.map(\.value))
183+
lines = lines.filter { line in
184+
let trimmedLine = line.trimmingCharacters(in: .whitespaces)
185+
return trimmedLine.isEmpty || trimmedLine.hasPrefix("#") || currentPatterns.contains(trimmedLine)
186+
}
187+
}
188+
189+
private func cleanUpWhitespace(in lines: [String]) -> [String] {
190+
var cleanedLines: [String] = []
191+
var previousLineWasBlank = false
192+
193+
for line in lines {
194+
let isBlank = line.trimmingCharacters(in: .whitespaces).isEmpty
195+
if !(isBlank && previousLineWasBlank) {
196+
cleanedLines.append(line)
197+
}
198+
previousLineWasBlank = isBlank
199+
}
200+
201+
// Trim extra blank lines at the end, ensuring only a single blank line
202+
while let lastLine = cleanedLines.last, lastLine.trimmingCharacters(in: .whitespaces).isEmpty {
203+
cleanedLines.removeLast()
204+
}
205+
cleanedLines.append("") // Ensure exactly one blank line at the end
206+
207+
// Trim whitespace at the top of the file
208+
while let firstLine = cleanedLines.first, firstLine.trimmingCharacters(in: .whitespaces).isEmpty {
209+
cleanedLines.removeFirst()
210+
}
211+
212+
return cleanedLines
213+
}
214+
41215
@MainActor
42216
func addPattern() {
43217
if patterns.isEmpty {
@@ -46,16 +220,12 @@ class IgnorePatternModel: ObservableObject {
46220
}
47221
}
48222
patterns.append(GlobPattern(value: ""))
49-
Task {
50-
savePatterns()
51-
}
52223
}
53224

54225
@MainActor
55-
func removePatterns(_ selection: Set<GlobPattern>? = nil) {
56-
let patternsToRemove = selection ?? self.selection
226+
func removePatterns(_ selection: Set<UUID>? = nil) {
227+
let patternsToRemove = selection?.compactMap { getPattern(for: $0) } ?? []
57228
patterns.removeAll { patternsToRemove.contains($0) }
58-
savePatterns()
59229
self.selection.removeAll()
60230
}
61231

CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ struct SourceControlGeneralView: View {
3838
.onAppear {
3939
Task {
4040
defaultBranch = try await gitConfig.get(key: "init.defaultBranch", global: true) ?? ""
41-
Task {
41+
DispatchQueue.main.async {
4242
hasAppeared = true
4343
}
4444
}

CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,24 @@ struct SourceControlGitView: View {
2727
Section {
2828
preferToRebaseWhenPulling
2929
showMergeCommitsInPerFileLog
30+
} footer: {
31+
Button("Open in Editor...", action: openGitConfigFile)
3032
}
3133
Section {
3234
IgnoredFilesListView()
3335
} header: {
3436
Text("Ignored Files")
37+
} footer: {
38+
Button("Open in Editor...", action: openGitIgnoreFile)
3539
}
3640
}
3741
.onAppear {
3842
Task {
3943
authorName = try await gitConfig.get(key: "user.name", global: true) ?? ""
4044
authorEmail = try await gitConfig.get(key: "user.email", global: true) ?? ""
4145
preferRebaseWhenPulling = try await gitConfig.get(key: "pull.rebase", global: true) ?? false
42-
Task {
43-
hasAppeared = true
44-
}
46+
try? await Task.sleep(for: .milliseconds(0))
47+
hasAppeared = true
4548
}
4649
}
4750
}
@@ -113,4 +116,43 @@ private extension SourceControlGitView {
113116
Spacer()
114117
}
115118
}
119+
120+
private func openGitConfigFile() {
121+
let fileURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".gitconfig")
122+
123+
if !FileManager.default.fileExists(atPath: fileURL.path) {
124+
FileManager.default.createFile(atPath: fileURL.path, contents: nil)
125+
}
126+
127+
NSDocumentController.shared.openDocument(
128+
withContentsOf: fileURL,
129+
display: true
130+
) { _, _, error in
131+
if let error = error {
132+
print("Failed to open document: \(error.localizedDescription)")
133+
}
134+
}
135+
}
136+
137+
private func openGitIgnoreFile() {
138+
let fileURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".gitignore_global")
139+
140+
if !FileManager.default.fileExists(atPath: fileURL.path) {
141+
FileManager.default.createFile(atPath: fileURL.path, contents: nil)
142+
guard !FileManager.default.fileExists(atPath: fileURL.path) else { return }
143+
FileManager.default.createFile(atPath: fileURL.path, contents: nil)
144+
Task {
145+
await gitConfig.set(key: "core.excludesfile", value: fileURL.path, global: true)
146+
}
147+
}
148+
149+
NSDocumentController.shared.openDocument(
150+
withContentsOf: fileURL,
151+
display: true
152+
) { _, _, error in
153+
if let error = error {
154+
print("Failed to open document: \(error.localizedDescription)")
155+
}
156+
}
157+
}
116158
}

0 commit comments

Comments
 (0)