ink_lsp_server/
initialize.rs

1//! LSP Server [initialization](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize) implementation.
2
3use crate::utils;
4use crate::utils::{COMMAND_CREATE_PROJECT, COMMAND_EXTRACT_EVENT, COMMAND_MIGRATE_PROJECT};
5
6/// Implements LSP server initialization.
7///
8/// Ref: <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize>.
9pub fn initialize(
10    connection: lsp_server::Connection,
11) -> anyhow::Result<(lsp_server::Connection, lsp_types::InitializeParams)> {
12    // Starts initialization (blocks and waits for initialize request from the client).
13    let (initialize_id, initialize_params_json) = connection.initialize_start()?;
14    let initialize_params: lsp_types::InitializeParams =
15        serde_json::from_value(initialize_params_json).map_err(|error| {
16            anyhow::format_err!("Failed to deserialize initialize parameters: {error}")
17        })?;
18
19    // Composes initialization result.
20    let initialize_result = serde_json::to_value(lsp_types::InitializeResult {
21        // Sets server capabilities based on client's capabilities.
22        capabilities: server_capabilities(&initialize_params.capabilities),
23        server_info: Some(lsp_types::ServerInfo {
24            name: "ink-analyzer".to_owned(),
25            version: Some(env!("CARGO_PKG_VERSION").to_owned()),
26        }),
27        offset_encoding: None,
28    })
29    .map_err(|error| anyhow::format_err!("Failed to serialize initialize result: {error}"))?;
30
31    // Finishes initialization (sends `InitializeResult` back to the client).
32    connection.initialize_finish(initialize_id, initialize_result)?;
33
34    Ok((connection, initialize_params))
35}
36
37/// Returns the capabilities of the language server based on the given client capabilities.
38///
39/// Ref: <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverCapabilities>.
40pub fn server_capabilities(
41    client_capabilities: &lsp_types::ClientCapabilities,
42) -> lsp_types::ServerCapabilities {
43    lsp_types::ServerCapabilities {
44        position_encoding: Some(utils::position_encoding(client_capabilities)),
45        text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Options(
46            lsp_types::TextDocumentSyncOptions {
47                open_close: Some(true),
48                // ink! projects are currently single file and tend to be pretty small,
49                // so full document sync is fine (for now).
50                change: Some(lsp_types::TextDocumentSyncKind::FULL),
51                will_save: None,
52                will_save_wait_until: None,
53                save: Some(lsp_types::SaveOptions::default().into()),
54            },
55        )),
56        hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
57        completion_provider: Some(lsp_types::CompletionOptions {
58            resolve_provider: None,
59            // ink! completions are all attribute based.
60            trigger_characters: Some(vec![
61                "[".to_owned(),
62                "(".to_owned(),
63                ",".to_owned(),
64                ":".to_owned(),
65            ]),
66            all_commit_characters: None,
67            work_done_progress_options: Default::default(),
68            completion_item: Default::default(),
69        }),
70        code_action_provider: Some(
71            utils::code_actions_kinds(client_capabilities)
72                .map(|code_action_kinds| {
73                    lsp_types::CodeActionProviderCapability::Options(lsp_types::CodeActionOptions {
74                        code_action_kinds: Some(code_action_kinds),
75                        work_done_progress_options: Default::default(),
76                        resolve_provider: Some(true),
77                    })
78                })
79                .unwrap_or_else(|| lsp_types::CodeActionProviderCapability::Simple(true)),
80        ),
81        inlay_hint_provider: Some(lsp_types::OneOf::Right(
82            lsp_types::InlayHintServerCapabilities::Options(lsp_types::InlayHintOptions {
83                work_done_progress_options: Default::default(),
84                resolve_provider: None,
85            }),
86        )),
87        signature_help_provider: Some(lsp_types::SignatureHelpOptions {
88            trigger_characters: Some(vec!["(".to_owned(), ",".to_owned()]),
89            retrigger_characters: None,
90            work_done_progress_options: Default::default(),
91        }),
92        execute_command_provider: Some(lsp_types::ExecuteCommandOptions {
93            commands: vec![
94                COMMAND_CREATE_PROJECT.to_owned(),
95                COMMAND_MIGRATE_PROJECT.to_owned(),
96                COMMAND_EXTRACT_EVENT.to_owned(),
97            ],
98            work_done_progress_options: Default::default(),
99        }),
100        ..Default::default()
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use std::thread;
108
109    #[test]
110    fn initialize_works() {
111        // Creates a pair of in-memory connections to simulate an LSP client and server.
112        let (server_connection, client_connection) = lsp_server::Connection::memory();
113
114        // Starts server initialization on a separate thread (because `initialize` function is blocking).
115        thread::spawn(|| initialize(server_connection));
116
117        // Verifies that an initialization request (from client to server) gets an `InitializeResult` response (from server to client).
118        // Creates initialization request.
119        use lsp_types::request::Request;
120        let init_req_id = lsp_server::RequestId::from(1);
121        let init_req = lsp_server::Request {
122            id: init_req_id.clone(),
123            method: lsp_types::request::Initialize::METHOD.to_owned(),
124            params: serde_json::to_value(lsp_types::InitializeParams::default()).unwrap(),
125        };
126        // Sends initialization request from client to server.
127        client_connection.sender.send(init_req.into()).unwrap();
128        // Confirms receipt of `InitializeResult` response by the client.
129        let message = client_connection.receiver.recv().unwrap();
130        let init_result_resp = match message {
131            lsp_server::Message::Response(it) => Some(it),
132            _ => None,
133        }
134        .unwrap();
135        assert_eq!(init_result_resp.id, init_req_id);
136        // Verifies that an initialization result is created with the expected server name.
137        let init_result: lsp_types::InitializeResult =
138            serde_json::from_value(init_result_resp.result.unwrap()).unwrap();
139        assert_eq!(init_result.server_info.unwrap().name, "ink-analyzer");
140    }
141
142    #[test]
143    fn server_capabilities_works() {
144        // Creates server capabilities based on client capabilities.
145        let server_capabilities = server_capabilities(&Default::default());
146
147        // Verifies the expected default server capabilities.
148        // NOTE: See the `utils` module for utilities for "reactive" server capabilities (i.e. change based on the client capabilities)
149        // and their unit tests
150        // (e.g. position_encoding, code action kinds, snippet support, signature information active parameter and label offset support e.t.c).
151        assert_eq!(
152            server_capabilities.position_encoding,
153            Some(lsp_types::PositionEncodingKind::UTF16)
154        );
155        assert_eq!(
156            server_capabilities.code_action_provider,
157            Some(lsp_types::CodeActionProviderCapability::Simple(true))
158        );
159    }
160}