Skip to content

Commit 26c7489

Browse files
committed
Merge tag 'tweak-caching' into develop
2 parents adf0070 + 241ae5a commit 26c7489

File tree

9 files changed

+81
-30
lines changed

9 files changed

+81
-30
lines changed

Copilot for Xcode/DebugView.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import SwiftUI
55
final class DebugSettings: ObservableObject {
66
@AppStorage(\.disableLazyVStack)
77
var disableLazyVStack: Bool
8+
@AppStorage(\.preCacheOnFileOpen)
9+
var preCacheOnFileOpen: Bool
810
init() {}
911
}
1012

@@ -18,6 +20,10 @@ struct DebugSettingsView: View {
1820
Text("Disable LazyVStack")
1921
}
2022
.toggleStyle(.switch)
23+
Toggle(isOn: $settings.preCacheOnFileOpen) {
24+
Text("Cache editor information on file open")
25+
}
26+
.toggleStyle(.switch)
2127
}
2228
}.buttonStyle(.copilot)
2329
}

Core/Sources/AXExtension/AXUIElement.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ public extension AXUIElement {
1111
var value: String {
1212
(try? copyValue(key: kAXValueAttribute)) ?? ""
1313
}
14+
15+
var doubleValue: Double {
16+
(try? copyValue(key: kAXValueAttribute)) ?? 0.0
17+
}
1418

1519
var document: String? {
1620
try? copyValue(key: kAXDocumentAttribute)
@@ -19,6 +23,10 @@ public extension AXUIElement {
1923
var description: String {
2024
(try? copyValue(key: kAXDescriptionAttribute)) ?? ""
2125
}
26+
27+
var isSourceEditor: Bool {
28+
description == "Source Editor"
29+
}
2230

2331
var selectedTextRange: Range<Int>? {
2432
guard let value: AXValue = try? copyValue(key: kAXSelectedTextRangeAttribute)

Core/Sources/Preferences/Keys.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,17 @@ public struct UserDefaultPreferenceKeys {
153153
}
154154

155155
public var disableLazyVStack: FeatureFlags.DisableLazyVStack { .init() }
156+
public var preCacheOnFileOpen: FeatureFlags.PreCacheOnFileOpen { .init() }
156157
}
157158

158159
public enum FeatureFlags {
159160
public struct DisableLazyVStack: UserDefaultPreferenceKey {
160161
public let defaultValue = false
161162
public let key = "FeatureFlag-DisableLazyVStack"
162163
}
164+
165+
public struct PreCacheOnFileOpen: UserDefaultPreferenceKey {
166+
public let defaultValue = true
167+
public let key = "FeatureFlag-PreCacheOnFileOpen"
168+
}
163169
}

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class RealtimeSuggestionController {
2525
private var activeApplicationMonitorTask: Task<Void, Error>?
2626
private var editorObservationTask: Task<Void, Error>?
2727
private var focusedUIElement: AXUIElement?
28+
private var sourceEditor: AXUIElement?
2829

2930
var isCommentMode: Bool {
3031
UserDefaults.shared.value(for: \.suggestionPresentationMode) == .comment
@@ -108,8 +109,9 @@ public class RealtimeSuggestionController {
108109
let application = AXUIElementCreateApplication(activeXcode.processIdentifier)
109110
guard let focusElement = application.focusedElement else { return }
110111
let focusElementType = focusElement.description
111-
guard focusElementType == "Source Editor" else { return }
112112
focusedUIElement = focusElement
113+
guard focusElementType == "Source Editor" else { return }
114+
sourceEditor = focusElement
113115

114116
editorObservationTask?.cancel()
115117
editorObservationTask = nil
@@ -134,6 +136,28 @@ public class RealtimeSuggestionController {
134136
}
135137
}
136138
}
139+
140+
Task { // Get cache ready for real-time suggestions.
141+
guard UserDefaults.shared.value(for: \.preCacheOnFileOpen) else { return }
142+
guard
143+
let fileURL = try? await Environment.fetchCurrentFileURL(),
144+
let (_, filespace) = try? await Workspace
145+
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
146+
else { return }
147+
148+
if filespace.uti == nil {
149+
Logger.service.info("Generate cache for file.")
150+
// avoid the command get called twice
151+
filespace.uti = ""
152+
do {
153+
try await Environment.triggerAction("Real-time Suggestions")
154+
} catch {
155+
if filespace.uti?.isEmpty ?? true {
156+
filespace.uti = nil
157+
}
158+
}
159+
}
160+
}
137161
}
138162

139163
func handleHIDEvent(event: CGEvent) async {
@@ -203,7 +227,7 @@ public class RealtimeSuggestionController {
203227
}
204228

205229
// So the editor won't be blocked (after information are cached)!
206-
await PseudoCommandHandler().generateRealtimeSuggestions()
230+
await PseudoCommandHandler().generateRealtimeSuggestions(sourceEditor: sourceEditor)
207231
}
208232
}
209233

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,10 @@ struct PseudoCommandHandler {
3838
))
3939
}
4040

41-
func generateRealtimeSuggestions() async {
42-
// Can't use handler directly if content is not available.
43-
guard let editor = await getEditorContent() else {
44-
try? await Environment.triggerAction("Prefetch Suggestions")
45-
return
46-
}
47-
48-
// If no cache is available, and completion panel is not displayed, try to get it with command.
49-
if editor.uti.isEmpty, await Environment.frontmostXcodeWindowIsEditor() {
50-
try? await Environment.triggerAction("Prefetch Suggestions")
51-
return
52-
}
53-
41+
func generateRealtimeSuggestions(sourceEditor: AXUIElement?) async {
42+
// Can't use handler if content is not available.
43+
guard let editor = await getEditorContent(sourceEditor: sourceEditor) else { return }
44+
5445
// Otherwise, get it from pseudo handler directly.
5546
let mode = UserDefaults.shared.value(for: \.suggestionPresentationMode)
5647
switch mode {
@@ -84,7 +75,8 @@ struct PseudoCommandHandler {
8475
guard let focusElement = application.focusedElement,
8576
focusElement.description == "Source Editor"
8677
else { return }
87-
guard let (content, lines, cursorPosition) = await getFileContent() else {
78+
guard let (content, lines, cursorPosition) = await getFileContent(sourceEditor: nil)
79+
else {
8880
PresentInWindowSuggestionPresenter()
8981
.presentErrorMessage("Unable to get file content.")
9082
return
@@ -103,6 +95,7 @@ struct PseudoCommandHandler {
10395
)) else { return }
10496

10597
let oldPosition = focusElement.selectedTextRange
98+
let oldScrollPosition = focusElement.parent?.verticalScrollBar?.doubleValue
10699

107100
let error = AXUIElementSetAttributeValue(
108101
focusElement,
@@ -138,6 +131,14 @@ struct PseudoCommandHandler {
138131
}
139132
}
140133

134+
if let oldScrollPosition, let scrollBar = focusElement.parent?.verticalScrollBar {
135+
AXUIElementSetAttributeValue(
136+
scrollBar,
137+
kAXValueAttribute as CFString,
138+
oldScrollPosition as CFTypeRef
139+
)
140+
}
141+
141142
} catch {
142143
PresentInWindowSuggestionPresenter().presentError(error)
143144
}
@@ -153,13 +154,13 @@ struct PseudoCommandHandler {
153154
}
154155

155156
private extension PseudoCommandHandler {
156-
func getFileContent() async
157+
func getFileContent(sourceEditor: AXUIElement?) async
157158
-> (content: String, lines: [String], cursorPosition: CursorPosition)?
158159
{
159160
guard let xcode = ActiveApplicationMonitor.activeXcode
160161
?? ActiveApplicationMonitor.latestXcode else { return nil }
161162
let application = AXUIElementCreateApplication(xcode.processIdentifier)
162-
guard let focusElement = application.focusedElement,
163+
guard let focusElement = sourceEditor ?? application.focusedElement,
163164
focusElement.description == "Source Editor"
164165
else { return nil }
165166
guard let selectionRange = focusElement.selectedTextRange else { return nil }
@@ -196,10 +197,10 @@ private extension PseudoCommandHandler {
196197
}
197198

198199
@ServiceActor
199-
func getEditorContent() async -> EditorContent? {
200+
func getEditorContent(sourceEditor: AXUIElement?) async -> EditorContent? {
200201
guard
201202
let filespace = await getFilespace(),
202-
let content = await getFileContent()
203+
let content = await getFileContent(sourceEditor: sourceEditor)
203204
else { return nil }
204205
let uti = filespace.uti ?? ""
205206
let tabSize = filespace.tabSize ?? 4

Core/Sources/Service/Workspace.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ final class Workspace {
152152
static func fetchOrCreateWorkspaceIfNeeded(fileURL: URL) async throws
153153
-> (workspace: Workspace, filespace: Filespace)
154154
{
155+
// never create duplicated filespaces
156+
for workspace in workspaces.values {
157+
if let filespace = workspace.filespaces[fileURL] {
158+
return (workspace, filespace)
159+
}
160+
}
161+
155162
let projectURL = try await Environment.fetchCurrentProjectRootURL(fileURL)
156163
let workspaceURL = projectURL ?? fileURL
157164
let workspace = workspaces[workspaceURL] ?? Workspace(projectRootURL: workspaceURL)

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,6 @@ fi
190190
- The first run of the extension will be slow. Be patient.
191191
- The extension uses some dirty tricks to get the file and project/workspace paths. It may fail, it may be incorrect, especially when you have multiple Xcode windows running, and maybe even worse when they are in different displays. I am not sure about that though.
192192
- The suggestions are presented as C-style comments in comment mode, they may break your code if you are editing a JSON file or something.
193-
- When a real-time suggestion request is triggered, there is a chance that it may briefly block the editor. This can occur at most once for each file after each restart of the extension because the extension needs to initiate real-time suggestion by clicking an item from the menu bar. However, once a command has been executed and some information is cached, the extension will be able to trigger real-time suggestion using a different method.
194193

195194
## License
196195

Version.xcconfig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
APP_VERSION = 0.11.1
2-
APP_BUILD = 81
1+
APP_VERSION = 0.11.2
2+
APP_BUILD = 84

appcast.xml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
<title>Copilot for Xcode</title>
55

66
<item>
7-
<title>0.11.1</title>
8-
<pubDate>Mon, 10 Apr 2023 19:04:48 +0800</pubDate>
9-
<sparkle:version>81</sparkle:version>
10-
<sparkle:shortVersionString>0.11.1</sparkle:shortVersionString>
7+
<title>0.11.2</title>
8+
<pubDate>Wed, 12 Apr 2023 12:23:29 +0800</pubDate>
9+
<sparkle:version>84</sparkle:version>
10+
<sparkle:shortVersionString>0.11.2</sparkle:shortVersionString>
1111
<sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
1212
<sparkle:releaseNotesLink>
13-
https://github.com/intitni/CopilotForXcode/releases/tag/0.11.1
13+
https://github.com/intitni/CopilotForXcode/releases/tag/0.11.2
1414
</sparkle:releaseNotesLink>
15-
<enclosure url="https://github.com/intitni/CopilotForXcode/releases/download/0.11.1/Copilot.for.Xcode.app.zip" length="19038951" type="application/octet-stream" sparkle:edSignature="fp0vSUUH7JyIeyeUrMa9p6u3UFkgl5EeNjkmuzPL9hTmjfkVR77IM/9BIe2730OYy7PR429ngSjWPS+PGIT0Bg=="/>
15+
<enclosure url="https://github.com/intitni/CopilotForXcode/releases/download/0.11.2/Copilot.for.Xcode.app.zip" length="19043705" type="application/octet-stream" sparkle:edSignature="oIfDoluwAzNJmZnN6zZtjRt7uyIMpS738kBNnOvsM8Ic06UQSW5v2bYyIRKnLJAfAygu7DM2/z+alzrUWvW6DA=="/>
1616
</item>
1717

1818
<item>
@@ -61,4 +61,4 @@
6161
type="application/octet-stream" />
6262
</item>
6363
</channel>
64-
</rss>
64+
</rss>

0 commit comments

Comments
 (0)