fix: editor whitespaces
This commit is contained in:
@@ -761,7 +761,7 @@ func normalizeLineEndings(value string) string {
|
|||||||
|
|
||||||
type editorVisualLine struct {
|
type editorVisualLine struct {
|
||||||
text string
|
text string
|
||||||
start, end int
|
start, displayStart, end int
|
||||||
logicalStart, logicalEnd int
|
logicalStart, logicalEnd int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,9 +826,11 @@ func moveEditorCursorLine(value string, cursor, delta, wrapWidth int, normal boo
|
|||||||
if targetIndex == index {
|
if targetIndex == index {
|
||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
column := lipgloss.Width(string(runes[lines[index].start:clamp(cursor, lines[index].start, lines[index].end)]))
|
current := lines[index]
|
||||||
|
columnStart := min(current.end, max(current.start, current.displayStart))
|
||||||
|
column := lipgloss.Width(string(runes[columnStart:clamp(cursor, columnStart, current.end)]))
|
||||||
target := lines[targetIndex]
|
target := lines[targetIndex]
|
||||||
position, usedWidth := target.start, 0
|
position, usedWidth := max(target.start, target.displayStart), 0
|
||||||
for position < target.end {
|
for position < target.end {
|
||||||
runeWidth := lipgloss.Width(string(runes[position]))
|
runeWidth := lipgloss.Width(string(runes[position]))
|
||||||
if usedWidth+runeWidth > column {
|
if usedWidth+runeWidth > column {
|
||||||
@@ -887,23 +889,39 @@ func editorCursorVisualPosition(editor textEditor, width int) (int, int) {
|
|||||||
visual := editorVisualLines(editor.Text, width)
|
visual := editorVisualLines(editor.Text, width)
|
||||||
index := editorVisualLineIndex(visual, cursor)
|
index := editorVisualLineIndex(visual, cursor)
|
||||||
line := visual[index]
|
line := visual[index]
|
||||||
column := lipgloss.Width(string(runes[line.start:clamp(cursor, line.start, line.end)]))
|
columnStart := min(line.end, max(line.start, line.displayStart))
|
||||||
|
column := lipgloss.Width(string(runes[columnStart:clamp(cursor, columnStart, line.end)]))
|
||||||
return index, column
|
return index, column
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapEditorLogicalLine(runes []rune, start, end, width int) []editorVisualLine {
|
func wrapEditorLogicalLine(runes []rune, start, end, width int) []editorVisualLine {
|
||||||
if start == end {
|
if start == end {
|
||||||
return []editorVisualLine{{
|
return []editorVisualLine{{
|
||||||
start: start, end: end, logicalStart: start, logicalEnd: end,
|
start: start, displayStart: start, end: end,
|
||||||
|
logicalStart: start, logicalEnd: end,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
var lines []editorVisualLine
|
var lines []editorVisualLine
|
||||||
for offset := start; offset < end; {
|
for offset := start; offset < end; {
|
||||||
next := offset
|
displayStart := offset
|
||||||
|
if offset > start {
|
||||||
|
for displayStart < end && unicode.IsSpace(runes[displayStart]) {
|
||||||
|
displayStart++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if displayStart == end {
|
||||||
|
lines = append(lines, editorVisualLine{
|
||||||
|
start: offset, displayStart: displayStart, end: end,
|
||||||
|
logicalStart: start, logicalEnd: end,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
next := displayStart
|
||||||
lineWidth := 0
|
lineWidth := 0
|
||||||
for next < end {
|
for next < end {
|
||||||
runeWidth := lipgloss.Width(string(runes[next]))
|
runeWidth := lipgloss.Width(string(runes[next]))
|
||||||
if next > offset && lineWidth+runeWidth > width {
|
if next > displayStart && lineWidth+runeWidth > width {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
lineWidth += runeWidth
|
lineWidth += runeWidth
|
||||||
@@ -912,14 +930,32 @@ func wrapEditorLogicalLine(runes []rune, start, end, width int) []editorVisualLi
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if next == offset {
|
if next == displayStart {
|
||||||
next++
|
next++
|
||||||
}
|
}
|
||||||
|
lineEnd := next
|
||||||
|
if next < end {
|
||||||
|
breakAt := -1
|
||||||
|
haveWord := false
|
||||||
|
for index := displayStart; index < next; index++ {
|
||||||
|
if unicode.IsSpace(runes[index]) {
|
||||||
|
if haveWord {
|
||||||
|
breakAt = index
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
haveWord = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if breakAt > displayStart {
|
||||||
|
lineEnd = breakAt
|
||||||
|
}
|
||||||
|
}
|
||||||
lines = append(lines, editorVisualLine{
|
lines = append(lines, editorVisualLine{
|
||||||
text: string(runes[offset:next]), start: offset, end: next,
|
text: string(runes[displayStart:lineEnd]),
|
||||||
|
start: offset, displayStart: displayStart, end: lineEnd,
|
||||||
logicalStart: start, logicalEnd: end,
|
logicalStart: start, logicalEnd: end,
|
||||||
})
|
})
|
||||||
offset = next
|
offset = lineEnd
|
||||||
}
|
}
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
@@ -941,12 +977,16 @@ func renderEditorVisualLine(
|
|||||||
underlineEnd = "\x1b[24m"
|
underlineEnd = "\x1b[24m"
|
||||||
)
|
)
|
||||||
runes := []rune(line.text)
|
runes := []rune(line.text)
|
||||||
|
displayCursor := cursor
|
||||||
|
if displayCursor < line.displayStart {
|
||||||
|
displayCursor = line.displayStart
|
||||||
|
}
|
||||||
var rendered strings.Builder
|
var rendered strings.Builder
|
||||||
selected := false
|
selected := false
|
||||||
protectedColor := ""
|
protectedColor := ""
|
||||||
markdownStyle := editorMarkdownPlain
|
markdownStyle := editorMarkdownPlain
|
||||||
for offset, value := range runes {
|
for offset, value := range runes {
|
||||||
position := line.start + offset
|
position := line.displayStart + offset
|
||||||
nextProtectedColor := ""
|
nextProtectedColor := ""
|
||||||
if position < protectedPrefix {
|
if position < protectedPrefix {
|
||||||
nextProtectedColor = editorMarkdownTheme.Dim
|
nextProtectedColor = editorMarkdownTheme.Dim
|
||||||
@@ -991,7 +1031,7 @@ func renderEditorVisualLine(
|
|||||||
}
|
}
|
||||||
selected = nowSelected
|
selected = nowSelected
|
||||||
}
|
}
|
||||||
if showCursor && position == cursor {
|
if showCursor && position == displayCursor {
|
||||||
switch mode {
|
switch mode {
|
||||||
case textEditorInsert:
|
case textEditorInsert:
|
||||||
if hardwareCursor {
|
if hardwareCursor {
|
||||||
|
|||||||
@@ -308,6 +308,65 @@ func TestEditorKeepsWrappedRowsAndContextRailsVisible(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEditorWordWrapHidesSoftWrapSpacesWithoutChangingText(t *testing.T) {
|
||||||
|
const value = "abcdefghij hello"
|
||||||
|
visual := editorVisualLines(value, 10)
|
||||||
|
if len(visual) != 2 || visual[0].text != "abcdefghij" || visual[1].text != "hello" {
|
||||||
|
t.Fatalf("word-wrapped lines = %#v", visual)
|
||||||
|
}
|
||||||
|
if visual[1].start != 10 || visual[1].displayStart != 11 {
|
||||||
|
t.Fatalf("wrapped separator offsets = %#v", visual[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
editor := newTextEditor(value, true)
|
||||||
|
editor.Cursor = len([]rune(value))
|
||||||
|
rendered := renderTextEditor(editor, 10, false)
|
||||||
|
if got := ansi.Strip(rendered[1].text); got != "hello" {
|
||||||
|
t.Fatalf("wrapped row begins with separator space: %q", got)
|
||||||
|
}
|
||||||
|
if editor.Text != value {
|
||||||
|
t.Fatalf("word wrapping changed stored text: %q", editor.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
visual = editorVisualLines("hello world", 10)
|
||||||
|
if len(visual) != 2 || visual[0].text != "hello" || visual[1].text != "world" {
|
||||||
|
t.Fatalf("overflowing word was split instead of moved: %#v", visual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEditorBoundarySpaceCreatesEmptyVisualRow(t *testing.T) {
|
||||||
|
const value = "abcdefghij "
|
||||||
|
visual := editorVisualLines(value, 10)
|
||||||
|
if len(visual) != 2 || visual[0].text != "abcdefghij" || visual[1].text != "" {
|
||||||
|
t.Fatalf("boundary-space lines = %#v", visual)
|
||||||
|
}
|
||||||
|
if visual[1].start != 10 || visual[1].displayStart != 11 || visual[1].end != 11 {
|
||||||
|
t.Fatalf("boundary-space offsets = %#v", visual[1])
|
||||||
|
}
|
||||||
|
editor := newTextEditor(value, true)
|
||||||
|
editor.Cursor = len([]rune(value))
|
||||||
|
if line, column := editorCursorVisualPosition(editor, 10); line != 1 || column != 0 {
|
||||||
|
t.Fatalf("boundary-space cursor = row %d column %d, want row 1 column 0", line, column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEditorWordWrapHardWrapsWordsWiderThanViewport(t *testing.T) {
|
||||||
|
const value = "hi abcdefghijklmnopqrstuv"
|
||||||
|
visual := editorVisualLines(value, 10)
|
||||||
|
want := []string{"hi", "abcdefghij", "klmnopqrst", "uv"}
|
||||||
|
if len(visual) != len(want) {
|
||||||
|
t.Fatalf("long-word rows = %#v, want %q", visual, want)
|
||||||
|
}
|
||||||
|
for index, line := range visual {
|
||||||
|
if line.text != want[index] {
|
||||||
|
t.Fatalf("long-word row %d = %q, want %q", index, line.text, want[index])
|
||||||
|
}
|
||||||
|
if ansi.StringWidth(line.text) > 10 {
|
||||||
|
t.Fatalf("long-word row %d exceeds viewport: %q", index, line.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVimEditorTreatsSoftWrapsAsVisualLinesWithoutChangingText(t *testing.T) {
|
func TestVimEditorTreatsSoftWrapsAsVisualLinesWithoutChangingText(t *testing.T) {
|
||||||
const value = "abcdefghijklmnopqrstuv"
|
const value = "abcdefghijklmnopqrstuv"
|
||||||
editor := newTextEditor(value, true)
|
editor := newTextEditor(value, true)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
const dipleVersion = "0.3.3"
|
const dipleVersion = "0.3.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user