fix: fixed the delimiter "same-line" issue.
With the help of Claude. I want this to be over with so I can move onto more fun things than actions.
This commit is contained in:
parent
5405d5a6bd
commit
21ed76bed5
@ -582,9 +582,9 @@ func TestTextObjectEdgeCases(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test text object cursor outside delimiters does nothing", func(t *testing.T) {
|
t.Run("test text object cursor after delimiters does nothing", func(t *testing.T) {
|
||||||
lines := []string{"before (hello) after"}
|
lines := []string{"before (hello) after"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
||||||
sendKeys(tm, "d", "i", "(")
|
sendKeys(tm, "d", "i", "(")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -593,4 +593,116 @@ func TestTextObjectEdgeCases(t *testing.T) {
|
|||||||
t.Errorf("lines[0] = %q, want 'before (hello) after'", m.ActiveBuffer().Lines[0])
|
t.Errorf("lines[0] = %q, want 'before (hello) after'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("test text object cursor before delimiters selects inside", func(t *testing.T) {
|
||||||
|
lines := []string{"before (hello) after"}
|
||||||
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
|
sendKeys(tm, "d", "i", "(")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ActiveBuffer().Lines[0] != "before () after" {
|
||||||
|
t.Errorf("lines[0] = %q, want 'before () after'", m.ActiveBuffer().Lines[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test text object cursor before delimiters with 'a' modifier", func(t *testing.T) {
|
||||||
|
lines := []string{"before (hello) after"}
|
||||||
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
|
sendKeys(tm, "d", "a", "(")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
// 'a' should delete including the delimiters
|
||||||
|
if m.ActiveBuffer().Lines[0] != "before after" {
|
||||||
|
t.Errorf("lines[0] = %q, want 'before after'", m.ActiveBuffer().Lines[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test text object cursor on opening delimiter", func(t *testing.T) {
|
||||||
|
lines := []string{"text (hello) more"}
|
||||||
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
|
sendKeys(tm, "d", "i", "(")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
// Cursor on '(' at position 5, should still select inside
|
||||||
|
if m.ActiveBuffer().Lines[0] != "text () more" {
|
||||||
|
t.Errorf("lines[0] = %q, want 'text () more'", m.ActiveBuffer().Lines[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test text object cursor on closing delimiter", func(t *testing.T) {
|
||||||
|
lines := []string{"text (hello) more"}
|
||||||
|
// "text (hello) more"
|
||||||
|
// 01234567891011 <- ')' is at position 11
|
||||||
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 11, Line: 0})
|
||||||
|
sendKeys(tm, "d", "i", "(")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
// Cursor on ')', should still select inside
|
||||||
|
if m.ActiveBuffer().Lines[0] != "text () more" {
|
||||||
|
t.Errorf("lines[0] = %q, want 'text () more'", m.ActiveBuffer().Lines[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test multiple delimiter pairs - cursor before first", func(t *testing.T) {
|
||||||
|
lines := []string{"(foo) bar (baz)"}
|
||||||
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
|
sendKeys(tm, "d", "i", "(")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
// Should select the first pair it finds
|
||||||
|
if m.ActiveBuffer().Lines[0] != "() bar (baz)" {
|
||||||
|
t.Errorf("lines[0] = %q, want '() bar (baz)'", m.ActiveBuffer().Lines[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test multiple delimiter pairs - cursor between pairs", func(t *testing.T) {
|
||||||
|
lines := []string{"(foo) bar (baz)"}
|
||||||
|
// Cursor on 'b' in "bar"
|
||||||
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
|
||||||
|
sendKeys(tm, "d", "i", "(")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
// Should search forward and find the second pair
|
||||||
|
if m.ActiveBuffer().Lines[0] != "(foo) bar ()" {
|
||||||
|
t.Errorf("lines[0] = %q, want '(foo) bar ()'", m.ActiveBuffer().Lines[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test multiple delimiter pairs - cursor inside first", func(t *testing.T) {
|
||||||
|
lines := []string{"(foo) bar (baz)"}
|
||||||
|
// Cursor on 'o' in "foo"
|
||||||
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
|
sendKeys(tm, "d", "i", "(")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
// Should select the first pair since cursor is inside it
|
||||||
|
if m.ActiveBuffer().Lines[0] != "() bar (baz)" {
|
||||||
|
t.Errorf("lines[0] = %q, want '() bar (baz)'", m.ActiveBuffer().Lines[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test multiple quoted strings - cursor before first", func(t *testing.T) {
|
||||||
|
lines := []string{`foo "bar" baz "qux"`}
|
||||||
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
|
sendKeys(tm, "d", "i", `"`)
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
// Should find and select first quoted string
|
||||||
|
if m.ActiveBuffer().Lines[0] != `foo "" baz "qux"` {
|
||||||
|
t.Errorf("lines[0] = %q, want 'foo \"\" baz \"qux\"'", m.ActiveBuffer().Lines[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test multiple quoted strings - cursor between pairs", func(t *testing.T) {
|
||||||
|
lines := []string{`"foo" bar "baz"`}
|
||||||
|
// Cursor on 'b' in "bar"
|
||||||
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
|
||||||
|
sendKeys(tm, "d", "i", `"`)
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
// Should search forward and find second string
|
||||||
|
if m.ActiveBuffer().Lines[0] != `"foo" bar ""` {
|
||||||
|
t.Errorf("lines[0] = %q, want '\"foo\" bar \"\"'", m.ActiveBuffer().Lines[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,9 +47,6 @@ type Delimiter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This should allow for many lines, not just a single line
|
// TODO: This should allow for many lines, not just a single line
|
||||||
//
|
|
||||||
// BUG: This does not work properly when the cursor is not inside a delimiter. If the cursor
|
|
||||||
// does not fall inside a delimiter range, it should search forward and find the delimiter.
|
|
||||||
func (to Delimiter) GetRange(m action.Model, cursor core.Position, modifier string) (core.Position, core.Position, core.MotionType) {
|
func (to Delimiter) GetRange(m action.Model, cursor core.Position, modifier string) (core.Position, core.Position, core.MotionType) {
|
||||||
buf := m.ActiveBuffer()
|
buf := m.ActiveBuffer()
|
||||||
line := buf.Lines[cursor.Line]
|
line := buf.Lines[cursor.Line]
|
||||||
@ -80,21 +77,19 @@ func (to Delimiter) GetRange(m action.Model, cursor core.Position, modifier stri
|
|||||||
return cursor, cursor, core.CharwiseExclusive
|
return cursor, cursor, core.CharwiseExclusive
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find object boundaries
|
// Try to find delimiters around the cursor position
|
||||||
start, sOk := findDelimiterStart(line, startDelim, cursor.Col, modifier == "a")
|
start, end, found := findDelimiterPair(line, startDelim, endDelim, cursor.Col, modifier == "a")
|
||||||
end, eOk := findDelimiterEnd(line, endDelim, cursor.Col, modifier == "a")
|
|
||||||
|
|
||||||
// Handle the case where they are not found
|
if !found {
|
||||||
if !sOk || !eOk {
|
|
||||||
return cursor, cursor, core.CharwiseExclusive
|
return cursor, cursor, core.CharwiseExclusive
|
||||||
}
|
}
|
||||||
|
|
||||||
// This happens when nothing is between the delimiter, fixes the bugs we found
|
// This happens when nothing is between the delimiter
|
||||||
if start.Col > end.Col {
|
if start.Col > end.Col {
|
||||||
return cursor, cursor, core.CharwiseExclusive
|
return cursor, cursor, core.CharwiseExclusive
|
||||||
}
|
}
|
||||||
|
|
||||||
// Word object's don't span lines
|
// Delimiter objects don't span lines (for now)
|
||||||
start.Line = cursor.Line
|
start.Line = cursor.Line
|
||||||
end.Line = cursor.Line
|
end.Line = cursor.Line
|
||||||
|
|
||||||
@ -124,3 +119,131 @@ func findDelimiterEnd(line string, delimiter rune, col int, includeDelimiter boo
|
|||||||
}
|
}
|
||||||
return core.Position{Line: 0, Col: 0}, false
|
return core.Position{Line: 0, Col: 0}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findDelimiterPair tries to find a matching pair of delimiters.
|
||||||
|
// First it tries to find delimiters around the cursor.
|
||||||
|
// If that fails, it searches forward for the next pair.
|
||||||
|
func findDelimiterPair(line string, startDelim, endDelim rune, cursorCol int, includeDelimiters bool) (core.Position, core.Position, bool) {
|
||||||
|
// First, try to find delimiters around cursor
|
||||||
|
start, sOk := findDelimiterStart(line, startDelim, cursorCol, includeDelimiters)
|
||||||
|
end, eOk := findDelimiterEnd(line, endDelim, cursorCol, includeDelimiters)
|
||||||
|
|
||||||
|
if sOk && eOk {
|
||||||
|
// Verify this is actually a valid pair by checking there are no
|
||||||
|
// unmatched delimiters between start and end
|
||||||
|
var startDelimPos, endDelimPos int
|
||||||
|
if includeDelimiters {
|
||||||
|
startDelimPos = start.Col
|
||||||
|
endDelimPos = end.Col
|
||||||
|
} else {
|
||||||
|
startDelimPos = start.Col - 1
|
||||||
|
endDelimPos = end.Col + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// For proper pair validation, check if this is the nearest matching pair
|
||||||
|
// by ensuring the end delimiter we found is the first one after the start
|
||||||
|
if isValidPair(line, startDelim, endDelim, startDelimPos, endDelimPos) {
|
||||||
|
// Cursor should be at or between the delimiters
|
||||||
|
if startDelimPos <= cursorCol && cursorCol <= endDelimPos {
|
||||||
|
return start, end, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not inside delimiters, search forward for next pair
|
||||||
|
startDelimPos, foundStart := findNextDelimiter(line, startDelim, cursorCol)
|
||||||
|
if !foundStart {
|
||||||
|
return core.Position{}, core.Position{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
endDelimPos, foundEnd := findNextDelimiter(line, endDelim, startDelimPos+1)
|
||||||
|
if !foundEnd {
|
||||||
|
return core.Position{}, core.Position{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate this pair as well
|
||||||
|
if !isValidPair(line, startDelim, endDelim, startDelimPos, endDelimPos) {
|
||||||
|
return core.Position{}, core.Position{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate start and end positions based on modifier
|
||||||
|
if includeDelimiters {
|
||||||
|
start = core.Position{Line: 0, Col: startDelimPos}
|
||||||
|
end = core.Position{Line: 0, Col: endDelimPos}
|
||||||
|
} else {
|
||||||
|
start = core.Position{Line: 0, Col: startDelimPos + 1}
|
||||||
|
end = core.Position{Line: 0, Col: endDelimPos - 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
return start, end, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidPair checks if the delimiters at startPos and endPos form a valid matching pair.
|
||||||
|
// For same-delimiter pairs (quotes), it checks they form an opening/closing pair.
|
||||||
|
// For directional pairs (parens, brackets), it ensures the end is the matching closer for the start.
|
||||||
|
func isValidPair(line string, startDelim, endDelim rune, startPos, endPos int) bool {
|
||||||
|
if startPos >= endPos {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// For quote-like delimiters where start and end are the same
|
||||||
|
if startDelim == endDelim {
|
||||||
|
// For quotes, we need to determine if startPos is an opening quote and endPos is a closing quote
|
||||||
|
// We do this by counting quotes before each position
|
||||||
|
// An opening quote has an even number of quotes before it (0, 2, 4, ...)
|
||||||
|
// A closing quote has an odd number of quotes before it (1, 3, 5, ...)
|
||||||
|
|
||||||
|
quotesBeforeStart := 0
|
||||||
|
for i := 0; i < startPos; i++ {
|
||||||
|
if rune(line[i]) == startDelim {
|
||||||
|
quotesBeforeStart++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quotesBeforeEnd := quotesBeforeStart + 1 // We know there's at least the startPos quote
|
||||||
|
for i := startPos + 1; i < endPos; i++ {
|
||||||
|
if rune(line[i]) == startDelim {
|
||||||
|
quotesBeforeEnd++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startPos should be an opening quote (even number before it)
|
||||||
|
// endPos should be a closing quote (odd number before it)
|
||||||
|
// AND there should be no quotes between them for a simple pair
|
||||||
|
startIsOpening := quotesBeforeStart%2 == 0
|
||||||
|
endIsClosing := quotesBeforeEnd%2 == 1
|
||||||
|
noQuotesBetween := quotesBeforeEnd == quotesBeforeStart+1
|
||||||
|
|
||||||
|
return startIsOpening && endIsClosing && noQuotesBetween
|
||||||
|
}
|
||||||
|
|
||||||
|
// For directional delimiters, check that endPos has the first unmatched closing delimiter
|
||||||
|
// Simple approach: ensure there's no end delimiter between startPos and endPos that would
|
||||||
|
// close an earlier start delimiter
|
||||||
|
nestLevel := 0
|
||||||
|
for i := startPos + 1; i < endPos; i++ {
|
||||||
|
if rune(line[i]) == startDelim {
|
||||||
|
nestLevel++
|
||||||
|
} else if rune(line[i]) == endDelim {
|
||||||
|
if nestLevel > 0 {
|
||||||
|
nestLevel--
|
||||||
|
} else {
|
||||||
|
// Found an unmatched end delimiter before our endPos
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The endPos should close the startPos
|
||||||
|
return nestLevel == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// findNextDelimiter searches forward from startCol for the next occurrence of delimiter
|
||||||
|
func findNextDelimiter(line string, delimiter rune, startCol int) (int, bool) {
|
||||||
|
for i := startCol; i < len(line); i++ {
|
||||||
|
if rune(line[i]) == delimiter {
|
||||||
|
return i, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user