@@ -16,12 +16,12 @@ package buflsp_test
1616
1717import (
1818 "context"
19- "fmt"
2019 "os"
2120 "path/filepath"
2221 "strings"
2322 "testing"
2423
24+ "github.com/bufbuild/buf/private/buf/buflsp"
2525 "github.com/bufbuild/buf/private/buf/bufformat"
2626 "github.com/bufbuild/buf/private/pkg/storage"
2727 "github.com/bufbuild/buf/private/pkg/storage/storageos"
@@ -156,7 +156,7 @@ func TestFormatting(t *testing.T) {
156156 }
157157
158158 originalContent := mustReadFile (t , testProtoPath )
159- result := applyTextEdits (originalContent , textEdits )
159+ result := buflsp . ApplyTextEdits (originalContent , textEdits )
160160 expectedFormatted := getExpectedFormattedContent (t , ctx , testProtoPath )
161161
162162 assert .Equal (t , expectedFormatted , result )
@@ -179,7 +179,7 @@ func TestFormattingIdempotency(t *testing.T) {
179179 // First format: get the formatted content
180180 firstEdits := callFormatting (t , ctx , testProtoPath )
181181 originalContent := mustReadFile (t , testProtoPath )
182- formattedContent := applyTextEdits (originalContent , firstEdits )
182+ formattedContent := buflsp . ApplyTextEdits (originalContent , firstEdits )
183183
184184 // Write formatted content to a temp directory with necessary dependencies
185185 tmpDir := setupTempFormatDir (t , testProtoPath , protoFile , formattedContent )
@@ -244,7 +244,7 @@ func assertEditsAreMinimal(t *testing.T, original, expected string, edits []prot
244244 totalEditSize , fullReplaceSize )
245245
246246 // Verify edits actually transform original to expected
247- result := applyTextEdits (original , edits )
247+ result := buflsp . ApplyTextEdits (original , edits )
248248 assert .Equal (t , expected , result , "applying edits should produce expected formatted text" )
249249
250250 // Verify no edit is entirely unchanged
@@ -257,11 +257,11 @@ func assertEditsDoNotOverlap(t *testing.T, edits []protocol.TextEdit) {
257257
258258 for i := 0 ; i < len (edits ); i ++ {
259259 for j := i + 1 ; j < len (edits ); j ++ {
260- assert .Falsef (t , rangesOverlapFormat (edits [i ].Range , edits [j ].Range ),
260+ assert .Falsef (t , buflsp . RangesOverlap (edits [i ].Range , edits [j ].Range ),
261261 "edits %d and %d overlap: %s and %s" ,
262262 i , j ,
263- formatRange (edits [i ].Range ),
264- formatRange (edits [j ].Range ))
263+ buflsp . FormatRange (edits [i ].Range ),
264+ buflsp . FormatRange (edits [j ].Range ))
265265 }
266266 }
267267}
@@ -288,112 +288,10 @@ func assertNoRedundantEdits(t *testing.T, original string, edits []protocol.Text
288288
289289 assert .NotEqualf (t , edit .NewText , replacedText ,
290290 "edit %d replaces text with identical text (not minimal): range %s" ,
291- i , formatRange (edit .Range ))
291+ i , buflsp . FormatRange (edit .Range ))
292292 }
293293}
294294
295- func applyTextEdits (text string , edits []protocol.TextEdit ) string {
296- lines := strings .Split (text , "\n " )
297- // Apply edits in reverse order to maintain line/character positions
298- for i := len (edits ) - 1 ; i >= 0 ; i -- {
299- edit := edits [i ]
300- startLine := int (edit .Range .Start .Line )
301- startChar := int (edit .Range .Start .Character )
302- endLine := int (edit .Range .End .Line )
303- endChar := int (edit .Range .End .Character )
304-
305- if startLine < 0 || startLine >= len (lines ) {
306- continue
307- }
308- if endLine < 0 || endLine > len (lines ) {
309- endLine = len (lines )
310- }
311-
312- // LSP TextEdit semantics: Range.End is exclusive at character level
313- // Get the prefix (part of start line before the edit)
314- prefix := ""
315- if startLine < len (lines ) && startChar <= len (lines [startLine ]) {
316- prefix = lines [startLine ][:startChar ]
317- }
318-
319- // Get the suffix (part of end line after the edit)
320- // If endChar is 0, we're at the start of endLine, so no suffix from that line
321- suffix := ""
322- if endChar > 0 && endLine < len (lines ) {
323- if endChar <= len (lines [endLine ]) {
324- suffix = lines [endLine ][endChar :]
325- }
326- } else if startLine == endLine && endChar == 0 {
327- // Pure insertion at the beginning of a line - need the whole line as suffix
328- if endLine < len (lines ) {
329- suffix = lines [endLine ]
330- }
331- }
332-
333- // Determine which lines to delete
334- // For a pure insertion (start == end), the line gets incorporated into replacement via suffix
335- // So we need to delete it from the original lines array
336- // Otherwise, we delete from startLine to endLine (inclusive or exclusive depending on endChar)
337- deleteEndLine := endLine
338- if startLine == endLine && startChar == endChar {
339- // Pure insertion - the line is preserved in suffix, so delete it from lines array
340- deleteEndLine = startLine
341- } else if endChar == 0 && endLine > startLine {
342- // Range ends at the beginning of endLine, so don't include endLine in deletion
343- deleteEndLine = endLine - 1
344- }
345-
346- // Split the new text into lines
347- newText := edit .NewText
348- var newLines []string
349- endsWithNewline := false
350- if newText != "" {
351- newLines = strings .Split (newText , "\n " )
352- // If newText ends with \n, split gives us a trailing empty string.
353- // This indicates that subsequent content should be on a new line.
354- endsWithNewline = strings .HasSuffix (newText , "\n " )
355- if endsWithNewline && len (newLines ) > 0 && newLines [len (newLines )- 1 ] == "" {
356- newLines = newLines [:len (newLines )- 1 ]
357- }
358- }
359-
360- // Build the replacement
361- var replacement []string
362- if len (newLines ) == 0 {
363- // No new content
364- combined := prefix + suffix
365- // Only create a line if there's actually content or we're doing a mid-line edit
366- if combined != "" || (startChar > 0 || endChar > 0 ) {
367- replacement = []string {combined }
368- } else {
369- // Deleting full lines with no replacement
370- replacement = []string {}
371- }
372- } else {
373- // Add prefix to first line
374- replacement = make ([]string , len (newLines ))
375- copy (replacement , newLines )
376- replacement [0 ] = prefix + replacement [0 ]
377- // Add suffix: if newText ended with \n, suffix goes on a new line
378- // Otherwise, append it to the last line
379- if suffix != "" {
380- if endsWithNewline {
381- replacement = append (replacement , suffix )
382- } else {
383- replacement [len (replacement )- 1 ] = replacement [len (replacement )- 1 ] + suffix
384- }
385- } else if startLine == endLine && startChar == endChar && endsWithNewline {
386- // Pure insertion at start of line where newText ends with newline
387- // Even if the original line is blank (suffix == ""), we should preserve it
388- replacement = append (replacement , "" )
389- }
390- }
391-
392- // Replace lines[startLine:deleteEndLine+1] with replacement
393- lines = append (lines [:startLine ], append (replacement , lines [deleteEndLine + 1 :]... )... )
394- }
395- return strings .Join (lines , "\n " )
396- }
397295
398296func TestApplyTextEdits (t * testing.T ) {
399297 t .Parallel ()
@@ -460,7 +358,7 @@ func TestApplyTextEdits(t *testing.T) {
460358
461359 for _ , tt := range tests {
462360 t .Run (tt .name , func (t * testing.T ) {
463- result := applyTextEdits (tt .input , tt .edits )
361+ result := buflsp . ApplyTextEdits (tt .input , tt .edits )
464362 assert .Equal (t , tt .expected , result )
465363 })
466364 }
@@ -475,7 +373,7 @@ func TestApplyTextEditsDebug(t *testing.T) {
475373
476374 edits := callFormatting (t , ctx , testProtoPath )
477375 originalContent := mustReadFile (t , testProtoPath )
478- result := applyTextEdits (originalContent , edits )
376+ result := buflsp . ApplyTextEdits (originalContent , edits )
479377 expected := getExpectedFormattedContent (t , ctx , testProtoPath )
480378
481379 assert .Equal (t , expected , result , "Result doesn't match expected" )
@@ -554,24 +452,3 @@ func setupTempFormatDir(t *testing.T, originalPath, protoFile, formattedContent
554452 return tmpDir
555453}
556454
557- // rangesOverlap returns true if two LSP ranges overlap.
558- // Two ranges overlap if one starts before the other ends.
559- func rangesOverlapFormat (r1 , r2 protocol.Range ) bool {
560- // Compare positions: r1.Start < r2.End && r2.Start < r1.End
561- return positionLess (r1 .Start , r2 .End ) && positionLess (r2 .Start , r1 .End )
562- }
563-
564- // positionLess returns true if p1 is before p2 in the document.
565- func positionLess (p1 , p2 protocol.Position ) bool {
566- if p1 .Line != p2 .Line {
567- return p1 .Line < p2 .Line
568- }
569- return p1 .Character < p2 .Character
570- }
571-
572- // formatRange returns a human-readable string representation of an LSP range.
573- func formatRange (r protocol.Range ) string {
574- return fmt .Sprintf ("[%d:%d-%d:%d]" ,
575- r .Start .Line , r .Start .Character ,
576- r .End .Line , r .End .Character )
577- }
0 commit comments