diff --git a/cmd/templ/lspcmd/proxy/documentcontents.go b/cmd/templ/lspcmd/proxy/documentcontents.go index e04bd9f0b..54c3a70c4 100644 --- a/cmd/templ/lspcmd/proxy/documentcontents.go +++ b/cmd/templ/lspcmd/proxy/documentcontents.go @@ -69,7 +69,7 @@ func (dc *DocumentContents) Apply(uri string, changes []lsp.TextDocumentContentC return } for _, change := range changes { - d.Overwrite(change.Range, change.Text) + d.Apply(change.Range, change.Text) } return } @@ -86,78 +86,144 @@ type Document struct { Lines []string } -func (d *Document) isEmptyRange(r lsp.Range) bool { - return r.Start.Line == 0 && r.Start.Character == 0 && - r.End.Line == 0 && r.End.Character == 0 +func (d *Document) LineLengths() (lens []int) { + lens = make([]int, len(d.Lines)) + for i, l := range d.Lines { + lens[i] = len(l) + } + return +} + +func (d *Document) Len() (line, col int) { + line = len(d.Lines) + col = len(d.Lines[len(d.Lines)-1]) + return } -func (d *Document) isRangeOfDocument(r lsp.Range) bool { - rangeStartsAtBeginningOfFile := r.Start.Line == 0 && r.Start.Character == 0 - rel, rec := int(r.End.Line), int(r.End.Character) - del, dec := int(len(d.Lines)-1), len(d.Lines[len(d.Lines)-1])-1 - rangeEndsPastTheEndOfFile := rel > del || rel == del && rec > dec - rangeEndsAtEndOfFile := rel == del && rec == dec - return rangeStartsAtBeginningOfFile && (rangeEndsPastTheEndOfFile || rangeEndsAtEndOfFile) +func (d *Document) Overwrite(fromLine, fromCol, toLine, toCol int, lines []string) { + suffix := d.Lines[toLine][toCol:] + toLen := d.LineLengths()[toLine] + d.Delete(fromLine, fromCol, toLine, toLen) + lines[len(lines)-1] = lines[len(lines)-1] + suffix + d.Insert(fromLine, fromCol, lines) } -func (d *Document) remove(i, j int) { - d.Lines = append(d.Lines[:i], d.Lines[j:]...) +func (d *Document) Insert(line, col int, lines []string) { + prefix := d.Lines[line][:col] + suffix := d.Lines[line][col:] + lines[0] = prefix + lines[0] + d.Lines[line] = lines[0] + + if len(lines) > 1 { + d.InsertLines(line+1, lines[1:]) + } + + d.Lines[line+len(lines)-1] = lines[len(lines)-1] + suffix } -func (d *Document) insert(i int, withLines []string) { +func (d *Document) InsertLines(i int, withLines []string) { d.Lines = append(d.Lines[:i], append(withLines, d.Lines[i:]...)...) } -func (d *Document) normaliseRange(r *lsp.Range) { - if r.Start.Line > uint32(len(d.Lines))-1 { - r.Start.Line = uint32(len(d.Lines)) - 1 - } - if r.End.Line > uint32(len(d.Lines))-1 { - r.End.Line = uint32(len(d.Lines)) - 1 - } - startLine := d.Lines[r.Start.Line] - startLineMaxCharIndex := len(startLine) - if r.Start.Character > uint32(startLineMaxCharIndex) { - r.Start.Character = uint32(startLineMaxCharIndex) +func (d *Document) Delete(fromLine, fromCol, toLine, toCol int) { + prefix := d.Lines[fromLine][:fromCol] + suffix := d.Lines[toLine][toCol:] + + lens := d.LineLengths() + isWithinLine := fromLine == toLine + toLineLen := lens[toLine] + fromIsWholeLine := (fromCol == 0 && !isWithinLine) || (fromCol == 0 && isWithinLine && toCol == toLineLen) + toIsWholeLine := !isWithinLine && toCol == toLineLen + + if isWithinLine { + d.Lines[fromLine] = prefix + suffix + } else { + d.Lines[fromLine] = prefix + d.Lines[toLine] = suffix } - endLine := d.Lines[r.End.Line] - endLineMaxCharIndex := len(endLine) - if r.End.Character > uint32(endLineMaxCharIndex) { - r.End.Character = uint32(endLineMaxCharIndex) + + if !isWithinLine { + deleteFromLineIndex := fromLine + deleteToLineIndex := toLine + 1 + if !fromIsWholeLine { + deleteFromLineIndex++ + } + if !toIsWholeLine { + deleteToLineIndex-- + } + d.DeleteLines(deleteFromLineIndex, deleteToLineIndex) } + +} + +func (d *Document) DeleteLines(i, j int) { + d.Lines = append(d.Lines[:i], d.Lines[j:]...) +} + +func (d *Document) String() string { + return strings.Join(d.Lines, "\n") } -func (d *Document) Overwrite(r *lsp.Range, with string) { +func (d *Document) Apply(r *lsp.Range, with string) { withLines := strings.Split(with, "\n") - if r == nil || d.isEmptyRange(*r) || len(d.Lines) == 0 { + d.normalize(r) + if d.isWholeDocument(r) { d.Lines = withLines return } - d.normaliseRange(r) - if d.isRangeOfDocument(*r) { - d.Lines = withLines + if d.isInsert(r, with) { + d.Insert(int(r.Start.Line), int(r.Start.Character), withLines) return } - if r.Start.Character > 0 { - prefix := d.Lines[r.Start.Line][:r.Start.Character] - withLines[0] = prefix + withLines[0] + if d.isDelete(r, with) { + d.Delete(int(r.Start.Line), int(r.Start.Character), int(r.End.Line), int(r.End.Character)) + return } - if r.End.Character > 0 { - suffix := d.Lines[r.End.Line][r.End.Character:] - withLines[len(withLines)-1] = withLines[len(withLines)-1] + suffix + if d.isOverwrite(r, with) { + d.Overwrite(int(r.Start.Line), int(r.Start.Character), int(r.End.Line), int(r.End.Character), withLines) } - if r.End.Line > r.Start.Line && r.End.Character == 0 { - // Neovim unexpectedly adds a newline when re-inserting content (dd, followed by u for undo). - if last := withLines[len(withLines)-1]; last == "" { - withLines = withLines[0 : len(withLines)-1] - } - d.remove(int(r.Start.Line), int(r.End.Line)) - } else { - d.remove(int(r.Start.Line), int(r.End.Line+1)) +} + +func (d *Document) normalize(r *lsp.Range) { + if r == nil { + return + } + lens := d.LineLengths() + if r.Start.Line >= uint32(len(lens)) { + r.Start.Line = uint32(len(lens) - 1) + r.Start.Character = uint32(lens[r.Start.Line]) + } + if r.Start.Character > uint32(lens[r.Start.Line]) { + r.Start.Character = uint32(lens[r.Start.Line]) + } + if r.End.Line >= uint32(len(lens)) { + r.End.Line = uint32(len(lens) - 1) + r.End.Character = uint32(lens[r.End.Line]) + } + if r.End.Character > uint32(lens[r.End.Line]) { + r.End.Character = uint32(lens[r.End.Line]) } - d.insert(int(r.Start.Line), withLines) } -func (d *Document) String() string { - return strings.Join(d.Lines, "\n") +func (d *Document) isOverwrite(r *lsp.Range, with string) bool { + return (r.End.Line != r.Start.Line || r.Start.Character != r.End.Character) && with != "" +} + +func (d *Document) isInsert(r *lsp.Range, with string) bool { + return r.End.Line == r.Start.Line && r.Start.Character == r.End.Character && with != "" +} + +func (d *Document) isDelete(r *lsp.Range, with string) bool { + return (r.End.Line != r.Start.Line || r.Start.Character != r.End.Character) && with == "" +} + +func (d *Document) isWholeDocument(r *lsp.Range) bool { + if r == nil { + return true + } + if r.Start.Line != 0 || r.Start.Character != 0 { + return false + } + l, c := d.Len() + return r.End.Line == uint32(l) || r.End.Character == uint32(c) } diff --git a/cmd/templ/lspcmd/proxy/documentcontents_test.go b/cmd/templ/lspcmd/proxy/documentcontents_test.go index 2bea386ee..b4a5052fb 100644 --- a/cmd/templ/lspcmd/proxy/documentcontents_test.go +++ b/cmd/templ/lspcmd/proxy/documentcontents_test.go @@ -15,22 +15,12 @@ func TestDocument(t *testing.T) { operations []func(d *Document) expected string }{ - { - name: "Replace all content if the range is empty", - start: "0\n1\n2", - operations: []func(d *Document){ - func(d *Document) { - d.Overwrite(&lsp.Range{}, "replaced") - }, - }, - expected: "replaced", - }, { name: "Replace all content if the range is nil", start: "0\n1\n2", operations: []func(d *Document){ func(d *Document) { - d.Overwrite(nil, "replaced") + d.Apply(nil, "replaced") }, }, expected: "replaced", @@ -40,14 +30,14 @@ func TestDocument(t *testing.T) { start: "0\n1\n2", operations: []func(d *Document){ func(d *Document) { - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 0, Character: 0, }, End: lsp.Position{ Line: 2, - Character: 0, + Character: 1, }, }, "replaced") }, @@ -59,7 +49,7 @@ func TestDocument(t *testing.T) { start: "0\n1\n2", operations: []func(d *Document){ func(d *Document) { - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 1, Character: 0, @@ -78,7 +68,7 @@ func TestDocument(t *testing.T) { start: "012345", operations: []func(d *Document){ func(d *Document) { - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 0, Character: 0, @@ -97,7 +87,7 @@ func TestDocument(t *testing.T) { start: "012345", operations: []func(d *Document){ func(d *Document) { - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 0, Character: 0, @@ -112,11 +102,11 @@ func TestDocument(t *testing.T) { expected: "ABCDEFG345", }, { - name: "Overwrite line", + name: "Apply line", start: "Line one\nLine two\nLine three", operations: []func(d *Document){ func(d *Document) { - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 0, Character: 4, @@ -131,11 +121,11 @@ func TestDocument(t *testing.T) { expected: "Line one test\nNew Line 2\nNew line three", }, { - name: "Add new line to end of single line", + name: "Overwrite new line at end of single line", start: `a`, operations: []func(d *Document){ func(d *Document) { - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 0, Character: 1, @@ -149,12 +139,31 @@ func TestDocument(t *testing.T) { }, expected: "a\nb", }, + { + name: "Insert new line at end of single line", + start: `a`, + operations: []func(d *Document){ + func(d *Document) { + d.Apply(&lsp.Range{ + Start: lsp.Position{ + Line: 0, + Character: 1, + }, + End: lsp.Position{ + Line: 0, + Character: 1, + }, + }, "\nb") + }, + }, + expected: "a\nb", + }, { name: "Exceeding the col and line count rounds down to the end of the file", start: `a`, operations: []func(d *Document){ func(d *Document) { - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 200, Character: 600, @@ -169,12 +178,12 @@ func TestDocument(t *testing.T) { expected: "a\nb", }, { - name: "Can remove a line and add it back from the end of the previous line", + name: "Can remove a line and add it back from the end of the previous line (insert)", start: "a\nb\nc", operations: []func(d *Document){ func(d *Document) { // Delete. - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 1, Character: 0, @@ -185,16 +194,47 @@ func TestDocument(t *testing.T) { }, }, "") // Put it back. - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 0, Character: 1, }, End: lsp.Position{ + Line: 0, + Character: 1, + }, + }, "\nb") + }, + }, + expected: "a\nb\nc", + }, + { + name: "Can remove a line and add it back from the end of the previous line (overwrite)", + start: "a\nb\nc", + operations: []func(d *Document){ + func(d *Document) { + // Delete. + d.Apply(&lsp.Range{ + Start: lsp.Position{ Line: 1, Character: 0, }, - }, "\nb") + End: lsp.Position{ + Line: 2, + Character: 0, + }, + }, "") + // Put it back. + d.Apply(&lsp.Range{ + Start: lsp.Position{ + Line: 0, + Character: 1, + }, + End: lsp.Position{ + Line: 1, + Character: 0, + }, + }, "\nb\n") }, }, expected: "a\nb\nc", @@ -213,7 +253,7 @@ templ personTemplate(p person) { `, operations: []func(d *Document){ func(d *Document) { - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 4, Character: 21, @@ -243,7 +283,7 @@ templ personTemplate(p person) { operations: []func(d *Document){ func(d *Document) { // Remove \t\tline2 - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 1, Character: 0, @@ -254,7 +294,7 @@ templ personTemplate(p person) { }, }, "") // Put it back. - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 0, Character: 5, @@ -281,7 +321,7 @@ templ personTemplate(p person) { operations: []func(d *Document){ func(d *Document) { // Remove
© { fmt.Sprintf("%d", time.Now().Year()) }
- d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 1, Character: 0, @@ -292,7 +332,7 @@ templ personTemplate(p person) { }, }, "") // Put it back. - d.Overwrite(&lsp.Range{ + d.Apply(&lsp.Range{ Start: lsp.Position{ Line: 0, Character: 38, @@ -311,6 +351,51 @@ templ personTemplate(p person) { } `, }, + { + name: "Insert at start of line", + // Based on log entry. + // {"level":"info","ts":"2023-03-25T17:17:38Z","caller":"proxy/server.go:393","msg":"client -> server: DidChange","params":{"textDocument":{"uri":"file:///Users/adrian/github.com/a-h/templ/generator/test-call/template.templ","version":5},"contentChanges":[{"range":{"start":{"line":6,"character":0},"end":{"line":6,"character":0}},"text":"a"}]}} + start: `b`, + operations: []func(d *Document){ + func(d *Document) { + d.Apply(&lsp.Range{ + Start: lsp.Position{ + Line: 0, + Character: 0, + }, + End: lsp.Position{ + Line: 0, + Character: 0, + }, + }, "a") + }, + }, + expected: `ab`, + }, + { + name: "Insert full new line", + start: `a +c +d`, + operations: []func(d *Document){ + func(d *Document) { + d.Apply(&lsp.Range{ + Start: lsp.Position{ + Line: 1, + Character: 0, + }, + End: lsp.Position{ + Line: 1, + Character: 0, + }, + }, "b\n") + }, + }, + expected: `a +b +c +d`, + }, } for _, tt := range tests {