| // 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" | |
| "./parser" | |
| "./schema" | |
| "./lsp/jsonrpc2" | |
| lsp "./lsp/protocol" | |
| ) | |
| // 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() { | |
| // 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) | |
| } else { | |
| log.SetOutput(ioutil.Discard) | |
| } | |
| 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 | |
| } | |
| } | |
| // Every good file ends with a new line. | |
| sb.WriteString("\n") | |
| return []lsp.TextEdit{ | |
| lsp.TextEdit{ | |
| Range: rangeToLSP(f.fullRange), | |
| NewText: sb.String(), | |
| }, | |
| }, 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), | |
| } | |
| } |