blob: b76e35f78d533a7c11f0b340f559eb64d3704738 [file] [log] [blame]
// Copyright (C) 2019 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// langsvr implements a Language Server for the SPIRV assembly language.
package main
import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"sort"
"strings"
"sync"
"unicode/utf8"
"github.com/KhronosGroup/SPIRV-Tools/utils/vscode/src/parser"
"github.com/KhronosGroup/SPIRV-Tools/utils/vscode/src/schema"
"github.com/KhronosGroup/SPIRV-Tools/utils/vscode/src/lsp/jsonrpc2"
lsp "github.com/KhronosGroup/SPIRV-Tools/utils/vscode/src/lsp/protocol"
)
const (
enableDebugLogging = false
)
// rSpy is a reader 'spy' that wraps an io.Reader, and logs all data that passes
// through it.
type rSpy struct {
prefix string
r io.Reader
}
func (s rSpy) Read(p []byte) (n int, err error) {
n, err = s.r.Read(p)
log.Printf("%v %v", s.prefix, string(p[:n]))
return n, err
}
// wSpy is a reader 'spy' that wraps an io.Writer, and logs all data that passes
// through it.
type wSpy struct {
prefix string
w io.Writer
}
func (s wSpy) Write(p []byte) (n int, err error) {
n, err = s.w.Write(p)
log.Printf("%v %v", s.prefix, string(p))
return n, err
}
// main entry point.
func main() {
log.SetOutput(ioutil.Discard)
if enableDebugLogging {
// create a log file in the executable's directory.
if logfile, err := os.Create(path.Join(path.Dir(os.Args[0]), "log.txt")); err == nil {
defer logfile.Close()
log.SetOutput(logfile)
}
}
log.Println("language server started")
stream := jsonrpc2.NewHeaderStream(rSpy{"IDE", os.Stdin}, wSpy{"LS", os.Stdout})
s := server{
files: map[string]*file{},
}
s.ctx, s.conn, s.client = lsp.NewServer(context.Background(), stream, &s)
if err := s.conn.Run(s.ctx); err != nil {
log.Panicln(err)
os.Exit(1)
}
log.Println("language server stopped")
}
type server struct {
ctx context.Context
conn *jsonrpc2.Conn
client lsp.Client
files map[string]*file
filesMutex sync.Mutex
}
// file represents a source file
type file struct {
fullRange parser.Range
res parser.Results
}
// tokAt returns the parser token at the given position lp
func (f *file) tokAt(lp lsp.Position) *parser.Token {
toks := f.res.Tokens
p := parser.Position{Line: int(lp.Line) + 1, Column: int(lp.Character) + 1}
i := sort.Search(len(toks), func(i int) bool { return p.LessThan(toks[i].Range.End) })
if i == len(toks) {
return nil
}
if toks[i].Range.Contains(p) {
return toks[i]
}
return nil
}
func (s *server) DidChangeWorkspaceFolders(ctx context.Context, p *lsp.DidChangeWorkspaceFoldersParams) error {
log.Println("server.DidChangeWorkspaceFolders()")
return nil
}
func (s *server) Initialized(ctx context.Context, p *lsp.InitializedParams) error {
log.Println("server.Initialized()")
return nil
}
func (s *server) Exit(ctx context.Context) error {
log.Println("server.Exit()")
return nil
}
func (s *server) DidChangeConfiguration(ctx context.Context, p *lsp.DidChangeConfigurationParams) error {
log.Println("server.DidChangeConfiguration()")
return nil
}
func (s *server) DidOpen(ctx context.Context, p *lsp.DidOpenTextDocumentParams) error {
log.Println("server.DidOpen()")
return s.processFile(ctx, p.TextDocument.URI, p.TextDocument.Text)
}
func (s *server) DidChange(ctx context.Context, p *lsp.DidChangeTextDocumentParams) error {
log.Println("server.DidChange()")
return s.processFile(ctx, p.TextDocument.URI, p.ContentChanges[0].Text)
}
func (s *server) DidClose(ctx context.Context, p *lsp.DidCloseTextDocumentParams) error {
log.Println("server.DidClose()")
return nil
}
func (s *server) DidSave(ctx context.Context, p *lsp.DidSaveTextDocumentParams) error {
log.Println("server.DidSave()")
return nil
}
func (s *server) WillSave(ctx context.Context, p *lsp.WillSaveTextDocumentParams) error {
log.Println("server.WillSave()")
return nil
}
func (s *server) DidChangeWatchedFiles(ctx context.Context, p *lsp.DidChangeWatchedFilesParams) error {
log.Println("server.DidChangeWatchedFiles()")
return nil
}
func (s *server) Progress(ctx context.Context, p *lsp.ProgressParams) error {
log.Println("server.Progress()")
return nil
}
func (s *server) SetTraceNotification(ctx context.Context, p *lsp.SetTraceParams) error {
log.Println("server.SetTraceNotification()")
return nil
}
func (s *server) LogTraceNotification(ctx context.Context, p *lsp.LogTraceParams) error {
log.Println("server.LogTraceNotification()")
return nil
}
func (s *server) Implementation(ctx context.Context, p *lsp.ImplementationParams) ([]lsp.Location, error) {
log.Println("server.Implementation()")
return nil, nil
}
func (s *server) TypeDefinition(ctx context.Context, p *lsp.TypeDefinitionParams) ([]lsp.Location, error) {
log.Println("server.TypeDefinition()")
return nil, nil
}
func (s *server) DocumentColor(ctx context.Context, p *lsp.DocumentColorParams) ([]lsp.ColorInformation, error) {
log.Println("server.DocumentColor()")
return nil, nil
}
func (s *server) ColorPresentation(ctx context.Context, p *lsp.ColorPresentationParams) ([]lsp.ColorPresentation, error) {
log.Println("server.ColorPresentation()")
return nil, nil
}
func (s *server) FoldingRange(ctx context.Context, p *lsp.FoldingRangeParams) ([]lsp.FoldingRange, error) {
log.Println("server.FoldingRange()")
return nil, nil
}
func (s *server) Declaration(ctx context.Context, p *lsp.DeclarationParams) ([]lsp.DeclarationLink, error) {
log.Println("server.Declaration()")
return nil, nil
}
func (s *server) SelectionRange(ctx context.Context, p *lsp.SelectionRangeParams) ([]lsp.SelectionRange, error) {
log.Println("server.SelectionRange()")
return nil, nil
}
func (s *server) Initialize(ctx context.Context, p *lsp.ParamInitia) (*lsp.InitializeResult, error) {
log.Println("server.Initialize()")
res := lsp.InitializeResult{
Capabilities: lsp.ServerCapabilities{
TextDocumentSync: lsp.TextDocumentSyncOptions{
OpenClose: true,
Change: lsp.Full, // TODO: Implement incremental
},
HoverProvider: true,
DefinitionProvider: true,
ReferencesProvider: true,
RenameProvider: true,
DocumentFormattingProvider: true,
},
}
return &res, nil
}
func (s *server) Shutdown(ctx context.Context) error {
log.Println("server.Shutdown()")
return nil
}
func (s *server) WillSaveWaitUntil(ctx context.Context, p *lsp.WillSaveTextDocumentParams) ([]lsp.TextEdit, error) {
log.Println("server.WillSaveWaitUntil()")
return nil, nil
}
func (s *server) Completion(ctx context.Context, p *lsp.CompletionParams) (*lsp.CompletionList, error) {
log.Println("server.Completion()")
return nil, nil
}
func (s *server) Resolve(ctx context.Context, p *lsp.CompletionItem) (*lsp.CompletionItem, error) {
log.Println("server.Resolve()")
return nil, nil
}
func (s *server) Hover(ctx context.Context, p *lsp.HoverParams) (*lsp.Hover, error) {
log.Println("server.Hover()")
f := s.getFile(p.TextDocument.URI)
if f == nil {
return nil, fmt.Errorf("Unknown file")
}
if tok := f.tokAt(p.Position); tok != nil {
sb := strings.Builder{}
switch v := f.res.Mappings[tok].(type) {
default:
sb.WriteString(fmt.Sprintf("<Unhandled type '%T'>", v))
case *parser.Instruction:
sb.WriteString(fmt.Sprintf("```\n%v\n```", v.Opcode.Opname))
case *parser.Identifier:
sb.WriteString(fmt.Sprintf("```\n%v\n```", v.Definition.Range.Text(f.res.Lines)))
case *parser.Operand:
if v.Name != "" {
sb.WriteString(strings.Trim(v.Name, `'`))
sb.WriteString("\n\n")
}
switch v.Kind.Category {
case schema.OperandCategoryBitEnum:
case schema.OperandCategoryValueEnum:
sb.WriteString("```\n")
sb.WriteString(strings.Trim(v.Kind.Kind, `'`))
sb.WriteString("\n```")
case schema.OperandCategoryID:
if s := tok.Text(f.res.Lines); s != "" {
if id, ok := f.res.Identifiers[s]; ok && id.Definition != nil {
sb.WriteString("```\n")
sb.WriteString(id.Definition.Range.Text(f.res.Lines))
sb.WriteString("\n```")
}
}
case schema.OperandCategoryLiteral:
case schema.OperandCategoryComposite:
}
case nil:
}
if sb.Len() > 0 {
res := lsp.Hover{
Contents: lsp.MarkupContent{
Kind: "markdown",
Value: sb.String(),
},
}
return &res, nil
}
}
return nil, nil
}
func (s *server) SignatureHelp(ctx context.Context, p *lsp.SignatureHelpParams) (*lsp.SignatureHelp, error) {
log.Println("server.SignatureHelp()")
return nil, nil
}
func (s *server) Definition(ctx context.Context, p *lsp.DefinitionParams) ([]lsp.Location, error) {
log.Println("server.Definition()")
if f := s.getFile(p.TextDocument.URI); f != nil {
if tok := f.tokAt(p.Position); tok != nil {
if s := tok.Text(f.res.Lines); s != "" {
if id, ok := f.res.Identifiers[s]; ok {
loc := lsp.Location{
URI: p.TextDocument.URI,
Range: rangeToLSP(id.Definition.Range),
}
return []lsp.Location{loc}, nil
}
}
}
}
return nil, nil
}
func (s *server) References(ctx context.Context, p *lsp.ReferenceParams) ([]lsp.Location, error) {
log.Println("server.References()")
if f := s.getFile(p.TextDocument.URI); f != nil {
if tok := f.tokAt(p.Position); tok != nil {
if s := tok.Text(f.res.Lines); s != "" {
if id, ok := f.res.Identifiers[s]; ok {
locs := make([]lsp.Location, len(id.References))
for i, r := range id.References {
locs[i] = lsp.Location{
URI: p.TextDocument.URI,
Range: rangeToLSP(r.Range),
}
}
return locs, nil
}
}
}
}
return nil, nil
}
func (s *server) DocumentHighlight(ctx context.Context, p *lsp.DocumentHighlightParams) ([]lsp.DocumentHighlight, error) {
log.Println("server.DocumentHighlight()")
return nil, nil
}
func (s *server) DocumentSymbol(ctx context.Context, p *lsp.DocumentSymbolParams) ([]lsp.DocumentSymbol, error) {
log.Println("server.DocumentSymbol()")
return nil, nil
}
func (s *server) CodeAction(ctx context.Context, p *lsp.CodeActionParams) ([]lsp.CodeAction, error) {
log.Println("server.CodeAction()")
return nil, nil
}
func (s *server) Symbol(ctx context.Context, p *lsp.WorkspaceSymbolParams) ([]lsp.SymbolInformation, error) {
log.Println("server.Symbol()")
return nil, nil
}
func (s *server) CodeLens(ctx context.Context, p *lsp.CodeLensParams) ([]lsp.CodeLens, error) {
log.Println("server.CodeLens()")
return nil, nil
}
func (s *server) ResolveCodeLens(ctx context.Context, p *lsp.CodeLens) (*lsp.CodeLens, error) {
log.Println("server.ResolveCodeLens()")
return nil, nil
}
func (s *server) DocumentLink(ctx context.Context, p *lsp.DocumentLinkParams) ([]lsp.DocumentLink, error) {
log.Println("server.DocumentLink()")
return nil, nil
}
func (s *server) ResolveDocumentLink(ctx context.Context, p *lsp.DocumentLink) (*lsp.DocumentLink, error) {
log.Println("server.ResolveDocumentLink()")
return nil, nil
}
func (s *server) Formatting(ctx context.Context, p *lsp.DocumentFormattingParams) ([]lsp.TextEdit, error) {
log.Println("server.Formatting()")
if f := s.getFile(p.TextDocument.URI); f != nil {
// Start by measuring the distance from the start of each line to the
// first opcode on that line.
lineInstOffsets, maxInstOffset, instOffset, curOffset := []int{}, 0, 0, -1
for _, t := range f.res.Tokens {
curOffset++ // whitespace between tokens
switch t.Type {
case parser.Ident:
if _, isInst := schema.Opcodes[t.Text(f.res.Lines)]; isInst && instOffset == 0 {
instOffset = curOffset
continue
}
case parser.Newline:
lineInstOffsets = append(lineInstOffsets, instOffset)
if instOffset > maxInstOffset {
maxInstOffset = instOffset
}
curOffset, instOffset = -1, 0
default:
curOffset += utf8.RuneCountInString(t.Text(f.res.Lines))
}
}
lineInstOffsets = append(lineInstOffsets, instOffset)
// Now rewrite each of the lines, adding padding at the start of the
// line for alignment.
sb, newline := strings.Builder{}, true
for _, t := range f.res.Tokens {
if newline {
newline = false
indent := maxInstOffset - lineInstOffsets[0]
lineInstOffsets = lineInstOffsets[1:]
switch t.Type {
case parser.Newline, parser.Comment:
default:
for s := 0; s < indent; s++ {
sb.WriteRune(' ')
}
}
} else if t.Type != parser.Newline {
sb.WriteString(" ")
}
sb.WriteString(t.Text(f.res.Lines))
if t.Type == parser.Newline {
newline = true
}
}
formatted := sb.String()
// Every good file ends with a single new line.
formatted = strings.TrimRight(formatted, "\n") + "\n"
return []lsp.TextEdit{
{
Range: rangeToLSP(f.fullRange),
NewText: formatted,
},
}, nil
}
return nil, nil
}
func (s *server) RangeFormatting(ctx context.Context, p *lsp.DocumentRangeFormattingParams) ([]lsp.TextEdit, error) {
log.Println("server.RangeFormatting()")
return nil, nil
}
func (s *server) OnTypeFormatting(ctx context.Context, p *lsp.DocumentOnTypeFormattingParams) ([]lsp.TextEdit, error) {
log.Println("server.OnTypeFormatting()")
return nil, nil
}
func (s *server) Rename(ctx context.Context, p *lsp.RenameParams) (*lsp.WorkspaceEdit, error) {
log.Println("server.Rename()")
if f := s.getFile(p.TextDocument.URI); f != nil {
if tok := f.tokAt(p.Position); tok != nil {
if s := tok.Text(f.res.Lines); s != "" {
if id, ok := f.res.Identifiers[s]; ok {
changes := make([]lsp.TextEdit, len(id.References))
for i, r := range id.References {
changes[i].Range = rangeToLSP(r.Range)
changes[i].NewText = p.NewName
}
m := map[string][]lsp.TextEdit{}
m[p.TextDocument.URI] = changes
return &lsp.WorkspaceEdit{Changes: &m}, nil
}
}
}
}
return nil, nil
}
func (s *server) PrepareRename(ctx context.Context, p *lsp.PrepareRenameParams) (*lsp.Range, error) {
log.Println("server.PrepareRename()")
return nil, nil
}
func (s *server) ExecuteCommand(ctx context.Context, p *lsp.ExecuteCommandParams) (interface{}, error) {
log.Println("server.ExecuteCommand()")
return nil, nil
}
func (s *server) processFile(ctx context.Context, uri, source string) error {
log.Println("server.DidOpen()")
res, err := parser.Parse(source)
if err != nil {
return err
}
fullRange := parser.Range{
Start: parser.Position{Line: 1, Column: 1},
End: parser.Position{Line: len(res.Lines), Column: utf8.RuneCountInString(res.Lines[len(res.Lines)-1]) + 1},
}
s.filesMutex.Lock()
s.files[uri] = &file{
fullRange: fullRange,
res: res,
}
s.filesMutex.Unlock()
dp := lsp.PublishDiagnosticsParams{URI: uri, Diagnostics: make([]lsp.Diagnostic, len(res.Diagnostics))}
for i, d := range res.Diagnostics {
dp.Diagnostics[i] = diagnosticToLSP(d)
}
s.client.PublishDiagnostics(ctx, &dp)
return nil
}
func (s *server) getFile(uri string) *file {
s.filesMutex.Lock()
defer s.filesMutex.Unlock()
return s.files[uri]
}
func diagnosticToLSP(d parser.Diagnostic) lsp.Diagnostic {
return lsp.Diagnostic{
Range: rangeToLSP(d.Range),
Severity: severityToLSP(d.Severity),
Message: d.Message,
}
}
func severityToLSP(s parser.Severity) lsp.DiagnosticSeverity {
switch s {
case parser.SeverityError:
return lsp.SeverityError
case parser.SeverityWarning:
return lsp.SeverityWarning
case parser.SeverityInformation:
return lsp.SeverityInformation
case parser.SeverityHint:
return lsp.SeverityHint
default:
log.Panicf("Invalid severity '%d'", int(s))
return lsp.SeverityError
}
}
func rangeToLSP(r parser.Range) lsp.Range {
return lsp.Range{
Start: positionToLSP(r.Start),
End: positionToLSP(r.End),
}
}
func positionToLSP(r parser.Position) lsp.Position {
return lsp.Position{
Line: float64(r.Line - 1),
Character: float64(r.Column - 1),
}
}