// Where a project starts: the directory the language server is given as its // workspace root. package app import ( "os" "path/filepath" "codeberg.org/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 }