diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index 465a7b48e..adaa1f650 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -1793,7 +1793,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error { newCx := mx - v.x0 - 1 newCy := my - v.y0 - 1 // newX and newY are relative to the view's content, independent of its scroll position - newX := newCx + v.ox + newX := v.contentX(newCx) newY := newCy + v.oy // if view is editable don't go further than the furthest character for that line if v.Editable { diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index ec20ab429..f48a4704e 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -2008,10 +2008,17 @@ func (v *View) viewLineLengthIgnoringTrailingBlankLines() int { return 0 } +// contentX returns the column of the view's content that is drawn at column x of the +// view. The content starts after the inclusion gutter, and is scrolled by the view's +// horizontal origin. +func (v *View) contentX(x int) int { + return x - v.inclusionGutterWidth() + v.ox +} + func (v *View) isPatternMatchedRune(x, y int) (bool, bool) { for i, pos := range v.searcher.searchPositions { adjustedY := y + v.oy - adjustedX := x + v.ox + adjustedX := v.contentX(x) if adjustedY == pos.Y && adjustedX >= pos.XStart && adjustedX < pos.XEnd { return true, i == v.searcher.currentSearchIndex } @@ -2022,39 +2029,12 @@ func (v *View) isPatternMatchedRune(x, y int) (bool, bool) { func (v *View) isHoveredHyperlink(x, y int) bool { if v.UnderlineHyperLinksOnlyOnHover && v.hoveredHyperlink != nil { adjustedY := y + v.oy - adjustedX := x + v.ox + adjustedX := v.contentX(x) return adjustedY == v.hoveredHyperlink.Y && adjustedX >= v.hoveredHyperlink.XStart && adjustedX < v.hoveredHyperlink.XEnd } return false } -// realPosition returns the position in the internal buffer corresponding to the -// point (x, y) of the view. -func (v *View) realPosition(vx, vy int) (x, y int, ok bool) { - vx = v.ox + vx - vy = v.oy + vy - - if vx < 0 || vy < 0 { - return 0, 0, false - } - - if len(v.viewLines) == 0 { - return vx, vy, true - } - - if vy < len(v.viewLines) { - vline := v.viewLines[vy] - x = vline.linesX + vx - y = vline.linesY - } else { - vline := v.viewLines[len(v.viewLines)-1] - x = vx - y = vline.linesY + vy - len(v.viewLines) + 1 - } - - return x, y, true -} - // clearRunes erases all the cells in the view. func (v *View) clearRunes() { maxX, maxY := v.InnerSize() @@ -2283,56 +2263,6 @@ func (v *View) ViewBuffer() string { return strings.Join(strs, "\n") } -// Line returns a string with the line of the view's internal buffer -// at the position corresponding to the point (x, y). -func (v *View) Line(y int) (string, bool) { - _, y, ok := v.realPosition(0, y) - if !ok { - return "", false - } - - if y < 0 || y >= len(v.buf.lines) { - return "", false - } - - return v.buf.lines[y].cells.String(), true -} - -// Word returns a string with the word of the view's internal buffer -// at the position corresponding to the point (x, y). -func (v *View) Word(x, y int) (string, bool) { - x, y, ok := v.realPosition(x, y) - if !ok { - return "", false - } - - if x < 0 || y < 0 || y >= len(v.buf.lines) || x >= len(v.buf.lines[y].cells) { - return "", false - } - - str := v.buf.lines[y].cells.String() - - nl := strings.LastIndexFunc(str[:x], indexFunc) - if nl == -1 { - nl = 0 - } else { - nl = nl + 1 - } - nr := strings.IndexFunc(str[x:], indexFunc) - if nr == -1 { - nr = len(str) - } else { - nr = nr + x - } - return str[nl:nr], true -} - -// indexFunc allows to split lines by words taking into account spaces -// and 0. -func indexFunc(r rune) bool { - return r == ' ' || r == 0 -} - // applySelTextColor adds the attributes of selTextColor to fgColor, and // replaces the color of fgColor with that of selTextColor if it has one. func applySelTextColor(fgColor, selTextColor Attribute) Attribute { @@ -2846,7 +2776,7 @@ func (v *View) onMouseMove(x int, y int) { newCx := x - v.x0 - 1 newCy := y - v.y0 - 1 // newX and newY are relative to the view's content, independent of its scroll position - newX := newCx + v.ox + newX := v.contentX(newCx) newY := newCy + v.oy if newY >= 0 && newY <= len(v.viewLines)-1 && newX >= 0 && newX <= len(v.viewLines[newY].line)-1 { diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index d5eed55ac..edc800990 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -1245,6 +1245,87 @@ func TestInclusionGutter(t *testing.T) { assert.Equal(t, "a", chr) } +// A search match beside the inclusion gutter is highlighted where it is drawn. +func TestSearchMatchBesideTheInclusionGutter(t *testing.T) { + WithSimulationScreen(t, 14, 6) + + v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth 10 + v.InclusionGutterMarker = "✓" + + v.writeString("0123456789\n") + v.SetInclusionGutter(true, []bool{true}) + v.UpdateSearchResults("34", nil) + v.draw(true) + + highlightedColumns := func(y int) string { + s := "" + for x := 1; x <= 10; x++ { + _, style, _ := Screen.Get(x, y) + s += lo.Ternary(style.GetBackground() != tcell.ColorDefault, "#", ".") + } + return s + } + // The row reads "✓ 01234567", so the match is at view columns 5 and 6. + assert.Equal(t, + ".....##...", + highlightedColumns(1)) +} + +// A hyperlink beside the inclusion gutter is underlined while the mouse is over it. +func TestHoveredHyperlinkBesideTheInclusionGutter(t *testing.T) { + WithSimulationScreen(t, 14, 6) + + v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth 10 + v.InclusionGutterMarker = "✓" + v.UnderlineHyperLinksOnlyOnHover = true + + v.writeString("ab\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\cd\n") + v.SetInclusionGutter(true, []bool{true}) + + // The row reads "✓ ablinkcd". Point at the "l" of "link", at view column 4, + // which the frame puts at screen column 5. + v.onMouseMove(5, 1) + v.draw(true) + + underlinedColumns := func(y int) string { + s := "" + for x := 1; x <= 10; x++ { + _, style, _ := Screen.Get(x, y) + s += lo.Ternary(style.HasUnderline(), "_", ".") + } + return s + } + assert.Equal(t, + "....____..", + underlinedColumns(1)) +} + +// Clicking a hyperlink beside the inclusion gutter opens it. +func TestClickHyperlinkBesideTheInclusionGutter(t *testing.T) { + g := newTestGui(t) + v, _ := g.SetView("main", 0, 0, 20, 10, 0) + v.InclusionGutterMarker = "✓" + + v.writeString("\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\ text\n") + v.SetInclusionGutter(true, []bool{true}) + + opened := []string{} + g.SetOpenHyperlinkFunc(func(link string, _ string) error { + opened = append(opened, link) + return nil + }) + + // The row reads "✓ link text". Click the "k" of "link", at view column 5, which + // the frame puts at screen column 6. + assert.NoError(t, g.onKey(&GocuiEvent{ + Type: eventMouse, MouseX: 6, MouseY: 1, + Key: NewKeyName(MouseLeft), + })) + assert.Equal(t, + []string{"https://example.com"}, + opened) +} + // A marked line the view wraps is marked on every segment it is drawn as, so that // the mark doesn't look like it belongs to the first part of the line alone. The // gutter takes its columns out of the width the content wraps in.