Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/files/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
protocol "github.com/tliron/glsp/protocol_3_16"
)

const MaxDiagnosticsDelay = time.Second * 1
const MaxDiagnosticsDelay = time.Second * 2

type Service interface {
CreateFile(ctx context.Context, path, content string) (*model.File, error)
Expand Down
3 changes: 3 additions & 0 deletions pkg/lsp/v2/languages/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type Binary struct {

// EnvAsKeyVal returns env vars in the form "key=value".
func (b *Binary) EnvAsKeyVal() []string {
if len(b.Env) == 0 {
return nil
}
// does not have a deterministic order
out := make([]string, 0, len(b.Env))
for k, v := range b.Env {
Expand Down
16 changes: 9 additions & 7 deletions pkg/lsp/v2/languages/gopls.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ func (a *gopls) FetchServerBinary(ctx context.Context, version interface{}, dele
Name: a.Name(),
Path: goplsPath,
Arguments: []string{"serve"},
Env: map[string]string{
"GOPATH": os.Getenv("GOPATH"),
"GOROOT": os.Getenv("GOROOT"),
Env: map[string]string{
// TODO: why this prevents LSP from working?
// "GOPATH": os.Getenv("GOPATH"),
// "GOROOT": os.Getenv("GOROOT"),
},
}, nil
}
Expand All @@ -99,14 +100,15 @@ func (a *gopls) FetchServerBinary(ctx context.Context, version interface{}, dele
Name: a.Name(),
Path: goplsPath,
Arguments: []string{"serve"},
Env: map[string]string{
"GOPATH": os.Getenv("GOPATH"),
"GOROOT": os.Getenv("GOROOT"),
Env: map[string]string{
// TODO: why this prevents LSP from working?
//"GOPATH": os.Getenv("GOPATH"),
//"GOROOT": os.Getenv("GOROOT"),
},
}, nil
}

func checkVersion(ctx context.Context, path string, wantVersion string, delegate Delegate) (bool, error) {
func checkVersion(_ context.Context, path string, wantVersion string, _ Delegate) (bool, error) {
if _, err := os.Stat(path); err != nil {
return false, nil
}
Expand Down
5 changes: 4 additions & 1 deletion pkg/lsp/v2/languages/languages.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ const (
JavaScript LanguageID = "JavaScript"
Python LanguageID = "Python"
TypeScript LanguageID = "TypeScript"
// TODO: check that const is consistenet with go-entry
TSX LanguageID = "TSX"
JSX LanguageID = "JSX"
)

var Adapters = []Adapter{
new(gopls),
new(gopls), new(tsserver),
}
211 changes: 211 additions & 0 deletions pkg/lsp/v2/languages/typescript_lsp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package lang

import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/rs/zerolog/log"
protocol "github.com/tliron/glsp/protocol_3_16"
)

var _ Adapter = (*tsserver)(nil)

type tsserver struct{}

type tsVersion struct {
Version string `json:"version"`
}

// checkNodeRequirements verifies that Node.js and npm are available
func checkNodeRequirements(ctx context.Context) error {
// Check node version
nodeCmd := exec.CommandContext(ctx, "node", "--version")
output, err := nodeCmd.Output()
if err != nil {
return fmt.Errorf("node.js is required but not available: %w", err)
}
nodeVersion := strings.TrimSpace(string(output))

// Ensure minimum Node.js version (example: v14.0.0)
version := strings.TrimPrefix(nodeVersion, "v")
if !isVersionSufficient(version, "14.0.0") {
return fmt.Errorf("node.js version %s is too old, minimum required is 14.0.0", version)
}
log.Debug().Msgf("Found Node.js version: %s", nodeVersion)

// Check npm version
npmBin := "npm"
if runtime.GOOS == "windows" {
npmBin += ".cmd"
}
npmCmd := exec.CommandContext(ctx, npmBin, "--version")
output, err = npmCmd.Output()
if err != nil {
return fmt.Errorf("npm is required but not available: %w", err)
}
npmVersion := strings.TrimSpace(string(output))
log.Debug().Msgf("Found npm version: %s", npmVersion)

return nil
}

// Helper function to compare versions
func isVersionSufficient(current, minimum string) bool {
currentParts := strings.Split(current, ".")
minimumParts := strings.Split(minimum, ".")

for i := 0; i < len(minimumParts); i++ {
if i >= len(currentParts) {
return false
}
if currentParts[i] < minimumParts[i] {
return false
}
if currentParts[i] > minimumParts[i] {
return true
}
}
return true
}

func (a *tsserver) Name() ServerName {
return "typescript-language-server"
}

func (a *tsserver) FetchServerBinary(ctx context.Context, version interface{}, delegate Delegate) (*Binary, error) {
if err := checkNodeRequirements(ctx); err != nil {
log.Error().Err(err).Msg("typescript-language-server: node requirements not met")
return nil, err
}

var ver string
if v, ok := version.(tsVersion); ok {
ver = v.Version
} else {
ver = "latest"
}

installDir, err := delegate.MakeInstallPath(ctx, "typescript-language-server", ver)
if err != nil {
log.Error().Err(err).Msgf("typescript-language-server: failed to make install path %s", ver)
return nil, err
}

serverPath := filepath.Join(installDir, "node_modules", ".bin", "typescript-language-server")
if runtime.GOOS == "windows" {
serverPath += ".cmd"
}

npmBin := "npm"
if runtime.GOOS == "windows" {
npmBin += ".cmd"
}

if !delegate.Exist(ctx, serverPath) {
// Create a package.json if it doesn't exist
pkgJSON := map[string]interface{}{
"name": "ts-lsp-install",
"version": "1.0.0",
"private": true,
}
pkgJSONBytes, _ := json.Marshal(pkgJSON)
pkgJSONPath := filepath.Join(installDir, "package.json")
if err := os.WriteFile(pkgJSONPath, pkgJSONBytes, 0644); err != nil {
return nil, fmt.Errorf("failed to create package.json: %w", err)
}

// Install with all dependencies
cmd := exec.CommandContext(ctx, npmBin, "install", "--save",
"typescript-language-server@"+ver,
"typescript@latest",
"@typescript/vfs@latest", // Virtual file system support
"@typescript-eslint/typescript-estree@latest", // For parsing
)
cmd.Dir = installDir // Set working directory to installDir
if output, err := cmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("failed to install typescript-language-server: %s: %w", string(output), err)
}
}

env := map[string]string{
"NODE_PATH": filepath.Join(installDir, "node_modules"),
"PATH": os.Getenv("PATH"),
// Enable logging for debugging
"TSS_LOG": "-level verbose -file " + filepath.Join(installDir, "tsserver.log"),
"DEBUG": "typescript-language-server:*",
}

return &Binary{
Name: a.Name(),
Path: serverPath,
Arguments: []string{
"--stdio", // Use stdio for communication
"--log-level", "4", // Enable debug logging
},
Env: env,
}, nil
}

func (a *tsserver) FetchLatestServerVersion(ctx context.Context, delegate Delegate) (interface{}, error) {
body, err := delegate.Get(ctx, "https://registry.npmjs.org/typescript-language-server/latest")
if err != nil {
log.Error().Err(err).Msg("typescript-language-server: failed to get version")
return nil, err
}

var version tsVersion
if err := json.Unmarshal(body, &version); err != nil {
log.Error().Err(err).Msg("typescript-language-server: failed to unmarshal version")
return nil, err
}

log.Debug().Msgf("typescript-language-server: got version %+v", version)
return version, nil
}

func (a *tsserver) InitializationOptions(ctx context.Context, delegate Delegate) json.RawMessage {
options := map[string]interface{}{
"documentSymbols": true, // Enable document outline/symbols
"hierarchicalDocumentSymbolSupport": true, // Enable hierarchical symbols
"diagnostics": true, // Enable diagnostics
"completions": true, // Enable completions
"codeActions": true, // Enable code actions
"hover": true, // Enable hover
"implementation": true, // Enable go to implementation
"references": true, // Enable find references
"definition": true, // Enable go to definition
"tsserver": map[string]interface{}{
"maxTsServerMemory": 4096,
"enableProjectDiagnostics": true,
},
}

out, _ := json.Marshal(options)
return out
}

func (a *tsserver) WorkspaceConfiguration(ctx context.Context, delegate Delegate) (json.RawMessage, error) {
return nil, nil
}

func (a *tsserver) CodeActions() ([]protocol.CodeActionKind, error) {
return []protocol.CodeActionKind{
"quickfix", // Standard quick fixes
"refactor", // Generic refactoring actions
"refactor.extract", // Extract code to function/variable
"refactor.inline", // Inline code from function/variable
"refactor.rewrite", // Rewrite code structure
"source", // Source code modifications
"source.organizeImports", // Organize imports
}, nil
}

func (a *tsserver) Languages() []LanguageID {
return []LanguageID{JavaScript, TypeScript, TSX, JSX}
}
3 changes: 1 addition & 2 deletions pkg/lsp/v2/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ type ProcessImpl struct {

func NewProcess(bin lang.Binary) (Process, error) {
cmd := exec.Command(bin.Path, bin.Arguments...)
// TODO: check why this prevents LSP server from working
// cmd.Env = bin.EnvAsKeyVal()
cmd.Env = bin.EnvAsKeyVal()

// Set SysProcAttr to create a new process group
cmd.SysProcAttr = &syscall.SysProcAttr{
Expand Down
4 changes: 2 additions & 2 deletions pkg/lsp/v2/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (r *run) setupServer(ctx context.Context, adapter lang.Adapter, delegate la
return err
}

if err := r.registerSupport(srv, adapter); err != nil {
if err := r.registerSupport(adapter); err != nil {
return err
}

Expand Down Expand Up @@ -140,7 +140,7 @@ func (r *run) registerBin(srv lang.ServerName, bin *lang.Binary) error {
return nil
}

func (r *run) registerSupport(srv lang.ServerName, adapter lang.Adapter) error {
func (r *run) registerSupport(adapter lang.Adapter) error {
r.Lock()
defer r.Unlock()

Expand Down
13 changes: 9 additions & 4 deletions pkg/lsp/v2/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ func (s *ServiceImpl) StartServer(ctx context.Context, languageId lang.LanguageI
DocumentSymbol: &protocol.DocumentSymbolClientCapabilities{
HierarchicalDocumentSymbolSupport: boolPointer(true),
},
PublishDiagnostics: &protocol.PublishDiagnosticsClientCapabilities{
RelatedInformation: boolPointer(true),
},
},
},
WorkspaceFolders: []protocol.WorkspaceFolder{
Expand All @@ -83,7 +86,7 @@ func (s *ServiceImpl) StartServer(ctx context.Context, languageId lang.LanguageI
return fmt.Errorf("failed to initialize language server: %w", err)
}

log.Debug().Str("languageId", languageId).Msg("Initialized language server")
log.Debug().Str("languageId", languageId).Any("initResult", initResult).Msg("Initialized language server")

// Check capabilities
if opt, ok := initResult.Capabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions); ok {
Expand Down Expand Up @@ -198,6 +201,8 @@ func (s *ServiceImpl) GetDocumentOutline(ctx context.Context, file model.File) (
return DocumentOutline{}, NewLanguageServerNotFoundError(lang)
}

log.Debug().Msgf("get document outline %s", file.Path)

symbols, err := cli.GetDocumentSymbols(ctx, protocol.DocumentSymbolParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: DocumentURI(file.Path),
Expand Down Expand Up @@ -269,12 +274,12 @@ func (s *ServiceImpl) Cleanup(ctx context.Context) error {
return nil
}

func (s *ServiceImpl) getClient(ctx context.Context, languageId lang.LanguageID) (Client, bool) {
func (s *ServiceImpl) getClient(_ context.Context, languageId lang.LanguageID) (Client, bool) {
client, ok := s.clientPool.Get(languageId)
return client, ok
}

func (s *ServiceImpl) getClients(ctx context.Context) []Client {
func (s *ServiceImpl) getClients(_ context.Context) []Client {
clients := make([]Client, 0)
for _, client := range s.clientPool.GetAll() {
clients = append(clients, client)
Expand All @@ -284,7 +289,7 @@ func (s *ServiceImpl) getClients(ctx context.Context) []Client {
}

func (s *ServiceImpl) listenForDiagnostics(channel <-chan protocol.PublishDiagnosticsParams) {
log.Debug().Msg("Start listening")
log.Debug().Msg("Start listening for diagnostics")

// reads fro chanel util it is closed
for diagnostics := range channel {
Expand Down
Loading