Skip to content
Draft
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
4 changes: 4 additions & 0 deletions macOS/Synapse Notes.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@
FBDE6F50B234E8BEEC81D70B /* GitServiceConflictsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 995602FCC2094AD94768D147 /* GitServiceConflictsTests.swift */; };
FD832B5AFBA2E311DEB0EF87 /* CommandPaletteScoringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CFDA7432187B97B46732652 /* CommandPaletteScoringTests.swift */; };
FEDF6C1421F4515A506A0F6B /* WikiLinkClickTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABD9B6E3A3ED294575CBF65 /* WikiLinkClickTests.swift */; };
4E8F901A2B3C4D5E6F708192 /* SearchReplaceStaleHighlightTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7E8F901A2B4C5D6E7F8091 /* SearchReplaceStaleHighlightTests.swift */; };
FFB6DAEB709AE113A8039AC1 /* CommandPaletteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA805991148562D94A36617 /* CommandPaletteView.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -288,6 +289,7 @@
2AF93A428A55CD52A1488410 /* AppThemeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppThemeTests.swift; sourceTree = "<group>"; };
3642C0B4C5DF51584C3FB7C7 /* AppStateContentChangeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateContentChangeTests.swift; sourceTree = "<group>"; };
3ABD9B6E3A3ED294575CBF65 /* WikiLinkClickTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikiLinkClickTests.swift; sourceTree = "<group>"; };
3D7E8F901A2B4C5D6E7F8091 /* SearchReplaceStaleHighlightTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchReplaceStaleHighlightTests.swift; sourceTree = "<group>"; };
3B5764CC787B08BA6E1D64B4 /* MarkdownPreviewRendererTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownPreviewRendererTests.swift; sourceTree = "<group>"; };
3DA805991148562D94A36617 /* CommandPaletteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteView.swift; sourceTree = "<group>"; };
3DB8C6CC66C5265161E6B171 /* CalendarDayActivityCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDayActivityCalculatorTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -668,6 +670,7 @@
2080178498715BA2E9CE7F9C /* VaultIndexTests.swift */,
46245289C81F5BC71F3DAA8D /* VaultRootResolverTests.swift */,
3ABD9B6E3A3ED294575CBF65 /* WikiLinkClickTests.swift */,
3D7E8F901A2B4C5D6E7F8091 /* SearchReplaceStaleHighlightTests.swift */,
);
path = SynapseNotesTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -1088,6 +1091,7 @@
5D395266B546904502376A52 /* VaultIndexTests.swift in Sources */,
2C5DA5CE689E982A146800B0 /* VaultRootResolverTests.swift in Sources */,
FEDF6C1421F4515A506A0F6B /* WikiLinkClickTests.swift in Sources */,
4E8F901A2B3C4D5E6F708192 /* SearchReplaceStaleHighlightTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
26 changes: 25 additions & 1 deletion macOS/SynapseNotes/EditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2717,10 +2717,34 @@ class LinkAwareTextView: NSTextView {
applyMarkdownStyling()
}

/// Used by replace-current and tests. Cached highlight ranges can lag the live document when the
/// user edits while the find bar stays open (`reapplySearchHighlights` skips invalid ranges but
/// does not remove them from `lastSearchHighlightRanges`).
internal static func cachedHighlightRangeStillMatchesQuery(
range: NSRange,
fullString: String,
query: String
) -> Bool {
let ns = fullString as NSString
let storageLength = ns.length
guard NSMaxRange(range) <= storageLength else { return false }
let fragment = ns.substring(with: range)
return fragment.compare(query, options: .caseInsensitive) == .orderedSame
}

private func replaceCurrentMatch(query: String, focusIndex: Int, replacement: String, advanceAfter: Bool) {
guard !query.isEmpty,
lastSearchHighlightRanges.indices.contains(focusIndex) else { return }
lastSearchHighlightRanges.indices.contains(focusIndex),
let storage = textStorage else { return }
let range = lastSearchHighlightRanges[focusIndex]
guard Self.cachedHighlightRangeStillMatchesQuery(
range: range,
fullString: storage.string,
query: query
) else {
applySearchHighlights(query: query, focusIndex: focusIndex)
return
}
guard shouldChangeText(in: range, replacementString: replacement) else { return }
textStorage?.replaceCharacters(in: range, with: replacement)
didChangeText()
Expand Down
36 changes: 36 additions & 0 deletions macOS/SynapseNotesTests/SearchReplaceStaleHighlightTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import XCTest
@testable import Synapse

/// Guards for inline find/replace: cached highlight ranges must not drive replace after the buffer changed.
final class SearchReplaceStaleHighlightTests: XCTestCase {

func test_cachedHighlightRangeStillMatchesQuery_validMatch() {
XCTAssertTrue(
LinkAwareTextView.cachedHighlightRangeStillMatchesQuery(
range: NSRange(location: 4, length: 3),
fullString: "foo bar baz",
query: "BAR"
)
)
}

func test_cachedHighlightRangeStillMatchesQuery_outOfBounds_returnsFalse() {
XCTAssertFalse(
LinkAwareTextView.cachedHighlightRangeStillMatchesQuery(
range: NSRange(location: 8, length: 10),
fullString: "short",
query: "x"
)
)
}

func test_cachedHighlightRangeStillMatchesQuery_wrongSpan_returnsFalse() {
XCTAssertFalse(
LinkAwareTextView.cachedHighlightRangeStillMatchesQuery(
range: NSRange(location: 0, length: 3),
fullString: "foo bar",
query: "bar"
)
)
}
}
Loading