Skip to content

Commit 4cd54a4

Browse files
hardfistCopilot
andauthored
refactor: use project service for lsp (#239)
Co-authored-by: Copilot <[email protected]>
1 parent f4834c0 commit 4cd54a4

File tree

6 files changed

+316
-352
lines changed

6 files changed

+316
-352
lines changed

cmd/rslint/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func (h *IPCHandler) HandleLint(req api.LintRequest) (*api.LintResponse, error)
134134
}
135135

136136
// Run linter
137-
lintedFilesCount, err := linter.RunLinter(
137+
lintedFilesCount, err := linter.RunLinter(
138138
programs,
139139
false, // Don't use single-threaded mode for IPC
140140
allowedFiles,

cmd/rslint/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ func runCMD() int {
482482
singleThreaded,
483483
nil,
484484
utils.ExcludePaths,
485+
485486
func(sourceFile *ast.SourceFile) []linter.ConfiguredRule {
486487
activeRules := rslintconfig.GlobalRuleRegistry.GetEnabledRules(rslintConfig, sourceFile.FileName())
487488
return activeRules

cmd/rslint/lsp.go

Lines changed: 93 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,21 @@ import (
1313

1414
"github.com/microsoft/typescript-go/shim/ast"
1515
"github.com/microsoft/typescript-go/shim/bundled"
16-
"github.com/microsoft/typescript-go/shim/compiler"
16+
"github.com/microsoft/typescript-go/shim/core"
1717
"github.com/microsoft/typescript-go/shim/ls"
1818
"github.com/microsoft/typescript-go/shim/lsp/lsproto"
1919
"github.com/microsoft/typescript-go/shim/project"
2020
"github.com/microsoft/typescript-go/shim/scanner"
21+
22+
// "github.com/microsoft/typescript-go/shim/tspath"
2123
"github.com/microsoft/typescript-go/shim/vfs"
22-
"github.com/microsoft/typescript-go/shim/vfs/cachedvfs"
2324
"github.com/microsoft/typescript-go/shim/vfs/osvfs"
25+
2426
"github.com/sourcegraph/jsonrpc2"
2527
"github.com/web-infra-dev/rslint/internal/config"
2628
"github.com/web-infra-dev/rslint/internal/linter"
2729
"github.com/web-infra-dev/rslint/internal/rule"
28-
"github.com/web-infra-dev/rslint/internal/utils"
30+
util "github.com/web-infra-dev/rslint/internal/utils"
2931
)
3032

3133
func ptrTo[T any](v T) *T {
@@ -41,16 +43,50 @@ type LSPServer struct {
4143
// align with https://github.com/microsoft/typescript-go/blob/5cdf239b02006783231dd4da8ca125cef398cd27/internal/lsp/server.go#L147
4244
//nolint
4345
projectService *project.Service
46+
//nolint
47+
logger *project.Logger
48+
fs vfs.FS
49+
defaultLibraryPath string
50+
typingsLocation string
51+
cwd string
52+
rslintConfig config.RslintConfig
53+
}
54+
55+
func (s *LSPServer) FS() vfs.FS {
56+
return s.fs
57+
}
58+
func (s *LSPServer) DefaultLibraryPath() string {
59+
return s.defaultLibraryPath
60+
}
61+
func (s *LSPServer) TypingsLocation() string {
62+
return s.typingsLocation
63+
}
64+
func (s *LSPServer) GetCurrentDirectory() string {
65+
return s.cwd
66+
}
67+
func (s *LSPServer) Client() project.Client {
68+
return nil
69+
}
70+
71+
// FIXME: support watcher in the future
72+
func (s *LSPServer) WatchFiles(ctx context.Context, watchers []*lsproto.FileSystemWatcher) (project.WatcherHandle, error) {
73+
return "", nil
4474
}
4575

4676
func NewLSPServer() *LSPServer {
77+
log.Printf("cwd: %v", util.Must(os.Getwd()))
4778
return &LSPServer{
4879
documents: make(map[lsproto.DocumentUri]string),
4980
diagnostics: make(map[lsproto.DocumentUri][]rule.RuleDiagnostic),
81+
fs: bundled.WrapFS(osvfs.FS()),
82+
cwd: util.Must(os.Getwd()),
5083
}
5184
}
5285

53-
func (s *LSPServer) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) {
86+
func (s *LSPServer) Handle(requestCtx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) {
87+
// FIXME: implement cancel logic
88+
ctx := core.WithRequestID(requestCtx, req.ID.String())
89+
5490
s.conn = conn
5591
switch req.Method {
5692
case "initialize":
@@ -108,6 +144,39 @@ func (s *LSPServer) handleInitialize(ctx context.Context, req *jsonrpc2.Request)
108144
}
109145
}
110146

147+
s.projectService = project.NewService(s, project.ServiceOptions{
148+
Logger: project.NewLogger([]io.Writer{os.Stderr}, "tsgo.log", project.LogLevelVerbose),
149+
PositionEncoding: lsproto.PositionEncodingKindUTF8,
150+
})
151+
// Try to find rslint configuration files with multiple strategies
152+
var rslintConfigPath string
153+
var configFound bool
154+
155+
// Use helper function to find config
156+
rslintConfigPath, configFound = findRslintConfig(s.fs, s.cwd)
157+
158+
if !configFound {
159+
return nil, errors.New("config file not found")
160+
}
161+
162+
// Load rslint configuration and extract tsconfig paths
163+
loader := config.NewConfigLoader(s.fs, s.cwd)
164+
rslintConfig, configDirectory, err := loader.LoadRslintConfig(rslintConfigPath)
165+
if err != nil {
166+
return nil, fmt.Errorf("could not load rslint config: %w", err)
167+
}
168+
s.rslintConfig = rslintConfig
169+
tsConfigs, err := loader.LoadTsConfigsFromRslintConfig(rslintConfig, configDirectory)
170+
if err != nil {
171+
return nil, fmt.Errorf("could not load TypeScript configs from rslint config: %w", err)
172+
}
173+
174+
if len(tsConfigs) == 0 {
175+
return nil, errors.New("no TypeScript configurations found in rslint config")
176+
}
177+
178+
// Do not pre-create configured projects here. The service will create
179+
// configured or inferred projects on demand when files are opened.
111180
result := &lsproto.InitializeResult{
112181
Capabilities: &lsproto.ServerCapabilities{
113182
TextDocumentSync: &lsproto.TextDocumentSyncOptionsOrKind{
@@ -123,7 +192,7 @@ func (s *LSPServer) handleInitialize(ctx context.Context, req *jsonrpc2.Request)
123192
}
124193

125194
func (s *LSPServer) handleDidOpen(ctx context.Context, req *jsonrpc2.Request) {
126-
log.Printf("Handling didOpen: %+v", req)
195+
log.Printf("Handling didOpen: %+v,%+v", req, ctx)
127196
var params lsproto.DidOpenTextDocumentParams
128197
if err := json.Unmarshal(*req.Params, &params); err != nil {
129198
return
@@ -173,7 +242,7 @@ func (s *LSPServer) handleShutdown(ctx context.Context, req *jsonrpc2.Request) (
173242
}
174243

175244
func (s *LSPServer) handleCodeAction(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) {
176-
log.Printf("Handling codeAction: %+v", req)
245+
log.Printf("Handling codeAction: %+v,%+v", req, ctx)
177246
var params lsproto.CodeActionParams
178247
if err := json.Unmarshal(*req.Params, &params); err != nil {
179248
return nil, &jsonrpc2.Error{
@@ -254,100 +323,10 @@ func (s *LSPServer) runDiagnostics(ctx context.Context, uri lsproto.DocumentUri,
254323
// Initialize rule registry with all available rules (ensure it's done once)
255324
config.RegisterAllRules()
256325

257-
// Convert URI to file path
258-
filePath := uriToPath(uri)
259-
260-
// Create a temporary file system with the content
261-
vfs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
262-
263-
// Determine working directory with improved fallback strategy
264-
workingDir := s.rootURI
265-
if workingDir == "" || workingDir == "." {
266-
// If rootURI is not set properly, try to infer from the file path
267-
if filePath != "" {
268-
// Use the directory of the current file as a starting point
269-
if idx := strings.LastIndex(filePath, "/"); idx != -1 {
270-
workingDir = filePath[:idx]
271-
} else {
272-
workingDir = "."
273-
}
274-
} else {
275-
workingDir = "."
276-
}
277-
}
278-
279-
host := utils.CreateCompilerHost(workingDir, vfs)
280-
281-
// Try to find rslint configuration files with multiple strategies
282-
var rslintConfigPath string
283-
var configFound bool
284-
285-
// Use helper function to find config
286-
rslintConfigPath, workingDir, configFound = findRslintConfig(vfs, workingDir, filePath)
287-
288-
if !configFound {
289-
return
290-
}
291-
292-
// Load rslint configuration and extract tsconfig paths
293-
loader := config.NewConfigLoader(vfs, workingDir)
294-
rslintConfig, configDirectory, err := loader.LoadRslintConfig(rslintConfigPath)
295-
if err != nil {
296-
log.Printf("Could not load rslint config: %v", err)
297-
return
298-
}
299-
300-
tsConfigs, err := loader.LoadTsConfigsFromRslintConfig(rslintConfig, configDirectory)
301-
if err != nil {
302-
log.Printf("Could not load TypeScript configs from rslint config: %v", err)
303-
return
304-
}
305-
306-
if len(tsConfigs) == 0 {
307-
log.Printf("No TypeScript configurations found in rslint config")
308-
return
309-
}
310-
311-
// Create multiple programs for all tsconfig files
312-
var programs []*compiler.Program
313-
var targetFile *ast.SourceFile
314-
315-
for _, tsConfigPath := range tsConfigs {
316-
program, err := utils.CreateProgram(true, vfs, workingDir, tsConfigPath, host)
317-
if err != nil {
318-
log.Printf("Could not create program for %s: %v", tsConfigPath, err)
319-
continue
320-
}
321-
programs = append(programs, program)
322-
323-
// Check if the current file is in this program
324-
sourceFiles := program.GetSourceFiles()
325-
326-
if targetFile == nil {
327-
for _, sf := range sourceFiles {
328-
if strings.HasSuffix(sf.FileName(), filePath) || sf.FileName() == filePath {
329-
targetFile = sf
330-
break
331-
}
332-
}
333-
}
334-
}
335-
336-
if len(programs) == 0 {
337-
log.Printf("Could not create any programs")
338-
return
339-
}
340-
341-
if targetFile == nil {
342-
// If we can't find the file in any program, skip diagnostics
343-
log.Printf("Could not find file %s in any program", filePath)
344-
return
345-
}
346-
347326
// Collect diagnostics
348327
var lsp_diagnostics []*lsproto.Diagnostic
349328

350-
rule_diags, err := runLintWithPrograms(uri, programs, rslintConfig)
329+
rule_diags, err := runLintWithProjectService(uri, s.projectService, ctx, s.rslintConfig)
351330

352331
if err != nil {
353332
log.Printf("Error running lint: %v", err)
@@ -410,41 +389,17 @@ func uriToPath(uri lsproto.DocumentUri) string {
410389
}
411390

412391
// findRslintConfig searches for rslint configuration files using multiple strategies
413-
func findRslintConfig(fs vfs.FS, workingDir, filePath string) (string, string, bool) {
392+
func findRslintConfig(fs vfs.FS, workingDir string) (string, bool) {
414393
defaultConfigs := []string{"rslint.json", "rslint.jsonc"}
415394

416395
// Strategy 1: Try in the working directory
417396
for _, configName := range defaultConfigs {
418397
configPath := workingDir + "/" + configName
419398
if fs.FileExists(configPath) {
420-
return configPath, workingDir, true
421-
}
422-
}
423-
424-
// Strategy 2: If not found, walk up the directory tree
425-
if filePath != "" {
426-
dir := filePath
427-
if idx := strings.LastIndex(dir, "/"); idx != -1 {
428-
dir = dir[:idx]
429-
}
430-
431-
for i := 0; i < 5 && dir != "/" && dir != ""; i++ { // Limit search depth
432-
for _, configName := range defaultConfigs {
433-
testPath := dir + "/" + configName
434-
if fs.FileExists(testPath) {
435-
return testPath, dir, true
436-
}
437-
}
438-
// Move up one directory
439-
if idx := strings.LastIndex(dir, "/"); idx != -1 {
440-
dir = dir[:idx]
441-
} else {
442-
break
443-
}
399+
return configPath, true
444400
}
445401
}
446-
447-
return "", workingDir, false
402+
return "", false
448403
}
449404

450405
func runLSP() int {
@@ -484,14 +439,20 @@ type LintResponse struct {
484439
RuleCount int `json:"ruleCount"`
485440
}
486441

487-
func runLintWithPrograms(uri lsproto.DocumentUri, programs []*compiler.Program, rslintConfig config.RslintConfig) ([]rule.RuleDiagnostic, error) {
488-
if len(programs) == 0 {
489-
return nil, errors.New("no programs provided")
490-
}
491-
442+
func runLintWithProjectService(uri lsproto.DocumentUri, service *project.Service, ctx context.Context, rslintConfig config.RslintConfig) ([]rule.RuleDiagnostic, error) {
443+
log.Printf("context: %v", ctx)
492444
// Initialize rule registry with all available rules
493445
config.RegisterAllRules()
494-
446+
filename := uriToPath(uri)
447+
content, ok := service.FS().ReadFile(filename)
448+
if !ok {
449+
return nil, fmt.Errorf("failed to read file %s", filename)
450+
}
451+
service.OpenFile(filename, content, core.GetScriptKindFromFileName(filename), service.GetCurrentDirectory())
452+
project := service.EnsureDefaultProjectForURI(uri)
453+
languageService, done := project.GetLanguageServiceForRequest(ctx)
454+
program := languageService.GetProgram()
455+
defer done()
495456
// Collect diagnostics
496457
var diagnostics []rule.RuleDiagnostic
497458
var diagnosticsLock sync.Mutex
@@ -502,23 +463,12 @@ func runLintWithPrograms(uri lsproto.DocumentUri, programs []*compiler.Program,
502463
defer diagnosticsLock.Unlock()
503464
diagnostics = append(diagnostics, d)
504465
}
505-
filename := uriToPath(uri)
506466

507-
// Run linter with all programs using rule registry
508-
_, err := linter.RunLinter(
509-
programs,
510-
false, // Don't use single-threaded mode for LSP
511-
[]string{filename},
512-
utils.ExcludePaths,
467+
linter.RunLinterInProgram(program, []string{filename}, util.ExcludePaths,
513468
func(sourceFile *ast.SourceFile) []linter.ConfiguredRule {
514469
activeRules := config.GlobalRuleRegistry.GetEnabledRules(rslintConfig, sourceFile.FileName())
515470
return activeRules
516-
},
517-
diagnosticCollector,
518-
)
519-
if err != nil {
520-
return nil, fmt.Errorf("error running linter: %w", err)
521-
}
471+
}, diagnosticCollector)
522472

523473
if diagnostics == nil {
524474
diagnostics = []rule.RuleDiagnostic{}

cmd/rslint/lsp_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,12 @@ func TestFindRslintConfig(t *testing.T) {
212212
}
213213

214214
// Call the function under test
215-
gotPath, gotDir, gotFound := findRslintConfig(mockFS, tt.workingDir, tt.filePath)
215+
gotPath, gotFound := findRslintConfig(mockFS, tt.workingDir)
216216

217217
// Assert results
218218
if gotPath != tt.expectedPath {
219219
t.Errorf("findRslintConfig() gotPath = %v, want %v", gotPath, tt.expectedPath)
220220
}
221-
if gotDir != tt.expectedDir {
222-
t.Errorf("findRslintConfig() gotDir = %v, want %v", gotDir, tt.expectedDir)
223-
}
224221
if gotFound != tt.expectedFound {
225222
t.Errorf("findRslintConfig() gotFound = %v, want %v", gotFound, tt.expectedFound)
226223
}

0 commit comments

Comments
 (0)