Skip to content

Commit 58fee6a

Browse files
Merge branch 'main' into feat/tahoe-tabbar
2 parents 06d3082 + e298776 commit 58fee6a

File tree

7 files changed

+97
-31
lines changed

7 files changed

+97
-31
lines changed

CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ extension CodeEditWindowController {
5959
items += [
6060
.activityViewer,
6161
.notificationItem,
62+
.flexibleSpace,
6263
]
6364
}
6465

CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ struct EditorTabView: View {
4646
/// By default, this value is `false`. When the root view is appeared, it turns `true`.
4747
@State private var isAppeared: Bool = false
4848

49+
@State private var keyMonitor: Any?
50+
4951
/// The id associating with the tab that is currently being dragged.
5052
///
5153
/// When `nil`, then there is no tab being dragged.
@@ -199,6 +201,22 @@ struct EditorTabView: View {
199201
}
200202
}
201203
}
204+
.onAppear {
205+
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .otherMouseDown) { event in
206+
if self.isHovering && event.type == .otherMouseDown && event.buttonNumber == 2 {
207+
DispatchQueue.main.async {
208+
editor.closeTab(file: tabFile)
209+
}
210+
}
211+
return event
212+
}
213+
}
214+
.onDisappear {
215+
if let keyMonitor = keyMonitor {
216+
NSEvent.removeMonitor(keyMonitor)
217+
self.keyMonitor = nil
218+
}
219+
}
202220
}
203221

204222
var body: some View {

CodeEdit/Features/Editor/Views/EditorAreaView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ struct EditorAreaView: View {
174174
// )
175175
// ```
176176
// When we can figure out how to disable the 'not focused' glass effect.
177+
177178
$0.background(EffectView(.headerView).ignoresSafeArea(.all))
178179
} else: {
179180
$0.background(EffectView(.headerView))

CodeEdit/Features/SourceControl/Client/GitClient+Status.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,25 @@ extension GitClient {
3636
/// - Throws: Can throw ``GitClient/GitClientError`` errors if it finds unexpected output.
3737
func getStatus() async throws -> Status {
3838
let output = try await run("status -z --porcelain=2 -u")
39+
return try parseStatusString(output)
40+
}
41+
42+
/// Parses a status string from ``getStatus()`` and returns a ``Status`` object if possible.
43+
/// - Parameter output: The git output from running `status`. Expects a porcelain v2 string.
44+
/// - Returns: A status object if parseable.
45+
func parseStatusString(_ output: borrowing String) throws -> Status {
46+
let endsInNull = output.last == Character(UnicodeScalar(0))
47+
let endIndex: String.Index
48+
if endsInNull && output.count > 1 {
49+
endIndex = output.index(before: output.endIndex)
50+
} else {
51+
endIndex = output.endIndex
52+
}
3953

4054
var status = Status(changedFiles: [], unmergedChanges: [], untrackedFiles: [])
4155

4256
var index = output.startIndex
43-
while index < output.endIndex {
57+
while index < endIndex {
4458
let typeIndex = index
4559

4660
// Move ahead no matter what.
@@ -100,7 +114,11 @@ extension GitClient {
100114
}
101115
index = newIndex
102116
}
103-
index = output.index(after: index)
117+
defer {
118+
if index < output.index(before: output.endIndex) {
119+
index = output.index(after: index)
120+
}
121+
}
104122
return output[startIndex..<index]
105123
}
106124

@@ -147,7 +165,8 @@ extension GitClient {
147165
try moveToNextSpace(from: &index, output: output)
148166
}
149167
try moveOneChar(from: &index, output: output)
150-
let filename = String(try substringToNextNull(from: &index, output: output))
168+
let substring = try substringToNextNull(from: &index, output: output)
169+
let filename = String(substring)
151170
return GitChangedFile(
152171
status: status,
153172
stagedStatus: stagedStatus,

CodeEdit/Features/SourceControl/Clone/GitCloneView.swift

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,12 @@ struct GitCloneView: View {
2828

2929
var body: some View {
3030
VStack(spacing: 8) {
31-
HStack {
31+
HStack(alignment: .top) {
3232
Image(nsImage: NSApp.applicationIconImage)
3333
.resizable()
3434
.frame(width: 64, height: 64)
35-
.padding(.bottom, 50)
3635
VStack(alignment: .leading) {
37-
Text("Clone a repository")
36+
Text("Clone a Repository")
3837
.bold()
3938
.padding(.bottom, 2)
4039
Text("Enter a git repository URL:")
@@ -46,9 +45,9 @@ struct GitCloneView: View {
4645
TextField("Git Repository URL", text: $viewModel.repoUrlStr)
4746
.lineLimit(1)
4847
.padding(.bottom, 15)
49-
.frame(width: 300)
5048

5149
HStack {
50+
Spacer()
5251
Button("Cancel") {
5352
dismiss()
5453
}
@@ -58,11 +57,8 @@ struct GitCloneView: View {
5857
.keyboardShortcut(.defaultAction)
5958
.disabled(!viewModel.isValidUrl(url: viewModel.repoUrlStr))
6059
}
61-
.offset(x: 185)
62-
.alignmentGuide(.leading) { context in
63-
context[.leading]
64-
}
6560
}
61+
.frame(width: 300)
6662
}
6763
.padding(.top, 20)
6864
.padding(.horizontal, 20)
@@ -71,28 +67,32 @@ struct GitCloneView: View {
7167
viewModel.checkClipboard()
7268
}
7369
.sheet(isPresented: $viewModel.isCloning) {
74-
NavigationStack {
75-
VStack {
76-
ProgressView(
77-
viewModel.cloningProgress.state.label,
78-
value: viewModel.cloningProgress.progress,
79-
total: 100
80-
)
81-
}
82-
}
83-
.toolbar {
84-
ToolbarItem {
85-
Button("Cancel Cloning") {
86-
viewModel.cloningTask?.cancel()
87-
viewModel.cloningTask = nil
88-
viewModel.isCloning = false
89-
}
90-
}
70+
cloningSheet
71+
}
72+
}
73+
}
74+
75+
@ViewBuilder private var cloningSheet: some View {
76+
NavigationStack {
77+
VStack {
78+
ProgressView(
79+
viewModel.cloningProgress.state.label,
80+
value: viewModel.cloningProgress.progress,
81+
total: 100
82+
)
83+
}
84+
}
85+
.toolbar {
86+
ToolbarItem {
87+
Button("Cancel Cloning") {
88+
viewModel.cloningTask?.cancel()
89+
viewModel.cloningTask = nil
90+
viewModel.isCloning = false
9191
}
92-
.padding()
93-
.frame(width: 350)
9492
}
9593
}
94+
.padding()
95+
.frame(width: 350)
9696
}
9797

9898
func cloneRepository() {

CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ class GitCloneViewModel: ObservableObject {
184184
dialog.prompt = "Clone"
185185
dialog.nameFieldStringValue = saveName
186186
dialog.nameFieldLabel = "Clone as"
187-
dialog.title = "Clone"
187+
dialog.title = "Clone a Repository"
188188

189189
guard dialog.runModal() == NSApplication.ModalResponse.OK,
190190
let result = dialog.url else {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// GitClientTests.swift
3+
// CodeEditTests
4+
//
5+
// Created by Khan Winter on 9/11/25.
6+
//
7+
8+
import Testing
9+
@testable import CodeEdit
10+
11+
@Suite
12+
struct GitClientTests {
13+
@Test
14+
func statusParseNullAtEnd() throws {
15+
try withTempDir { dirURL in
16+
// swiftlint:disable:next line_length
17+
let string = "1 .M N... 100644 100644 100644 eaef31cfa2a22418c00d7477da0b7151d122681e eaef31cfa2a22418c00d7477da0b7151d122681e CodeEdit/Features/SourceControl/Client/GitClient+Status.swift\01 AM N... 000000 100644 100644 0000000000000000000000000000000000000000 e0f5ce250b32cf6610a284b7a33ac114079f5159 CodeEditTests/Features/SourceControl/GitClientTests.swift\0"
18+
let client = GitClient(directoryURL: dirURL, shellClient: .live())
19+
let status = try client.parseStatusString(string)
20+
21+
#expect(status.changedFiles.count == 2)
22+
// No null string at the end
23+
#expect(status.changedFiles[0].fileURL.lastPathComponent == "GitClient+Status.swift")
24+
#expect(status.changedFiles[1].fileURL.lastPathComponent == "GitClientTests.swift")
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)