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 {
|
type Position struct {
|
||||||
Line int
|
Line int
|
||||||
Column 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{}
|
var _ fmt.Stringer = Position{}
|
||||||
|
|
@ -276,7 +277,13 @@ func (p Position) SubtractString(s string, byUTF16 bool) Position {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Position) Before(p2 Position) bool {
|
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.
|
// 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
|
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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"oss.terrastruct.com/d2/d2ast"
|
||||||
"oss.terrastruct.com/d2/d2lsp"
|
"oss.terrastruct.com/d2/d2lsp"
|
||||||
"oss.terrastruct.com/util-go/assert"
|
"oss.terrastruct.com/util-go/assert"
|
||||||
)
|
)
|
||||||
|
|
@ -143,3 +144,179 @@ layers: {
|
||||||
_, _, err = d2lsp.GetRefRanges("index.d2", fs, []string{"y"}, "hello")
|
_, _, err = d2lsp.GetRefRanges("index.d2", fs, []string{"y"}, "hello")
|
||||||
assert.Equal(t, `board "[y]" not found`, err.Error())
|
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