Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 424453d

Browse files
Suggestion Window Fixes (#350)
### Description Generally improves a few UX bugs with the suggestion window. Specifically the window would flash often even if the controller did not re-request new items, and would sometimes move the window when it shouldn't have. Also adjusts the window's x position to align the completion labels with the text. - Centralizes suggestion presentation logic into a single class. - Moves the trigger character logic out of a filter and into a textview delegate-like method that checks if the last typed character was a trigger character. - Ensures the textview and cursor positions are up-to-date when the notification is sent. - Helps remove duplicate cursor update notifications sent to the suggestion controller by checking if an update is a duplicate in the centralized logic controller. - Adjusts the suggestion window's x position to align the text in the completion labels with the text being typed. Also includes a few changes fixing some build warnings. ### Related Issues * #282 ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots https://github.com/user-attachments/assets/14662210-0c15-422d-8dea-a5ae55b5d836
1 parent ee0c00a commit 424453d

File tree

14 files changed

+118
-63
lines changed

14 files changed

+118
-63
lines changed

‎Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/MockCompletionDelegate.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,12 @@ private let text = [
4545
]
4646

4747
class MockCompletionDelegate: CodeSuggestionDelegate, ObservableObject {
48+
var lastPosition: CursorPosition?
49+
4850
class Suggestion: CodeSuggestionEntry {
4951
var label: String
5052
var detail: String?
53+
var documentation: String?
5154
var pathComponents: [String]?
5255
var targetPosition: CursorPosition? = CursorPosition(line: 10, column: 20)
5356
var sourcePreview: String?
@@ -89,19 +92,32 @@ class MockCompletionDelegate: CodeSuggestionDelegate, ObservableObject {
8992
cursorPosition: CursorPosition
9093
) async -> (windowPosition: CursorPosition, items: [CodeSuggestionEntry])? {
9194
try? await Task.sleep(for: .seconds(0.2))
95+
lastPosition = cursorPosition
9296
return (cursorPosition, randomSuggestions())
9397
}
9498

9599
func completionOnCursorMove(
96100
textView: TextViewController,
97101
cursorPosition: CursorPosition
98102
) -> [CodeSuggestionEntry]? {
103+
// Check if we're typing all in a row.
104+
guard (lastPosition?.range.location ?? 0) + 1 == cursorPosition.range.location else {
105+
lastPosition = nil
106+
moveCount = 0
107+
return nil
108+
}
109+
110+
lastPosition = cursorPosition
99111
moveCount += 1
100112
switch moveCount {
101113
case 1:
102114
return randomSuggestions(2)
103115
case 2:
104116
return randomSuggestions(20)
117+
case 3:
118+
return randomSuggestions(4)
119+
case 4:
120+
return randomSuggestions(1)
105121
default:
106122
moveCount = 0
107123
return nil

‎Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/MockJumpToDefinitionDelegate.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ final class MockJumpToDefinitionDelegate: JumpToDefinitionDelegate, ObservableOb
1515
url: nil,
1616
targetRange: CursorPosition(line: 0, column: 10),
1717
typeName: "Start of Document",
18-
sourcePreview: "// Comment at start"
18+
sourcePreview: "// Comment at start",
19+
documentation: "Jumps to the comment at the start of the document. Useful?"
1920
),
2021
JumpToDefinitionLink(
2122
url: URL(string: "https://codeedit.app/"),
2223
targetRange: CursorPosition(line: 1024, column: 10),
2324
typeName: "CodeEdit Website",
24-
sourcePreview: "https://codeedit.app/"
25+
sourcePreview: "https://codeedit.app/",
26+
documentation: "Opens CodeEdit's homepage! You can customize how links are handled, this one opens a "
27+
+ "URL."
2528
)
2629
]
2730
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// SuggestionTriggerCharacterModel.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 8/25/25.
6+
//
7+
8+
import AppKit
9+
import CodeEditTextView
10+
import TextStory
11+
12+
/// Triggers the suggestion window when trigger characters are typed.
13+
/// Designed to be called in the ``TextViewDelegate``'s didReplaceCharacters method.
14+
///
15+
/// Was originally a `TextFilter` model, however those are called before text is changed and cursors are updated.
16+
/// The suggestion model expects up-to-date cursor positions as well as complete text contents. This being
17+
/// essentially a textview delegate ensures both of those promises are upheld.
18+
final class SuggestionTriggerCharacterModel {
19+
weak var controller: TextViewController?
20+
private var lastPosition: NSRange?
21+
22+
var triggerCharacters: Set<String>? {
23+
controller?.configuration.peripherals.codeSuggestionTriggerCharacters
24+
}
25+
26+
func textView(_ textView: TextView, didReplaceContentsIn range: NSRange, with string: String) {
27+
guard let controller, let completionDelegate = controller.completionDelegate, let triggerCharacters else {
28+
return
29+
}
30+
31+
let mutation = TextMutation(
32+
string: string,
33+
range: range,
34+
limit: textView.textStorage.length
35+
)
36+
guard mutation.delta >= 0,
37+
let lastChar = mutation.string.last else {
38+
lastPosition = nil
39+
return
40+
}
41+
42+
guard triggerCharacters.contains(String(lastChar)) || lastChar.isNumber || lastChar.isLetter else {
43+
lastPosition = nil
44+
return
45+
}
46+
47+
let range = NSRange(location: mutation.postApplyRange.max, length: 0)
48+
lastPosition = range
49+
SuggestionController.shared.cursorsUpdated(
50+
textView: controller,
51+
delegate: completionDelegate,
52+
position: CursorPosition(range: range),
53+
presentIfNot: true
54+
)
55+
}
56+
57+
func selectionUpdated(_ position: CursorPosition) {
58+
guard let controller, let completionDelegate = controller.completionDelegate else {
59+
return
60+
}
61+
62+
if lastPosition != position.range {
63+
SuggestionController.shared.cursorsUpdated(
64+
textView: controller,
65+
delegate: completionDelegate,
66+
position: position
67+
)
68+
}
69+
}
70+
}

‎Sources/CodeEditSourceEditor/CodeSuggestion/Model/SuggestionViewModel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ final class SuggestionViewModel: ObservableObject {
1616

1717
weak var delegate: CodeSuggestionDelegate?
1818

19+
private var cursorPosition: CursorPosition?
1920
private var syntaxHighlightedCache: [Int: NSAttributedString] = [:]
2021

2122
func showCompletions(

‎Sources/CodeEditSourceEditor/CodeSuggestion/TableView/CodeSuggestionLabelView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import AppKit
99
import SwiftUI
1010

1111
struct CodeSuggestionLabelView: View {
12+
static let HORIZONTAL_PADDING: CGFloat = 13
13+
1214
let suggestion: CodeSuggestionEntry
1315
let labelColor: NSColor
1416
let secondaryLabelColor: NSColor
@@ -45,7 +47,7 @@ struct CodeSuggestionLabelView: View {
4547
}
4648
}
4749
.padding(.vertical, 3)
48-
.padding(.horizontal, 13)
50+
.padding(.horizontal, Self.HORIZONTAL_PADDING)
4951
.buttonStyle(PlainButtonStyle())
5052
}
5153
}

‎Sources/CodeEditSourceEditor/CodeSuggestion/Window/SuggestionController+Window.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import AppKit
99

1010
extension SuggestionController {
1111
/// Will constrain the window's frame to be within the visible screen
12-
public func constrainWindowToScreenEdges(cursorRect: NSRect) {
12+
public func constrainWindowToScreenEdges(cursorRect: NSRect, font:NSFont) {
1313
guard let window = self.window,
1414
let screenFrame = window.screen?.visibleFrame else {
1515
return
@@ -18,7 +18,8 @@ extension SuggestionController {
1818
let windowSize = window.frame.size
1919
let padding: CGFloat = 22
2020
var newWindowOrigin = NSPoint(
21-
x: cursorRect.origin.x - Self.WINDOW_PADDING,
21+
x: cursorRect.origin.x - Self.WINDOW_PADDING
22+
- CodeSuggestionLabelView.HORIZONTAL_PADDING - font.pointSize,
2223
y: cursorRect.origin.y
2324
)
2425

‎Sources/CodeEditSourceEditor/CodeSuggestion/Window/SuggestionController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public final class SuggestionController: NSWindowController {
9191
self.popover = popover
9292
} else {
9393
self.showWindow(attachedTo: parentWindow)
94-
self.constrainWindowToScreenEdges(cursorRect: cursorRect)
94+
self.constrainWindowToScreenEdges(cursorRect: cursorRect, font: textView.font)
9595

9696
if let controller = self.contentViewController as? SuggestionViewController {
9797
controller.styleView(using: textView)

‎Sources/CodeEditSourceEditor/Controller/TextViewController+Cursor.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ extension TextViewController {
8282
}
8383
isPostingCursorNotification = false
8484

85-
if let completionDelegate = completionDelegate,letposition = cursorPositions.first {
86-
SuggestionController.shared.cursorsUpdated(textView:self, delegate: completionDelegate, position:position)
85+
if let position = cursorPositions.first {
86+
suggestionTriggerModel.selectionUpdated(position)
8787
}
8888
}
8989

@@ -96,7 +96,7 @@ extension TextViewController {
9696
let linePosition = textView.layoutManager.textLineForIndex(position.start.line - 1) else {
9797
return nil
9898
}
99-
if let end =position.end,let endPosition = textView.layoutManager.textLineForIndex(end.line -1) {
99+
if position.end!=nil {
100100
range = NSRange(
101101
location: linePosition.range.location + position.start.column,
102102
length: linePosition.range.max

‎Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ extension TextViewController {
2424
setUpNewlineTabFilters(indentOption: configuration.behavior.indentOption)
2525
setUpDeletePairFilters(pairs: BracketPairs.allValues)
2626
setUpDeleteWhitespaceFilter(indentOption: configuration.behavior.indentOption)
27-
setUpSuggestionsFilter()
2827
}
2928

3029
/// Returns a `TextualIndenter` based on available language configuration.
@@ -121,24 +120,4 @@ extension TextViewController {
121120

122121
return true
123122
}
124-
125-
func setUpSuggestionsFilter() {
126-
textFilters.append(
127-
CodeSuggestionTriggerFilter(
128-
triggerCharacters: configuration.peripherals.codeSuggestionTriggerCharacters,
129-
didTrigger: { [weak self] in
130-
guard let self else { return }
131-
if let completionDelegate = self.completionDelegate,
132-
let position = self.cursorPositions.first {
133-
SuggestionController.shared.cursorsUpdated(
134-
textView: self,
135-
delegate: completionDelegate,
136-
position: position,
137-
presentIfNot: true
138-
)
139-
}
140-
}
141-
)
142-
)
143-
}
144123
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /