Skip to content

Commit a49de0c

Browse files
committed
Merge branch 'itembox' into jump-to-definition
2 parents 4000b67 + 058e165 commit a49de0c

File tree

12 files changed

+215
-143
lines changed

12 files changed

+215
-143
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/MockCompletionDelegate.swift

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,87 @@
55
// Created by Khan Winter on 7/22/25.
66
//
77

8-
import AppKit
8+
import SwiftUI
99
import CodeEditSourceEditor
1010
import CodeEditTextView
1111

12+
private let text = [
13+
"Lorem",
14+
"ipsum",
15+
"dolor",
16+
"sit",
17+
"amet,",
18+
"consectetur",
19+
"adipiscing",
20+
"elit.",
21+
"Ut",
22+
"condimentum",
23+
"dictum",
24+
"malesuada.",
25+
"Praesent",
26+
"ut",
27+
"imperdiet",
28+
"nulla.",
29+
"Vivamus",
30+
"feugiat,",
31+
"ante",
32+
"non",
33+
"sagittis",
34+
"pellentesque,",
35+
"dui",
36+
"massa",
37+
"consequat",
38+
"odio,",
39+
"ac",
40+
"vestibulum",
41+
"augue",
42+
"erat",
43+
"et",
44+
"nunc."
45+
]
46+
1247
class MockCompletionDelegate: CodeSuggestionDelegate, ObservableObject {
1348
class Suggestion: CodeSuggestionEntry {
14-
let text: String
15-
var view: NSView {
16-
let view = NSTextField(string: text)
17-
view.isEditable = false
18-
view.isSelectable = false
19-
view.isBezeled = false
20-
view.isBordered = false
21-
view.backgroundColor = .clear
22-
view.textColor = .black
23-
return view
24-
}
49+
var label: String
50+
var detail: String?
51+
var pathComponents: [String]? { nil }
52+
var targetPosition: CursorPosition? { nil }
53+
var sourcePreview: String? { nil }
54+
var image: Image = Image(systemName: "dot.square.fill")
55+
var imageColor: Color = .gray
56+
var deprecated: Bool = false
2557

2658
init(text: String) {
27-
self.text = text
59+
self.label = text
60+
}
61+
}
62+
63+
private func randomSuggestions() -> [Suggestion] {
64+
let count = Int.random(in: 0..<20)
65+
var suggestions: [Suggestion] = []
66+
for _ in 0..<count {
67+
let randomString = (0..<Int.random(in: 1..<text.count)).map {
68+
text[$0]
69+
}.shuffled().joined(separator: " ")
70+
suggestions.append(Suggestion(text: randomString))
2871
}
72+
return suggestions
2973
}
3074

3175
func completionSuggestionsRequested(
3276
textView: TextViewController,
3377
cursorPosition: CursorPosition
3478
) async -> (windowPosition: CursorPosition, items: [CodeSuggestionEntry])? {
3579
try? await Task.sleep(for: .seconds(0.2))
36-
return (cursorPosition, [Suggestion(text: "Hello"), Suggestion(text: "World")])
80+
return (cursorPosition, randomSuggestions())
3781
}
3882

3983
func completionOnCursorMove(
4084
textView: TextViewController,
4185
cursorPosition: CursorPosition
4286
) -> [CodeSuggestionEntry]? {
4387
if Bool.random() {
44-
[Suggestion(text: "Another one")]
88+
randomSuggestions()
4589
} else {
4690
nil
4791
}
@@ -56,7 +100,7 @@ class MockCompletionDelegate: CodeSuggestionDelegate, ObservableObject {
56100
return
57101
}
58102
textView.textView.undoManager?.beginUndoGrouping()
59-
textView.textView.insertText(suggestion.text)
103+
textView.textView.insertText(suggestion.label)
60104
textView.textView.undoManager?.endUndoGrouping()
61105
}
62106
}

Sources/CodeEditSourceEditor/CodeSuggestion/AutoCompleteCoordinatorProtocol.swift

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

Sources/CodeEditSourceEditor/CodeSuggestion/CodeSuggestionEntry.swift

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

Sources/CodeEditSourceEditor/CodeSuggestion/CodeSuggestionDelegate.swift renamed to Sources/CodeEditSourceEditor/CodeSuggestion/Model/CodeSuggestionDelegate.swift

File renamed without changes.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// CodeSuggestionEntry.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 7/22/25.
6+
//
7+
8+
import AppKit
9+
import SwiftUI
10+
11+
/// Represents an item that can be displayed in the code suggestion view
12+
public protocol CodeSuggestionEntry {
13+
var label: String { get }
14+
var detail: String? { get }
15+
16+
/// Leave as `nil` if the link is in the same document.
17+
var pathComponents: [String]? { get }
18+
var targetPosition: CursorPosition? { get }
19+
var sourcePreview: String? { get }
20+
21+
var image: Image { get }
22+
var imageColor: Color { get }
23+
24+
var deprecated: Bool { get }
25+
}

Sources/CodeEditSourceEditor/CodeSuggestion/SuggestionViewModel.swift renamed to Sources/CodeEditSourceEditor/CodeSuggestion/Model/SuggestionViewModel.swift

File renamed without changes.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// CodeSuggestionLabelView.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 7/24/25.
6+
//
7+
8+
import AppKit
9+
import SwiftUI
10+
11+
struct CodeSuggestionLabelView: View {
12+
let suggestion: CodeSuggestionEntry
13+
let labelColor: NSColor
14+
let secondaryLabelColor: NSColor
15+
let font: NSFont
16+
17+
var body: some View {
18+
HStack(alignment: .center, spacing: 2) {
19+
suggestion.image
20+
.font(.system(size: font.pointSize + 2))
21+
.foregroundStyle(
22+
.white,
23+
suggestion.deprecated ? .gray : suggestion.imageColor
24+
)
25+
26+
// Main label
27+
HStack(spacing: 0) {
28+
Text(suggestion.label)
29+
.foregroundStyle(suggestion.deprecated ? Color(secondaryLabelColor) : Color(labelColor))
30+
31+
if let detail = suggestion.detail {
32+
Text(detail)
33+
.foregroundStyle(Color(secondaryLabelColor))
34+
}
35+
}
36+
.font(Font(font))
37+
38+
Spacer(minLength: 0)
39+
40+
// Right side indicators
41+
if suggestion.deprecated {
42+
Image(systemName: "exclamationmark.triangle")
43+
.font(.system(size: font.pointSize + 2))
44+
.foregroundStyle(Color(labelColor), Color(secondaryLabelColor))
45+
}
46+
}
47+
.padding(.vertical, 3)
48+
.padding(.horizontal, 13)
49+
.buttonStyle(PlainButtonStyle())
50+
}
51+
}

Sources/CodeEditSourceEditor/CodeSuggestion/CodeSuggestionRowView.swift renamed to Sources/CodeEditSourceEditor/CodeSuggestion/TableView/CodeSuggestionRowView.swift

File renamed without changes.

Sources/CodeEditSourceEditor/CodeSuggestion/NoSlotScroller.swift renamed to Sources/CodeEditSourceEditor/CodeSuggestion/TableView/NoSlotScroller.swift

File renamed without changes.

Sources/CodeEditSourceEditor/CodeSuggestion/SuggestionViewController.swift renamed to Sources/CodeEditSourceEditor/CodeSuggestion/TableView/SuggestionViewController.swift

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66
//
77

88
import AppKit
9+
import SwiftUI
910
import Combine
1011

1112
class SuggestionViewController: NSViewController {
1213
var tableView: NSTableView!
1314
var scrollView: NSScrollView!
14-
var tintView: NSView!
1515
var noItemsLabel: NSTextField!
1616

1717
var itemObserver: AnyCancellable?
18+
1819
weak var model: SuggestionViewModel? {
1920
didSet {
2021
itemObserver?.cancel()
@@ -28,13 +29,7 @@ class SuggestionViewController: NSViewController {
2829
super.loadView()
2930
view.wantsLayer = true
3031
view.layer?.cornerRadius = 8.5
31-
view.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
32-
33-
tintView = NSView()
34-
tintView.translatesAutoresizingMaskIntoConstraints = false
35-
tintView.wantsLayer = true
36-
tintView.layer?.cornerRadius = 8.5
37-
view.addSubview(tintView)
32+
view.layer?.backgroundColor = .clear
3833

3934
tableView = NSTableView()
4035
configureTableView()
@@ -46,24 +41,19 @@ class SuggestionViewController: NSViewController {
4641
noItemsLabel.alignment = .center
4742
noItemsLabel.translatesAutoresizingMaskIntoConstraints = false
4843
noItemsLabel.isHidden = false
49-
// TODO: GET FONT SIZE FROM THEME
50-
noItemsLabel.font = .monospacedSystemFont(ofSize: 12, weight: .regular)
5144

52-
tintView.addSubview(noItemsLabel)
53-
tintView.addSubview(scrollView)
45+
view.addSubview(noItemsLabel)
46+
view.addSubview(scrollView)
5447

5548
NSLayoutConstraint.activate([
56-
tintView.topAnchor.constraint(equalTo: view.topAnchor),
57-
tintView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
58-
tintView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
59-
tintView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
60-
61-
noItemsLabel.centerXAnchor.constraint(equalTo: tintView.centerXAnchor),
62-
noItemsLabel.centerYAnchor.constraint(equalTo: tintView.centerYAnchor),
63-
scrollView.topAnchor.constraint(equalTo: tintView.topAnchor),
64-
scrollView.leadingAnchor.constraint(equalTo: tintView.leadingAnchor),
65-
scrollView.trailingAnchor.constraint(equalTo: tintView.trailingAnchor),
66-
scrollView.bottomAnchor.constraint(equalTo: tintView.bottomAnchor)
49+
noItemsLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
50+
noItemsLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
51+
noItemsLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
52+
53+
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
54+
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
55+
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
56+
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
6757
])
6858
}
6959

@@ -77,6 +67,7 @@ class SuggestionViewController: NSViewController {
7767
}
7868

7969
func styleView(using controller: TextViewController) {
70+
noItemsLabel.font = controller.font
8071
switch controller.systemAppearance {
8172
case .aqua:
8273
let color = controller.theme.background
@@ -87,15 +78,42 @@ class SuggestionViewController: NSViewController {
8778
blue: color.blueComponent * 0.95,
8879
alpha: 1.0
8980
)
90-
tintView.layer?.backgroundColor = newColor.cgColor
81+
view.layer?.backgroundColor = newColor.cgColor
9182
} else {
92-
tintView.layer?.backgroundColor = .clear
83+
view.layer?.backgroundColor = .clear
9384
}
9485
case .darkAqua:
95-
tintView.layer?.backgroundColor = controller.theme.background.cgColor
86+
view.layer?.backgroundColor = controller.theme.background.cgColor
9687
default:
9788
return
9889
}
90+
91+
guard model?.items.isEmpty == false else {
92+
let size = NSSize(width: 256, height: noItemsLabel.fittingSize.height + 20)
93+
preferredContentSize = size
94+
view.window?.setContentSize(size)
95+
view.window?.contentMinSize = size
96+
view.window?.contentMaxSize = size
97+
return
98+
}
99+
guard let rowView = tableView.view(atColumn: 0, row: 0, makeIfNecessary: true) else {
100+
return
101+
}
102+
let rowHeight = rowView.fittingSize.height
103+
104+
let numberOfVisibleRows = min(CGFloat(model?.items.count ?? 0), SuggestionController.MAX_VISIBLE_ROWS)
105+
let newHeight = rowHeight * numberOfVisibleRows + SuggestionController.WINDOW_PADDING * 2
106+
107+
let maxLength = min((model?.items.max(by: { $0.label.count < $1.label.count })?.label.count ?? 16) + 4, 48)
108+
let newWidth = CGFloat(maxLength) * controller.font.charWidth
109+
110+
view.constraints.filter({ $0.firstAnchor == view.heightAnchor }).forEach { $0.isActive = false }
111+
view.heightAnchor.constraint(equalToConstant: newHeight).isActive = true
112+
113+
preferredContentSize = NSSize(width: newWidth, height: newHeight)
114+
view.window?.setContentSize(NSSize(width: newWidth, height: newHeight))
115+
view.window?.contentMinSize = NSSize(width: newWidth, height: newHeight)
116+
view.window?.contentMaxSize = NSSize(width: .infinity, height: newHeight)
99117
}
100118

101119
func configureTableView() {
@@ -107,9 +125,7 @@ class SuggestionViewController: NSViewController {
107125
tableView.allowsEmptySelection = false
108126
tableView.selectionHighlightStyle = .regular
109127
tableView.style = .plain
110-
tableView.usesAutomaticRowHeights = false
111-
tableView.rowSizeStyle = .custom
112-
tableView.rowHeight = 21
128+
tableView.usesAutomaticRowHeights = true
113129
tableView.gridStyleMask = []
114130
tableView.target = self
115131
tableView.action = #selector(tableViewClicked(_:))
@@ -157,7 +173,7 @@ class SuggestionViewController: NSViewController {
157173
clipView.scroll(to: NSPoint(x: 0, y: -SuggestionController.WINDOW_PADDING))
158174

159175
// Select the first item
160-
if !(model?.items.isEmpty ?? true) {
176+
if model?.items.isEmpty == false {
161177
tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
162178
}
163179
}
@@ -179,8 +195,19 @@ extension SuggestionViewController: NSTableViewDataSource, NSTableViewDelegate {
179195
}
180196

181197
public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
182-
guard row >= 0, row < model?.items.count ?? 0 else { return nil }
183-
return model?.items[row].view
198+
guard let model = model,
199+
row >= 0, row < model.items.count,
200+
let textView = model.activeTextView else {
201+
return nil
202+
}
203+
return NSHostingView(
204+
rootView: CodeSuggestionLabelView(
205+
suggestion: model.items[row],
206+
labelColor: textView.theme.text.color,
207+
secondaryLabelColor: textView.theme.comments.color,
208+
font: textView.font
209+
)
210+
)
184211
}
185212

186213
public func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {

0 commit comments

Comments
 (0)