Skip to content

Commit

Permalink
Code action for OPA fmt (StyraInc#630)
Browse files Browse the repository at this point in the history
* WIP code action

Signed-off-by: Anders Eknert <anders@styra.com>

* Support sending of workspace/applyEdits

Signed-off-by: Charlie Egan <charlie@styra.com>

* Fix linter errors

Signed-off-by: Charlie Egan <charlie@styra.com>

* Remove logging of unknown messages

This is not really the best place to do this, so I'll do it properly in
another PR.

Signed-off-by: Charlie Egan <charlie@styra.com>

* Add OptionalVersionedTextDocumentIdentifier

This is now used where it's used in the spec.

Signed-off-by: Charlie Egan <charlie@styra.com>

* Add license for ComputeEdits

Signed-off-by: Charlie Egan <charlie@styra.com>

---------

Signed-off-by: Anders Eknert <anders@styra.com>
Signed-off-by: Charlie Egan <charlie@styra.com>
Co-authored-by: Anders Eknert <anders@styra.com>
  • Loading branch information
2 people authored and srenatus committed Oct 1, 2024
1 parent df14e35 commit 8d876bb
Show file tree
Hide file tree
Showing 7 changed files with 529 additions and 6 deletions.
4 changes: 4 additions & 0 deletions .regal/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ignore:
files:
- e2e/*
- pkg/*
1 change: 1 addition & 0 deletions cmd/languageserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func init() {
ls.SetConn(conn)
go ls.StartDiagnosticsWorker(ctx)
go ls.StartHoverWorker(ctx)
go ls.StartCommandWorker(ctx)

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
Expand Down
19 changes: 19 additions & 0 deletions internal/lsp/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package lsp

func toAnySlice(a []string) []any {
b := make([]any, len(a))
for i := range a {
b[i] = a[i]
}

return b
}

func FmtCommand(args []string) Command {
return Command{
Title: "Format using opa-fmt",
Command: "regal.fmt",
Tooltip: "Format using opa-fmt",
Arguments: toAnySlice(args),
}
}
216 changes: 216 additions & 0 deletions internal/lsp/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.package langserver
// Source:
// https://github.com/golang/tools/blob/78b158585360beccadc3faac6e35759f491831f3/internal/lsp/diff/myers/diff.go

package lsp

import (
"strings"
)

// OpKind is used to denote the type of operation a line represents.
type OpKind int

const (
// Delete is the operation kind for a line that is present in the input
// but not in the output.
Delete OpKind = iota
// Insert is the operation kind for a line that is new in the output.
Insert
// Equal is the operation kind for a line that is the same in the input and
// output, often used to provide context around edited lines.
Equal
)

// Sources:
// https://blog.jcoglan.com/2017/02/17/the-myers-diff-algorithm-part-3/
// https://www.codeproject.com/Articles/42279/%2FArticles%2F42279%2FInvestigating-Myers-diff-algorithm-Part-1-of-2

type operation struct {
Kind OpKind
Content []string // content from b
I1, I2 uint // indices of the line in a
J1 uint // indices of the line in b, J2 implied by len(Content)
}

// operations returns the list of operations to convert a into b, consolidating
// operations for multiple lines and not including equal lines.
func operations(a, b []string) []*operation {
if len(a) == 0 && len(b) == 0 {
return nil
}

trace, offset := shortestEditSequence(a, b)
snakes := backtrack(trace, len(a), len(b), offset)

M, N := len(a), len(b)

var i int

solution := make([]*operation, len(a)+len(b))

add := func(op *operation, i2, j2 int) {
if op == nil {
return
}

op.I2 = uint(i2)
if op.Kind == Insert {
op.Content = b[op.J1:j2]
}

solution[i] = op
i++
}

x, y := 0, 0

for _, snake := range snakes {
if len(snake) < 2 {
continue
}

var op *operation
// delete (horizontal)
for snake[0]-snake[1] > x-y {
if op == nil {
op = &operation{
Kind: Delete,
I1: uint(x),
J1: uint(y),
}
}

x++
if x == M {
break
}
}
add(op, x, y)
op = nil
// insert (vertical)
for snake[0]-snake[1] < x-y {
if op == nil {
op = &operation{
Kind: Insert,
I1: uint(x),
J1: uint(y),
}
}

y++
}
add(op, x, y)
op = nil
// equal (diagonal)
for x < snake[0] {
x++
y++
}

if x >= M && y >= N {
break
}
}

return solution[:i]
}

// backtrack uses the trace for the edit sequence computation and returns the
// "snakes" that make up the solution. A "snake" is a single deletion or
// insertion followed by zero or diagonals.
func backtrack(trace [][]int, x, y, offset int) [][]int {
snakes := make([][]int, len(trace))
d := len(trace) - 1

for ; x > 0 && y > 0 && d > 0; d-- {
V := trace[d]
if len(V) == 0 {
continue
}

snakes[d] = []int{x, y}

k := x - y

var kPrev int
if k == -d || (k != d && V[k-1+offset] < V[k+1+offset]) {
kPrev = k + 1
} else {
kPrev = k - 1
}

x = V[kPrev+offset]
y = x - kPrev
}

if x < 0 || y < 0 {
return snakes
}

snakes[d] = []int{x, y}

return snakes
}

// shortestEditSequence returns the shortest edit sequence that converts a into b.
func shortestEditSequence(a, b []string) ([][]int, int) {
M, N := len(a), len(b)
V := make([]int, 2*(N+M)+1)
offset := N + M
trace := make([][]int, N+M+1)

// Iterate through the maximum possible length of the SES (N+M).
for d := 0; d <= N+M; d++ {
copyV := make([]int, len(V))
// k lines are represented by the equation y = x - k. We move in
// increments of 2 because end points for even d are on even k lines.
for k := -d; k <= d; k += 2 {
// At each point, we either go down or to the right. We go down if
// k == -d, and we go to the right if k == d. We also prioritize
// the maximum x value, because we prefer deletions to insertions.
var x int
if k == -d || (k != d && V[k-1+offset] < V[k+1+offset]) {
x = V[k+1+offset] // down
} else {
x = V[k-1+offset] + 1 // right
}

y := x - k

// Diagonal moves while we have equal contents.
for x < M && y < N && a[x] == b[y] {
x++
y++
}

V[k+offset] = x

// Return if we've exceeded the maximum values.
if x == M && y == N {
// Makes sure to save the state of the array before returning.
copy(copyV, V)
trace[d] = copyV

return trace, offset
}
}

// Save the state of the array.
copy(copyV, V)
trace[d] = copyV
}

return nil, 0
}

func splitLines(text string) []string {
lines := strings.SplitAfter(text, "\n")
if lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}

return lines
}
73 changes: 73 additions & 0 deletions internal/lsp/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// ComputeEdits is copied from https://github.com/kitagry/regols, the source repo's license is MIT and is copied below:
//
// MIT License
//
// # Copyright (c) 2023 Ryo Kitagawa
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

package lsp

import (
"fmt"
"path/filepath"
"strings"

"github.com/open-policy-agent/opa/format"
)

func Format(path, contents string, opts format.Opts) (string, error) {
formatted, err := format.SourceWithOpts(filepath.Base(path), []byte(contents), opts)
if err != nil {
return "", fmt.Errorf("failed to format Rego source file: %w", err)
}

return string(formatted), nil
}

// ComputeEdits computes diff edits from 2 string inputs.
func ComputeEdits(before, after string) []TextEdit {
ops := operations(splitLines(before), splitLines(after))
edits := make([]TextEdit, 0, len(ops))

for _, op := range ops {
switch op.Kind {
case Delete:
// Delete: unformatted[i1:i2] is deleted.
edits = append(edits, TextEdit{Range: Range{
Start: Position{Line: op.I1, Character: 0},
End: Position{Line: op.I2, Character: 0},
}})
case Insert:
// Insert: formatted[j1:j2] is inserted at unformatted[i1:i1].
if content := strings.Join(op.Content, ""); content != "" {
edits = append(edits, TextEdit{
Range: Range{
Start: Position{Line: op.I1, Character: 0},
End: Position{Line: op.I2, Character: 0},
},
NewText: content,
})
}
case Equal:
}
}

return edits
}
Loading

0 comments on commit 8d876bb

Please sign in to comment.