Skip to content

Commit c7e8e79

Browse files
committed
add context level functionality
1 parent 52f76af commit c7e8e79

File tree

9 files changed

+208
-28
lines changed

9 files changed

+208
-28
lines changed

Core/Sources/HostApp/Benchmark/Data/Manager/MultiFileContextBenchmarkManager.swift

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ class MultiFileContextBenchmarkManager: BenchmarkManager {
2929
var selectedGenAIModel: AnyPublisher<GenAILanguageModel, Never> {
3030
selectedGenAIModelSubject.eraseToAnyPublisher()
3131
}
32+
33+
private let contextLevelLimitSubject = CurrentValueSubject<ContextLevelLimit, Never>(.firstLevel)
34+
var contextLevelLimit: AnyPublisher<ContextLevelLimit, Never> {
35+
contextLevelLimitSubject.eraseToAnyPublisher()
36+
}
37+
38+
private var contextFileAmountLimitSubject = CurrentValueSubject<Int?, Never>(nil)
39+
var contextFileAmountLimit: AnyPublisher<Int?, Never> {
40+
contextFileAmountLimitSubject.eraseToAnyPublisher()
41+
}
42+
3243
private var cancellables = Set<AnyCancellable>()
3344

3445
init(benchmarkSettingsRepository: BenchmarkSettingsRepository) {
@@ -239,25 +250,23 @@ class MultiFileContextBenchmarkManager: BenchmarkManager {
239250
}
240251
}
241252

242-
private func retrieveRelevantSymbolsForFileContent(file: FileContent, workspace: Workspace) async -> RelevantSymbolsSummary? {
243-
let multiFileContextManager = MultiFileContextManager(
253+
private func retrieveRelevantSymbolsForFileContent(
254+
file: FileContent,
255+
workspace: Workspace
256+
) async -> RelevantSymbolsSummary? {
257+
guard isMultiFileEnabledSubject.value else { return nil }
258+
259+
let manager = MultiFileContextManager(
244260
workspaceProvider: ManualWorkspaceProvider(workspace: workspace),
245261
parser: SwiftProgrammingLanguageSyntaxParser()
246262
)
247-
if isMultiFileEnabledSubject.value {
248-
let start = Date()
249-
let symbols = await multiFileContextManager.retrieveRelevantSymbolsForFileContent(file: file, ignoreWithinPaths: ["/Benchmark/"])
250-
let end = Date()
251-
let timeTakenInSeconds = end.timeIntervalSince(start)
252-
return RelevantSymbolsSummary(
253-
symbolsAtLevel: [
254-
Array(symbols.values)
255-
],
256-
durationInSeconds: timeTakenInSeconds
257-
)
258-
} else {
259-
return nil
260-
}
263+
264+
return await manager.expandRelevantSymbols(
265+
from: file,
266+
ignoreWithinPaths: ["/Benchmark/"],
267+
maxDepth: contextLevelLimitSubject.value.indexLimit, // e.g. First=0, Second=1, …, nil=no limit
268+
maxFiles: contextFileAmountLimitSubject.value // Int?, nil=no cap
269+
)
261270
}
262271

263272
private func applyCodeSuggestion(suggestion: SuggestionBasic.CodeSuggestion, at fileURL: URL) async {
@@ -470,6 +479,16 @@ class MultiFileContextBenchmarkManager: BenchmarkManager {
470479
currentStates[directory] = currentStatesInDirectory
471480
taskStatesSubject.send(currentStates)
472481
}
482+
483+
@MainActor
484+
func saveContextLevelLimit(_ limit: ContextLevelLimit) {
485+
contextLevelLimitSubject.send(limit)
486+
}
487+
488+
@MainActor
489+
func saveContextFileAmountLimit(_ limit: Int?) {
490+
contextFileAmountLimitSubject.send(limit)
491+
}
473492
}
474493

475494
extension MetadataDTO {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
enum ContextLevelLimit: CaseIterable {
2+
case firstLevel
3+
case secondLevel
4+
case thirdLevel
5+
6+
var name: String {
7+
switch self {
8+
case .firstLevel: return "First Level"
9+
case .secondLevel: return "Second Level"
10+
case .thirdLevel: return "Third Level"
11+
}
12+
}
13+
14+
var indexLimit: Int? {
15+
switch self {
16+
case .firstLevel: return 0
17+
case .secondLevel: return 1
18+
case .thirdLevel: return 2
19+
}
20+
}
21+
}

Core/Sources/HostApp/Benchmark/Domain/Entity/RelevantSymbolsSummary.swift

Lines changed: 0 additions & 6 deletions
This file was deleted.

Core/Sources/HostApp/Benchmark/Domain/Manager/BenchmarkManager.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,12 @@ protocol BenchmarkManager {
1010
func changeGenAIModel(to newModel: GenAILanguageModel)
1111
@MainActor
1212
func updateMultiFileContextState(_ newValue: Bool)
13+
14+
var contextLevelLimit: AnyPublisher<ContextLevelLimit, Never> { get }
15+
@MainActor
16+
func saveContextLevelLimit(_ limit: ContextLevelLimit)
17+
18+
var contextFileAmountLimit: AnyPublisher<Int?, Never> { get }
19+
@MainActor
20+
func saveContextFileAmountLimit(_ limit: Int?)
1321
}

Core/Sources/HostApp/Benchmark/Presentation/View/BenchmarkView.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,28 @@ struct BenchmarkView: View {
2828
ConfigurationButtonView(module: module)
2929
}
3030
.padding(.vertical)
31+
32+
HStack(spacing: 20) {
33+
Spacer()
34+
Picker("Context Level Limit: ", selection: $viewModel.contextLevelLimit) {
35+
ForEach(ContextLevelLimit.allCases, id: \.self) { limit in
36+
Text(limit.name).tag(limit)
37+
}
38+
}
39+
.fixedSize(horizontal: true, vertical: false)
40+
41+
Text("Context File Amount Limit:")
42+
let numberFormatter: NumberFormatter = {
43+
let f = NumberFormatter()
44+
f.numberStyle = .none
45+
return f
46+
}()
47+
TextField("Optional", value: $viewModel.contextFileAmountLimit, formatter: numberFormatter)
48+
.textFieldStyle(RoundedBorderTextFieldStyle())
49+
.frame(width: 100)
50+
}
51+
.padding(.bottom)
52+
3153
ForEach(viewModel.benchmarkDirectories, id: \.self) { directory in
3254
BenchmarkDirectoryEntryView(
3355
directory: directory,

Core/Sources/HostApp/Benchmark/Presentation/ViewModel/BenchmarkViewModel.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ class BenchmarkViewModel: ObservableObject {
1616
Task { await benchmarkManager.changeGenAIModel(to: selectedLanguageModel) }
1717
}
1818
}
19+
@Published var contextLevelLimit: ContextLevelLimit = .firstLevel {
20+
didSet {
21+
guard oldValue != contextLevelLimit else { return }
22+
Task { await benchmarkManager.saveContextLevelLimit(contextLevelLimit) }
23+
}
24+
}
25+
@Published var contextFileAmountLimit: Int? = nil {
26+
didSet {
27+
guard oldValue != contextFileAmountLimit else { return }
28+
Task { await benchmarkManager.saveContextFileAmountLimit(contextFileAmountLimit) }
29+
}
30+
}
1931

2032
private let benchmarkSettingsRepository: BenchmarkSettingsRepository
2133
private let benchmarkManager: BenchmarkManager
@@ -35,6 +47,12 @@ class BenchmarkViewModel: ObservableObject {
3547
benchmarkManager.selectedGenAIModel
3648
.assign(to: \.selectedLanguageModel, on: self)
3749
.store(in: &cancellables)
50+
benchmarkManager.contextLevelLimit
51+
.assign(to: \.contextLevelLimit, on: self)
52+
.store(in: &cancellables)
53+
benchmarkManager.contextFileAmountLimit
54+
.assign(to: \.contextFileAmountLimit, on: self)
55+
.store(in: &cancellables)
3856
}
3957

4058
func loadBenchmarkDirectories() {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
public struct RelevantSymbolsSummary {
2+
public let symbolsAtLevel: [[SymbolContent]]
3+
public let durationInSeconds: Double
4+
}

Core/Sources/Service/MultiFileContext/MultiFileContextManager.swift

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,87 @@ public class MultiFileContextManager {
6262
return result
6363
}
6464

65+
private func readFile(_ urlString: String) -> FileContent? {
66+
guard let url = URL(string: urlString) else { return nil }
67+
guard let content = try? String(contentsOf: url, encoding: .utf8) else { return nil }
68+
return FileContent(fileURL: urlString, content: content)
69+
}
70+
71+
public func expandRelevantSymbols(
72+
from root: FileContent,
73+
ignoreWithinPaths: [String] = [],
74+
maxDepth: Int? = nil,
75+
maxFiles: Int? = nil
76+
) async -> RelevantSymbolsSummary {
77+
@inline(__always)
78+
func isIgnored(_ url: String) -> Bool {
79+
ignoreWithinPaths.contains { url.contains($0) }
80+
}
81+
82+
let start = Date()
83+
84+
let level0 = await retrieveRelevantSymbolsForFileContent(file: root, ignoreWithinPaths: ignoreWithinPaths)
85+
var level0Unique: [SymbolContent] = []
86+
var includedFiles = Set<String>()
87+
for s in level0.values {
88+
if includedFiles.insert(s.fileURL).inserted {
89+
level0Unique.append(s)
90+
}
91+
}
92+
var levels: [[SymbolContent]] = [level0Unique]
93+
94+
var visited = Set<String>([root.fileURL])
95+
var depth = 0
96+
var remainingFiles = maxFiles
97+
98+
while maxDepth == nil || depth < maxDepth! {
99+
let prev = levels.last ?? []
100+
var nextURLs = Set(prev.map(\.fileURL))
101+
nextURLs.subtract(visited)
102+
nextURLs = nextURLs.filter { !isIgnored($0) }
103+
104+
if nextURLs.isEmpty { break }
105+
106+
if let remaining = remainingFiles, nextURLs.count > remaining {
107+
nextURLs = Set(nextURLs.prefix(remaining))
108+
}
109+
110+
var rawNextLevel: [SymbolContent] = []
111+
for url in nextURLs {
112+
visited.insert(url)
113+
guard let fc = readFile(url) else { continue }
114+
let dict = await retrieveRelevantSymbolsForFileContent(file: fc, ignoreWithinPaths: ignoreWithinPaths)
115+
rawNextLevel.append(contentsOf: dict.values)
116+
}
117+
118+
var nextLevel: [SymbolContent] = []
119+
var seenThisLevel = Set<String>()
120+
for s in rawNextLevel {
121+
guard !includedFiles.contains(s.fileURL) else { continue }
122+
if seenThisLevel.insert(s.fileURL).inserted {
123+
nextLevel.append(s)
124+
}
125+
}
126+
127+
if nextLevel.isEmpty { break }
128+
129+
includedFiles.formUnion(nextLevel.map(\.fileURL))
130+
131+
levels.append(nextLevel)
132+
depth += 1
133+
134+
if remainingFiles != nil {
135+
remainingFiles! -= nextURLs.count
136+
if remainingFiles! <= 0 { break }
137+
}
138+
}
139+
140+
return RelevantSymbolsSummary(
141+
symbolsAtLevel: levels,
142+
durationInSeconds: Date().timeIntervalSince(start)
143+
)
144+
}
145+
65146
public func retrieveRelevantSymbolsForFileContent(
66147
file: FileContent,
67148
ignoreWithinPaths: [String] = []
@@ -171,7 +252,7 @@ extension String {
171252
let before = (range.lowerBound == startIndex) ? nil : self[index(before: range.lowerBound)]
172253
let after = (range.upperBound == endIndex) ? nil : self[range.upperBound]
173254

174-
let boundaryBefore = before.map { !isIdentChar($0) } ?? true
255+
let boundaryBefore = before.map { !isIdentChar($0) && $0 != "." } ?? true
175256
let boundaryAfter = after.map { !isIdentChar($0) } ?? true
176257

177258
if boundaryBefore && boundaryAfter {

Core/Sources/Service/MultiFileContext/SwiftDeclarationCollector.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,45 @@ class SwiftDeclarationCollector: SyntaxVisitor {
1010
self.sourceText = sourceText
1111
super.init(viewMode: .all)
1212
}
13-
14-
// override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind {
15-
// recordSymbol(name: node.name.text, kind: "import", node: node)
16-
// return .skipChildren
17-
// }
1813

1914
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
15+
guard !isPrivateOrFilePrivate(node.modifiers) else { return .skipChildren }
2016
recordSymbol(name: node.name.text, kind: ClassificationKeywords.classWord, node: node)
2117
return .skipChildren
2218
}
2319

2420
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
21+
guard !isPrivateOrFilePrivate(node.modifiers) else { return .skipChildren }
2522
recordSymbol(name: node.name.text, kind: ClassificationKeywords.structWord, node: node)
2623
return .skipChildren
2724
}
2825

2926
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
27+
guard !isPrivateOrFilePrivate(node.modifiers) else { return .skipChildren }
3028
recordSymbol(name: node.name.text, kind: ClassificationKeywords.enumWord, node: node)
3129
return .skipChildren
3230
}
3331

3432
override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
33+
guard !isPrivateOrFilePrivate(node.modifiers) else { return .skipChildren }
3534
recordSymbol(name: node.name.text, kind: ClassificationKeywords.protocolWord, node: node)
3635
return .skipChildren
3736
}
3837

3938
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
39+
guard !isPrivateOrFilePrivate(node.modifiers) else { return .skipChildren }
4040
recordSymbol(name: node.name.text, kind: ClassificationKeywords.actorWord, node: node)
4141
return .skipChildren
4242
}
4343

4444
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
45+
guard !isPrivateOrFilePrivate(node.modifiers) else { return .skipChildren }
4546
recordSymbol(name: node.name.text, kind: ClassificationKeywords.funcWord, node: node)
4647
return .skipChildren
4748
}
4849

4950
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
51+
guard !isPrivateOrFilePrivate(node.modifiers) else { return .skipChildren }
5052
guard let binding = node.bindings.first,
5153
let pattern = binding.pattern.as(IdentifierPatternSyntax.self),
5254
let keyword: ClassificationKeywords = ClassificationKeywords(rawValue: node.bindingSpecifier.text) else {
@@ -58,11 +60,14 @@ class SwiftDeclarationCollector: SyntaxVisitor {
5860

5961
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
6062
let name = node.extendedType.trimmedDescription
63+
guard !isPrivateOrFilePrivate(node.modifiers) && name != "View" else { return .skipChildren }
64+
6165
recordSymbol(name: name, kind: ClassificationKeywords.extensionWord, node: node)
6266
return .skipChildren
6367
}
6468

6569
override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind {
70+
guard !isPrivateOrFilePrivate(node.modifiers) else { return .skipChildren }
6671
recordSymbol(name: node.name.text, kind: ClassificationKeywords.typealiasWord, node: node)
6772
return .skipChildren
6873
}
@@ -87,4 +92,12 @@ class SwiftDeclarationCollector: SyntaxVisitor {
8792
)
8893
symbols.append(symbol)
8994
}
95+
96+
private func isPrivateOrFilePrivate(_ modifiers: DeclModifierListSyntax?) -> Bool {
97+
guard let modifiers else { return false }
98+
return modifiers.contains { modifier in
99+
let text = modifier.name.text
100+
return text == "private" || text == "fileprivate"
101+
}
102+
}
90103
}

0 commit comments

Comments
 (0)