forked from StyraInc/regal
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Code action for OPA fmt (StyraInc#630)
* 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
Showing
7 changed files
with
529 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
ignore: | ||
files: | ||
- e2e/* | ||
- pkg/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.