1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
use lsp_types::PublishDiagnosticsParams;
use tokio_util::sync::CancellationToken;
use tracing::Span;
use crate::client::Client;
use crate::documents::DocumentsView;
use crate::error::ClientError;
use crate::raw::RequestId;
use crate::workspace::Workspace;
/// Per-request handle to framework state (see ADR 0009).
///
/// The handle exposes connection-scoped capabilities — the established
/// [`Workspace`] (initialization metadata, roots, workspace folders, and the
/// read-only [`DocumentsView`]) and the [`Client`] — without exposing
/// protocol-owned queues or registries.
#[derive(Debug, Clone)]
pub struct Context {
pub(crate) request_id: Option<RequestId>,
pub(crate) span: Span,
pub(crate) client: Client,
/// The connection's established [`Workspace`]. Handlers only run after
/// the initialize transaction has established it, so user code always
/// observes one — there is no workspace-less dispatch.
pub(crate) workspace: Workspace,
pub(crate) cancellation: Option<CancellationToken>,
}
impl Context {
pub(crate) fn for_request(
id: RequestId,
span: Span,
client: Client,
workspace: Workspace,
) -> Self {
Self {
request_id: Some(id),
span,
client,
workspace,
cancellation: None,
}
}
pub(crate) fn for_notification(span: Span, client: Client, workspace: Workspace) -> Self {
Self {
request_id: None,
span,
client,
workspace,
cancellation: None,
}
}
pub(crate) fn with_cancellation(mut self, cancellation: CancellationToken) -> Self {
self.cancellation = Some(cancellation);
self
}
pub(crate) fn cancellation(&self) -> Option<&CancellationToken> {
self.cancellation.as_ref()
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
pub fn span(&self) -> &Span {
&self.span
}
/// The connection's documents, as a read-only [`DocumentsView`] — the
/// same view [`Workspace::documents`] hands out.
///
/// The framework owns and mutates the documents; a handler reads the
/// retained documents and converts positions through this view, and a
/// registered document hook sees it already carrying the built-in
/// mutation (ADR 0018).
pub fn documents(&self) -> DocumentsView {
self.workspace.documents()
}
/// A cheap clone of the typed handle for this connection's LSP client.
pub fn client(&self) -> Client {
self.client.clone()
}
/// The connection's [`Workspace`], established from `InitializeParams`
/// during the initialize transaction (ADR 0017, ADR 0018): the one
/// cheap, shared, read-only view of the initialization metadata, roots,
/// workspace folders, and documents every handler observes.
pub fn workspace(&self) -> &Workspace {
&self.workspace
}
/// Push a `textDocument/publishDiagnostics` notification through the
/// connection's typed [`Client`] (fire-and-forget); see
/// [`Client::publish_diagnostics`] for the exact semantics.
///
/// Serialization and enqueue failures are returned to the caller (the
/// client helper also reports them through `tracing`). A failed publish
/// never invalidates the handler that triggered it, so a handler that
/// treats publishing as best effort may simply drop the error.
pub fn publish_diagnostics(&self, params: PublishDiagnosticsParams) -> Result<(), ClientError> {
self.client.publish_diagnostics(params)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use lsp_types::{InitializeParams, TextDocumentItem, Uri};
use super::*;
use crate::client::{OutboundQueue, OutboundRegistry};
use crate::documents::Documents;
fn context() -> (Context, Documents) {
let (out_tx, _out_rx) = OutboundQueue::new(crate::DEFAULT_OUTBOUND_WARNING_THRESHOLD);
let documents = Documents::new();
let workspace = Workspace::from_params(&InitializeParams::default(), documents.clone());
let client = Client::new(out_tx, OutboundRegistry::default());
(
Context::for_notification(Span::none(), client, workspace),
documents,
)
}
#[test]
fn cloning_shares_connection_state_instead_of_copying_it() {
let (ctx, documents) = context();
let clone = ctx.clone();
let uri = Uri::from_str("file:///shared.rs").unwrap();
documents.open(TextDocumentItem {
uri: uri.clone(),
language_id: "rust".to_string(),
version: 1,
text: "fn main() {}".to_string(),
});
let doc = clone
.documents()
.get(&uri)
.expect("a cloned context reads the same connection documents");
assert_eq!(doc.text(), "fn main() {}");
assert_eq!(
clone.workspace().roots(),
ctx.workspace().roots(),
"a cloned context shares the one workspace"
);
}
}