| // 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), |
| } |
| } |