d2lsp: get board at position
This commit is contained in:
parent
b2ce591947
commit
62a26cc451
3 changed files with 247 additions and 2 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue