Summary
When syncOnStartup is enabled but the LSP client does not send rootUri in the initialize request, vale sync fails with OS error 2 (ENOENT) without ever invoking the vale binary.
Root cause
do_sync passes root_path() as the working directory to cli.sync(). root_path() is populated from rootUri in the LSP initialize request. When the client does not send rootUri (common in IDE-embedded LS clients such as Qt Creator's Vale extension), cwd remains an empty string. Passing "" to Command::current_dir causes the OS to reject the process spawn with ENOENT before vale is ever executed.
// server.rs
async fn do_sync(&self) {
match self.cli.sync(self.config_path(), self.root_path()) { // root_path() may be ""
// vale.rs
pub(crate) fn sync(&self, config_path: String, cwd: String) -> Result<(), Error> {
...
let _ = Command::new(exe.as_os_str())
.current_dir(cwd.clone()) // "" → OS error 2 before vale is spawned
Why linting still works
run() always sets cwd = fp.parent() — the parent directory of the file being linted — so it never relies on root_path() and is unaffected.
Why cwd doesn't matter for sync
vale sync only downloads packages listed in the config. When configPath is an absolute path, vale does not use cwd to locate the config or StylesPath. Any valid directory works — cwd is only needed to satisfy the OS process spawn requirement.
Fix
Fall back to env::current_dir() in sync() when cwd is empty:
let resolved_cwd = if cwd.is_empty() {
env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))
} else {
PathBuf::from(&cwd)
};
let exe = self.exe_path(false)?;
let _ = Command::new(exe.as_os_str())
.current_dir(resolved_cwd)
.args(args)
.output()?;
Steps to reproduce
- Configure vale-ls with
syncOnStartup: true and an absolute configPath.
- Use an LSP client that does not send
rootUri on initialize (e.g. Qt Creator's Vale LS extension).
- Start vale-ls — it reports "Failed to sync CLI: OS error 2".
- Run
vale sync manually in a terminal → succeeds.
- Reopen the file — linting works normally.
Summary
When
syncOnStartupis enabled but the LSP client does not sendrootUriin theinitializerequest,vale syncfails with OS error 2 (ENOENT) without ever invoking the vale binary.Root cause
do_syncpassesroot_path()as the working directory tocli.sync().root_path()is populated fromrootUriin the LSPinitializerequest. When the client does not sendrootUri(common in IDE-embedded LS clients such as Qt Creator's Vale extension),cwdremains an empty string. Passing""toCommand::current_dircauses the OS to reject the process spawn with ENOENT before vale is ever executed.Why linting still works
run()always setscwd = fp.parent()— the parent directory of the file being linted — so it never relies onroot_path()and is unaffected.Why cwd doesn't matter for sync
vale synconly downloads packages listed in the config. WhenconfigPathis an absolute path, vale does not usecwdto locate the config orStylesPath. Any valid directory works —cwdis only needed to satisfy the OS process spawn requirement.Fix
Fall back to
env::current_dir()insync()whencwdis empty:Steps to reproduce
syncOnStartup: trueand an absoluteconfigPath.rootUrion initialize (e.g. Qt Creator's Vale LS extension).vale syncmanually in a terminal → succeeds.