diff --git a/ModernTests/VT100ScreenTests.swift b/ModernTests/VT100ScreenTests.swift index bb18e8a0a3..dc6b9e35c4 100644 --- a/ModernTests/VT100ScreenTests.swift +++ b/ModernTests/VT100ScreenTests.swift @@ -679,6 +679,67 @@ class VT100ScreenTests: XCTestCase { }) } + func testCommandMarkAtReturnsNilWhenLineIsOutOfBounds() { + let screen = self.screen(width: 10, height: 4) + + screen.performBlock(joinedThreads: { _, mutableState, _ in + var range = VT100GridWindowedRange() + let negative = mutableState.commandMark(at: VT100GridCoordMake(0, -1), + mustHaveCommand: false, + range: &range) + XCTAssertNil(negative) + + let tooLargeY = mutableState.numberOfLines + let tooLarge = mutableState.commandMark(at: VT100GridCoordMake(0, tooLargeY), + mustHaveCommand: false, + range: &range) + XCTAssertNil(tooLarge) + }) + } + + func testCommandMarkAtReturnsNilWhenNoMarkOnLine() { + let screen = self.screen(width: 10, height: 4) + + appendLinesNoNewline([ + "https://example.com", + "next line" + ], screen: screen) + + screen.performBlock(joinedThreads: { _, mutableState, _ in + var range = VT100GridWindowedRange() + let mark = mutableState.commandMark(at: VT100GridCoordMake(0, 0), + mustHaveCommand: false, + range: &range) + XCTAssertNil(mark) + }) + } + + func testCommandMarkAtReturnsNilWhenMarkHasNoCommand() { + let screen = self.screen(width: 10, height: 4) + + appendLinesNoNewline([ + "prompt", + "next line" + ], screen: screen) + + screen.performBlock(joinedThreads: { _, mutableState, _ in + let mark = mutableState.addMark(onLine: 0, of: VT100ScreenMark.self) as! VT100ScreenMark + let absLine = mutableState.cumulativeScrollbackOverflow + mutableState.mutableIntervalTree().mutate(mark) { obj in + let m = obj as! VT100ScreenMark + m.promptRange = VT100GridAbsCoordRangeMake(0, absLine, 6, absLine) + } + + var range = VT100GridWindowedRange() + XCTAssertNotNil(mutableState.commandMark(at: VT100GridCoordMake(0, 0), + mustHaveCommand: false, + range: &range)) + XCTAssertNil(mutableState.commandMark(at: VT100GridCoordMake(0, 0), + mustHaveCommand: true, + range: &range)) + }) + } + private func commonNoteResizeRegressionTest(initialRange range1: VT100GridCoordRange, intermediateRange range2: VT100GridCoordRange) { var screen = self.screen(width: 80, height: 25) diff --git a/docs/notes-3.7.txt b/docs/notes-3.7.txt index 705612d4c2..db568c41c9 100644 --- a/docs/notes-3.7.txt +++ b/docs/notes-3.7.txt @@ -648,6 +648,9 @@ Bug Fixes: Korean) in the AI Chat input no longer prematurely submits the unfinalized candidate text. +- Command-clicking links in terminal output + no longer crashes when command-mark data + is unavailable. - Open Quickly now updates the MRU order when you jump to a session, so repeated Cmd-Shift-O Enter cycles between the two diff --git a/sources/VT100Screen/VT100ScreenState.m b/sources/VT100Screen/VT100ScreenState.m index 4d2fce59ae..7361eb69f2 100644 --- a/sources/VT100Screen/VT100ScreenState.m +++ b/sources/VT100Screen/VT100ScreenState.m @@ -1251,8 +1251,21 @@ - (NSIndexSet *)foldsInRange:(VT100GridRange)gridRange { - (id)commandMarkAt:(VT100GridCoord)coord mustHaveCommand:(BOOL)mustHaveCommand range:(out VT100GridWindowedRange *)rangeOut { + if (coord.y < 0 || coord.y >= self.numberOfLines) { + return nil; + } + id mark = [self screenMarkOnLine:coord.y]; - const VT100GridCoordRange range = [self coordRangeForInterval:mark.entry.interval]; + if (!mark) { + return nil; + } + + IntervalTreeEntry *entry = mark.entry; + if (!entry || !entry.interval) { + return nil; + } + + const VT100GridCoordRange range = [self coordRangeForInterval:entry.interval]; if (mustHaveCommand && mark.firstLineOfCommand == nil) { return nil; }