Skip to content

Commit 24c0e99

Browse files
committed
Make output picker live update
1 parent 66197d7 commit 24c0e99

3 files changed

Lines changed: 215 additions & 133 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// UtilityAreaOutputLogList.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 7/18/25.
6+
//
7+
8+
import SwiftUI
9+
10+
struct UtilityAreaOutputLogList<Source: UtilityAreaOutputSource, Toolbar: View>: View {
11+
let source: Source
12+
13+
@State var output: [Source.Message] = []
14+
@Binding var filterText: String
15+
var toolbar: () -> Toolbar
16+
17+
init(source: Source, filterText: Binding<String>, @ViewBuilder toolbar: @escaping () -> Toolbar) {
18+
self.source = source
19+
self._filterText = filterText
20+
self.toolbar = toolbar
21+
}
22+
23+
var filteredOutput: [Source.Message] {
24+
if filterText.isEmpty {
25+
return output
26+
}
27+
return output.filter { item in
28+
return filterText == "" ? true : item.message.contains(filterText)
29+
}
30+
}
31+
32+
var body: some View {
33+
List(filteredOutput.reversed()) { item in
34+
VStack(spacing: 2) {
35+
HStack(spacing: 0) {
36+
Text(item.message)
37+
.fontDesign(.monospaced)
38+
.font(.system(size: 12, weight: .regular).monospaced())
39+
Spacer(minLength: 0)
40+
}
41+
HStack(spacing: 6) {
42+
HStack(spacing: 4) {
43+
Image(systemName: item.level.iconName)
44+
.foregroundColor(.white)
45+
.font(.system(size: 7, weight: .semibold))
46+
.frame(width: 12, height: 12)
47+
.background(
48+
RoundedRectangle(cornerRadius: 2)
49+
.fill(item.level.color)
50+
.aspectRatio(1.0, contentMode: .fit)
51+
)
52+
Text(item.date.logFormatted())
53+
.fontWeight(.medium)
54+
}
55+
if let subsystem = item.subsystem {
56+
HStack(spacing: 2) {
57+
Image(systemName: "gearshape.2")
58+
.font(.system(size: 8, weight: .regular))
59+
Text(subsystem)
60+
}
61+
}
62+
if let category = item.category {
63+
HStack(spacing: 2) {
64+
Image(systemName: "square.grid.3x3")
65+
.font(.system(size: 8, weight: .regular))
66+
Text(category)
67+
}
68+
}
69+
Spacer(minLength: 0)
70+
}
71+
.foregroundStyle(.secondary)
72+
.font(.system(size: 9, weight: .semibold).monospaced())
73+
}
74+
.rotationEffect(.radians(.pi))
75+
.scaleEffect(x: -1, y: 1, anchor: .center)
76+
.alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
77+
.listRowBackground(item.level.backgroundColor)
78+
}
79+
.listStyle(.plain)
80+
.listRowInsets(EdgeInsets())
81+
.rotationEffect(.radians(.pi))
82+
.scaleEffect(x: -1, y: 1, anchor: .center)
83+
.task(id: source.id) {
84+
output = source.cachedMessages()
85+
for await item in source.streamMessages() {
86+
output.append(item)
87+
}
88+
}
89+
.paneToolbar {
90+
toolbar()
91+
Spacer()
92+
UtilityAreaFilterTextField(title: "Filter", text: $filterText)
93+
.frame(maxWidth: 175)
94+
Button {
95+
output.removeAll(keepingCapacity: true)
96+
} label: {
97+
Image(systemName: "trash")
98+
}
99+
}
100+
}
101+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// UtilityAreaOutputSourcePicker.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 7/18/25.
6+
//
7+
8+
import SwiftUI
9+
10+
struct UtilityAreaOutputSourcePicker: View {
11+
typealias Sources = UtilityAreaOutputView.Sources
12+
13+
@EnvironmentObject private var workspace: WorkspaceDocument
14+
15+
@AppSettings(\.developerSettings.showInternalDevelopmentInspector)
16+
var showInternalDevelopmentInspector
17+
18+
@Binding var selectedSource: Sources?
19+
20+
@ObservedObject var extensionManager = ExtensionManager.shared
21+
22+
@Service var lspService: LSPService
23+
@State private var updater: UUID = UUID()
24+
@State private var languageServerClients: [LSPService.LanguageServerType] = []
25+
26+
var body: some View {
27+
Picker("Output Source", selection: $selectedSource) {
28+
if languageServerClients.isEmpty {
29+
Text("No Language Servers")
30+
} else {
31+
ForEach(languageServerClients, id: \.languageId) { server in
32+
Text(server.languageId.rawValue)
33+
.tag(Sources.languageServer(server.logContainer))
34+
}
35+
}
36+
37+
Divider()
38+
39+
if extensionManager.extensions.isEmpty {
40+
Text("No Extensions")
41+
} else {
42+
ForEach(extensionManager.extensions) { extensionInfo in
43+
Text(extensionInfo.name)
44+
.tag(Sources.extensions(.init(extensionInfo: extensionInfo)))
45+
}
46+
}
47+
48+
if showInternalDevelopmentInspector {
49+
Divider()
50+
Text("Development Output")
51+
.tag(Sources.devOutput)
52+
}
53+
}
54+
.id(updater)
55+
.buttonStyle(.borderless)
56+
.labelsHidden()
57+
.controlSize(.small)
58+
.onAppear {
59+
updateLanguageServers(lspService.languageClients)
60+
}
61+
.onReceive(lspService.$languageClients) { clients in
62+
updateLanguageServers(clients)
63+
}
64+
.onReceive(extensionManager.$extensions) { _ in
65+
updater = UUID()
66+
}
67+
}
68+
69+
func updateLanguageServers(_ clients: [LSPService.ClientKey: LSPService.LanguageServerType]) {
70+
languageServerClients = clients
71+
.compactMap { (key, value) in
72+
if key.workspacePath == workspace.fileURL?.absolutePath {
73+
return value
74+
}
75+
return nil
76+
}
77+
.sorted(by: { $0.languageId.rawValue < $1.languageId.rawValue })
78+
if selectedSource == nil, let client = languageServerClients.first {
79+
selectedSource = Sources.languageServer(client.logContainer)
80+
}
81+
updater = UUID()
82+
}
83+
}

CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputView.swift

Lines changed: 31 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ struct UtilityAreaOutputView: View {
1414
case languageServer(LanguageServerLogContainer)
1515
case devOutput
1616

17+
var title: String {
18+
switch self {
19+
case .extensions(let source):
20+
"Extension - \(source.extensionInfo.name)"
21+
case .languageServer(let source):
22+
"Language Server - \(source.id)"
23+
case .devOutput:
24+
"Internal Development Output"
25+
}
26+
}
27+
1728
public static func == (_ lhs: Sources, _ rhs: Sources) -> Bool {
1829
switch (lhs, rhs) {
1930
case let (.extensions(lhs), .extensions(rhs)):
@@ -41,160 +52,47 @@ struct UtilityAreaOutputView: View {
4152
}
4253
}
4354

44-
@AppSettings(\.developerSettings.showInternalDevelopmentInspector)
45-
var showInternalDevelopmentInspector
46-
47-
@EnvironmentObject private var workspace: WorkspaceDocument
4855
@EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel
49-
@ObservedObject var extensionManager = ExtensionManager.shared
50-
@Service var lspService: LSPService
5156

5257
@State private var filterText: String = ""
5358
@State private var selectedSource: Sources?
5459

55-
var languageServerClients: [LSPService.LanguageServerType] {
56-
lspService.languageClients.compactMap { (key: LSPService.ClientKey, value: LSPService.LanguageServerType) in
57-
if key.workspacePath == workspace.fileURL?.absolutePath {
58-
return value
59-
}
60-
return nil
61-
}
62-
}
63-
6460
var body: some View {
6561
UtilityAreaTabView(model: utilityAreaViewModel.tabViewModel) { _ in
6662
Group {
6763
if let selectedSource {
6864
switch selectedSource {
6965
case .extensions(let source):
70-
OutputView(source: source, filterText: $filterText)
66+
UtilityAreaOutputLogList(source: source, filterText: $filterText) {
67+
UtilityAreaOutputSourcePicker(selectedSource: $selectedSource)
68+
}
7169
case .languageServer(let source):
72-
OutputView(source: source, filterText: $filterText)
70+
UtilityAreaOutputLogList(source: source, filterText: $filterText) {
71+
UtilityAreaOutputSourcePicker(selectedSource: $selectedSource)
72+
}
7373
case .devOutput:
74-
OutputView(source: InternalDevelopmentOutputSource.shared, filterText: $filterText)
74+
UtilityAreaOutputLogList(
75+
source: InternalDevelopmentOutputSource.shared,
76+
filterText: $filterText
77+
) {
78+
UtilityAreaOutputSourcePicker(selectedSource: $selectedSource)
79+
}
7580
}
7681
} else {
7782
Text("No output")
7883
.font(.system(size: 16))
7984
.foregroundColor(.secondary)
8085
.frame(maxHeight: .infinity)
81-
}
82-
}
83-
.paneToolbar {
84-
Picker("Output Source", selection: $selectedSource) {
85-
if selectedSource == nil {
86-
Text("No Selection")
87-
.tag(nil as Sources?)
88-
}
89-
90-
if extensionManager.extensions.isEmpty {
91-
Text("No Extensions")
92-
}
93-
ForEach(extensionManager.extensions) { extensionInfo in
94-
Text(extensionInfo.name)
95-
.tag(Sources.extensions(.init(extensionInfo: extensionInfo)))
96-
}
97-
Divider()
98-
99-
if languageServerClients.isEmpty {
100-
Text("No Language Servers")
101-
}
102-
ForEach(languageServerClients, id: \.languageId) { server in
103-
Text(server.languageId.rawValue)
104-
.tag(Sources.languageServer(server.logContainer))
105-
}
106-
107-
if showInternalDevelopmentInspector {
108-
Divider()
109-
Text("Development Output")
110-
.tag(Sources.devOutput)
111-
}
112-
}
113-
.buttonStyle(.borderless)
114-
.labelsHidden()
115-
.controlSize(.small)
116-
Spacer()
117-
UtilityAreaFilterTextField(title: "Filter", text: $filterText)
118-
.frame(maxWidth: 175)
119-
// Button {
120-
// output = []
121-
// } label: {
122-
// Image(systemName: "trash")
123-
// }
124-
}
125-
}
126-
}
127-
128-
struct OutputView<Source: UtilityAreaOutputSource>: View {
129-
let source: Source
130-
131-
@State var output: [Source.Message] = []
132-
@Binding var filterText: String
133-
134-
var filteredOutput: [Source.Message] {
135-
if filterText.isEmpty {
136-
return output
137-
}
138-
return output.filter { item in
139-
return filterText == "" ? true : item.message.contains(filterText)
140-
}
141-
}
142-
143-
var body: some View {
144-
List(filteredOutput.reversed()) { item in
145-
VStack(spacing: 2) {
146-
HStack(spacing: 0) {
147-
Text(item.message)
148-
.fontDesign(.monospaced)
149-
.font(.system(size: 12, weight: .regular).monospaced())
150-
Spacer(minLength: 0)
151-
}
152-
HStack(spacing: 6) {
153-
HStack(spacing: 4) {
154-
Image(systemName: item.level.iconName)
155-
.foregroundColor(.white)
156-
.font(.system(size: 7, weight: .semibold))
157-
.frame(width: 12, height: 12)
158-
.background(
159-
RoundedRectangle(cornerRadius: 2)
160-
.fill(item.level.color)
161-
.aspectRatio(1.0, contentMode: .fit)
162-
)
163-
Text(item.date.logFormatted())
164-
.fontWeight(.medium)
165-
}
166-
if let subsystem = item.subsystem {
167-
HStack(spacing: 2) {
168-
Image(systemName: "gearshape.2")
169-
.font(.system(size: 8, weight: .regular))
170-
Text(subsystem)
86+
.paneToolbar {
87+
UtilityAreaOutputSourcePicker(selectedSource: $selectedSource)
88+
Spacer()
89+
UtilityAreaFilterTextField(title: "Filter", text: $filterText)
90+
.frame(maxWidth: 175)
91+
Button { } label: {
92+
Image(systemName: "trash")
17193
}
94+
.disabled(true)
17295
}
173-
if let category = item.category {
174-
HStack(spacing: 2) {
175-
Image(systemName: "square.grid.3x3")
176-
.font(.system(size: 8, weight: .regular))
177-
Text(category)
178-
}
179-
}
180-
Spacer(minLength: 0)
181-
}
182-
.foregroundStyle(.secondary)
183-
.font(.system(size: 9, weight: .semibold).monospaced())
184-
}
185-
.rotationEffect(.radians(.pi))
186-
.scaleEffect(x: -1, y: 1, anchor: .center)
187-
.alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
188-
.listRowBackground(item.level.backgroundColor)
189-
}
190-
.listStyle(.plain)
191-
.listRowInsets(EdgeInsets())
192-
.rotationEffect(.radians(.pi))
193-
.scaleEffect(x: -1, y: 1, anchor: .center)
194-
.task(id: source.id) {
195-
output = source.cachedMessages()
196-
for await item in source.streamMessages() {
197-
output.append(item)
19896
}
19997
}
20098
}

0 commit comments

Comments
 (0)