Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions crates/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2602,7 +2602,7 @@ pub enum LspCommand {
/// Position, 1-based (e.g. src/main.rs:10:4)
spec: String,

/// Workspace location on the server (default: server cwd)
/// Workspace path on the server (relative to the client's cwd)
#[arg(long, default_value = ".")]
root: String,

Expand All @@ -2620,7 +2620,7 @@ pub enum LspCommand {
#[arg(long)]
declaration: bool,

/// Workspace location on the server (default: server cwd)
/// Workspace path on the server (relative to the client's cwd)
#[arg(long, default_value = ".")]
root: String,

Expand All @@ -2634,7 +2634,7 @@ pub enum LspCommand {
/// Position, 1-based (e.g. src/main.rs:10:4)
spec: String,

/// Workspace location on the server (default: server cwd)
/// Workspace path on the server (relative to the client's cwd)
#[arg(long, default_value = ".")]
root: String,

Expand All @@ -2651,7 +2651,7 @@ pub enum LspCommand {
/// Position, 1-based (e.g. src/main.rs:10:4)
spec: String,

/// Workspace location on the server (default: server cwd)
/// Workspace path on the server (relative to the client's cwd)
#[arg(long, default_value = ".")]
root: String,

Expand All @@ -2668,7 +2668,7 @@ pub enum LspCommand {
/// Position, 1-based (e.g. src/main.rs:10:4)
spec: String,

/// Workspace location on the server (default: server cwd)
/// Workspace path on the server (relative to the client's cwd)
#[arg(long, default_value = ".")]
root: String,

Expand All @@ -2687,7 +2687,7 @@ pub enum LspCommand {
#[arg(long)]
file: Option<String>,

/// Workspace location on the server (default: server cwd)
/// Workspace path on the server (relative to the client's cwd)
#[arg(long, default_value = ".")]
root: String,

Expand All @@ -2708,11 +2708,12 @@ pub enum LspCommand {
#[arg(long)]
watch: bool,

/// Wait for language servers to finish indexing first
/// Wait for language servers to finish indexing, then for the next
/// published diagnostics (or 5 seconds without any server update)
#[arg(long)]
wait: bool,

/// Workspace location on the server (default: server cwd)
/// Workspace path on the server (relative to the client's cwd)
#[arg(long, default_value = ".")]
root: String,

Expand All @@ -2730,7 +2731,7 @@ pub enum LspCommand {
/// The new name
new_name: String,

/// Workspace location on the server (default: server cwd)
/// Workspace path on the server (relative to the client's cwd)
#[arg(long, default_value = ".")]
root: String,

Expand All @@ -2741,7 +2742,7 @@ pub enum LspCommand {

/// Block until the workspace's language servers are ready
Wait {
/// Workspace location on the server (default: server cwd)
/// Workspace path on the server (relative to the client's cwd)
#[arg(long, default_value = ".")]
root: String,

Expand Down
11 changes: 7 additions & 4 deletions crates/cli/src/learn.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,12 +337,13 @@ having refused one refspec of several).
Language servers (rust-analyzer, gopls, clangd, …) are discovered by
project markers, spawned on the server, and stay warm across
invocations. Positions are 1-based PATH:LINE:COL; all commands take
`--root` (default: server cwd) and `--json` (NDJSON).
`--root` (default: the client's cwd; relative paths resolve against it) and
`--json` (NDJSON).

```bash
yas lsp wait # block until servers finish indexing
yas lsp diag # current diagnostics (exit 1 if any)
yas lsp diag --wait # settle first — use after editing files
yas lsp diag --wait # wait for the next publish — use after editing files
yas lsp def src/main.rs:10:4 # definition of the symbol at 10:4
yas lsp refs src/main.rs:10:4 # references (--declaration to include it)
yas lsp hover src/main.rs:10:4 # type and docs
Expand All @@ -354,9 +355,11 @@ yas lsp rename src/main.rs:10:4 nm # rename plan (prints edits, never applies)
yas lsp list # running servers (ref, phase, memory)
```

A first call in a fresh workspace may exit 2 with "warming up" — run
A first call in a fresh workspace may exit 2 with `Busy: Warming` — run
`yas lsp wait` once, then query. The edit loop: change files, then
`yas lsp diag --wait` to see resulting errors.
`yas lsp diag --wait` to see resulting errors. `--wait` waits for indexing,
then for the next published diagnostics, or 5 seconds without any server
update when nothing changes.

## Key/value store

Expand Down
42 changes: 38 additions & 4 deletions crates/cli/src/yas_lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::{cli::LspCommand, yas_native::NativeClient};
const STATE_CREDIT: u64 = yas_wire::schema::transport::RECOMMENDED_BUFFERED;
const QUERY_CREDIT: u64 = lsp::MAX_QUERY_BYTES as u64;
const MAX_COLLECTED_RECORDS: usize = 65_536;
const DIAGNOSTICS_LULL: Duration = Duration::from_secs(5);

struct Workspace {
client: NativeClient,
Expand Down Expand Up @@ -431,7 +432,14 @@ async fn cmd_diagnostics(
lsp::request_kind::WATCH,
&Watch {
workspace_handle: workspace.handle,
datasets: yas_wire::schema::lsp::WATCH_DIAGNOSTICS as u16,
// Backend updates carry indexing and check progress, which
// keep --wait from mistaking a running check for a lull.
datasets: if wait {
(yas_wire::schema::lsp::WATCH_DIAGNOSTICS
| yas_wire::schema::lsp::WATCH_BACKEND) as u16
} else {
yas_wire::schema::lsp::WATCH_DIAGNOSTICS as u16
},
state: yas_wire::state::Watch {
initial_credit: STATE_CREDIT,
resume: None,
Expand All @@ -444,11 +452,25 @@ async fn cmd_diagnostics(
let mut diagnostics = BTreeMap::<String, DiagnosticRecord>::new();
let mut snapshot_done = false;
let mut cumulative_credit = STATE_CREDIT;
let mut awaiting_publish = wait && !watch_forever;
let mut baseline: Option<BTreeMap<String, DiagnosticRecord>> = None;
loop {
let frame = workspace
let next = workspace
.client
.next_matching_event(family::LSP, lsp::event_kind::STATE)
.await?;
.next_matching_event(family::LSP, lsp::event_kind::STATE);
let frame = if awaiting_publish && snapshot_done {
match tokio::time::timeout(DIAGNOSTICS_LULL, next).await {
Ok(frame) => frame?,
// A clean workspace publishes nothing distinguishable, so a
// lull with no updates at all ends the wait.
Err(_) => {
let count = print_diagnostics(&diagnostics, filter.as_deref(), json);
return Ok(if count == 0 { 0 } else { 1 });
}
}
} else {
next.await?
};
let event = StateEvent::decode(&frame.payload).map_err(wire_error)?;
if event.subscription_id != result.subscription_id {
continue;
Expand Down Expand Up @@ -503,6 +525,18 @@ async fn cmd_diagnostics(
}
if event.phase == Phase::SnapshotEnd || (snapshot_done && event.phase == Phase::Delta) {
snapshot_done = true;
if awaiting_publish {
// Retained diagnostics may predate the edit being checked;
// wait for a publish that changes them.
match &baseline {
None => {
baseline = Some(diagnostics.clone());
continue;
}
Some(baseline) if *baseline == diagnostics => continue,
Some(_) => awaiting_publish = false,
}
}
let count = print_diagnostics(&diagnostics, filter.as_deref(), json);
if !watch_forever {
workspace
Expand Down
40 changes: 37 additions & 3 deletions crates/server/src/yas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32526,12 +32526,43 @@ async fn run_lsp_watch(
let mut revision = 1u64;
let mut backend = Vec::new();
let mut diagnostics = Vec::new();
// The engine replays its retained diagnostics as the first Diagnostics
// update, so waiting for it lets the initial snapshot carry them rather
// than an empty set that a one-shot reader would take as clean.
let mut initial_acks = Vec::new();
if watch.wants_diagnostics() {
loop {
let event = tokio::select! {
event = watch.next() => event,
_ = cancellation.cancelled() => return,
};
let Some(Ok(super::yas_lsp_adapter::WatchEvent::Snapshot {
stream,
update_id,
entities,
})) = event
else {
return;
};
initial_acks.push((stream, update_id));
match stream {
super::yas_lsp_adapter::WatchStream::Backend => backend = entities,
super::yas_lsp_adapter::WatchStream::Diagnostics => {
diagnostics = entities;
break;
}
}
}
}
let mut initial = session
.buffer_snapshot(workspace_handle)
.unwrap_or_default();
initial.extend(backend.iter().cloned());
initial.extend(diagnostics.iter().cloned());
if send_lsp_snapshot(
subscription_id,
revision,
session
.buffer_snapshot(workspace_handle)
.unwrap_or_default(),
initial,
event_limit,
&mut sent_bytes,
&control,
Expand All @@ -32543,6 +32574,9 @@ async fn run_lsp_watch(
{
return;
}
for (stream, update_id) in initial_acks {
watch.acknowledge(stream, update_id);
}
enum Input {
Backend(Option<Result<super::yas_lsp_adapter::WatchEvent, super::yas_lsp_adapter::Error>>),
Buffer(Result<(), tokio::sync::watch::error::RecvError>),
Expand Down
4 changes: 4 additions & 0 deletions crates/server/src/yas_lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,10 @@ impl Session {
}

impl Watch {
pub(crate) fn wants_diagnostics(&self) -> bool {
self.datasets & yas_wire::schema::lsp::WATCH_DIAGNOSTICS as u16 != 0
}

pub(crate) async fn next(&mut self) -> Option<Result<WatchEvent, Error>> {
while let Some(event) = self.receiver.as_mut()?.recv().await {
match event {
Expand Down
4 changes: 3 additions & 1 deletion docs/design/lsp.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ loss with an exact reason.
`WATCH` subscribes independently to backend lifecycle, diagnostics, and buffer
overlays. Backend records carry phase, progress, epoch, capabilities, stable
identity, resource data, and failure detail. Diagnostic records are complete
per-path replacements, including an explicit empty set to clear a file. State
per-path replacements, including an explicit empty set to clear a file. A
diagnostics subscription's initial snapshot carries the diagnostics the
backends have already published. State
credit and staged snapshots make reconnect and slow-consumer recovery exact.

Every document target combines a component-vector path, revision, and
Expand Down
Loading