Skip to content

Commit

Permalink
feat(tools/spxls): implement textDocument/hover (#1141)
Browse files Browse the repository at this point in the history
Updates #1059

Signed-off-by: Aofei Sheng <[email protected]>
  • Loading branch information
aofei authored Dec 19, 2024
1 parent fb662b5 commit 87be919
Show file tree
Hide file tree
Showing 19 changed files with 1,695 additions and 337 deletions.
4 changes: 2 additions & 2 deletions tools/spxls/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ interface SpxDefinitionIdentifier {
*/
name?: string;

/** Index in overloads. */
overloadIndex?: number;
/** Overload Identifier.. */
overloadId?: string;
}
```

Expand Down
4 changes: 2 additions & 2 deletions tools/spxls/internal/importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ func (imp *importer) Import(path string) (*types.Package, error) {

export, err := pkgdata.OpenExport(path)
if err != nil {
return nil, fmt.Errorf("failed to open export file: %w", err)
return nil, fmt.Errorf("failed to open package export file: %w", err)
}
defer export.Close()

pkg, err := gcexportdata.Read(export, imp.fset, imp.loaded, path)
if err != nil {
return nil, fmt.Errorf("failed to parse export data: %w", err)
return nil, fmt.Errorf("failed to parse package export data: %w", err)
}
return pkg, nil
}
245 changes: 177 additions & 68 deletions tools/spxls/internal/pkgdata/gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,116 +3,197 @@ package main
import (
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
"go/types"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"slices"
"strings"

"github.com/goplus/builder/tools/spxls/internal/pkgdoc"
_ "github.com/goplus/spx"
"golang.org/x/tools/go/gcexportdata"
)

// pkgs is the list of packages to generate the exported symbols for.
var pkgs = []string{
"archive/...",
"bufio",
"bytes",
"cmp",
"compress/...",
"container/...",
"context",
"crypto/...",
"database/...",
"debug/...",
"embed",
"encoding/...",
"errors",
"expvar",
"flag",
"fmt",
"go/...",
"hash/...",
"html/...",
"image/...",
"index/...",
"io/...",
"log/...",
"maps",
"math/...",
"mime/...",
"net/...",
"os/...",
"path/...",
"plugin",
"reflect/...",
"regexp/...",
"runtime/...",
"slices",
"sort",
"strconv",
"strings",
"sync/...",
"syscall/...",
"testing/...",
"text/...",
"time/...",
"unicode/...",

"github.com/goplus/spx/...",
// modulePaths is the list of modules to generate the exported symbols for.
var modulePaths = []string{
"github.com/goplus/spx",
}

// generate generates the pkgdata.zip file containing the exported symbols of
// the given packages.
func generate() error {
var pkgPaths []string

// Scan the stdlib packages.
gorootSrcDir := filepath.Join(runtime.GOROOT(), "src")
if err := filepath.Walk(gorootSrcDir, func(p string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if !fi.IsDir() {
return nil
}

importPath := strings.TrimPrefix(p, gorootSrcDir+string(os.PathSeparator))
if !isSkippable(importPath) {
pkgPaths = append(pkgPaths, importPath)
}
return nil
}); err != nil {
return fmt.Errorf("failed to walk goroot src dir: %w", err)
}

// Scan the modules.
for _, modulePath := range modulePaths {
importPaths, err := execGo("list", "-f", "{{.ImportPath}}", modulePath+"/...")
if err != nil {
return err
}
for _, importPath := range strings.Split(string(importPaths), "\n") {
if !isSkippable(importPath) {
pkgPaths = append(pkgPaths, importPath)
}
}
}

var zipBuf bytes.Buffer
zw := zip.NewWriter(&zipBuf)
for _, pkg := range pkgs {
cmd := exec.Command("go", "list", "-f", "{{.ImportPath}}:{{.Export}}", "-export", pkg)
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
output, err := cmd.Output()
for _, pkgPath := range pkgPaths {
buildPkg, err := build.Import(pkgPath, "", build.ImportComment)
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("%w: %s", err, ee.Stderr)
}
return fmt.Errorf("failed to go list: %w", err)
continue
}

for _, line := range strings.Split(string(output), "\n") {
if line == "" {
continue
var pkgDoc *pkgdoc.PkgDoc
if pkgPath == "builtin" {
astFile, err := parser.ParseFile(token.NewFileSet(), filepath.Join(buildPkg.Dir, "builtin.go"), nil, parser.ParseComments)
if err != nil {
return fmt.Errorf("failed to parse builtin.go: %w", err)
}

pkgPath, exportFile, ok := strings.Cut(line, ":")
if !ok || exportFile == "" {
pkgDoc = &pkgdoc.PkgDoc{
Path: pkgPath,
Name: pkgPath,
Vars: make(map[string]string),
Consts: make(map[string]string),
Types: make(map[string]pkgdoc.TypeDoc),
Funcs: make(map[string]string),
}
for _, decl := range astFile.Decls {
switch decl := decl.(type) {
case *ast.GenDecl:
switch decl.Tok {
case token.VAR:
for _, spec := range decl.Specs {
switch spec := spec.(type) {
case *ast.ValueSpec:
for _, name := range spec.Names {
doc := spec.Doc.Text()
if doc == "" {
doc = decl.Doc.Text()
}
pkgDoc.Vars[name.Name] = doc
}
default:
return fmt.Errorf("unknown builtin gen decl spec: %T", spec)
}
}
case token.CONST:
for _, spec := range decl.Specs {
switch spec := spec.(type) {
case *ast.ValueSpec:
for _, name := range spec.Names {
doc := spec.Doc.Text()
if doc == "" {
doc = decl.Doc.Text()
}
pkgDoc.Consts[name.Name] = doc
}
default:
return fmt.Errorf("unknown builtin gen decl spec: %T", spec)
}
}
case token.IMPORT:
case token.TYPE:
for _, spec := range decl.Specs {
switch spec := spec.(type) {
case *ast.TypeSpec:
doc := spec.Doc.Text()
if doc == "" {
doc = decl.Doc.Text()
}
pkgDoc.Types[spec.Name.Name] = pkgdoc.TypeDoc{
Doc: doc,
}
default:
return fmt.Errorf("unknown builtin gen decl spec: %T", spec)
}
}
default:
return fmt.Errorf("unknown builtin gen decl token: %s", decl.Tok)
}
case *ast.FuncDecl:
pkgDoc.Funcs[decl.Name.Name] = decl.Doc.Text()
default:
return fmt.Errorf("unknown builtin decl: %T", decl)
}
}
} else {
exportFile, err := execGo("list", "-export", "-f", "{{.Export}}", pkgPath)
if err != nil {
return err
}
exportFile = bytes.TrimSpace(exportFile)
if len(exportFile) == 0 {
continue
}

f, err := os.Open(exportFile)
f, err := os.Open(string(exportFile))
if err != nil {
return err
}
defer f.Close()

r, err := gcexportdata.NewReader(f)
if err != nil {
return fmt.Errorf("failed to create export reader: %w", err)
return fmt.Errorf("failed to create package export reader: %w", err)
}

fset := token.NewFileSet()
pkg, err := gcexportdata.Read(r, fset, make(map[string]*types.Package), pkgPath)
typesPkg, err := gcexportdata.Read(r, fset, make(map[string]*types.Package), pkgPath)
if err != nil {
return fmt.Errorf("failed to read export data: %w", err)
return fmt.Errorf("failed to read package export data: %w", err)
}
if zf, err := zw.Create(pkgPath + ".pkgexport"); err != nil {
return err
} else if err := gcexportdata.Write(zf, fset, typesPkg); err != nil {
return fmt.Errorf("failed to write optimized package export data: %w", err)
}

zf, err := zw.Create(pkgPath + ".export")
astPkgs, err := parser.ParseDir(token.NewFileSet(), buildPkg.Dir, nil, parser.ParseComments)
if err != nil {
return err
return fmt.Errorf("failed to parse package: %w", err)
}
if err := gcexportdata.Write(zf, fset, pkg); err != nil {
return fmt.Errorf("failed to write optimized export data: %w", err)
astPkg, ok := astPkgs[path.Base(buildPkg.ImportPath)]
if !ok {
continue
}

pkgDoc = pkgdoc.New(astPkg, pkgPath)
}
if zf, err := zw.Create(pkgPath + ".pkgdoc"); err != nil {
return err
} else if err := json.NewEncoder(zf).Encode(pkgDoc); err != nil {
return fmt.Errorf("failed to encode package doc: %w", err)
}
}
if err := zw.Close(); err != nil {
Expand All @@ -121,6 +202,34 @@ func generate() error {
return os.WriteFile("pkgdata.zip", zipBuf.Bytes(), 0644)
}

// isSkippable reports whether the import path should be skipped.
func isSkippable(importPath string) bool {
if importPath == "" {
return true
}
return slices.ContainsFunc(strings.Split(importPath, string(os.PathSeparator)), func(part string) bool {
switch part {
case "internal", "test", "testdata", "vendor":
return true
}
return false
})
}

// execGo executes the given go command.
func execGo(args ...string) ([]byte, error) {
cmd := exec.Command("go", args...)
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
output, err := cmd.Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("%w: %s", err, ee.Stderr)
}
return nil, fmt.Errorf("failed to execute go command: %w", err)
}
return output, nil
}

func main() {
if err := generate(); err != nil {
panic(err)
Expand Down
37 changes: 33 additions & 4 deletions tools/spxls/internal/pkgdata/pkgdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import (
"archive/zip"
"bytes"
_ "embed"
"encoding/json"
"fmt"
"io"
"io/fs"

"github.com/goplus/builder/tools/spxls/internal/pkgdoc"
)

//go:generate go run ./gen/main.go
Expand All @@ -17,13 +21,38 @@ var pkgdataZip []byte
func OpenExport(pkgPath string) (io.ReadCloser, error) {
zr, err := zip.NewReader(bytes.NewReader(pkgdataZip), int64(len(pkgdataZip)))
if err != nil {
return nil, fmt.Errorf("create zip reader: %w", err)
return nil, fmt.Errorf("failed to create zip reader: %w", err)
}
exportFile := pkgPath + ".export"
pkgExportFile := pkgPath + ".pkgexport"
for _, f := range zr.File {
if f.Name == exportFile {
if f.Name == pkgExportFile {
return f.Open()
}
}
return nil, fmt.Errorf("package %q not found", pkgPath)
return nil, fmt.Errorf("failed to find export file for package %q: %w", pkgPath, fs.ErrNotExist)
}

// GetPkgDoc gets the documentation for a package.
func GetPkgDoc(pkgPath string) (*pkgdoc.PkgDoc, error) {
zr, err := zip.NewReader(bytes.NewReader(pkgdataZip), int64(len(pkgdataZip)))
if err != nil {
return nil, fmt.Errorf("failed to create zip reader: %w", err)
}
pkgDocFile := pkgPath + ".pkgdoc"
for _, f := range zr.File {
if f.Name == pkgDocFile {
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("failed to open doc file for package %q: %w", pkgPath, err)
}
defer rc.Close()

var pkgDoc pkgdoc.PkgDoc
if err := json.NewDecoder(rc).Decode(&pkgDoc); err != nil {
return nil, fmt.Errorf("failed to decode doc for package %q: %w", pkgPath, err)
}
return &pkgDoc, nil
}
}
return nil, fmt.Errorf("failed to find doc file for package %q: %w", pkgPath, fs.ErrNotExist)
}
Binary file modified tools/spxls/internal/pkgdata/pkgdata.zip
Binary file not shown.
Loading

0 comments on commit 87be919

Please sign in to comment.