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