confval v0.9.0
This release lets one language server process serve a configuration that spans several document shapes, lets a closed set of words apply to a list rather than only to a single value, and repairs a completion edit that could damage a list.
Highlights
Multi-document configuration
Some configurations span several document shapes: an entrypoint file and included files that carry their own root specs. Until now each shape needed its own server process and its own editor registration, which duplicated your file patterns into editor config.
serve_multi serves every shape from one process.
You declare one binding per shape, pairing a matcher with the shape's root spec and its frontend.
The server picks the schema per document when the editor opens it.
use confval_lsp::{Matcher, bind, serve_multi, Hcl};
serve_multi(vec![
bind::<EntrypointSpec, _>(Matcher::FileName("app.hcl".into()), Hcl),
bind::<MiddlewareSpec, _>(Matcher::Fn(Box::new(middleware_matcher)), Hcl),
])
Bindings are tried in declaration order, and the first match wins.
A document that matches no binding stays open but inert, and one warning log names it.
A matcher must not panic.
On any problem, such as an unreadable file, answer no match.
serve answers every request as it did, including for a never-saved buffer with no file path.
Under serve, each opened document now produces a log message naming the binding it matched.
See the language server guide for the routing rules and the matcher contract.
A keyword set applies to a list
If a field accepts several words from a fixed vocabulary, you used to iterate the list yourself in a Validate implementation.
#[confval(keywords = ...)] now applies to a string list in both shapes.
#[derive(confval::Spec)]
struct LoggingSpec {
#[confval(keywords = LogLevel)]
level: Located<String>,
#[confval(default, keywords = LogEvent)]
events: Vec<Located<String>>,
#[confval(keywords = LogPhase)]
phases: Option<Located<Vec<Located<String>>>>,
}
The derive runs the check once for each element and reports a bad entry at that entry's own span, so a diagnostic underlines the one word you need to change. The set also reaches the schema, so an editor offers the same completions inside the list that it offers on a scalar.
range stays on a scalar leaf, because a list of numbers is not a field shape confval parses.
references stays on a scalar leaf, because the reference pass resolves one value against the labels in scope.
Added
confval_lsp::serve_multi, running one server process over a set of bindings.confval_lsp::Router, the server behind both entry points. Build it withRouter::new(bindings), which returns an error for an empty list, and run it over anylsp-serverconnection you already own.confval_lsp::Bindingandconfval_lsp::bind, pairing a matcher with a root spec and a frontend.confval_lsp::Matcherwith theAny,FileName, andFnvariants.confval_lsp::Validator, the validate pass for one root spec, built withValidator::of::<S>()and passed tohandlers::diagnostics.confval_lsp::LspError, the errorserve,serve_multi, andRouterreturn. It prints throughDisplay, and every error type converts into it, so a function returning it propagates with the question mark.- The server logs routing through
window/logMessage: the matched binding at LOG level, an unmatched document at WARNING. #[confval(keywords = ...)]onVec<Located<String>>and onOption<Located<Vec<Located<String>>>>.KeywordSet::check_each_in, the check the derive emits for a list. Its message names the list, as inunknown value in modes: nope, andcheck_eachkeeps the singular wording for a handwritten caller.SchemaType::scalar,SchemaType::string_list,SchemaType::block, andSchemaType::string_map, the constructors a handwrittenToSchemabuilds with.SchemaType::constraint, which returns the constraint on a scalar or on a list's elements, so your own code renders a constraint without matching both variants.
Changed
confval_lsp::handlers::diagnosticstakes aValidatorinstead of the root spec type parameter. Replacediagnostics::<S>(schema, ...)withdiagnostics(Validator::of::<S>(), schema, ...). The function also computes the line index from the text it receives, so theindexparameter is gone.confval_lsp::serverequiresF: Frontend + Send + 'staticandS: 'static. The shipped frontends and every derived spec already satisfy the bounds, so only a handwritten frontend or spec type that borrows, or a frontend that is notSend, needs a change.confval_lsp::FrontendhasDebugas a supertrait. Add#[derive(Debug)]to a handwritten frontend.LspErrordoes not implementstd::error::Error. Amainthat returnsanyhow::Result<()>orResult<(), Box<dyn Error>>no longer accepts it. ReturnResult<(), confval_lsp::LspError>, or map it withanyhow::anyhow!(error).SchemaType::StringListcarries the constraint its elements declare.SchemaType::StringList,SchemaType::Scalar, andSchemaType::Blockare#[non_exhaustive]. Amatcharm on any of them takes a rest pattern, and a struct literal becomes a constructor call.Constraint::RangeandConstraint::Referencesare#[non_exhaustive]too, and a constraint builds throughConstraint::range,Constraint::keywords, andConstraint::references. MatchingConstraint::Keywords(words)keeps working as it did.handlers::ClientSupportandhandlers::SymbolShapeare#[non_exhaustive]and build throughnew, or throughDefaultforClientSupport.- The derive's messages for a misplaced constraint attribute are reworded. If you snapshot compiler errors, re-record those snapshots.
- The
confval initskills gained a starter-configuration step and a multi file section. Re-run the install on a project scaffolded at 0.8.0 to pick them up. - The minimum supported Rust version is 1.95. Run
rustup updateif your toolchain is older.
Removed
confval_lsp::Server<S, F>.servekeeps the single shape behavior. A host that owns its connection builds the router with one match-everything binding:
// Before
Server::<ServerSpec, Hcl>::new(Hcl).run(&connection)
// After
Router::new(vec![bind::<ServerSpec, Hcl>(Matcher::Any, Hcl)])?.run(&connection)
Fixed
- Accepting a keyword completion inside a list replaced the whole list literal, which deleted the brackets and every other entry. The edit now replaces the one element under the cursor, inserts in an empty container or after a separator, and offers nothing at a position where the accepted text would not parse.
- Hover inside a list element answered nothing. It now names the list field and its keyword set.
- HCL reads and writes
i64::MIN. Parsing-9223372036854775808produces the value instead of an error, andemit_hclwrites it. This requires hcl-edit 0.9.7, whichcargo updatepicks up. An HCL integer past thei64range now reportsoversized integer, the same diagnostic JSON, KDL, and YAML produce.
Upgrading
A single shape server that uses the shipped frontends and calls serve needs no code change.
A handwritten frontend adds #[derive(Debug)], and one that borrows or is not Send no longer compiles.
A direct handlers::diagnostics caller switches to Validator::of::<S>() and drops the line-index argument, which
the function now computes from the text.
A handwritten ToSchema builds every SchemaType and Constraint through the constructors, and a match on
SchemaType takes a rest pattern in each variant's arm.
A derived spec needs neither change.
// Before
SchemaType::StringList
SchemaType::Block { schema: Box::new(inner), repeated: false }
SchemaType::Scalar { leaf, constraint } => ...
SchemaType::StringList => ...
// After
SchemaType::string_list(None)
SchemaType::block(inner, false)
SchemaType::Scalar { leaf, constraint, .. } => ...
SchemaType::StringList { .. } => ...
[dependencies]
confval = { version = "0.9", features = ["derive", "hcl", "color"] }