1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
// Where a project starts: the directory the language server is given as its
// workspace root.
package app
import (
"os"
"path/filepath"
"rickub.com/turbo-editors/turbo-core/profile"
)
// ProjectRoot returns the directory the language server should work in: the
// project root above the first file named on the command line, or the working
// directory when none was.
//
// "Project root" means the nearest directory at or above the file holding one
// of the profile's RootMarkers — go.mod for Go, Cargo.toml for Rust. A
// language server given the wrong root loads the wrong package set and then
// answers nothing at all, with no error, which is the single most confusing way
// completion can fail.
//
// This is deliberately **not** the rule the editor uses for "the project"
// elsewhere: settings and the project tree are rooted in the working directory
// with no walk up, because a module has a real boundary and "the project" does
// not. A language server is the one thing that genuinely needs the module's
// boundary, so it is the one thing that walks.
//
// root := app.ProjectRoot(golang.Profile(), []string{"internal/app/app.go"})
// editor.StartLanguageServer(ctx, root)
func ProjectRoot(p profile.Profile, files []string) string {
start, err := os.Getwd()
if err != nil {
start = "."
}
if len(files) > 0 {
if absolute, err := filepath.Abs(files[0]); err == nil {
start = filepath.Dir(absolute)
}
}
return markedRoot(start, p.RootMarkers)
}
// markedRoot walks up from a directory looking for one of the markers, and
// returns the directory it started from when there is none.
//
// Returning the starting directory rather than the filesystem root is what
// keeps a file edited outside any project working: the server is given
// somewhere plausible instead of somewhere absurd.
func markedRoot(start string, markers []string) string {
if len(markers) == 0 {
return start
}
directory := start
for {
if holdsAny(directory, markers) {
return directory
}
parent := filepath.Dir(directory)
if parent == directory {
return start
}
directory = parent
}
}
// holdsAny reports whether a directory holds one of the marker files.
func holdsAny(directory string, markers []string) bool {
for _, marker := range markers {
if _, err := os.Stat(filepath.Join(directory, marker)); err == nil {
return true
}
}
return false
}
|