auto_lsp_server/
init.rs

1/*
2This file is part of auto-lsp.
3Copyright (C) 2025 CLAUZEL Adrien
4
5auto-lsp is free software: you can redistribute it and/or modify
6it under the terms of the GNU General Public License as published by
7the Free Software Foundation, either version 3 of the License, or
8(at your option) any later version.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License
16along with this program.  If not, see <http://www.gnu.org/licenses/>
17*/
18
19use std::collections::HashMap;
20use std::num::NonZeroUsize;
21
22use super::task_pool::TaskPool;
23use super::InitOptions;
24use super::Session;
25use auto_lsp_core::errors::{ExtensionError, RuntimeError};
26use lsp_server::{Connection, ReqQueue};
27use lsp_types::{InitializeParams, InitializeResult, PositionEncodingKind};
28use serde::Deserialize;
29#[cfg(target_arch = "wasm32")]
30use std::fs;
31use texter::core::text::Text;
32
33#[allow(non_snake_case, reason = "JSON")]
34#[derive(Debug, Deserialize)]
35struct InitializationOptions {
36    /// Maps file extensions to parser names.
37    ///
38    /// Example: { "rs": "rust", "py": "python" }
39    /// This option is provided by the client to define how different file types should be parsed.
40    perFileParser: HashMap<String, String>,
41}
42
43/// Function to create a new [`Text`] from a [`String`]
44pub(crate) type TextFn = fn(String) -> Text;
45
46fn decide_encoding(encs: Option<&[PositionEncodingKind]>) -> (TextFn, PositionEncodingKind) {
47    const DEFAULT: (TextFn, PositionEncodingKind) = (Text::new_utf16, PositionEncodingKind::UTF16);
48    let Some(encs) = encs else {
49        return DEFAULT;
50    };
51
52    for enc in encs {
53        if *enc == PositionEncodingKind::UTF16 {
54            return (Text::new_utf16, enc.clone());
55        } else if *enc == PositionEncodingKind::UTF8 {
56            return (Text::new, enc.clone());
57        }
58    }
59
60    DEFAULT
61}
62
63impl<Db: salsa::Database> Session<Db> {
64    pub(crate) fn new(
65        init_options: InitOptions,
66        connection: Connection,
67        text_fn: TextFn,
68        db: Db,
69    ) -> Self {
70        let (sender, task_rx) = crossbeam_channel::unbounded();
71
72        let max_threads = std::thread::available_parallelism()
73            .unwrap_or_else(|_| NonZeroUsize::new(1).unwrap())
74            .get();
75
76        log::info!("Max threads: {max_threads}");
77
78        Self {
79            init_options,
80            connection,
81            text_fn,
82            extensions: HashMap::new(),
83            req_queue: ReqQueue::default(),
84            db,
85            task_rx,
86            task_pool: TaskPool::new_with_threads(sender, max_threads),
87        }
88    }
89
90    /// Create a new session with the given initialization options.
91    ///
92    /// This will establish the connection with the client and send the server capabilities.
93    pub fn create(
94        mut init_options: InitOptions,
95        connection: Connection,
96        db: Db,
97    ) -> anyhow::Result<(Session<Db>, InitializeParams)> {
98        // This is a workaround for a deadlock issue in WASI libc.
99        // See https://github.com/WebAssembly/wasi-libc/pull/491
100        #[cfg(target_arch = "wasm32")]
101        fs::metadata("/workspace").unwrap();
102
103        log::info!("Starting LSP server");
104        log::info!("");
105
106        // Create the transport. Includes the stdio (stdin and stdout) versions but this could
107        // also be implemented to use sockets or HTTP.
108        let (id, resp) = connection.initialize_start()?;
109        let params: InitializeParams = serde_json::from_value(resp)?;
110
111        let pos_encoding = params
112            .capabilities
113            .general
114            .as_ref()
115            .and_then(|g| g.position_encodings.as_deref());
116
117        let (t_fn, enc) = decide_encoding(pos_encoding);
118        init_options.capabilities.position_encoding = Some(enc);
119
120        let server_capabilities = serde_json::to_value(&InitializeResult {
121            capabilities: init_options.capabilities.clone(),
122            server_info: init_options.server_info.clone(),
123        })
124        .unwrap();
125
126        connection.initialize_finish(id, server_capabilities)?;
127
128        let mut session = Session::new(init_options, connection, t_fn, db);
129
130        let options = InitializationOptions::deserialize(
131            params
132                .clone()
133                .initialization_options
134                .ok_or(RuntimeError::MissingPerFileParser)?,
135        )
136        .unwrap();
137
138        // Validate that the parsers provided by the client exist
139        for (file_extension, parser) in &options.perFileParser {
140            if !session.init_options.parsers.contains_key(parser.as_str()) {
141                return Err(RuntimeError::from(ExtensionError::UnknownParser {
142                    extension: file_extension.clone(),
143                    available: session.init_options.parsers.keys().cloned().collect(),
144                })
145                .into());
146            }
147        }
148
149        // Store the client's per file parser options
150        session.extensions = options.perFileParser;
151
152        Ok((session, params))
153    }
154}