mirror of
https://github.com/Myzel394/config-lsp.git
synced 2025-06-19 07:25:27 +02:00
feat(server): Add concept of virtual lines
This commit is contained in:
parent
1dd6be38d8
commit
5c4f2d6aff
@ -1,16 +1,21 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type ParseFeatures struct {
|
type ParseFeatures struct {
|
||||||
ParseDoubleQuotes bool
|
ParseDoubleQuotes bool
|
||||||
ParseEscapedCharacters bool
|
ParseEscapedCharacters bool
|
||||||
|
TrimWhitespace bool
|
||||||
Replacements *map[string]string
|
Replacements *map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
var FullFeatures = ParseFeatures{
|
var FullFeatures = ParseFeatures{
|
||||||
ParseDoubleQuotes: true,
|
ParseDoubleQuotes: true,
|
||||||
ParseEscapedCharacters: true,
|
ParseEscapedCharacters: true,
|
||||||
|
TrimWhitespace: false,
|
||||||
Replacements: &map[string]string{},
|
Replacements: &map[string]string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,6 +34,10 @@ func ParseRawString(
|
|||||||
value = ParseReplacements(value, *features.Replacements)
|
value = ParseReplacements(value, *features.Replacements)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if features.TrimWhitespace {
|
||||||
|
value = TrimWhitespace(value, features.ParseDoubleQuotes)
|
||||||
|
}
|
||||||
|
|
||||||
// Parse double quotes
|
// Parse double quotes
|
||||||
if features.ParseDoubleQuotes {
|
if features.ParseDoubleQuotes {
|
||||||
value = ParseDoubleQuotes(value)
|
value = ParseDoubleQuotes(value)
|
||||||
@ -45,6 +54,49 @@ func ParseRawString(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var trimPattern = regexp.MustCompile(`\s+`)
|
||||||
|
|
||||||
|
func TrimWhitespace(
|
||||||
|
raw string,
|
||||||
|
respectDoubleQuotes bool,
|
||||||
|
) string {
|
||||||
|
if !respectDoubleQuotes {
|
||||||
|
return trimPattern.ReplaceAllString(
|
||||||
|
strings.TrimSpace(raw),
|
||||||
|
" ",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
value := raw
|
||||||
|
currentIndex := 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
nextStart, found := findNextDoubleQuote(value, currentIndex)
|
||||||
|
|
||||||
|
if found {
|
||||||
|
part := value[:nextStart]
|
||||||
|
value = strings.TrimSpace(part) + value[nextStart:]
|
||||||
|
}
|
||||||
|
|
||||||
|
nextEnd, found := findNextDoubleQuote(value, nextStart+1)
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex = nextEnd + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// last part
|
||||||
|
if currentIndex < len(value) {
|
||||||
|
part := value[currentIndex:]
|
||||||
|
|
||||||
|
value = value[:currentIndex] + strings.TrimSpace(part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
func ParseDoubleQuotes(
|
func ParseDoubleQuotes(
|
||||||
raw string,
|
raw string,
|
||||||
) string {
|
) string {
|
||||||
|
@ -54,6 +54,32 @@ func TestStringsMultipleQuotesFullFeatures(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrimWhitespaceNoQuotes(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
input := " hello world "
|
||||||
|
expected := "hello world"
|
||||||
|
|
||||||
|
actual := TrimWhitespace(input, false)
|
||||||
|
|
||||||
|
if expected != actual {
|
||||||
|
t.Errorf("Expected %v, got %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimWhitespaceQuotes(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
input := ` "hello world" `
|
||||||
|
expected := `"hello world"`
|
||||||
|
|
||||||
|
actual := TrimWhitespace(input, true)
|
||||||
|
|
||||||
|
if expected != actual {
|
||||||
|
t.Errorf("Expected %v, got %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStringsSimpleEscapedFullFeatures(
|
func TestStringsSimpleEscapedFullFeatures(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
) {
|
) {
|
||||||
|
23
server/common/strings.go
Normal file
23
server/common/strings.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
var UnicodeWhitespace = map[rune]struct{}{
|
||||||
|
'\u0020': {}, // Space
|
||||||
|
'\u0009': {}, // Horizontal tab
|
||||||
|
'\u000A': {}, // Line feed
|
||||||
|
'\u000B': {}, // Vertical tab
|
||||||
|
'\u000C': {}, // Form feed
|
||||||
|
'\u000D': {}, // Carriage return
|
||||||
|
'\u0085': {}, // Next line
|
||||||
|
'\u00A0': {}, // No-break space
|
||||||
|
'\u1680': {}, // Ogham space mark
|
||||||
|
'\u2000': {}, // En quad
|
||||||
|
'\u2001': {}, // Em quad
|
||||||
|
'\u2002': {}, // En space
|
||||||
|
'\u2003': {}, // Em space
|
||||||
|
'\u2004': {}, // Three-per-em space
|
||||||
|
'\u2005': {}, // Four-per-em space
|
||||||
|
'\u2006': {}, // Six-per-em space
|
||||||
|
'\u2007': {}, // Figure space
|
||||||
|
'\u2008': {}, // Punctuation space
|
||||||
|
'\u2009': {}, // Thin space
|
||||||
|
}
|
205
server/common/virtual-line.go
Normal file
205
server/common/virtual-line.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VirtualLine struct {
|
||||||
|
// This is the true location of the text
|
||||||
|
// This ranges from the start of the first line and character
|
||||||
|
// to the end of the last line and character
|
||||||
|
LocationRange
|
||||||
|
|
||||||
|
Parts []VirtualLinePart
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l VirtualLine) GetText() string {
|
||||||
|
text := ""
|
||||||
|
|
||||||
|
for _, part := range l.Parts {
|
||||||
|
text += part.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSubset Get a subset of the virtual line starting from `start` and ending at `end`
|
||||||
|
func (l VirtualLine) GetSubset(start uint32, end uint32) VirtualLine {
|
||||||
|
parts := make([]VirtualLinePart, 0, 5)
|
||||||
|
currentIndex := uint32(0)
|
||||||
|
|
||||||
|
for _, part := range l.Parts {
|
||||||
|
partStart := currentIndex
|
||||||
|
partEnd := currentIndex + uint32(len(part.Text))
|
||||||
|
|
||||||
|
if partEnd < start {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if start <= partEnd {
|
||||||
|
var rangeStart uint32
|
||||||
|
var rangeEnd uint32
|
||||||
|
|
||||||
|
if start >= partStart {
|
||||||
|
rangeStart = start - partStart
|
||||||
|
} else {
|
||||||
|
rangeStart = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if end <= partEnd {
|
||||||
|
rangeEnd = end - partStart
|
||||||
|
} else {
|
||||||
|
rangeEnd = partEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = append(parts, VirtualLinePart{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: Location{
|
||||||
|
Line: part.Start.Line,
|
||||||
|
Character: part.Start.Character + rangeStart,
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: part.Start.Line,
|
||||||
|
Character: part.Start.Character + rangeEnd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Text: part.Text[rangeStart:rangeEnd],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex = partEnd
|
||||||
|
|
||||||
|
if currentIndex >= end {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return VirtualLine{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: parts[0].Start,
|
||||||
|
End: parts[len(parts)-1].End,
|
||||||
|
},
|
||||||
|
Parts: parts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertRangeToTextRange Convert a given start and end range to a text range
|
||||||
|
// start and end are the start and end ranges of the virtual line
|
||||||
|
// This will return the start and end ranges of the actual text lines so that they
|
||||||
|
// match to the text of the virtual line
|
||||||
|
// The `start` and `end` are expected to be within the range of the virtual line
|
||||||
|
func (l VirtualLine) ConvertRangeToTextRange(start uint32, end uint32) []LocationRange {
|
||||||
|
virtualLine := l.GetSubset(start, end)
|
||||||
|
|
||||||
|
ranges := make([]LocationRange, 0, 5)
|
||||||
|
|
||||||
|
for _, part := range virtualLine.Parts {
|
||||||
|
ranges = append(ranges, part.LocationRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l VirtualLine) AsTrimmed() VirtualLine {
|
||||||
|
if len(l.Parts) == 0 {
|
||||||
|
// There's nothing that could be trimmed, so we can also just
|
||||||
|
// return the original line, as it's identical
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := make([]VirtualLinePart, len(l.Parts))
|
||||||
|
|
||||||
|
for index, part := range l.Parts {
|
||||||
|
parts[index] = part.AsTrimmed()
|
||||||
|
}
|
||||||
|
|
||||||
|
return VirtualLine{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: parts[0].Start,
|
||||||
|
End: parts[len(parts)-1].End,
|
||||||
|
},
|
||||||
|
Parts: parts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualLinePart struct {
|
||||||
|
// This is the true location of the text
|
||||||
|
LocationRange
|
||||||
|
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p VirtualLinePart) AsTrimmed() VirtualLinePart {
|
||||||
|
firstNonWhitespace := utils.FindFirstNonMatch(p.Text, UnicodeWhitespace, 0)
|
||||||
|
|
||||||
|
if firstNonWhitespace == -1 {
|
||||||
|
// Empty line
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
lastNonWhitespace := utils.FindLastNonMatch(p.Text, UnicodeWhitespace, len(p.Text)-1)
|
||||||
|
|
||||||
|
if lastNonWhitespace == -1 {
|
||||||
|
lastNonWhitespace = len(p.Text) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return VirtualLinePart{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: Location{
|
||||||
|
Line: p.Start.Line,
|
||||||
|
Character: p.Start.Character + uint32(firstNonWhitespace),
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: p.Start.Line,
|
||||||
|
Character: p.Start.Character + uint32(lastNonWhitespace) + 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Text: p.Text[firstNonWhitespace : lastNonWhitespace+1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitIntoVirtualLines(input string) []VirtualLine {
|
||||||
|
stringLines := utils.SplitIntoVirtualLines(input)
|
||||||
|
|
||||||
|
lines := make([]VirtualLine, 0, len(stringLines))
|
||||||
|
|
||||||
|
for rawLineNumber, line := range stringLines {
|
||||||
|
parts := make([]VirtualLinePart, 0)
|
||||||
|
|
||||||
|
for virtualLineNumber, part := range line {
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lineNumber := uint32(rawLineNumber) + uint32(virtualLineNumber)
|
||||||
|
|
||||||
|
parts = append(parts, VirtualLinePart{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: uint32(len(part)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Text: part,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, VirtualLine{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: parts[0].Start,
|
||||||
|
End: parts[len(parts)-1].End,
|
||||||
|
},
|
||||||
|
Parts: parts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
182
server/common/virtual-line_test.go
Normal file
182
server/common/virtual-line_test.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/utils"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSplitIntoVirtualLinesSimpleExample(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
input := utils.Dedent(`
|
||||||
|
Hello
|
||||||
|
World \
|
||||||
|
how are you
|
||||||
|
`)
|
||||||
|
expected := []VirtualLine{
|
||||||
|
{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: Location{
|
||||||
|
Line: 0,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: 0,
|
||||||
|
Character: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Parts: []VirtualLinePart{
|
||||||
|
{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: Location{
|
||||||
|
Line: 0,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: 0,
|
||||||
|
Character: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Text: "Hello",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: Location{
|
||||||
|
Line: 1,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: 2,
|
||||||
|
Character: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Parts: []VirtualLinePart{
|
||||||
|
{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: Location{
|
||||||
|
Line: 1,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: 1,
|
||||||
|
Character: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Text: "World ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: Location{
|
||||||
|
Line: 2,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: 2,
|
||||||
|
Character: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Text: "how are you",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := SplitIntoVirtualLines(input)
|
||||||
|
|
||||||
|
if !cmp.Equal(expected, lines) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedText := "World how are you"
|
||||||
|
actualText := lines[1].GetText()
|
||||||
|
|
||||||
|
if expectedText != actualText {
|
||||||
|
t.Fatalf("Expected %v, got %v", expectedText, actualText)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedText = "rld how are"
|
||||||
|
actualText = lines[1].GetText()[2:13]
|
||||||
|
|
||||||
|
if expectedText != actualText {
|
||||||
|
t.Fatalf("Expected %v, got %v", expectedText, actualText)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedRanges := []LocationRange{
|
||||||
|
{
|
||||||
|
Start: Location{
|
||||||
|
Line: 1,
|
||||||
|
Character: 2,
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: 1,
|
||||||
|
Character: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Start: Location{
|
||||||
|
Line: 2,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: 2,
|
||||||
|
Character: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
actualRanges := lines[1].ConvertRangeToTextRange(2, 13)
|
||||||
|
|
||||||
|
if !cmp.Equal(expectedRanges, actualRanges) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expectedRanges, actualRanges)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitIntoVirtualLinesIndentedExample(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
// 4 spaces
|
||||||
|
input := utils.Dedent(`
|
||||||
|
Hello
|
||||||
|
`)
|
||||||
|
expected := []VirtualLine{
|
||||||
|
{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: Location{
|
||||||
|
Line: 0,
|
||||||
|
Character: 4,
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: 0,
|
||||||
|
Character: 9,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Parts: []VirtualLinePart{
|
||||||
|
{
|
||||||
|
LocationRange: LocationRange{
|
||||||
|
Start: Location{
|
||||||
|
Line: 0,
|
||||||
|
Character: 4,
|
||||||
|
},
|
||||||
|
End: Location{
|
||||||
|
Line: 0,
|
||||||
|
Character: 9,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Text: "Hello",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := SplitIntoVirtualLines(input)
|
||||||
|
|
||||||
|
for index, line := range actual {
|
||||||
|
actual[index] = line.AsTrimmed()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmp.Equal(expected, actual) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
@ -3,15 +3,16 @@ module config-lsp
|
|||||||
go 1.22.5
|
go 1.22.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||||
|
github.com/emirpasic/gods v1.18.1
|
||||||
github.com/tliron/commonlog v0.2.17
|
github.com/tliron/commonlog v0.2.17
|
||||||
github.com/tliron/glsp v0.2.2
|
github.com/tliron/glsp v0.2.2
|
||||||
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||||
|
github.com/google/go-cmp v0.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/iancoleman/strcase v0.3.0 // indirect
|
github.com/iancoleman/strcase v0.3.0 // indirect
|
||||||
github.com/k0kubun/pp v3.0.1+incompatible // indirect
|
github.com/k0kubun/pp v3.0.1+incompatible // indirect
|
||||||
@ -28,7 +29,6 @@ require (
|
|||||||
github.com/sourcegraph/jsonrpc2 v0.2.0 // indirect
|
github.com/sourcegraph/jsonrpc2 v0.2.0 // indirect
|
||||||
github.com/tliron/kutil v0.3.24 // indirect
|
github.com/tliron/kutil v0.3.24 // indirect
|
||||||
golang.org/x/crypto v0.25.0 // indirect
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
golang.org/x/term v0.22.0 // indirect
|
golang.org/x/term v0.22.0 // indirect
|
||||||
)
|
)
|
||||||
|
@ -4,6 +4,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
|
|||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
|
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||||
|
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
@ -76,3 +76,67 @@ func AllIndexes(s string, sub string) []int {
|
|||||||
|
|
||||||
return indexes
|
return indexes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FindFirstNonMatch(s string, substr map[rune]struct{}, startIndex int) int {
|
||||||
|
for index := startIndex; index < len(s); index++ {
|
||||||
|
if _, found := substr[rune(s[index])]; !found {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindLastNonMatch(s string, substr map[rune]struct{}, startIndex int) int {
|
||||||
|
for index := startIndex; index >= 0; index-- {
|
||||||
|
if _, found := substr[rune(s[index])]; !found {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineContinuationPattern = regexp.MustCompile(`\\\s*$`)
|
||||||
|
|
||||||
|
// SplitSmartlyIntoLines Split a string into lines while respecting "\" as a line continuation character
|
||||||
|
// This function is useful for parsing configuration files
|
||||||
|
// You will need to handle whitespace trimming yourself
|
||||||
|
// For example, the following input:
|
||||||
|
// ```
|
||||||
|
// key1 = value1
|
||||||
|
//
|
||||||
|
// key2 = value2 \
|
||||||
|
// value3
|
||||||
|
//
|
||||||
|
// key3 = value4
|
||||||
|
// ```
|
||||||
|
// Will be split into:
|
||||||
|
// ```go
|
||||||
|
//
|
||||||
|
// [][]string{
|
||||||
|
// []string{"key1 = value1"},
|
||||||
|
// []string{"key2 = value2 ", " value3"},
|
||||||
|
// []string{"key3 = value4"},
|
||||||
|
// }
|
||||||
|
func SplitIntoVirtualLines(input string) [][]string {
|
||||||
|
lines := make([][]string, 0, len(input))
|
||||||
|
currentLine := make([]string, 0, 1)
|
||||||
|
|
||||||
|
for _, line := range SplitIntoLines(input) {
|
||||||
|
if lineContinuationPattern.MatchString(line) {
|
||||||
|
currentLine = append(currentLine, line[:len(line)-1])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLine = append(currentLine, line)
|
||||||
|
lines = append(lines, currentLine)
|
||||||
|
currentLine = make([]string, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(currentLine) > 0 {
|
||||||
|
lines = append(lines, currentLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
119
server/utils/strings_test.go
Normal file
119
server/utils/strings_test.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSplitIntoVirtualLinesSimpleExample(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
input := Dedent(`
|
||||||
|
Hello
|
||||||
|
World\
|
||||||
|
how are you
|
||||||
|
`)
|
||||||
|
expected := [][]string{
|
||||||
|
{"Hello"},
|
||||||
|
{"World", "how are you"},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := SplitIntoVirtualLines(input)
|
||||||
|
|
||||||
|
if cmp.Equal(expected, actual) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitIntoVirtualLinesEmptyString(t *testing.T) {
|
||||||
|
input := ""
|
||||||
|
expected := [][]string{
|
||||||
|
{""},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := SplitIntoVirtualLines(input)
|
||||||
|
|
||||||
|
if !cmp.Equal(expected, actual) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitIntoVirtualLinesSingleLine(t *testing.T) {
|
||||||
|
input := Dedent(`
|
||||||
|
Hello`)
|
||||||
|
expected := [][]string{
|
||||||
|
{" Hello"},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := SplitIntoVirtualLines(input)
|
||||||
|
|
||||||
|
if !cmp.Equal(expected, actual) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitIntoVirtualLinesMultipleLinesWithoutContinuation(t *testing.T) {
|
||||||
|
input := Dedent(`
|
||||||
|
Hello
|
||||||
|
World
|
||||||
|
How are you`)
|
||||||
|
expected := [][]string{
|
||||||
|
{" Hello"},
|
||||||
|
{" World"},
|
||||||
|
{" How are you"},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := SplitIntoVirtualLines(input)
|
||||||
|
|
||||||
|
if !cmp.Equal(expected, actual) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitIntoVirtualLinesMultipleLinesWithContinuation(t *testing.T) {
|
||||||
|
input := Dedent(`
|
||||||
|
Hello \
|
||||||
|
World \
|
||||||
|
How are you`)
|
||||||
|
expected := [][]string{
|
||||||
|
{" Hello ", "World ", "How are you"},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := SplitIntoVirtualLines(input)
|
||||||
|
|
||||||
|
if !cmp.Equal(expected, actual) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitIntoVirtualLinesMixedContinuation(t *testing.T) {
|
||||||
|
input := Dedent(`
|
||||||
|
Hello
|
||||||
|
World\
|
||||||
|
How are you`)
|
||||||
|
expected := [][]string{
|
||||||
|
{"Hello"},
|
||||||
|
{"World", " How are you"},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := SplitIntoVirtualLines(input)
|
||||||
|
|
||||||
|
if !cmp.Equal(expected, actual) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitIntoVirtualLinesTrailingContinuation(t *testing.T) {
|
||||||
|
input := Dedent(`
|
||||||
|
Hello\
|
||||||
|
`)
|
||||||
|
expected := [][]string{
|
||||||
|
{"Hello", " "},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := SplitIntoVirtualLines(input)
|
||||||
|
|
||||||
|
if !cmp.Equal(expected, actual) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user