Skip to main content

rune/languageserver/
mod.rs

1//! Utility for building a language server.
2
3#![allow(clippy::too_many_arguments)]
4
5// The tests drive a whole server, which needs a tokio runtime with timers and
6// asks the filesystem whether a path the client named is a file. Neither is
7// something miri can run, and there is no unsafe code here for it to check, so
8// they are left out of it the way the tests in `crate::tests` are.
9#[cfg(all(test, not(miri)))]
10mod tests;
11
12mod completion;
13mod connection;
14pub mod envelope;
15mod fs;
16mod state;
17mod url;
18
19use anyhow::Context as _;
20use lsp::notification::Notification;
21use lsp::request::Request;
22use serde::Deserialize;
23#[cfg(feature = "std")]
24use tokio::io::{self, Stdin, Stdout};
25use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt as _};
26use tokio::sync::Notify;
27
28use crate::alloc::{try_format, String};
29use crate::languageserver::envelope::Code;
30use crate::languageserver::state::State;
31use crate::support::Result;
32use crate::workspace::MANIFEST_FILE;
33use crate::{Context, Options};
34
35use self::connection::Input;
36use self::state::StateEncoding;
37
38/// Construct a new empty builder without any configured I/O.
39///
40/// In order to actually call build, the input and output streams must be
41/// configured using [`with_input`], and [`with_output`], or a method such as
42/// [`with_stdio`].
43///
44/// [`with_input`]: Builder::with_input
45/// [`with_output`]: Builder::with_output
46/// [`with_stdio`]: Builder::with_stdio
47///
48/// # Examples
49///
50/// ```no_run
51/// use rune::Context;
52/// use rune::languageserver;
53///
54/// let context = Context::with_default_modules()?;
55///
56/// let languageserver = languageserver::builder()
57///     .with_context(context)
58///     .with_stdio()
59///     .build()?;
60///
61/// # Ok::<_, rune::support::Error>(())
62/// ```
63pub fn builder() -> Builder<Unset, Unset> {
64    Builder {
65        input: Unset,
66        output: Unset,
67        context: None,
68        options: None,
69    }
70}
71
72/// A builder for a language server.
73///
74/// See [`builder()`] for more details.
75pub struct Builder<I, O> {
76    input: I,
77    output: O,
78    context: Option<Context>,
79    options: Option<Options>,
80}
81
82/// Unset placeholder I/O types for language server.
83///
84/// These must be replaced in order to actually construct a language server.
85///
86/// See [`builder()`] for more details.
87pub struct Unset;
88
89impl<I, O> Builder<I, O> {
90    /// Associate the specified input with the builder.
91    pub fn with_input<T>(self, input: T) -> Builder<T, O>
92    where
93        T: Unpin + AsyncRead,
94    {
95        Builder {
96            input,
97            output: self.output,
98            context: self.context,
99            options: self.options,
100        }
101    }
102
103    /// Associate the specified output with the builder.
104    pub fn with_output<T>(self, output: T) -> Builder<I, T>
105    where
106        T: Unpin + AsyncWrite,
107    {
108        Builder {
109            input: self.input,
110            output,
111            context: self.context,
112            options: self.options,
113        }
114    }
115
116    /// Associate [`Stdin`] and [`Stdout`] as the input and output of the
117    /// builder.
118    #[cfg(feature = "std")]
119    #[cfg_attr(rune_docsrs, doc(cfg(feature = "std")))]
120    pub fn with_stdio(self) -> Builder<Stdin, Stdout> {
121        self.with_input(io::stdin()).with_output(io::stdout())
122    }
123
124    /// Associate the specified context with the builder.
125    ///
126    /// If none is specified, a default context will be constructed.
127    pub fn with_context(self, context: Context) -> Self {
128        Self {
129            input: self.input,
130            output: self.output,
131            context: Some(context),
132            options: self.options,
133        }
134    }
135
136    /// Associate the specified options with the builder.
137    pub fn with_options(self, options: Options) -> Self {
138        Self {
139            input: self.input,
140            output: self.output,
141            context: self.context,
142            options: Some(options),
143        }
144    }
145
146    /// Build a new language server using the provided options.
147    pub fn build(self) -> Result<LanguageServer<I, O>>
148    where
149        I: Unpin + AsyncRead,
150        O: Unpin + AsyncWrite,
151    {
152        let context = match self.context {
153            Some(context) => context,
154            None => Context::with_default_modules()?,
155        };
156
157        let options = match self.options {
158            Some(options) => options,
159            None => Options::from_default_env()?,
160        };
161
162        Ok(LanguageServer {
163            input: self.input,
164            output: self.output,
165            context,
166            options,
167        })
168    }
169}
170
171enum Language {
172    Rune,
173    Other,
174}
175
176/// The instance of a language server, as constructed through [`builder()`].
177pub struct LanguageServer<I, O> {
178    input: I,
179    output: O,
180    context: Context,
181    options: Options,
182}
183
184impl<I, O> LanguageServer<I, O>
185where
186    I: Unpin + AsyncRead,
187    O: Unpin + AsyncWrite,
188{
189    /// Run a language server.
190    pub async fn run(mut self) -> Result<()> {
191        let mut input = Input::new(self.input);
192
193        let rebuild_notify = Notify::new();
194
195        let rebuild = rebuild_notify.notified();
196        tokio::pin!(rebuild);
197
198        let mut state = State::new(&rebuild_notify, self.context, self.options);
199        tracing::info!("Starting server");
200        state.rebuild()?;
201
202        let mut content = rust_alloc::vec::Vec::new();
203
204        while !state.is_stopped() {
205            tokio::select! {
206                _ = rebuild.as_mut() => {
207                    tracing::info!("Rebuilding project");
208                    state.rebuild()?;
209                    rebuild.set(rebuild_notify.notified());
210                },
211                len = self.output.write(state.out.readable()), if !state.out.is_empty() => {
212                    let len = len.context("writing output")?;
213                    state.out.advance(len);
214
215                    if state.out.is_empty() {
216                        self.output.flush().await.context("flushing output")?;
217                    }
218                },
219                frame = input.next(&mut content) => {
220                    if !frame? {
221                        break;
222                    };
223
224                    let incoming: envelope::IncomingMessage<'_> = serde_json::from_slice(&content)?;
225                    tracing::trace!(?incoming);
226
227                    // If server is not initialized, reject incoming requests.
228                    if !state.is_initialized() && incoming.method != lsp::request::Initialize::METHOD {
229                        state.out
230                            .error(
231                                incoming.id,
232                                Code::InvalidRequest,
233                                "Server not initialized",
234                                None::<()>,
235                            )?;
236
237                        continue;
238                    }
239
240                    macro_rules! handle {
241                        ($(req($req_ty:ty, $req_handle:ident)),* $(, notif($notif_ty:ty, $notif_handle:ident))* $(,)?) => {
242                            match incoming.method {
243                                $(<$req_ty>::METHOD => {
244                                    // A request which cannot be served is
245                                    // answered with an error. Leaving it to
246                                    // end the loop instead takes the whole
247                                    // session down - every open file, every
248                                    // diagnostic - over one request, and a
249                                    // position the document does not have is
250                                    // just a client which is a keystroke
251                                    // ahead of us.
252                                    match <$req_ty as Request>::Params::deserialize(incoming.params) {
253                                        Ok(params) => match $req_handle(&mut state, params) {
254                                            Ok(result) => {
255                                                state.out.response(incoming.id, result)?;
256                                            }
257                                            Err(error) => {
258                                                state.out.error(
259                                                    incoming.id,
260                                                    Code::InternalError,
261                                                    try_format!("{}: {error}", <$req_ty>::METHOD),
262                                                    None::<()>,
263                                                )?;
264                                            }
265                                        },
266                                        Err(error) => {
267                                            state.out.error(
268                                                incoming.id,
269                                                Code::InvalidParams,
270                                                try_format!("{}: {error}", <$req_ty>::METHOD),
271                                                None::<()>,
272                                            )?;
273                                        }
274                                    }
275                                })*
276                                $(<$notif_ty>::METHOD => {
277                                    // A notification has no reply to put an
278                                    // error in, so it is logged.
279                                    let result = match <$notif_ty as Notification>::Params::deserialize(incoming.params) {
280                                        Ok(params) => $notif_handle(&mut state, params),
281                                        Err(error) => Err(error.into()),
282                                    };
283
284                                    if let Err(error) = result {
285                                        state.out.log(
286                                            lsp::MessageType::WARNING,
287                                            format_args!("{}: {error}", <$notif_ty>::METHOD),
288                                        )?;
289                                    }
290                                })*
291                                _ => {
292                                    state.out.log(
293                                        lsp::MessageType::INFO,
294                                        format!("Unhandled method `{}`", incoming.method),
295                                    )?;
296                                    state.out.method_not_found(incoming.id)?;
297                                }
298                            }
299                        }
300                    }
301
302                    handle! {
303                        req(lsp::request::Initialize, initialize),
304                        req(lsp::request::Shutdown, shutdown),
305                        req(lsp::request::GotoDefinition, goto_definition),
306                        req(lsp::request::Completion, completion),
307                        req(lsp::request::Formatting, formatting),
308                        req(lsp::request::RangeFormatting, range_formatting),
309                        notif(lsp::notification::DidOpenTextDocument, did_open_text_document),
310                        notif(lsp::notification::DidChangeTextDocument, did_change_text_document),
311                        notif(lsp::notification::DidCloseTextDocument, did_close_text_document),
312                        notif(lsp::notification::DidSaveTextDocument, did_save_text_document),
313                        notif(lsp::notification::Initialized, initialized),
314                    }
315
316                    content.clear();
317                },
318            }
319        }
320
321        while !state.out.is_empty() {
322            let len = self.output.write(state.out.readable()).await?;
323            state.out.advance(len);
324        }
325
326        Ok(())
327    }
328}
329
330fn is_utf8(params: &lsp::InitializeParams) -> bool {
331    let Some(general) = &params.capabilities.general else {
332        return false;
333    };
334
335    let Some(encodings) = &general.position_encodings else {
336        return false;
337    };
338
339    for encoding in encodings {
340        if *encoding == lsp::PositionEncodingKind::UTF8 {
341            return true;
342        }
343    }
344
345    false
346}
347
348/// Initialize the language state.
349fn initialize(s: &mut State<'_>, params: lsp::InitializeParams) -> Result<lsp::InitializeResult> {
350    s.initialize();
351
352    s.out
353        .log(lsp::MessageType::INFO, "Starting language server")?;
354
355    let position_encoding = if is_utf8(&params) {
356        s.encoding = StateEncoding::Utf8;
357        Some(lsp::PositionEncodingKind::UTF8)
358    } else {
359        None
360    };
361
362    s.out.log(
363        lsp::MessageType::INFO,
364        format_args!("Using {} position encoding", s.encoding),
365    )?;
366
367    let capabilities = lsp::ServerCapabilities {
368        position_encoding,
369        text_document_sync: Some(lsp::TextDocumentSyncCapability::Kind(
370            lsp::TextDocumentSyncKind::INCREMENTAL,
371        )),
372        definition_provider: Some(lsp::OneOf::Left(true)),
373        completion_provider: Some(lsp::CompletionOptions {
374            all_commit_characters: None,
375            resolve_provider: Some(false),
376            trigger_characters: Some(vec![".".into(), "::".into()]),
377            work_done_progress_options: lsp::WorkDoneProgressOptions {
378                work_done_progress: None,
379            },
380            completion_item: Some(lsp::CompletionOptionsCompletionItem {
381                label_details_support: Some(true),
382            }),
383        }),
384        document_formatting_provider: Some(lsp::OneOf::Left(true)),
385        document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
386        ..Default::default()
387    };
388
389    let server_info = lsp::ServerInfo {
390        name: String::try_from("Rune Language Server")?.into_std(),
391        version: None,
392    };
393
394    let mut rebuild = false;
395
396    #[allow(deprecated)]
397    if let Some(root_uri) = &params.root_uri {
398        let mut manifest_uri = root_uri.clone();
399
400        if let Ok(mut path) = manifest_uri.path_segments_mut() {
401            path.push(MANIFEST_FILE);
402        }
403
404        if let Ok(manifest_path) = manifest_uri.to_file_path() {
405            if fs::is_file(&manifest_path)? {
406                tracing::trace!(?manifest_uri, ?manifest_path, "Activating workspace");
407                s.workspace.manifest_path = Some((manifest_uri, manifest_path));
408                rebuild = true;
409            }
410        }
411    }
412
413    if rebuild {
414        s.rebuild_interest();
415    }
416
417    Ok(lsp::InitializeResult {
418        capabilities,
419        server_info: Some(server_info),
420    })
421}
422
423fn shutdown(s: &mut State<'_>, _: ()) -> Result<()> {
424    s.stop();
425    Ok(())
426}
427
428/// Handle initialized notification.
429fn initialized(_: &mut State<'_>, _: lsp::InitializedParams) -> Result<()> {
430    tracing::info!("Initialized");
431    Ok(())
432}
433
434/// Handle initialized notification.
435fn goto_definition(
436    s: &mut State<'_>,
437    params: lsp::GotoDefinitionParams,
438) -> Result<Option<lsp::GotoDefinitionResponse>> {
439    let position = s.goto_definition(
440        &params.text_document_position_params.text_document.uri,
441        params.text_document_position_params.position,
442    )?;
443
444    Ok(position.map(lsp::GotoDefinitionResponse::Scalar))
445}
446
447/// Handle initialized notification.
448fn completion(
449    state: &mut State<'_>,
450    params: lsp::CompletionParams,
451) -> Result<Option<lsp::CompletionResponse>> {
452    let Some(results) = state.complete(
453        &params.text_document_position.text_document.uri,
454        params.text_document_position.position,
455    )?
456    else {
457        return Ok(None);
458    };
459
460    Ok(Some(lsp::CompletionResponse::Array(results.into_std())))
461}
462
463/// Handle formatting request.
464fn formatting(
465    state: &mut State<'_>,
466    params: lsp::DocumentFormattingParams,
467) -> Result<Option<rust_alloc::vec::Vec<lsp::TextEdit>>> {
468    state
469        .format(&params.text_document.uri, &params.options)
470        .map(|option| option.map(|formatted| vec![formatted]))
471}
472
473/// Handle formatting request.
474fn range_formatting(
475    state: &mut State<'_>,
476    params: lsp::DocumentRangeFormattingParams,
477) -> Result<Option<rust_alloc::vec::Vec<lsp::TextEdit>>> {
478    state
479        .range_format(&params.text_document.uri, &params.range, &params.options)
480        .map(|option| option.map(|formatted| vec![formatted]))
481}
482
483/// Handle open text document.
484fn did_open_text_document(s: &mut State<'_>, params: lsp::DidOpenTextDocumentParams) -> Result<()> {
485    let lagnuage = match params.text_document.language_id.as_str() {
486        "rune" => Language::Rune,
487        _ => Language::Other,
488    };
489
490    if s.workspace
491        .insert_source(
492            params.text_document.uri.clone(),
493            params.text_document.text.try_into()?,
494            lagnuage,
495        )?
496        .is_some()
497    {
498        tracing::warn!(
499            "opened text document `{}`, but it was already open!",
500            params.text_document.uri
501        );
502    }
503
504    s.rebuild_interest();
505    Ok(())
506}
507
508/// Handle open text document.
509fn did_change_text_document(
510    s: &mut State<'_>,
511    params: lsp::DidChangeTextDocumentParams,
512) -> Result<()> {
513    let mut interest = false;
514    let mut rejected = crate::alloc::Vec::new();
515
516    if let Some(source) = s.workspace.get_mut(&params.text_document.uri) {
517        for change in params.content_changes {
518            let result = if let Some(range) = change.range {
519                source.modify_lsp_range(&s.encoding, range, &change.text)
520            } else {
521                source.modify_lsp_full_range(&change.text)
522            };
523
524            // A range which the document does not have is a client which is
525            // out of step with us. Stopping over it loses the whole session -
526            // every open file, every diagnostic - for one edit which did not
527            // land, so it is reported and the rest are applied.
528            if let Err(error) = result {
529                rejected.try_push(error)?;
530                continue;
531            }
532
533            interest = true;
534        }
535    } else {
536        tracing::warn!(
537            "tried to modify `{}`, but it was not open!",
538            params.text_document.uri
539        );
540    }
541
542    for error in rejected {
543        s.out.log(
544            lsp::MessageType::WARNING,
545            format_args!(
546                "{}: a change could not be applied: {error}",
547                params.text_document.uri
548            ),
549        )?;
550    }
551
552    if interest {
553        s.rebuild_interest();
554    }
555
556    Ok(())
557}
558
559/// Handle open text document.
560fn did_close_text_document(
561    s: &mut State<'_>,
562    params: lsp::DidCloseTextDocumentParams,
563) -> Result<()> {
564    s.workspace.remove(&params.text_document.uri)?;
565    s.rebuild_interest();
566    Ok(())
567}
568
569/// Handle saving of text documents.
570fn did_save_text_document(s: &mut State<'_>, _: lsp::DidSaveTextDocumentParams) -> Result<()> {
571    s.rebuild_interest();
572    Ok(())
573}