@@ -6074,6 +6074,9 @@ open class Terminal {
60746074 guard let lineMap = buildGhosttyImplicitLineMap ( at: position, in: buffer) else {
60756075 return nil
60766076 }
6077+ if let quoted = quotedPathMatch ( in: lineMap) {
6078+ return quoted
6079+ }
60776080 guard let regex = Self . ghosttyImplicitLinkRegex else {
60786081 return nil
60796082 }
@@ -6092,65 +6095,126 @@ open class Terminal {
60926095
60936096 let startOffset = lineMap. text. distance ( from: lineMap. text. startIndex, to: textRange. lowerBound)
60946097 let endOffset = lineMap. text. distance ( from: lineMap. text. startIndex, to: textRange. upperBound)
6095- guard startOffset < lineMap. cells. count else {
6096- continue
6097- }
6098- let boundedEndOffset = min ( endOffset, lineMap. cells. count)
6099- guard boundedEndOffset > startOffset else {
6100- continue
6098+ if let match = implicitMatch ( in: lineMap,
6099+ text: String ( lineMap. text [ textRange] ) ,
6100+ startOffset: startOffset,
6101+ endOffset: endOffset) {
6102+ return match
61016103 }
6104+ }
6105+ return nil
6106+ }
6107+
6108+ /// Maps a character-offset range in the line map's text back to buffer
6109+ /// cells, and produces a match when the lookup target falls inside it.
6110+ private func implicitMatch( in lineMap: GhosttyImplicitLineMap , text: String , startOffset: Int , endOffset: Int ) -> LinkMatch ?
6111+ {
6112+ guard startOffset < lineMap. cells. count else {
6113+ return nil
6114+ }
6115+ let boundedEndOffset = min ( endOffset, lineMap. cells. count)
6116+ guard boundedEndOffset > startOffset else {
6117+ return nil
6118+ }
61026119
6103- var containsTarget = false
6104- var rowStart : Int ?
6105- var rowEnd : Int ?
6106- var rowBounds : [ Int : ( start: Int , end: Int ) ] = [ : ]
6107- for idx in startOffset..< boundedEndOffset {
6108- let cell = lineMap. cells [ idx]
6109- let cellEnd = cell. col + max( 1 , cell. width)
6120+ var containsTarget = false
6121+ var rowStart : Int ?
6122+ var rowEnd : Int ?
6123+ var rowBounds : [ Int : ( start: Int , end: Int ) ] = [ : ]
6124+ for idx in startOffset..< boundedEndOffset {
6125+ let cell = lineMap. cells [ idx]
6126+ let cellEnd = cell. col + max( 1 , cell. width)
61106127
6111- if var bounds = rowBounds [ cell. row] {
6112- bounds. start = min ( bounds. start, cell. col)
6113- bounds. end = max ( bounds. end, cellEnd)
6114- rowBounds [ cell. row] = bounds
6115- } else {
6116- rowBounds [ cell. row] = ( start: cell. col, end: cellEnd)
6128+ if var bounds = rowBounds [ cell. row] {
6129+ bounds. start = min ( bounds. start, cell. col)
6130+ bounds. end = max ( bounds. end, cellEnd)
6131+ rowBounds [ cell. row] = bounds
6132+ } else {
6133+ rowBounds [ cell. row] = ( start: cell. col, end: cellEnd)
6134+ }
6135+
6136+ if cell. row == lineMap. targetRow {
6137+ rowStart = min ( rowStart ?? cell. col, cell. col)
6138+ rowEnd = max ( rowEnd ?? cellEnd, cellEnd)
6139+ if lineMap. targetCol >= cell. col && lineMap. targetCol < cellEnd {
6140+ containsTarget = true
61176141 }
6142+ }
6143+ }
6144+ guard containsTarget,
6145+ let rowStart,
6146+ let rowEnd,
6147+ rowStart < rowEnd
6148+ else {
6149+ return nil
6150+ }
61186151
6119- if cell . row == lineMap . targetRow {
6120- rowStart = min ( rowStart ?? cell . col , cell . col )
6121- rowEnd = max ( rowEnd ?? cellEnd , cellEnd )
6122- if lineMap . targetCol >= cell . col && lineMap . targetCol < cellEnd {
6123- containsTarget = true
6124- }
6152+ let rowRanges = rowBounds
6153+ . keys
6154+ . sorted ( )
6155+ . compactMap { row -> LinkMatch . RowRange ? in
6156+ guard let bounds = rowBounds [ row ] , bounds . start < bounds . end else {
6157+ return nil
61256158 }
6159+ return . init( row: row, range: bounds. start..< bounds. end)
6160+ }
6161+
6162+ return LinkMatch (
6163+ text: text,
6164+ row: lineMap. targetRow,
6165+ range: rowStart..< rowEnd,
6166+ isExplicit: false ,
6167+ rowRanges: rowRanges
6168+ )
6169+ }
6170+
6171+ /// Paths that contain spaces defeat the Ghostty-style regex, but
6172+ /// shell-quoted output gives an unambiguous boundary: when the lookup
6173+ /// target sits inside a '...' or "..." pair whose content looks like a
6174+ /// filesystem path, the whole quoted content (quotes excluded) is the
6175+ /// link. Innermost wins so "'/a b.png'" resolves to /a b.png.
6176+ private func quotedPathMatch( in lineMap: GhosttyImplicitLineMap ) -> LinkMatch ?
6177+ {
6178+ let chars = Array ( lineMap. text)
6179+ var best : LinkMatch ?
6180+ var bestLength = Int . max
6181+ for (offset, ch) in chars. enumerated ( ) {
6182+ guard ch == " ' " || ch == " \" " else {
6183+ continue
6184+ }
6185+ // Treat any quote directly followed by a path-looking prefix as
6186+ // an opener, closed by the nearest quote of the same kind. This
6187+ // stays robust against apostrophes in surrounding prose, which
6188+ // would confuse strict sequential pairing.
6189+ let contentStart = offset + 1
6190+ guard let contentEnd = ( contentStart..< chars. count) . first ( where: { chars [ $0] == ch } ) else {
6191+ continue
6192+ }
6193+ let length = contentEnd - contentStart
6194+ guard length > 1 && length < bestLength else {
6195+ continue
61266196 }
6127- guard containsTarget,
6128- let rowStart,
6129- let rowEnd,
6130- rowStart < rowEnd
6197+ let content = String ( chars [ contentStart..< contentEnd] )
6198+ guard looksLikeQuotedPath ( content) ,
6199+ let match = implicitMatch ( in: lineMap,
6200+ text: content,
6201+ startOffset: contentStart,
6202+ endOffset: contentEnd)
61316203 else {
61326204 continue
61336205 }
6206+ best = match
6207+ bestLength = length
6208+ }
6209+ return best
6210+ }
61346211
6135- let rowRanges = rowBounds
6136- . keys
6137- . sorted ( )
6138- . compactMap { row -> LinkMatch . RowRange ? in
6139- guard let bounds = rowBounds [ row] , bounds. start < bounds. end else {
6140- return nil
6141- }
6142- return . init( row: row, range: bounds. start..< bounds. end)
6143- }
6144-
6145- return LinkMatch (
6146- text: String ( lineMap. text [ textRange] ) ,
6147- row: lineMap. targetRow,
6148- range: rowStart..< rowEnd,
6149- isExplicit: false ,
6150- rowRanges: rowRanges
6151- )
6212+ private func looksLikeQuotedPath( _ text: String ) -> Bool
6213+ {
6214+ if text. hasPrefix ( " // " ) {
6215+ return false
61526216 }
6153- return nil
6217+ return text . hasPrefix ( " / " ) || text . hasPrefix ( " ~/ " ) || text . hasPrefix ( " ./ " ) || text . hasPrefix ( " ../ " )
61546218 }
61556219
61566220 private func payloadCode( at position: Position , in buffer: Buffer ) -> UInt16 ?
0 commit comments