Skip to content

Commit

Permalink
Simple auto-completion for yolol
Browse files Browse the repository at this point in the history
  • Loading branch information
dbaumgarten committed Nov 30, 2020
1 parent f54cd3d commit 8d31c91
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 75 deletions.
33 changes: 29 additions & 4 deletions pkg/langserver/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,28 @@ import (
"sync"

"github.com/dbaumgarten/yodk/pkg/lsp"
"github.com/dbaumgarten/yodk/pkg/nolol/nast"
)

var NotFoundError = fmt.Errorf("File not found in cache")

type Cache struct {
Files map[lsp.DocumentURI]string
Lock *sync.Mutex
Files map[lsp.DocumentURI]string
Diagnostics map[lsp.DocumentURI]DiagnosticResults
Lock *sync.Mutex
}

type DiagnosticResults struct {
Macros map[string]*nast.MacroDefinition
Definitions map[string]*nast.Definition
Variables []string
}

func NewCache() *Cache {
return &Cache{
Files: make(map[lsp.DocumentURI]string),
Lock: &sync.Mutex{},
Files: make(map[lsp.DocumentURI]string),
Diagnostics: make(map[lsp.DocumentURI]DiagnosticResults),
Lock: &sync.Mutex{},
}
}

Expand All @@ -36,3 +45,19 @@ func (c *Cache) Set(uri lsp.DocumentURI, content string) {
defer c.Lock.Unlock()
c.Files[uri] = content
}

func (c *Cache) GetDiagnostics(uri lsp.DocumentURI) (*DiagnosticResults, error) {
c.Lock.Lock()
defer c.Lock.Unlock()
f, found := c.Diagnostics[uri]
if !found {
return nil, NotFoundError
}
return &f, nil
}

func (c *Cache) SetDiagnostics(uri lsp.DocumentURI, content DiagnosticResults) {
c.Lock.Lock()
defer c.Lock.Unlock()
c.Diagnostics[uri] = content
}
117 changes: 117 additions & 0 deletions pkg/langserver/completions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package langserver

import (
"strings"

"github.com/dbaumgarten/yodk/pkg/lsp"
"github.com/dbaumgarten/yodk/pkg/parser/ast"
)

var completionItemGroups = map[string][]string{
"unaryOps": {"abs", "sqrt", "sin", "cos", "tan", "asin", "acos", "atan", "not"},
"keywords": {"if", "then", "else", "end", "goto"},
"binaryOps": {"and", "or"},
}

// see here: https://microsoft.github.io/language-server-protocol/specifications/specification-current/
var completionItemTypes = map[string]float64{
"unaryOps": 24,
"keywords": 14,
"binaryOps": 24,
}

var completionItemDocs = map[string]string{
"abs": "abs X: Returns the absolute value of X",
"sqrt": "sqrt X: Returns the square-root of X",
"sin": "sin X: Return the sine (degree) of X",
"cos": "cos X: Return the cosine (degree) of X",
"tan": "sin X: Return the tangent (degree) of X",
"asin": "asin X: Return the inverse sine (degree) of X",
"acos": "asin X: Return the inverse cosine (degree) of X",
"atan": "asin X: Return the inverse tanget (degree) of X",
"not": "not X: Returns 1 if X is 0, otherwise it returns 0",
"and": "X and Y: Returns true if X and Y are true",
"or": "X or Y: Returns true if X or Y are true",
}

func buildDefaultCompletionItems() []lsp.CompletionItem {
items := make([]lsp.CompletionItem, 0, 50)
for k, v := range completionItemGroups {
kind := completionItemTypes[k]
for _, str := range v {
docs, hasDocs := completionItemDocs[str]
item := lsp.CompletionItem{
Label: str,
Kind: kind,
}
if hasDocs {
item.Detail = docs
}
items = append(items, item)
}
}
return items
}

var defaultCompletionItems []lsp.CompletionItem

func init() {
defaultCompletionItems = buildDefaultCompletionItems()
}

func (s *LangServer) GetCompletions(params *lsp.CompletionParams) (*lsp.CompletionList, error) {
items := make([]lsp.CompletionItem, 0, len(defaultCompletionItems)+15)

items = append(items, defaultCompletionItems...)
items = append(items, s.getVariableCompletions(params)...)

return &lsp.CompletionList{
IsIncomplete: true,
Items: items,
}, nil
}

func (s *LangServer) getVariableCompletions(params *lsp.CompletionParams) []lsp.CompletionItem {
diags, err := s.cache.GetDiagnostics(params.TextDocument.URI)
if err != nil {
return []lsp.CompletionItem{}
}
if diags.Variables == nil {
return []lsp.CompletionItem{}
}
items := make([]lsp.CompletionItem, len(diags.Variables))

for i, v := range diags.Variables {
item := lsp.CompletionItem{
Label: v,
Kind: 6,
}
if strings.HasPrefix(v, ":") {
item.Kind = 5
}
items[i] = item
}
return items
}

// Find all variable-names that are used inside a program
func findUsedVariables(prog *ast.Program) []string {
variables := make(map[string]bool)
f := func(node ast.Node, visitType int) error {
if assign, is := node.(*ast.Assignment); visitType == ast.PreVisit && is {
variables[assign.Variable] = true
}
if deref, is := node.(*ast.Dereference); visitType == ast.SingleVisit && is {
variables[deref.Variable] = true
}
return nil
}
prog.Accept(ast.VisitorFunc(f))

vars := make([]string, 0, len(variables))
for k := range variables {
vars = append(vars, k)
}

return vars
}
177 changes: 107 additions & 70 deletions pkg/langserver/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,47 +55,76 @@ func (f fs) Get(name string) (string, error) {
return string(content), err
}

func (s *LangServer) Diagnose(ctx context.Context, uri lsp.DocumentURI) {

go func() {
var errs error
var parsed *ast.Program
text, _ := s.cache.Get(uri)
func convertToErrorlist(errs error) parser.Errors {
if errs == nil {
return make(parser.Errors, 0)
}
switch e := errs.(type) {
case parser.Errors:
return e
case *parser.Error:
// if it is a single error, convert it to a one-element list
errlist := make(parser.Errors, 1)
errlist[0] = e
return errlist
default:
log.Printf("Unknown error type: %T\n (%s)", errs, errs.Error())
return nil
}
}

if strings.HasSuffix(string(uri), ".yolol") {
p := parser.NewParser()
parsed, errs = p.Parse(text)
} else if strings.HasSuffix(string(uri), ".nolol") {
conv := nolol.NewConverter()
mainfile := string(uri)
_, errs = conv.ConvertFileEx(mainfile, newfs(s, uri))
} else {
return
func convertErrorsToDiagnostics(errs parser.Errors) []lsp.Diagnostic {
diags := make([]lsp.Diagnostic, 0)

for _, err := range errs {
diag := lsp.Diagnostic{
Source: "parser",
Message: err.Message,
Severity: lsp.SeverityError,
Range: lsp.Range{
Start: lsp.Position{
Line: float64(err.StartPosition.Line) - 1,
Character: float64(err.StartPosition.Coloumn) - 1,
},
End: lsp.Position{
Line: float64(err.EndPosition.Line) - 1,
Character: float64(err.EndPosition.Coloumn) - 1,
},
},
}
diags = append(diags, diag)
}

diags := make([]lsp.Diagnostic, 0)
return diags
}

if errs == nil {
errs = make(parser.Errors, 0)
}
switch e := errs.(type) {
case parser.Errors:
break
case *parser.Error:
// if it is a single error, convert it to a one-element list
errlist := make(parser.Errors, 1)
errlist[0] = e
errs = errlist
default:
log.Printf("Unknown error type: %T\n (%s)", errs, errs.Error())
return
func (s *LangServer) validateCodeLength(uri lsp.DocumentURI, text string, parsed *ast.Program) *lsp.Diagnostic {
// check if the code-length of yolol-code is OK
if s.settings.Yolol.LengthChecking.Mode != LengthCheckModeOff && strings.HasSuffix(string(uri), ".yolol") {
lengtherror := validators.ValidateCodeLength(text)

// check if the code is small enough after optimizing it
if lengtherror != nil && s.settings.Yolol.LengthChecking.Mode == LengthCheckModeOptimize && parsed != nil {

opt := optimizers.NewCompoundOptimizer()
err := opt.Optimize(parsed)
if err == nil {
printer := parser.Printer{
Mode: parser.PrintermodeSpaceless,
}
optimized, err := printer.Print(parsed)
if err == nil {
lengtherror = validators.ValidateCodeLength(optimized)
}
}
}

for _, err := range errs.(parser.Errors) {
diag := lsp.Diagnostic{
Source: "parser",
if lengtherror != nil {
err := lengtherror.(*parser.Error)
return &lsp.Diagnostic{
Source: "validator",
Message: err.Message,
Severity: lsp.SeverityError,
Severity: lsp.SeverityWarning,
Range: lsp.Range{
Start: lsp.Position{
Line: float64(err.StartPosition.Line) - 1,
Expand All @@ -107,49 +136,57 @@ func (s *LangServer) Diagnose(ctx context.Context, uri lsp.DocumentURI) {
},
},
}
diags = append(diags, diag)
}
}
return nil
}

// check if the code-length of yolol-code is OK
if len(diags) == 0 && s.settings.Yolol.LengthChecking.Mode != LengthCheckModeOff && strings.HasSuffix(string(uri), ".yolol") {
lengtherror := validators.ValidateCodeLength(text)
func (s *LangServer) Diagnose(ctx context.Context, uri lsp.DocumentURI) {

// check if the code is small enough after optimizing it
if lengtherror != nil && s.settings.Yolol.LengthChecking.Mode == LengthCheckModeOptimize && parsed != nil {
go func() {
var errs error
var parsed *ast.Program
var diagRes DiagnosticResults
text, _ := s.cache.Get(uri)

opt := optimizers.NewCompoundOptimizer()
err := opt.Optimize(parsed)
if err == nil {
printer := parser.Printer{
Mode: parser.PrintermodeSpaceless,
}
optimized, err := printer.Print(parsed)
if err == nil {
lengtherror = validators.ValidateCodeLength(optimized)
}
}
}
prevDiag, err := s.cache.GetDiagnostics(uri)
if err == nil {
diagRes = *prevDiag
}

if lengtherror != nil {
err := lengtherror.(*parser.Error)
diag := lsp.Diagnostic{
Source: "validator",
Message: err.Message,
Severity: lsp.SeverityWarning,
Range: lsp.Range{
Start: lsp.Position{
Line: float64(err.StartPosition.Line) - 1,
Character: float64(err.StartPosition.Coloumn) - 1,
},
End: lsp.Position{
Line: float64(err.EndPosition.Line) - 1,
Character: float64(err.EndPosition.Coloumn) - 1,
},
},
}
diags = append(diags, diag)
if strings.HasSuffix(string(uri), ".yolol") {
p := parser.NewParser()
parsed, errs = p.Parse(text)

if parsed != nil {
diagRes.Variables = findUsedVariables(parsed)
}

} else if strings.HasSuffix(string(uri), ".nolol") {
conv := nolol.NewConverter()
mainfile := string(uri)
_, errs = conv.ConvertFileEx(mainfile, newfs(s, uri))
diagRes.Definitions = conv.GetDefinitions()
diagRes.Macros = conv.GetMacros()

} else {
return
}

s.cache.SetDiagnostics(uri, diagRes)

parserErrors := convertToErrorlist(errs)
if parserErrors == nil {
return
}

diags := convertErrorsToDiagnostics(parserErrors)

if len(diags) == 0 {
diag := s.validateCodeLength(uri, text, parsed)
if diag != nil {
diags = append(diags, *diag)
}
}

s.client.PublishDiagnostics(ctx, &lsp.PublishDiagnosticsParams{
Expand Down
5 changes: 4 additions & 1 deletion pkg/langserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ func (ls *LangServer) Initialize(ctx context.Context, params *lsp.InitializePara
OpenClose: true,
},
DocumentFormattingProvider: true,
CompletionProvider: &lsp.CompletionOptions{
TriggerCharacters: []string{" ", ":", "+", "-", "*", "/", "%", "=", "^", ">", "<"},
},
},
}, nil
}
Expand Down Expand Up @@ -107,7 +110,7 @@ func (ls *LangServer) DidClose(ctx context.Context, params *lsp.DidCloseTextDocu
return unsupported()
}
func (ls *LangServer) Completion(ctx context.Context, params *lsp.CompletionParams) (*lsp.CompletionList, error) {
return nil, unsupported()
return ls.GetCompletions(params)
}
func (ls *LangServer) CompletionResolve(ctx context.Context, params *lsp.CompletionItem) (*lsp.CompletionItem, error) {
return nil, unsupported()
Expand Down
Loading

0 comments on commit 8d31c91

Please sign in to comment.