Skip to content

Commit 1e6947a

Browse files
committed
Merge branch 'release/0.8.2'
2 parents 3cbfda0 + 0f988d6 commit 1e6947a

16 files changed

Lines changed: 198 additions & 50 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,4 @@ iOSInjectionProject/
123123

124124
# End of
125125
https://www.toptal.com/developers/gitignore/api/xcode,macos,swift,swiftpackagemanager
126+
Secrets.xcconfig

Config.debug.xcconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "Version.xcconfig"
2+
#include "Secrets.xcconfig"
23

34
BUNDLE_IDENTIFIER_BASE = dev.com.intii.CopilotForXcode
45
EXTENSION_BUNDLE_NAME = Copilot Dev

Config.xcconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "Version.xcconfig"
2+
#include "Secrets.xcconfig"
23

34
BUNDLE_IDENTIFIER_BASE = com.intii.CopilotForXcode
45
EXTENSION_BUNDLE_NAME = Copilot

Copilot for Xcode.xcodeproj/project.pbxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
C81458982939EFDC00135263 /* EditorExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EditorExtension.entitlements; sourceTree = "<group>"; };
155155
C81458AD293A009600135263 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
156156
C81458AE293A009800135263 /* Config.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.debug.xcconfig; sourceTree = "<group>"; };
157+
C815034B29B3A7CC00751CAC /* Secrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Secrets.xcconfig; sourceTree = "<group>"; };
157158
C8189B162938972F00C9DCDA /* Copilot for Xcode Dev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Copilot for Xcode Dev.app"; sourceTree = BUILT_PRODUCTS_DIR; };
158159
C8189B192938972F00C9DCDA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
159160
C8189B1B2938972F00C9DCDA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -262,6 +263,7 @@
262263
C8520308293D805800460097 /* README.md */,
263264
C887BC832965D96000931567 /* DEVELOPMENT.md */,
264265
C81E867D296FE4420026E908 /* Version.xcconfig */,
266+
C815034B29B3A7CC00751CAC /* Secrets.xcconfig */,
265267
C81458AD293A009600135263 /* Config.xcconfig */,
266268
C81458AE293A009800135263 /* Config.debug.xcconfig */,
267269
C8189B282938979000C9DCDA /* Core */,

Copilot for Xcode/SettingsView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ final class Settings: ObservableObject {
1111
var realtimeSuggestionDebounce: Double = 0.7
1212
@AppStorage(SettingsKey.suggestionPresentationMode, store: .shared)
1313
var suggestionPresentationModeRawValue: Int = 0
14+
@AppStorage(SettingsKey.automaticallyCheckForUpdate, store: .shared)
15+
var automaticallyCheckForUpdate: Bool = false
1416
init() {}
1517
}
1618

@@ -26,6 +28,11 @@ struct SettingsView: View {
2628
Text("Quit service when Xcode and host app are terminated")
2729
}
2830
.toggleStyle(.switch)
31+
32+
Toggle(isOn: $settings.automaticallyCheckForUpdate) {
33+
Text("Automatically Check for Update")
34+
}
35+
.toggleStyle(.switch)
2936

3037
Picker(selection: $settings.suggestionPresentationModeRawValue) {
3138
ForEach(PresentationMode.allCases, id: \.rawValue) {

Core/Package.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ let package = Package(
99
products: [
1010
.library(
1111
name: "Service",
12-
targets: ["Service", "SuggestionInjector", "FileChangeChecker", "LaunchAgentManager"]
12+
targets: [
13+
"Service",
14+
"SuggestionInjector",
15+
"FileChangeChecker",
16+
"LaunchAgentManager",
17+
"UpdateChecker",
18+
]
1319
),
1420
.library(
1521
name: "Client",
@@ -97,5 +103,6 @@ let package = Package(
97103
"Environment",
98104
]
99105
),
106+
.target(name: "UpdateChecker"),
100107
]
101108
)

Core/Sources/Environment/Environment.swift

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -41,43 +41,42 @@ public enum Environment {
4141
}
4242

4343
#warning("""
44-
The current version causes an issue in real-time suggestion that when completion panel is own,
45-
the command handler can not find the correct workspace, so we are not using it for now.
44+
TODO: The current version causes an issue in real-time suggestion that when completion panel is open,
45+
the command handler can not find the correct workspace.
4646
""")
4747
public static var fetchCurrentProjectRootURL: (_ fileURL: URL?) async throws
4848
-> URL? = { fileURL in
49-
return URL(fileURLWithPath: "/")
50-
// let appleScript = """
51-
// tell application "Xcode"
52-
// return path of document of the first window
53-
// end tell
54-
// """
55-
//
56-
// let path = (try? await runAppleScript(appleScript)) ?? ""
57-
// if !path.isEmpty {
58-
// let trimmedNewLine = path.trimmingCharacters(in: .newlines)
59-
// var url = URL(fileURLWithPath: trimmedNewLine)
60-
// while !FileManager.default.fileIsDirectory(atPath: url.path) ||
61-
// !url.pathExtension.isEmpty
62-
// {
63-
// url = url.deletingLastPathComponent()
64-
// }
65-
// return url
66-
// }
67-
//
68-
// guard var currentURL = fileURL else { return nil }
69-
// var firstDirectoryURL: URL?
70-
// while currentURL.pathComponents.count > 1 {
71-
// defer { currentURL.deleteLastPathComponent() }
72-
// guard FileManager.default.fileIsDirectory(atPath: currentURL.path) else { continue }
73-
// if firstDirectoryURL == nil { firstDirectoryURL = currentURL }
74-
// let gitURL = currentURL.appendingPathComponent(".git")
75-
// if FileManager.default.fileIsDirectory(atPath: gitURL.path) {
76-
// return currentURL
77-
// }
78-
// }
79-
//
80-
// return firstDirectoryURL ?? fileURL
49+
let appleScript = """
50+
tell application "Xcode"
51+
return path of document of the first window
52+
end tell
53+
"""
54+
55+
let path = (try? await runAppleScript(appleScript)) ?? ""
56+
if !path.isEmpty {
57+
let trimmedNewLine = path.trimmingCharacters(in: .newlines)
58+
var url = URL(fileURLWithPath: trimmedNewLine)
59+
while !FileManager.default.fileIsDirectory(atPath: url.path) ||
60+
!url.pathExtension.isEmpty
61+
{
62+
url = url.deletingLastPathComponent()
63+
}
64+
return url
65+
}
66+
67+
guard var currentURL = fileURL else { return nil }
68+
var firstDirectoryURL: URL?
69+
while currentURL.pathComponents.count > 1 {
70+
defer { currentURL.deleteLastPathComponent() }
71+
guard FileManager.default.fileIsDirectory(atPath: currentURL.path) else { continue }
72+
if firstDirectoryURL == nil { firstDirectoryURL = currentURL }
73+
let gitURL = currentURL.appendingPathComponent(".git")
74+
if FileManager.default.fileIsDirectory(atPath: gitURL.path) {
75+
return currentURL
76+
}
77+
}
78+
79+
return firstDirectoryURL ?? fileURL
8180
}
8281

8382
public static var fetchCurrentFileURL: () async throws -> URL = {

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ public class RealtimeSuggestionController {
166166
#warning(
167167
"TODO: Any method to avoid using AppleScript to check that completion panel is presented?"
168168
)
169-
if isCommentMode, await Environment.frontmostXcodeWindowIsEditor() {
169+
if await Environment.frontmostXcodeWindowIsEditor() {
170170
if Task.isCancelled { return }
171171
self.triggerPrefetchDebounced(force: true)
172172
}
@@ -191,12 +191,7 @@ public class RealtimeSuggestionController {
191191

192192
os_log(.info, "Prefetch suggestions.")
193193

194-
let isCommentMode = PresentationMode(
195-
rawValue: UserDefaults.shared
196-
.integer(forKey: SettingsKey.suggestionPresentationMode)
197-
) == .comment
198-
199-
if isCommentMode, !force, await !Environment.frontmostXcodeWindowIsEditor() {
194+
if !force, await !Environment.frontmostXcodeWindowIsEditor() {
200195
os_log(.info, "Completion panel is open, blocked.")
201196
return
202197
}

Core/Sources/SuggestionWidget/SuggestionWidgetController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ public final class SuggestionWidgetController {
247247
if foundSize, foundPosition, let screen, let firstScreen {
248248
let proposedAnchorFrameOnTheRightSide = CGRect(
249249
x: frame.maxX - Style.widgetPadding - Style.widgetWidth,
250-
y: max(firstScreen.frame.height - frame.maxY + Style.widgetPadding, 4),
250+
y: max(firstScreen.frame.height - frame.maxY + Style.widgetPadding, 4 + screen.frame.minY),
251251
width: Style.widgetWidth,
252252
height: Style.widgetHeight
253253
)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import AppKit
2+
import Foundation
3+
import os.log
4+
import SwiftUI
5+
6+
struct Release: Codable {
7+
let tag_name: String?
8+
let html_url: String?
9+
let body: String?
10+
let published_at: String?
11+
}
12+
13+
let skippedUpdateVersionKey = "skippedUpdateVersion"
14+
15+
public struct UpdateChecker {
16+
var skippedUpdateVersion: String? {
17+
UserDefaults.standard.string(forKey: skippedUpdateVersionKey)
18+
}
19+
20+
public init() {}
21+
22+
public func checkForUpdate() async {
23+
let url =
24+
URL(string: "https://api.github.com/repos/intitni/CopilotForXcode/releases/latest")!
25+
do {
26+
var request = URLRequest(url: url)
27+
let token = (Bundle.main.infoDictionary?["GITHUB_TOKEN"] as? String) ?? ""
28+
if !token.isEmpty {
29+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
30+
}
31+
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
32+
request.setValue("X-GitHub-Api-Version", forHTTPHeaderField: "2022-11-28")
33+
34+
let (data, _) = try await URLSession.shared.data(for: request)
35+
let decoder = JSONDecoder()
36+
let release = try decoder.decode(Release.self, from: data)
37+
guard let version = release.tag_name,
38+
version != skippedUpdateVersion,
39+
version != Bundle.main
40+
.infoDictionary?["CFBundleShortVersionString"] as? String
41+
else { return }
42+
43+
Task { @MainActor in
44+
let alert = NSAlert()
45+
alert.messageText = "Copilot for Xcode \(version) is available!"
46+
alert.informativeText = "Would you like to visit the release page?"
47+
let view = NSHostingView(
48+
rootView:
49+
AccessoryView(releaseNote: release.body)
50+
)
51+
view.frame = .init(origin: .zero, size: .init(width: 400, height: 200))
52+
alert.accessoryView = view
53+
54+
alert.addButton(withTitle: "Visit Release Page")
55+
alert.addButton(withTitle: "Skip This Version")
56+
alert.addButton(withTitle: "Cancel")
57+
alert.alertStyle = .informational
58+
let screenFrame = NSScreen.main?.frame ?? .zero
59+
let window = NSWindow(
60+
contentRect: .init(
61+
x: screenFrame.midX,
62+
y: screenFrame.midY,
63+
width: 1,
64+
height: 1
65+
),
66+
styleMask: .borderless,
67+
backing: .buffered,
68+
defer: true
69+
)
70+
window.level = .statusBar
71+
window.isReleasedWhenClosed = false
72+
alert.beginSheetModal(for: window) { [window] response in
73+
switch response {
74+
case .alertFirstButtonReturn:
75+
if let url = URL(string: release.html_url ?? "") {
76+
NSWorkspace.shared.open(url)
77+
}
78+
case .alertSecondButtonReturn:
79+
UserDefaults.standard.set(version, forKey: skippedUpdateVersionKey)
80+
default:
81+
break
82+
}
83+
window.close()
84+
}
85+
}
86+
} catch {
87+
os_log(.error, "%@", error.localizedDescription)
88+
}
89+
}
90+
}
91+
92+
struct AccessoryView: View {
93+
let releaseNote: String?
94+
95+
var body: some View {
96+
if let releaseNote {
97+
ScrollView {
98+
Text(releaseNote)
99+
.padding()
100+
101+
Spacer()
102+
}
103+
.background(Color(nsColor: .textBackgroundColor))
104+
.overlay {
105+
Rectangle()
106+
.stroke(Color(nsColor: .separatorColor), style: .init(lineWidth: 2))
107+
}
108+
} else {
109+
EmptyView()
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)