Skip to content

Commit

Permalink
Merge pull request #2209 from alixander/lsp-board-pos
Browse files Browse the repository at this point in the history
d2lsp: get board at position
  • Loading branch information
alixander authored Nov 13, 2024
2 parents b2ce591 + 62a26cc commit 269f4f5
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 2 deletions.
11 changes: 9 additions & 2 deletions d2ast/d2ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down Expand Up @@ -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.
Expand Down
61 changes: 61 additions & 0 deletions d2lsp/d2lsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
177 changes: 177 additions & 0 deletions d2lsp/d2lsp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
})
}

0 comments on commit 269f4f5

Please sign in to comment.