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;
20
21use super::task_pool::TaskPool;
22use super::InitOptions;
23use super::Session;
24use auto_lsp_core::errors::{ExtensionError, RuntimeError};
25use lsp_server::{Connection, ReqQueue};
26use lsp_types::{InitializeParams, InitializeResult, PositionEncodingKind};
27use serde::Deserialize;
28#[cfg(target_arch = "wasm32")]
29use std::fs;
30use texter::core::text::Text;
31
32#[allow(non_snake_case, reason = "JSON")]
33#[derive(Debug, Deserialize)]
34struct InitializationOptions {
35    /// Maps file extensions to parser names.
36    ///
37    /// Example: { "rs": "rust", "py": "python" }
38    /// This option is provided by the client to define how different file types should be parsed.
39    perFileParser: HashMap<String, String>,
40}
41
42/// Function to create a new [`Text`] from a [`String`]
43pub(crate) type TextFn = fn(String) -> Text;
44
45fn decide_encoding(encs: Option<&[PositionEncodingKind]>) -> (TextFn, PositionEncodingKind) {
46    const DEFAULT: (TextFn, PositionEncodingKind) = (Text::new_utf16, PositionEncodingKind::UTF16);
47    let Some(encs) = encs else {
48        return DEFAULT;
49    };
50
51    for enc in encs {
52        if *enc == PositionEncodingKind::UTF16 {
53            return (Text::new_utf16, enc.clone());
54        } else if *enc == PositionEncodingKind::UTF8 {
55            return (Text::new, enc.clone());
56        }
57    }
58
59    DEFAULT
60}
61
62impl<Db: salsa::Database> Session<Db> {
63    pub(crate) fn new(
64        init_options: InitOptions,
65        connection: Connection,
66        text_fn: TextFn,
67        db: Db,
68    ) -> Self {
69        let (sender, task_rx) = crossbeam_channel::unbounded();
70
71        let max_threads = std::thread::available_parallelism().unwrap().get();
72
73        log::info!("Max threads: {max_threads}");
74
75        Self {
76            init_options,
77            connection,
78            text_fn,
79            extensions: HashMap::new(),
80            req_queue: ReqQueue::default(),
81            db,
82            task_rx,
83            task_pool: TaskPool::new_with_threads(sender, max_threads),
84        }
85    }
86
87    /// Create a new session with the given initialization options.
88    ///
89    /// This will establish the connection with the client and send the server capabilities.
90    pub fn create(
91        mut init_options: InitOptions,
92        connection: Connection,
93        db: Db,
94    ) -> anyhow::Result<(Session<Db>, InitializeParams)> {
95        // This is a workaround for a deadlock issue in WASI libc.
96        // See https://github.com/WebAssembly/wasi-libc/pull/491
97        #[cfg(target_arch = "wasm32")]
98        fs::metadata("/workspace").unwrap();
99
100        log::info!("Starting LSP server");
101        log::info!("");
102
103        // Create the transport. Includes the stdio (stdin and stdout) versions but this could
104        // also be implemented to use sockets or HTTP.
105        let (id, resp) = connection.initialize_start()?;
106        let params: InitializeParams = serde_json::from_value(resp)?;
107
108        let pos_encoding = params
109            .capabilities
110            .general
111            .as_ref()
112            .and_then(|g| g.position_encodings.as_deref());
113
114        let (t_fn, enc) = decide_encoding(pos_encoding);
115        init_options.capabilities.position_encoding = Some(enc);
116
117        let server_capabilities = serde_json::to_value(&InitializeResult {
118            capabilities: init_options.capabilities.clone(),
119            server_info: init_options.server_info.clone(),
120        })
121        .unwrap();
122
123        connection.initialize_finish(id, server_capabilities)?;
124
125        let mut session = Session::new(init_options, connection, t_fn, db);
126
127        let options = InitializationOptions::deserialize(
128            params
129                .clone()
130                .initialization_options
131                .ok_or(RuntimeError::MissingPerFileParser)?,
132        )
133        .unwrap();
134
135        // Validate that the parsers provided by the client exist
136        for (file_extension, parser) in &options.perFileParser {
137            if !session.init_options.parsers.contains_key(parser.as_str()) {
138                return Err(RuntimeError::from(ExtensionError::UnknownParser {
139                    extension: file_extension.clone(),
140                    available: session.init_options.parsers.keys().cloned().collect(),
141                })
142                .into());
143            }
144        }
145
146        // Store the client's per file parser options
147        session.extensions = options.perFileParser;
148
149        Ok((session, params))
150    }
151}