Skip to content

Commit 9947256

Browse files
Moved code from TextView, added more functionality to delegate
1 parent 924d86f commit 9947256

File tree

5 files changed

+565
-4
lines changed

5 files changed

+565
-4
lines changed

Package.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ let package = Package(
1616
dependencies: [
1717
// A fast, efficient, text view for code.
1818
.package(
19-
// url: "https://github.com/CodeEditApp/CodeEditTextView.git",
20-
// from: "0.7.7"
21-
path: "../CodeEditTextView"
19+
url: "https://github.com/CodeEditApp/CodeEditTextView.git",
20+
from: "0.7.7"
2221
),
2322
// tree-sitter languages
2423
.package(
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// NoSlotScroller.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Abe Malla on 12/26/24.
6+
//
7+
8+
import AppKit
9+
10+
class NoSlotScroller: NSScroller {
11+
override class var isCompatibleWithOverlayScrollers: Bool { true }
12+
13+
override func drawKnobSlot(in slotRect: NSRect, highlight flag: Bool) {
14+
// Don't draw the knob slot (the background track behind the knob)
15+
}
16+
}
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
//
2+
// SuggestionController+Window.swift
3+
// CodeEditTextView
4+
//
5+
// Created by Abe Malla on 12/22/24.
6+
//
7+
8+
import AppKit
9+
10+
extension SuggestionController {
11+
/// Will constrain the window's frame to be within the visible screen
12+
public func constrainWindowToScreenEdges(cursorRect: NSRect) {
13+
guard let window = self.window,
14+
let screenFrame = window.screen?.visibleFrame else {
15+
return
16+
}
17+
18+
let windowSize = window.frame.size
19+
let padding: CGFloat = 22
20+
// TODO: PASS IN OFFSET
21+
var newWindowOrigin = NSPoint(
22+
x: cursorRect.origin.x - Self.WINDOW_PADDING - 13 - 16.5,
23+
y: cursorRect.origin.y
24+
)
25+
26+
// Keep the horizontal position within the screen and some padding
27+
let minX = screenFrame.minX + padding
28+
let maxX = screenFrame.maxX - windowSize.width - padding
29+
30+
if newWindowOrigin.x < minX {
31+
newWindowOrigin.x = minX
32+
} else if newWindowOrigin.x > maxX {
33+
newWindowOrigin.x = maxX
34+
}
35+
36+
// Check if the window will go below the screen
37+
// We determine whether the window drops down or upwards by choosing which
38+
// corner of the window we will position: `setFrameOrigin` or `setFrameTopLeftPoint`
39+
if newWindowOrigin.y - windowSize.height < screenFrame.minY {
40+
// If the cursor itself is below the screen, then position the window
41+
// at the bottom of the screen with some padding
42+
if newWindowOrigin.y < screenFrame.minY {
43+
newWindowOrigin.y = screenFrame.minY + padding
44+
} else {
45+
// Place above the cursor
46+
newWindowOrigin.y += cursorRect.height
47+
}
48+
49+
isWindowAboveCursor = true
50+
window.setFrameOrigin(newWindowOrigin)
51+
} else {
52+
// If the window goes above the screen, position it below the screen with padding
53+
let maxY = screenFrame.maxY - padding
54+
if newWindowOrigin.y > maxY {
55+
newWindowOrigin.y = maxY
56+
}
57+
58+
isWindowAboveCursor = false
59+
window.setFrameTopLeftPoint(newWindowOrigin)
60+
}
61+
}
62+
63+
// MARK: - Private Methods
64+
65+
static func makeWindow() -> NSWindow {
66+
let window = NSWindow(
67+
contentRect: NSRect(origin: .zero, size: self.DEFAULT_SIZE),
68+
styleMask: [.resizable, .fullSizeContentView, .nonactivatingPanel],
69+
backing: .buffered,
70+
defer: false
71+
)
72+
73+
configureWindow(window)
74+
configureWindowContent(window)
75+
return window
76+
}
77+
78+
static func configureWindow(_ window: NSWindow) {
79+
window.titleVisibility = .hidden
80+
window.titlebarAppearsTransparent = true
81+
window.isExcludedFromWindowsMenu = true
82+
window.isReleasedWhenClosed = false
83+
window.level = .popUpMenu
84+
window.hasShadow = true
85+
window.isOpaque = false
86+
window.tabbingMode = .disallowed
87+
window.hidesOnDeactivate = true
88+
window.backgroundColor = .clear
89+
window.minSize = Self.DEFAULT_SIZE
90+
}
91+
92+
static func configureWindowContent(_ window: NSWindow) {
93+
guard let contentView = window.contentView else { return }
94+
95+
contentView.wantsLayer = true
96+
// TODO: GET COLOR FROM THEME
97+
contentView.layer?.backgroundColor = CGColor(
98+
srgbRed: 31.0 / 255.0,
99+
green: 31.0 / 255.0,
100+
blue: 36.0 / 255.0,
101+
alpha: 1.0
102+
)
103+
contentView.layer?.cornerRadius = 8.5
104+
contentView.layer?.borderWidth = 1
105+
contentView.layer?.borderColor = NSColor.gray.withAlphaComponent(0.45).cgColor
106+
107+
let innerShadow = NSShadow()
108+
innerShadow.shadowColor = NSColor.black.withAlphaComponent(0.1)
109+
innerShadow.shadowOffset = NSSize(width: 0, height: -1)
110+
innerShadow.shadowBlurRadius = 2
111+
contentView.shadow = innerShadow
112+
}
113+
114+
func configureTableView() {
115+
tableView.delegate = self
116+
tableView.dataSource = self
117+
tableView.headerView = nil
118+
tableView.backgroundColor = .clear
119+
tableView.intercellSpacing = .zero
120+
tableView.allowsEmptySelection = false
121+
tableView.selectionHighlightStyle = .regular
122+
tableView.style = .plain
123+
tableView.usesAutomaticRowHeights = false
124+
tableView.rowSizeStyle = .custom
125+
tableView.rowHeight = 21
126+
tableView.gridStyleMask = []
127+
tableView.target = self
128+
tableView.action = #selector(tableViewClicked(_:))
129+
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("ItemsCell"))
130+
tableView.addTableColumn(column)
131+
}
132+
133+
@objc private func tableViewClicked(_ sender: Any?) {
134+
if NSApp.currentEvent?.clickCount == 2 {
135+
let row = tableView.selectedRow
136+
guard row >= 0, row < items.count else {
137+
return
138+
}
139+
let selectedItem = items[row]
140+
delegate?.applyCompletionItem(item: selectedItem)
141+
self.close()
142+
}
143+
}
144+
145+
func configureScrollView() {
146+
scrollView.documentView = tableView
147+
scrollView.hasVerticalScroller = true
148+
scrollView.verticalScroller = NoSlotScroller()
149+
scrollView.scrollerStyle = .overlay
150+
scrollView.autohidesScrollers = true
151+
scrollView.drawsBackground = false
152+
scrollView.automaticallyAdjustsContentInsets = false
153+
scrollView.translatesAutoresizingMaskIntoConstraints = false
154+
scrollView.verticalScrollElasticity = .allowed
155+
scrollView.contentInsets = NSEdgeInsets(
156+
top: Self.WINDOW_PADDING,
157+
left: 0,
158+
bottom: Self.WINDOW_PADDING,
159+
right: 0
160+
)
161+
162+
guard let contentView = window?.contentView else { return }
163+
contentView.addSubview(scrollView)
164+
165+
NSLayoutConstraint.activate([
166+
scrollView.topAnchor.constraint(equalTo: contentView.topAnchor),
167+
scrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
168+
scrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
169+
scrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
170+
])
171+
}
172+
173+
/// Updates the item box window's height based on the number of items.
174+
/// If there are no items, the default label will be displayed instead.
175+
func updateSuggestionWindowAndContents() {
176+
guard let window = self.window else {
177+
return
178+
}
179+
180+
noItemsLabel.isHidden = !items.isEmpty
181+
scrollView.isHidden = items.isEmpty
182+
183+
// Update window dimensions
184+
let numberOfVisibleRows = min(CGFloat(items.count), Self.MAX_VISIBLE_ROWS)
185+
let newHeight = items.count == 0 ?
186+
Self.rowsToWindowHeight(for: 1) : // Height for 1 row when empty
187+
Self.rowsToWindowHeight(for: numberOfVisibleRows)
188+
189+
let currentFrame = window.frame
190+
if isWindowAboveCursor {
191+
// When window is above cursor, maintain the bottom position
192+
let bottomY = currentFrame.minY
193+
let newFrame = NSRect(
194+
x: currentFrame.minX,
195+
y: bottomY,
196+
width: Self.DEFAULT_SIZE.width,
197+
height: newHeight
198+
)
199+
window.setFrame(newFrame, display: true)
200+
} else {
201+
// When window is below cursor, maintain the top position
202+
window.setContentSize(NSSize(width: Self.DEFAULT_SIZE.width, height: newHeight))
203+
}
204+
205+
// Dont allow vertical resizing
206+
window.maxSize = NSSize(width: CGFloat.infinity, height: newHeight)
207+
window.minSize = NSSize(width: Self.DEFAULT_SIZE.width, height: newHeight)
208+
}
209+
210+
func configureNoItemsLabel() {
211+
window?.contentView?.addSubview(noItemsLabel)
212+
213+
NSLayoutConstraint.activate([
214+
noItemsLabel.centerXAnchor.constraint(equalTo: window!.contentView!.centerXAnchor),
215+
noItemsLabel.centerYAnchor.constraint(equalTo: window!.contentView!.centerYAnchor)
216+
])
217+
}
218+
219+
/// Calculate the window height for a given number of rows.
220+
static func rowsToWindowHeight(for numberOfRows: CGFloat) -> CGFloat {
221+
let wholeRows = floor(numberOfRows)
222+
let partialRow = numberOfRows - wholeRows
223+
224+
let baseHeight = ROW_HEIGHT * wholeRows
225+
let partialHeight = partialRow > 0 ? ROW_HEIGHT * partialRow : 0
226+
227+
// Add window padding only for whole numbers
228+
let padding = numberOfRows.truncatingRemainder(dividingBy: 1) == 0 ? WINDOW_PADDING * 2 : WINDOW_PADDING
229+
230+
return baseHeight + partialHeight + padding
231+
}
232+
}
233+
234+
extension SuggestionController: NSTableViewDataSource, NSTableViewDelegate {
235+
public func numberOfRows(in tableView: NSTableView) -> Int {
236+
return items.count
237+
}
238+
239+
public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
240+
(items[row] as? any CodeSuggestionEntry)?.view
241+
}
242+
243+
public func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
244+
CodeSuggestionRowView()
245+
}
246+
247+
public func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
248+
// Only allow selection through keyboard navigation or single clicks
249+
let event = NSApp.currentEvent
250+
if event?.type == .leftMouseDragged {
251+
return false
252+
}
253+
return true
254+
}
255+
}
256+
257+
private class CodeSuggestionRowView: NSTableRowView {
258+
override func drawSelection(in dirtyRect: NSRect) {
259+
guard isSelected else { return }
260+
guard let context = NSGraphicsContext.current?.cgContext else { return }
261+
262+
context.saveGState()
263+
defer { context.restoreGState() }
264+
265+
// Create a rect that's inset from the edges and has proper padding
266+
// TODO: We create a new selectionRect instead of using dirtyRect
267+
// because there is a visual bug when holding down the arrow keys
268+
// to select the first or last item, which draws a clipped
269+
// rectangular highlight shape instead of the whole rectangle.
270+
// Replace this when it gets fixed.
271+
let selectionRect = NSRect(
272+
x: SuggestionController.WINDOW_PADDING,
273+
y: 0,
274+
width: bounds.width - (SuggestionController.WINDOW_PADDING * 2),
275+
height: bounds.height
276+
)
277+
let cornerRadius: CGFloat = 5
278+
let path = NSBezierPath(roundedRect: selectionRect, xRadius: cornerRadius, yRadius: cornerRadius)
279+
let selectionColor = NSColor.gray.withAlphaComponent(0.19)
280+
281+
context.setFillColor(selectionColor.cgColor)
282+
path.fill()
283+
}
284+
}

0 commit comments

Comments
 (0)