Skip to content

Commit 65bef99

Browse files
Additional Git Features and Navigator UI Improvements (#1471)
* Added reusable SidebarTextField view. Started using it in the navigator by replacing some text fields. Added more functionality to Find navigator's UI. * Added SearchTextField and started using it in the Navigator. Added functionality to Find Navigator UI. * Refactored source control filter to use new sidebar text field view * Refactored UI in source control navigator and find navigator. Using PaneTextField in navigators. Still a WIP. Crash occurs in changes tab of source control navigator because of duplicate keys of type 'CEWorkspaceFile' * Staging and unstaging all changed files for commit now possible. We can now click a changed file to open it as a temporary tab in the editor, double-click to open as a normal tab. * Checking and unchecking changed files now performs a git add/reset command. Checked state is synced with staged git status even when stage occurs externally. * Detects if project is or is not a git repository. If it isn't the source control navigator displays a content unavailable state letting the user know it isn't and offering an option to do a git init. * Added CEContentUnavailableView to replace all instances of zero state text we were each manually styling. * Rearranged the source control navigator to be cleaner. Using sourcControlManager as an EnvironmentObject instead of passing it down the view hierarchy * Displaying number of commits ahead and behind remote. Added ability to pull and fetch. Fetching on an interval when source control navigator is visible. * Moved axis and lineLimit out of PaneTextView so it can be customized outside. Defaulting to horitontal axis. * Fixed SwiftLint issue * Fixed SwiftLint error * Added missing slash to file change help * Improved readability in EditorView * Corrected comment * Simplified git init button label * Added ability to stash, pop, and list stash entries. Refactored repositories tab. * Update CodeEdit/Features/Git/SourceControlManager.swift Co-authored-by: Khan Winter <[email protected]> * Checking to see if the current branch changed or if we checked out a different branch so we can update the UI to reflect. Revised the new branch view. Repositories tab now uses a new CEOutlineGroup view which allows for expand state control. * Moved git related file changes into a separate function to fix SwiftLint error. Added the ability to delete branches, stashes, and remotes. Added ability to add an existing remote from the repositories tab. * Replaced buttons with toggles in find navigator form * Added the ability to rename a branch. Selected the first item by default in Settings sidebar navigation list. * Resolved PR issues and fixed SwiftLint errors * Made CEWorkspaceFile.url immutable and used it in the hash function along with the id * Removed comment * Added ability to apply stash entries. Cleaned up code * Added the ability to apply and delete stash, * SwiftLint error fix * Revised how we were checking if the project is a git repository * Addressed an issue in PR by elimiating extra initializer * Preparing to recieve search changes from main. * Updated symbols package so that branches symbol is compatible with Ventura. * Using message details in git commit command --------- Co-authored-by: Khan Winter <[email protected]>
1 parent e75665e commit 65bef99

File tree

69 files changed

+3019
-1212
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+3019
-1212
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 146 additions & 42 deletions
Large diffs are not rendered by default.

CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "line.3.horizontal.decrease.chevron.filled.pdf",
5+
"idiom" : "universal"
6+
}
7+
],
8+
"info" : {
9+
"author" : "xcode",
10+
"version" : 1
11+
},
12+
"properties" : {
13+
"preserves-vector-representation" : true,
14+
"template-rendering-intent" : "template"
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "line.3.horizontal.decrease.chevron.pdf",
5+
"idiom" : "universal"
6+
}
7+
],
8+
"info" : {
9+
"author" : "xcode",
10+
"version" : 1
11+
},
12+
"properties" : {
13+
"preserves-vector-representation" : true,
14+
"template-rendering-intent" : "template"
15+
}
16+
}

CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor
4545
var type: FileIcon.FileType { .init(rawValue: url.pathExtension) ?? .txt }
4646

4747
/// Returns the URL of the ``CEWorkspaceFile``
48-
var url: URL
48+
let url: URL
4949

5050
/// Return the icon of the file as `Image`
5151
var icon: Image { Image(systemName: systemImage) }
@@ -73,6 +73,9 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor
7373
/// Returns the Git status of a file as ``GitType``
7474
var gitStatus: GitType?
7575

76+
/// Returns a boolean that is true if the file is staged for commit
77+
var staged: Bool?
78+
7679
/// Returns the `id` in ``EditorTabID`` enum form
7780
var tabID: EditorTabID { .codeEditor(id) }
7881

@@ -128,27 +131,32 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor
128131

129132
init(
130133
url: URL,
131-
changeType: GitType? = nil
134+
changeType: GitType? = nil,
135+
staged: Bool? = false
132136
) {
133137
self.url = url
134138
self.gitStatus = changeType
139+
self.staged = staged
135140
}
136141

137142
enum CodingKeys: String, CodingKey {
138143
case url
139144
case changeType
145+
case staged
140146
}
141147

142148
required init(from decoder: Decoder) throws {
143149
let values = try decoder.container(keyedBy: CodingKeys.self)
144150
url = try values.decode(URL.self, forKey: .url)
145151
gitStatus = try values.decode(GitType.self, forKey: .changeType)
152+
staged = try values.decode(Bool.self, forKey: .staged)
146153
}
147154

148155
func encode(to encoder: Encoder) throws {
149156
var container = encoder.container(keyedBy: CodingKeys.self)
150157
try container.encode(url, forKey: .url)
151158
try container.encode(gitStatus, forKey: .changeType)
159+
try container.encode(staged, forKey: .staged)
152160
}
153161

154162
/// Returns a string describing a SFSymbol for folders
@@ -234,7 +242,7 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor
234242
// MARK: Hashable
235243

236244
func hash(into hasher: inout Hasher) {
237-
hasher.combine(fileIdentifier)
245+
hasher.combine(url)
238246
hasher.combine(id)
239247
}
240248

CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ protocol CEWorkspaceFileManagerObserver: AnyObject {
3838
/// ``CEWorkspaceFileManagerObserver`` protocol. Use the ``CEWorkspaceFileManager/addObserver(_:)``
3939
/// and ``CEWorkspaceFileManager/removeObserver(_:)`` to add or remove observers. Observers are kept as weak references.
4040
final class CEWorkspaceFileManager {
41-
private(set) var fileManager = FileManager.default
41+
private(set) var fileManager: FileManager
4242
private(set) var ignoredFilesAndFolders: Set<String>
4343
private(set) var flattenedFileItems: [String: CEWorkspaceFile]
4444
/// Maps all directories to it's children's paths.
@@ -58,6 +58,7 @@ final class CEWorkspaceFileManager {
5858
init(
5959
folderUrl: URL,
6060
ignoredFilesAndFolders: Set<String>,
61+
fileManager: FileManager = FileManager.default,
6162
sourceControlManager: SourceControlManager?
6263
) {
6364
self.folderUrl = folderUrl
@@ -66,12 +67,17 @@ final class CEWorkspaceFileManager {
6667
self.workspaceItem = CEWorkspaceFile(url: folderUrl)
6768
self.flattenedFileItems = [workspaceItem.id: workspaceItem]
6869
self.sourceControlManager = sourceControlManager
70+
self.fileManager = fileManager
6971

7072
self.loadChildrenForFile(self.workspaceItem)
7173

7274
fsEventStream = DirectoryEventStream(directory: self.folderUrl.path) { [weak self] events in
7375
self?.fileSystemEventReceived(events: events)
7476
}
77+
78+
Task {
79+
try await self.sourceControlManager?.validate()
80+
}
7581
}
7682

7783
// MARK: - Public API
@@ -149,7 +155,7 @@ final class CEWorkspaceFileManager {
149155
}
150156
childrenMap[file.id] = children.map { $0.relativePath }
151157
Task {
152-
await sourceControlManager?.refresAllChangesFiles()
158+
await sourceControlManager?.refreshAllChangedFiles()
153159
}
154160
}
155161

@@ -220,12 +226,83 @@ final class CEWorkspaceFileManager {
220226
self.notifyObservers(updatedItems: files)
221227
}
222228

223-
// Ignore changes to .git folder
224-
let notGitChanges = events.filter({ !$0.path.contains(".git/") })
225-
if !notGitChanges.isEmpty {
226-
Task {
227-
await self.sourceControlManager?.refresAllChangesFiles()
228-
}
229+
self.handleGitEvents(events: events)
230+
}
231+
}
232+
233+
func handleGitEvents(events: [DirectoryEventStream.Event]) {
234+
// Changes excluding .git folder
235+
let notGitChanges = events.filter({ !$0.path.contains(".git/") })
236+
237+
// .git folder was changed
238+
let gitFolderChange = events.first(where: {
239+
$0.path == "\(self.folderUrl.relativePath)/.git"
240+
})
241+
242+
// Change made to git index file, staged/unstaged files
243+
let gitIndexChange = events.first(where: {
244+
$0.path == "\(self.folderUrl.relativePath)/.git/index"
245+
})
246+
247+
// Change made to git stash
248+
let gitStashChange = events.first(where: {
249+
$0.path == "\(self.folderUrl.relativePath)/.git/refs/stash"
250+
})
251+
252+
// Changes made to git branches
253+
let gitBranchChange = events.first(where: {
254+
$0.path.contains("\(self.folderUrl.relativePath)/.git/refs/heads")
255+
})
256+
257+
// Changes made to git HEAD - current branch changed
258+
let gitHeadChange = events.first(where: {
259+
$0.path.contains("\(self.folderUrl.relativePath)/.git/HEAD")
260+
})
261+
262+
// Change made to remotes by looking at .git/config
263+
let gitConfigChange = events.first(where: {
264+
$0.path == "\(self.folderUrl.relativePath)/.git/config"
265+
})
266+
267+
// If changes were made to project OR files were staged, refresh changes
268+
if !notGitChanges.isEmpty || gitIndexChange != nil {
269+
Task {
270+
await self.sourceControlManager?.refreshAllChangedFiles()
271+
}
272+
}
273+
274+
// If changes were stashed, refresh stashed entries
275+
if gitStashChange != nil {
276+
Task {
277+
try await self.sourceControlManager?.refreshStashEntries()
278+
}
279+
}
280+
281+
// If branches were added or removed, refresh branches
282+
if gitBranchChange != nil {
283+
Task {
284+
await self.sourceControlManager?.refreshBranches()
285+
}
286+
}
287+
288+
// If HEAD was changed, refresh the current branch
289+
if gitHeadChange != nil {
290+
Task {
291+
await self.sourceControlManager?.refreshCurrentBranch()
292+
}
293+
}
294+
295+
// If git config changed, refresh remotes
296+
if gitConfigChange != nil {
297+
Task {
298+
try await self.sourceControlManager?.refreshRemotes()
299+
}
300+
}
301+
302+
// If .git folder was added or removed, check if repository is valid
303+
if gitFolderChange != nil {
304+
Task {
305+
try await self.sourceControlManager?.validate()
229306
}
230307
}
231308
}

CodeEdit/Features/CodeEditUI/Views/AreaTabBar.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,17 @@ struct AreaTabBar<Tab: AreaTab>: View {
104104
getSafeImage(named: tab.systemImage, accessibilityDescription: tab.title)
105105
.font(.system(size: 12.5))
106106
.symbolVariant(tab == selection ? .fill : .none)
107-
.frame(
108-
width: position == .side ? 40 : 24,
109-
height: position == .side ? 28 : size.height,
110-
alignment: .center
111-
)
112107
.help(tab.title)
113108
}
114-
.buttonStyle(.icon(isActive: tab == selection, size: nil))
109+
.buttonStyle(
110+
.icon(
111+
isActive: tab == selection,
112+
size: CGSize(
113+
width: position == .side ? 40 : 24,
114+
height: position == .side ? 28 : size.height
115+
)
116+
)
117+
)
115118
}
116119

117120
private func makeAreaTabDragGesture(tab: Tab) -> some Gesture {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// CEContentUnavailableView.swift
3+
// CodeEdit
4+
//
5+
// Created by Austin Condiff on 11/17/23.
6+
//
7+
8+
import SwiftUI
9+
10+
struct CEContentUnavailableView<Actions: View>: View {
11+
var label: String
12+
var description: String?
13+
var systemImage: String?
14+
var actions: Actions?
15+
16+
init(
17+
_ label: String,
18+
description: String? = nil,
19+
systemImage: String? = nil,
20+
@ViewBuilder actions: () -> Actions? = { EmptyView() }
21+
) {
22+
self.label = label
23+
self.description = description
24+
self.systemImage = systemImage
25+
self.actions = actions()
26+
}
27+
28+
var contentUnavaiableView: some View {
29+
VStack(spacing: 14) {
30+
VStack(spacing: 5) {
31+
if systemImage != nil {
32+
Image(systemName: systemImage ?? "questionmark.app.dashed")
33+
.font(.system(size: 28))
34+
.foregroundStyle(.tertiary)
35+
.padding(.bottom, 8)
36+
}
37+
Text(label)
38+
.font(.system(size: 16.5, weight: systemImage != nil ? .bold : .regular))
39+
if description != nil {
40+
Text(description ?? "")
41+
.font(.system(size: 10))
42+
}
43+
}
44+
if let actionsView = actions {
45+
HStack { actionsView }
46+
}
47+
}
48+
.foregroundColor(.secondary)
49+
.frame(maxWidth: .infinity, maxHeight: .infinity)
50+
.contentShape(Rectangle())
51+
.controlSize(.small)
52+
}
53+
54+
var body: some View {
55+
if #available(macOS 14, *) {
56+
contentUnavaiableView
57+
.buttonStyle(.accessoryBarAction)
58+
} else {
59+
contentUnavaiableView
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)