d2lsp: get board at position

This commit is contained in:
Alexander Wang 2024-11-12 22:38:21 -07:00
parent b2ce591947
commit 62a26cc451
No known key found for this signature in database
GPG key ID: BE3937D0D52D8927
3 changed files with 247 additions and 2 deletions

View file

@ -166,7 +166,8 @@ func (r Range) Before(r2 Range) bool {
type Position struct {
Line int
Column int
Byte int
// -1 is used as sentinel that a constructed position is missing byte offset (for LSP usage)
Byte int
}
var _ fmt.Stringer = Position{}
@ -276,7 +277,13 @@ func (p Position) SubtractString(s string, byUTF16 bool) Position {
}
func (p Position) Before(p2 Position) bool {
return p.Byte < p2.Byte
if p.Byte != p2.Byte && p.Byte != -1 && p2.Byte != -1 {
return p.Byte < p2.Byte
}
if p.Line != p2.Line {
return p.Line < p2.Line
}
return p.Column < p2.Column
}
// MapNode is implemented by nodes that may be children of Maps.

View file

@ -92,3 +92,64 @@ func getBoardMap(path string, fs map[string]string, boardPath []string) (*d2ir.M
}
return m, nil
}
func GetBoardAtPosition(path string, fs map[string]string, pos d2ast.Position) ([]string, error) {
if _, ok := fs[path]; !ok {
return nil, fmt.Errorf(`"%s" not found`, path)
}
r := strings.NewReader(fs[path])
ast, err := d2parser.Parse(path, r, nil)
if err != nil {
return nil, err
}
pos.Byte = -1
return getBoardPathAtPosition(*ast, nil, pos), nil
}
func getBoardPathAtPosition(m d2ast.Map, currPath []string, pos d2ast.Position) []string {
inRange := func(r d2ast.Range) bool {
return !pos.Before(r.Start) && pos.Before(r.End)
}
if !inRange(m.Range) {
return nil
}
for _, n := range m.Nodes {
if n.MapKey == nil {
continue
}
mk := n.MapKey
if mk.Key == nil || len(mk.Key.Path) == 0 {
continue
}
if mk.Value.Map == nil {
continue
}
keyName := mk.Key.Path[0].Unbox().ScalarString()
if len(currPath)%2 == 0 {
isBoardType := keyName == "layers" || keyName == "scenarios" || keyName == "steps"
if !isBoardType {
continue
}
}
if inRange(mk.Value.Map.Range) {
newPath := append(currPath, keyName)
// Check deeper
if deeperPath := getBoardPathAtPosition(*mk.Value.Map, newPath, pos); deeperPath != nil {
return deeperPath
}
// Nothing deeper matched but we're in this map's range, return current path
return newPath
}
}
return nil
}

View file

@ -3,6 +3,7 @@ package d2lsp_test
import (
"testing"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2lsp"
"oss.terrastruct.com/util-go/assert"
)
@ -143,3 +144,179 @@ layers: {
_, _, err = d2lsp.GetRefRanges("index.d2", fs, []string{"y"}, "hello")
assert.Equal(t, `board "[y]" not found`, err.Error())
}
func TestGetBoardAtPosition(t *testing.T) {
tests := []struct {
name string
fs map[string]string
path string
position d2ast.Position
want []string
}{
{
name: "cursor in layer",
fs: map[string]string{
"index.d2": `x
layers: {
basic: {
x -> y
}
}`,
},
path: "index.d2",
position: d2ast.Position{Line: 3, Column: 4},
want: []string{"layers", "basic"},
},
{
name: "cursor in nested layer",
fs: map[string]string{
"index.d2": `
layers: {
outer: {
layers: {
inner: {
x -> y
}
}
}
}`,
},
path: "index.d2",
position: d2ast.Position{Line: 5, Column: 4},
want: []string{"layers", "outer", "layers", "inner"},
},
{
name: "cursor in second sibling nested layer",
fs: map[string]string{
"index.d2": `
layers: {
outer: {
layers: {
first: {
a -> b
}
second: {
x -> y
}
}
}
}`,
},
path: "index.d2",
position: d2ast.Position{Line: 8, Column: 4},
want: []string{"layers", "outer", "layers", "second"},
},
{
name: "cursor in root container",
fs: map[string]string{
"index.d2": `
wumbo: {
car
}`,
},
path: "index.d2",
position: d2ast.Position{Line: 2, Column: 4},
want: nil,
},
{
name: "cursor in layer container",
fs: map[string]string{
"index.d2": `
layers: {
x: {
wumbo: {
car
}
}
}`,
},
path: "index.d2",
position: d2ast.Position{Line: 4, Column: 4},
want: []string{"layers", "x"},
},
{
name: "cursor in scenario",
fs: map[string]string{
"index.d2": `
scenarios: {
happy: {
x -> y
}
}`,
},
path: "index.d2",
position: d2ast.Position{Line: 3, Column: 4},
want: []string{"scenarios", "happy"},
},
{
name: "cursor in step",
fs: map[string]string{
"index.d2": `
steps: {
first: {
x -> y
}
}`,
},
path: "index.d2",
position: d2ast.Position{Line: 3, Column: 4},
want: []string{"steps", "first"},
},
{
name: "cursor outside any board",
fs: map[string]string{
"index.d2": `
x -> y
layers: {
basic: {
a -> b
}
}`,
},
path: "index.d2",
position: d2ast.Position{Line: 1, Column: 1},
want: nil,
},
{
name: "cursor in empty board",
fs: map[string]string{
"index.d2": `
layers: {
basic: {
}
}`,
},
path: "index.d2",
position: d2ast.Position{Line: 3, Column: 2},
want: []string{"layers", "basic"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := d2lsp.GetBoardAtPosition(tt.path, tt.fs, tt.position)
assert.Success(t, err)
if tt.want == nil {
assert.Equal(t, true, got == nil)
} else {
assert.Equal(t, len(tt.want), len(got))
assert.Equal(t, tt.want[0], got[0]) // board type
assert.Equal(t, tt.want[1], got[1]) // board id
}
})
}
// Error cases
t.Run("invalid file", func(t *testing.T) {
fs := map[string]string{
"index.d2": "x ->",
}
_, err := d2lsp.GetBoardAtPosition("index.d2", fs, d2ast.Position{Line: 0, Column: 0})
assert.Error(t, err)
})
t.Run("file not found", func(t *testing.T) {
_, err := d2lsp.GetBoardAtPosition("notfound.d2", nil, d2ast.Position{Line: 0, Column: 0})
assert.Error(t, err)
})
}