Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Cancelling a SQLite query no longer races a disconnect happening at the same moment. (#1610)
- Typing in the query editor no longer erases characters or drops focus on each keystroke, a timing-dependent bug most visible on macOS 15. (#1608)
- The autocomplete popup now filters in place as you type instead of closing and reopening on every keystroke. (#1608)
- Syntax highlighting no longer disappears after formatting a query. (#1612)

## [0.49.1] - 2026-06-06

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import Foundation
import SwiftTreeSitter

extension TextViewController {
/// Tears down and rebuilds the highlighter for the text view's current storage.
///
/// Ends with an explicit `invalidate()` so the rebuilt highlighter queries the
/// visible text immediately. A fresh highlighter only highlights in response to
/// triggers (an edit, a frame change, or an invalidation); after a mid-session
/// storage swap such as `setText`, none of those is guaranteed to fire, and
/// without this the document stays unstyled until the next layout change.
package func setUpHighlighter() {
if let highlighter {
textView.removeStorageDelegate(highlighter)
Expand All @@ -24,6 +31,7 @@ extension TextViewController {
)
textView.addStorageDelegate(highlighter)
self.highlighter = highlighter
highlighter.invalidate()
}

/// Sets new highlight providers. Recognizes when objects move in the array or are removed or inserted.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,17 @@ extension SourceEditor {
/// fires suggestion triggers, none of which should happen for a programmatic
/// whole-document replacement. `setText` clearing the undo stack matches the
/// new-document semantics of that replacement.
///
/// Must go through `TextViewController.setText`, not `textView.setText`: the
/// controller-level call rebuilds the highlighter for the replacement storage.
/// Calling the text view directly leaves tree-sitter state for the old document
/// and highlighting never recovers.
func syncBindingText(_ newValue: String, controller: TextViewController) {
guard !isUpdateFromTextView else { return }
guard newValue != lastSyncedText else { return }
textBindingTask?.cancel()
isUpdatingFromRepresentable = true
controller.textView.setText(newValue)
controller.setText(newValue)
isUpdatingFromRepresentable = false
lastSyncedText = newValue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,49 @@ final class SourceEditorBindingSyncTests: XCTestCase {
XCTAssertEqual(coordinator.lastSyncedText, "SELECT 1")
}

@MainActor
func test_syncRebuildsHighlighterForReplacementStorage() {
var bound = "select * from users"
let coordinator = makeCoordinator(get: { bound }, set: { bound = $0 })
controller.textView.setText("select * from users")
controller.setUpHighlighter()
let highlighterBefore = controller.highlighter
XCTAssertNotNil(highlighterBefore)

bound = "SELECT\n *\nFROM\n users"
coordinator.syncBindingText(bound, controller: controller)

XCTAssertEqual(controller.textView.string, "SELECT\n *\nFROM\n users")
XCTAssertNotNil(controller.highlighter)
XCTAssertNotIdentical(controller.highlighter, highlighterBefore)
}

@MainActor
func test_syncQueriesHighlightsForReplacementStorage() {
let provider = HighlighterTests.MockHighlightProvider()
let providerController = TextViewController(
string: "select * from users",
language: .html,
configuration: Mock.config(),
cursorPositions: [],
highlightProviders: [provider]
)
providerController.loadView()
providerController.view.frame = NSRect(x: 0, y: 0, width: 1_000, height: 1_000)
providerController.view.layoutSubtreeIfNeeded()

let setUpsBefore = provider.setUpCount
let queriesBefore = provider.queryCount

var bound = "select * from users"
let coordinator = makeCoordinator(get: { bound }, set: { bound = $0 })
bound = "SELECT\n *\nFROM\n users"
coordinator.syncBindingText(bound, controller: providerController)

XCTAssertGreaterThan(provider.setUpCount, setUpsBefore)
XCTAssertGreaterThan(provider.queryCount, queriesBefore)
}

@MainActor
func test_repeatedSyncWithSameValueLeavesTextViewUntouched() {
var bound = "stable"
Expand Down
Loading