mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-05 20:56:17 +00:00
Merge remote-tracking branch 'origin/master' into v2-exp
This commit is contained in:
@@ -1 +1,2 @@
|
||||
.DS_Store
|
||||
dist/
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||
version: 2
|
||||
includes:
|
||||
- from_url:
|
||||
url: charmbracelet/meta/main/goreleaser-lib.yaml
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ require (
|
||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20241126192050-a8ed96118b08
|
||||
github.com/charmbracelet/harmonica v0.2.0
|
||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20241121164047-8448a9be4804
|
||||
github.com/charmbracelet/x/ansi v0.5.1
|
||||
github.com/charmbracelet/x/ansi v0.6.1-0.20250107110353-48b574af22a5
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
|
||||
@@ -12,8 +12,8 @@ github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20241121164047-8448a9be4804 h1:7CYjb9YMZA4kMhLgGdtlXvq+nu1oyENpMyMQlTvqSFw=
|
||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20241121164047-8448a9be4804/go.mod h1:F/6E/LGdH3eHCJf2rG8/O3CjlW8cZFL5YJCknJs1GkI=
|
||||
github.com/charmbracelet/x/ansi v0.5.1 h1:+mg6abP9skvsu/JQZrIJ9Z/4O1YDnLVkpfutar3dUnc=
|
||||
github.com/charmbracelet/x/ansi v0.5.1/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
|
||||
github.com/charmbracelet/x/ansi v0.6.1-0.20250107110353-48b574af22a5 h1:TSjbA80sXnABV/Vxhnb67Ho7p8bEYqz6NIdhLAx+1yg=
|
||||
github.com/charmbracelet/x/ansi v0.6.1-0.20250107110353-48b574af22a5/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.6 h1:pJUWN/G1jbt1Nj/+ILfC2/ABQoZzWu1vG73yHQEYELI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.6/go.mod h1:d72o71glp8flkCz54PHLe3+nuw5u2v3UxmKqruUERWQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
|
||||
@@ -703,10 +703,12 @@ func (m Model) View() string {
|
||||
func (m Model) placeholderView() string {
|
||||
var (
|
||||
v string
|
||||
p = []rune(m.Placeholder)
|
||||
style = m.PlaceholderStyle.Inline(true).Render
|
||||
)
|
||||
|
||||
p := make([]rune, m.Width+1)
|
||||
copy(p, []rune(m.Placeholder))
|
||||
|
||||
m.Cursor.TextStyle = m.PlaceholderStyle
|
||||
m.Cursor.SetChar(string(p[:1]))
|
||||
v += m.Cursor.View()
|
||||
|
||||
@@ -30,3 +30,10 @@ func Test_CurrentSuggestion(t *testing.T) {
|
||||
t.Fatalf("Error: expected first suggestion but was %s", suggestion)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SlicingOutsideCap(t *testing.T) {
|
||||
textinput := New()
|
||||
textinput.Placeholder = "作業ディレクトリを指定してください"
|
||||
textinput.Width = 32
|
||||
textinput.View()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ type KeyMap struct {
|
||||
HalfPageDown key.Binding
|
||||
Down key.Binding
|
||||
Up key.Binding
|
||||
Left key.Binding
|
||||
Right key.Binding
|
||||
}
|
||||
|
||||
// DefaultKeyMap returns a set of pager-like default keybindings.
|
||||
@@ -42,5 +44,13 @@ func DefaultKeyMap() KeyMap {
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("↓/j", "down"),
|
||||
),
|
||||
Left: key.NewBinding(
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithHelp("←/h", "move left"),
|
||||
),
|
||||
Right: key.NewBinding(
|
||||
key.WithKeys("right", "l"),
|
||||
key.WithHelp("→/l", "move right"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
+97
-4
@@ -7,6 +7,11 @@ import (
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHorizontalStep = 6
|
||||
)
|
||||
|
||||
// Option is a configuration option that works in conjunction with [New]. For
|
||||
@@ -57,12 +62,24 @@ type Model struct {
|
||||
// YOffset is the vertical scroll position.
|
||||
YOffset int
|
||||
|
||||
// xOffset is the horizontal scroll position.
|
||||
xOffset int
|
||||
|
||||
// horizontalStep is the number of columns we move left or right during a
|
||||
// default horizontal scroll.
|
||||
horizontalStep int
|
||||
|
||||
// YPosition is the position of the viewport in relation to the terminal
|
||||
// window. It's used in high performance rendering only.
|
||||
YPosition int
|
||||
|
||||
// Style applies a lipgloss style to the viewport. Realistically, it's most
|
||||
// useful for setting borders, margins and padding.
|
||||
Style lipgloss.Style
|
||||
|
||||
initialized bool
|
||||
lines []string
|
||||
initialized bool
|
||||
lines []string
|
||||
longestLineWidth int
|
||||
}
|
||||
|
||||
func (m *Model) setInitialValues() {
|
||||
@@ -70,6 +87,7 @@ func (m *Model) setInitialValues() {
|
||||
m.MouseWheelEnabled = true
|
||||
m.MouseWheelDelta = 3
|
||||
m.initialized = true
|
||||
m.horizontalStep = defaultHorizontalStep
|
||||
}
|
||||
|
||||
// Init exists to satisfy the tea.Model interface for composability purposes.
|
||||
@@ -126,10 +144,24 @@ func (m Model) ScrollPercent() float64 {
|
||||
return math.Max(0.0, math.Min(1.0, v))
|
||||
}
|
||||
|
||||
// HorizontalScrollPercent returns the amount horizontally scrolled as a float
|
||||
// between 0 and 1.
|
||||
func (m Model) HorizontalScrollPercent() float64 {
|
||||
if m.xOffset >= m.longestLineWidth-m.Width() {
|
||||
return 1.0
|
||||
}
|
||||
y := float64(m.xOffset)
|
||||
h := float64(m.Width())
|
||||
t := float64(m.longestLineWidth)
|
||||
v := y / (t - h)
|
||||
return math.Max(0.0, math.Min(1.0, v))
|
||||
}
|
||||
|
||||
// SetContent set the pager's text content.
|
||||
func (m *Model) SetContent(s string) {
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
|
||||
m.lines = strings.Split(s, "\n")
|
||||
m.longestLineWidth = findLongestLineWidth(m.lines)
|
||||
|
||||
if m.YOffset > len(m.lines)-1 {
|
||||
m.GotoBottom()
|
||||
@@ -145,12 +177,24 @@ func (m Model) maxYOffset() int {
|
||||
// visibleLines returns the lines that should currently be visible in the
|
||||
// viewport.
|
||||
func (m Model) visibleLines() (lines []string) {
|
||||
h := m.Height() - m.Style.GetVerticalFrameSize()
|
||||
w := m.Width() - m.Style.GetHorizontalFrameSize()
|
||||
|
||||
if len(m.lines) > 0 {
|
||||
top := max(0, m.YOffset)
|
||||
bottom := clamp(m.YOffset+m.Height(), top, len(m.lines))
|
||||
bottom := clamp(m.YOffset+h, top, len(m.lines))
|
||||
lines = m.lines[top:bottom]
|
||||
}
|
||||
return lines
|
||||
|
||||
if (m.xOffset == 0 && m.longestLineWidth <= w) || w == 0 {
|
||||
return lines
|
||||
}
|
||||
|
||||
cutLines := make([]string, len(lines))
|
||||
for i := range lines {
|
||||
cutLines[i] = ansi.Cut(lines[i], m.xOffset, m.xOffset+w)
|
||||
}
|
||||
return cutLines
|
||||
}
|
||||
|
||||
// SetYOffset sets the Y offset.
|
||||
@@ -245,6 +289,39 @@ func (m *Model) GotoBottom() (lines []string) {
|
||||
return m.visibleLines()
|
||||
}
|
||||
|
||||
// SetHorizontalStep sets the amount of cells that the viewport moves in the
|
||||
// default viewport keymapping. If set to 0 or less, horizontal scrolling is
|
||||
// disabled.
|
||||
func (m *Model) SetHorizontalStep(n int) {
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
|
||||
m.horizontalStep = n
|
||||
}
|
||||
|
||||
// MoveLeft moves the viewport to the left by the given number of columns.
|
||||
func (m *Model) MoveLeft(cols int) {
|
||||
m.xOffset -= cols
|
||||
if m.xOffset < 0 {
|
||||
m.xOffset = 0
|
||||
}
|
||||
}
|
||||
|
||||
// MoveRight moves viewport to the right by the given number of columns.
|
||||
func (m *Model) MoveRight(cols int) {
|
||||
// prevents over scrolling to the right
|
||||
if m.xOffset >= m.longestLineWidth-m.Width() {
|
||||
return
|
||||
}
|
||||
m.xOffset += cols
|
||||
}
|
||||
|
||||
// Resets lines indent to zero.
|
||||
func (m *Model) ResetIndent() {
|
||||
m.xOffset = 0
|
||||
}
|
||||
|
||||
// Update handles standard message-based viewport updates.
|
||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m = m.updateAsModel(msg)
|
||||
@@ -278,6 +355,12 @@ func (m Model) updateAsModel(msg tea.Msg) Model {
|
||||
|
||||
case key.Matches(msg, m.KeyMap.Up):
|
||||
m.LineUp(1)
|
||||
|
||||
case key.Matches(msg, m.KeyMap.Left):
|
||||
m.MoveLeft(m.horizontalStep)
|
||||
|
||||
case key.Matches(msg, m.KeyMap.Right):
|
||||
m.MoveRight(m.horizontalStep)
|
||||
}
|
||||
|
||||
case tea.MouseWheelMsg:
|
||||
@@ -339,3 +422,13 @@ func max(a, b int) int {
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func findLongestLineWidth(lines []string) int {
|
||||
w := 0
|
||||
for _, l := range lines {
|
||||
if ww := ansi.StringWidth(l); ww > w {
|
||||
w = ww
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
package viewport
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("default values on create by New", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := New(10, 10)
|
||||
|
||||
if !m.initialized {
|
||||
t.Errorf("on create by New, Model should be initialized")
|
||||
}
|
||||
|
||||
if m.horizontalStep != defaultHorizontalStep {
|
||||
t.Errorf("default horizontalStep should be %d, got %d", defaultHorizontalStep, m.horizontalStep)
|
||||
}
|
||||
|
||||
if m.MouseWheelDelta != 3 {
|
||||
t.Errorf("default MouseWheelDelta should be 3, got %d", m.MouseWheelDelta)
|
||||
}
|
||||
|
||||
if !m.MouseWheelEnabled {
|
||||
t.Error("mouse wheel should be enabled by default")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetInitialValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("default horizontalStep", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := Model{}
|
||||
m.setInitialValues()
|
||||
|
||||
if m.horizontalStep != defaultHorizontalStep {
|
||||
t.Errorf("default horizontalStep should be %d, got %d", defaultHorizontalStep, m.horizontalStep)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetHorizontalStep(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("change default", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := New(10, 10)
|
||||
|
||||
if m.horizontalStep != defaultHorizontalStep {
|
||||
t.Errorf("default horizontalStep should be %d, got %d", defaultHorizontalStep, m.horizontalStep)
|
||||
}
|
||||
|
||||
newStep := 8
|
||||
m.SetHorizontalStep(newStep)
|
||||
if m.horizontalStep != newStep {
|
||||
t.Errorf("horizontalStep should be %d, got %d", newStep, m.horizontalStep)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no negative", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := New(10, 10)
|
||||
|
||||
if m.horizontalStep != defaultHorizontalStep {
|
||||
t.Errorf("default horizontalStep should be %d, got %d", defaultHorizontalStep, m.horizontalStep)
|
||||
}
|
||||
|
||||
zero := 0
|
||||
m.SetHorizontalStep(-1)
|
||||
if m.horizontalStep != zero {
|
||||
t.Errorf("horizontalStep should be %d, got %d", zero, m.horizontalStep)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveLeft(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
zeroPosition := 0
|
||||
|
||||
t.Run("zero position", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := New(10, 10)
|
||||
if m.xOffset != zeroPosition {
|
||||
t.Errorf("default indent should be %d, got %d", zeroPosition, m.xOffset)
|
||||
}
|
||||
|
||||
m.MoveLeft(m.horizontalStep)
|
||||
if m.xOffset != zeroPosition {
|
||||
t.Errorf("indent should be %d, got %d", zeroPosition, m.xOffset)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("move", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := New(10, 10)
|
||||
if m.xOffset != zeroPosition {
|
||||
t.Errorf("default indent should be %d, got %d", zeroPosition, m.xOffset)
|
||||
}
|
||||
|
||||
m.xOffset = defaultHorizontalStep * 2
|
||||
m.MoveLeft(m.horizontalStep)
|
||||
newIndent := defaultHorizontalStep
|
||||
if m.xOffset != newIndent {
|
||||
t.Errorf("indent should be %d, got %d", newIndent, m.xOffset)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveRight(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("move", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
zeroPosition := 0
|
||||
|
||||
m := New(10, 10)
|
||||
m.SetContent("Some line that is longer than width")
|
||||
if m.xOffset != zeroPosition {
|
||||
t.Errorf("default indent should be %d, got %d", zeroPosition, m.xOffset)
|
||||
}
|
||||
|
||||
m.MoveRight(m.horizontalStep)
|
||||
newIndent := defaultHorizontalStep
|
||||
if m.xOffset != newIndent {
|
||||
t.Errorf("indent should be %d, got %d", newIndent, m.xOffset)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestResetIndent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("reset", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
zeroPosition := 0
|
||||
|
||||
m := New(10, 10)
|
||||
m.xOffset = 500
|
||||
|
||||
m.ResetIndent()
|
||||
if m.xOffset != zeroPosition {
|
||||
t.Errorf("indent should be %d, got %d", zeroPosition, m.xOffset)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVisibleLines(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
defaultList := []string{
|
||||
`57 Precepts of narcissistic comedy character Zote from an awesome "Hollow knight" game (https://store.steampowered.com/app/367520/Hollow_Knight/).`,
|
||||
`Precept One: 'Always Win Your Battles'. Losing a battle earns you nothing and teaches you nothing. Win your battles, or don't engage in them at all!`,
|
||||
`Precept Two: 'Never Let Them Laugh at You'. Fools laugh at everything, even at their superiors. But beware, laughter isn't harmless! Laughter spreads like a disease, and soon everyone is laughing at you. You need to strike at the source of this perverse merriment quickly to stop it from spreading.`,
|
||||
`Precept Three: 'Always Be Rested'. Fighting and adventuring take their toll on your body. When you rest, your body strengthens and repairs itself. The longer you rest, the stronger you become.`,
|
||||
`Precept Four: 'Forget Your Past'. The past is painful, and thinking about your past can only bring you misery. Think about something else instead, such as the future, or some food.`,
|
||||
`Precept Five: 'Strength Beats Strength'. Is your opponent strong? No matter! Simply overcome their strength with even more strength, and they'll soon be defeated.`,
|
||||
`Precept Six: 'Choose Your Own Fate'. Our elders teach that our fate is chosen for us before we are even born. I disagree.`,
|
||||
`Precept Seven: 'Mourn Not the Dead'. When we die, do things get better for us or worse? There's no way to tell, so we shouldn't bother mourning. Or celebrating for that matter.`,
|
||||
`Precept Eight: 'Travel Alone'. You can rely on nobody, and nobody will always be loyal. Therefore, nobody should be your constant companion.`,
|
||||
`Precept Nine: 'Keep Your Home Tidy'. Your home is where you keep your most prized possession - yourself. Therefore, you should make an effort to keep it nice and clean.`,
|
||||
`Precept Ten: 'Keep Your Weapon Sharp'. I make sure that my weapon, 'Life Ender', is kept well-sharpened at all times. This makes it much easier to cut things.`,
|
||||
`Precept Eleven: 'Mothers Will Always Betray You'. This Precept explains itself.`,
|
||||
`Precept Twelve: 'Keep Your Cloak Dry'. If your cloak gets wet, dry it as soon as you can. Wearing wet cloaks is unpleasant, and can lead to illness.`,
|
||||
`Precept Thirteen: 'Never Be Afraid'. Fear can only hold you back. Facing your fears can be a tremendous effort. Therefore, you should just not be afraid in the first place.`,
|
||||
`Precept Fourteen: 'Respect Your Superiors'. If someone is your superior in strength or intellect or both, you need to show them your respect. Don't ignore them or laugh at them.`,
|
||||
`Precept Fifteen: 'One Foe, One Blow'. You should only use a single blow to defeat an enemy. Any more is a waste. Also, by counting your blows as you fight, you'll know how many foes you've defeated.`,
|
||||
`...`,
|
||||
}
|
||||
|
||||
t.Run("empty list", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := New(10, 10)
|
||||
list := m.visibleLines()
|
||||
|
||||
if len(list) != 0 {
|
||||
t.Errorf("list should be empty, got %d", len(list))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty list: with indent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := New(10, 10)
|
||||
list := m.visibleLines()
|
||||
m.xOffset = 5
|
||||
|
||||
if len(list) != 0 {
|
||||
t.Errorf("list should be empty, got %d", len(list))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
numberOfLines := 10
|
||||
|
||||
m := New(10, numberOfLines)
|
||||
m.SetContent(strings.Join(defaultList, "\n"))
|
||||
|
||||
list := m.visibleLines()
|
||||
if len(list) != numberOfLines {
|
||||
t.Errorf("list should have %d lines, got %d", numberOfLines, len(list))
|
||||
}
|
||||
|
||||
lastItemIdx := numberOfLines - 1
|
||||
// we trim line if it doesn't fit to width of the viewport
|
||||
shouldGet := defaultList[lastItemIdx][:m.Width]
|
||||
if list[lastItemIdx] != shouldGet {
|
||||
t.Errorf(`%dth list item should be '%s', got '%s'`, lastItemIdx, shouldGet, list[lastItemIdx])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list: with y offset", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
numberOfLines := 10
|
||||
|
||||
m := New(10, numberOfLines)
|
||||
m.SetContent(strings.Join(defaultList, "\n"))
|
||||
m.YOffset = 5
|
||||
|
||||
list := m.visibleLines()
|
||||
if len(list) != numberOfLines {
|
||||
t.Errorf("list should have %d lines, got %d", numberOfLines, len(list))
|
||||
}
|
||||
|
||||
if list[0] == defaultList[0] {
|
||||
t.Error("first item of list should not be the first item of initial list because of Y offset")
|
||||
}
|
||||
|
||||
lastItemIdx := numberOfLines - 1
|
||||
// we trim line if it doesn't fit to width of the viewport
|
||||
shouldGet := defaultList[m.YOffset+lastItemIdx][:m.Width]
|
||||
if list[lastItemIdx] != shouldGet {
|
||||
t.Errorf(`%dth list item should be '%s', got '%s'`, lastItemIdx, shouldGet, list[lastItemIdx])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list: with y offset: horizontal scroll", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
numberOfLines := 10
|
||||
|
||||
m := New(10, numberOfLines)
|
||||
m.lines = defaultList
|
||||
m.YOffset = 7
|
||||
|
||||
// default list
|
||||
list := m.visibleLines()
|
||||
if len(list) != numberOfLines {
|
||||
t.Errorf("list should have %d lines, got %d", numberOfLines, len(list))
|
||||
}
|
||||
|
||||
lastItem := numberOfLines - 1
|
||||
defaultLastItem := len(defaultList) - 1
|
||||
if list[lastItem] != defaultList[defaultLastItem] {
|
||||
t.Errorf("%dth list item should the the same as %dth default list item", lastItem, defaultLastItem)
|
||||
}
|
||||
|
||||
perceptPrefix := "Precept"
|
||||
if !strings.HasPrefix(list[0], perceptPrefix) {
|
||||
t.Errorf("first list item has to have prefix %s", perceptPrefix)
|
||||
}
|
||||
|
||||
// move right
|
||||
m.MoveRight(m.horizontalStep)
|
||||
list = m.visibleLines()
|
||||
|
||||
newPrefix := perceptPrefix[m.xOffset:]
|
||||
if !strings.HasPrefix(list[0], newPrefix) {
|
||||
t.Errorf("first list item has to have prefix %s, get %s", newPrefix, list[0])
|
||||
}
|
||||
|
||||
if list[lastItem] != "..." {
|
||||
t.Errorf("last item should be empty, got %s", list[lastItem])
|
||||
}
|
||||
|
||||
// move left
|
||||
m.MoveLeft(m.horizontalStep)
|
||||
list = m.visibleLines()
|
||||
if !strings.HasPrefix(list[0], perceptPrefix) {
|
||||
t.Errorf("first list item has to have prefix %s", perceptPrefix)
|
||||
}
|
||||
|
||||
if list[lastItem] != defaultList[defaultLastItem] {
|
||||
t.Errorf("%dth list item should the the same as %dth default list item", lastItem, defaultLastItem)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list: with 2 cells symbols: horizontal scroll", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const horizontalStep = 5
|
||||
|
||||
initList := []string{
|
||||
"あいうえお",
|
||||
"Aあいうえお",
|
||||
"あいうえお",
|
||||
"Aあいうえお",
|
||||
}
|
||||
numberOfLines := len(initList)
|
||||
|
||||
m := New(20, numberOfLines)
|
||||
m.lines = initList
|
||||
m.longestLineWidth = 30 // dirty hack: not checking right overscroll for this test case
|
||||
|
||||
// default list
|
||||
list := m.visibleLines()
|
||||
if len(list) != numberOfLines {
|
||||
t.Errorf("list should have %d lines, got %d", numberOfLines, len(list))
|
||||
}
|
||||
|
||||
lastItemIdx := numberOfLines - 1
|
||||
initLastItem := len(initList) - 1
|
||||
shouldGet := initList[initLastItem]
|
||||
if list[lastItemIdx] != shouldGet {
|
||||
t.Errorf("%dth list item should the the same as %dth default list item", lastItemIdx, initLastItem)
|
||||
}
|
||||
|
||||
// move right
|
||||
m.MoveRight(horizontalStep)
|
||||
list = m.visibleLines()
|
||||
|
||||
for i := range list {
|
||||
cutLine := "うえお"
|
||||
if list[i] != cutLine {
|
||||
t.Errorf("line must be `%s`, get `%s`", cutLine, list[i])
|
||||
}
|
||||
}
|
||||
|
||||
// move left
|
||||
m.MoveLeft(horizontalStep)
|
||||
list = m.visibleLines()
|
||||
for i := range list {
|
||||
if list[i] != initList[i] {
|
||||
t.Errorf("line must be `%s`, get `%s`", list[i], initList[i])
|
||||
}
|
||||
}
|
||||
|
||||
// move left second times do not change lites if indent == 0
|
||||
m.xOffset = 0
|
||||
m.MoveLeft(horizontalStep)
|
||||
list = m.visibleLines()
|
||||
for i := range list {
|
||||
if list[i] != initList[i] {
|
||||
t.Errorf("line must be `%s`, get `%s`", list[i], initList[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRightOverscroll(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("prevent right overscroll", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
content := "Content is short"
|
||||
m := New(len(content)+1, 5)
|
||||
m.SetContent(content)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
m.MoveRight(m.horizontalStep)
|
||||
}
|
||||
|
||||
visibleLines := m.visibleLines()
|
||||
visibleLine := visibleLines[0]
|
||||
|
||||
if visibleLine != content {
|
||||
t.Error("visible line should stay the same as content")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user