mimium_language_server/
lib.rs

1use std::path::PathBuf;
2
3use mimium_lang::interner::Symbol;
4
5pub mod semantic_token;
6
7use dashmap::DashMap;
8use log::debug;
9use mimium_lang::compiler::mirgen;
10use mimium_lang::interner::{ExprNodeId, TypeNodeId};
11use mimium_lang::plugin::Plugin;
12use mimium_lang::utils::error::ReportableError;
13use mimium_lang::{Config, ExecContext};
14use ropey::Rope;
15use semantic_token::{ImCompleteSemanticToken, LEGEND_TYPE, ParseResult, parse};
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use tower_lsp::jsonrpc::Result;
19use tower_lsp::lsp_types::notification::Notification;
20use tower_lsp::lsp_types::*;
21use tower_lsp::{Client, LanguageServer, LspService, Server};
22type SrcUri = String;
23
24/// Construct an [`ExecContext`] with the default set of plugins.
25fn get_default_context(path: Option<PathBuf>, with_gui: bool, config: Config) -> ExecContext {
26    let plugins: Vec<Box<dyn Plugin>> = vec![];
27    let mut ctx = ExecContext::new(plugins.into_iter(), path, config);
28    ctx.add_system_plugin(mimium_symphonia::SamplerPlugin::default());
29    ctx.add_system_plugin(mimium_scheduler::get_default_scheduler_plugin());
30    if let Some(midi_plug) = mimium_midi::MidiPlugin::try_new() {
31        ctx.add_system_plugin(midi_plug);
32    } else {
33        log::warn!("Midi is not supported on this platform.")
34    }
35
36    if with_gui {
37        #[cfg(not(target_arch = "wasm32"))]
38        ctx.add_system_plugin(mimium_guitools::GuiToolPlugin::default());
39    }
40
41    ctx
42}
43struct MimiumCtx {
44    builtin_types: Vec<(Symbol, TypeNodeId)>,
45}
46impl MimiumCtx {
47    fn new() -> Self {
48        let mut execctx = get_default_context(None, true, Default::default());
49        execctx.prepare_compiler();
50        let builtin_types = execctx.get_compiler().unwrap().get_ext_typeinfos();
51        Self { builtin_types }
52    }
53}
54impl std::fmt::Display for MimiumCtx {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        write!(f, "mimium context")
57    }
58}
59impl std::fmt::Debug for MimiumCtx {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(f, "mimium context")
62    }
63}
64
65#[derive(Debug)]
66struct Backend {
67    client: Client,
68    compiler_ctx: MimiumCtx,
69    ast_map: DashMap<SrcUri, ExprNodeId>,
70    // semantic_map: DashMap<SrcUri, Semantic>,
71    document_map: DashMap<SrcUri, Rope>,
72    semantic_token_map: DashMap<SrcUri, Vec<ImCompleteSemanticToken>>,
73}
74
75#[tower_lsp::async_trait]
76impl LanguageServer for Backend {
77    async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
78        debug!("initialize: {params:#?}");
79        Ok(InitializeResult {
80            capabilities: ServerCapabilities {
81                text_document_sync: Some(TextDocumentSyncCapability::Options(
82                    TextDocumentSyncOptions {
83                        open_close: Some(true),
84                        change: Some(TextDocumentSyncKind::FULL),
85                        save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
86                            include_text: Some(true),
87                        })),
88                        ..Default::default()
89                    },
90                )),
91                semantic_tokens_provider: Some(
92                    SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
93                        SemanticTokensRegistrationOptions {
94                            text_document_registration_options: TextDocumentRegistrationOptions {
95                                document_selector: Some(vec![DocumentFilter {
96                                    language: Some("mimium".to_string()),
97                                    scheme: Some("file".to_string()),
98                                    pattern: None,
99                                }]),
100                            },
101                            semantic_tokens_options: SemanticTokensOptions {
102                                legend: SemanticTokensLegend {
103                                    token_types: LEGEND_TYPE.to_vec(),
104                                    token_modifiers: vec![],
105                                },
106                                full: Some(SemanticTokensFullOptions::Bool(true)),
107                                range: Some(false),
108                                work_done_progress_options: WorkDoneProgressOptions {
109                                    work_done_progress: None,
110                                },
111                            },
112
113                            static_registration_options: StaticRegistrationOptions::default(),
114                        },
115                    ),
116                ),
117                workspace: Some(WorkspaceServerCapabilities {
118                    workspace_folders: Some(WorkspaceFoldersServerCapabilities {
119                        supported: Some(true),
120                        change_notifications: Some(OneOf::Left(true)),
121                    }),
122                    file_operations: None,
123                }),
124                ..ServerCapabilities::default()
125            },
126            server_info: None,
127            offset_encoding: None,
128        })
129    }
130
131    async fn initialized(&self, _: InitializedParams) {
132        self.client
133            .log_message(MessageType::INFO, "mimium-language-server initialized!")
134            .await;
135    }
136
137    async fn shutdown(&self) -> Result<()> {
138        Ok(())
139    }
140
141    async fn semantic_tokens_full(
142        &self,
143        params: SemanticTokensParams,
144    ) -> Result<Option<SemanticTokensResult>> {
145        let uri = params.text_document.uri.to_string();
146        let mut pre_line = 0;
147        let mut pre_start = 0;
148        let mut semantic_token = || -> Option<_> {
149            let rope = self.document_map.get(&uri)?;
150            let mut imcomplete_semantic_tokens = self.semantic_token_map.get_mut(&uri)?;
151            imcomplete_semantic_tokens.sort_by(|a, b| a.start.cmp(&b.start));
152
153            let semantic_tokens = imcomplete_semantic_tokens.iter().filter_map(|token| {
154                let line = rope.try_byte_to_line(token.start).ok()? as u32;
155                let first = rope.try_line_to_char(line as usize).ok()? as u32;
156                let start = rope.try_byte_to_char(token.start).ok()? as u32 - first;
157                let delta_line = line - pre_line;
158                let delta_start = if delta_line == 0 {
159                    start - pre_start
160                } else {
161                    start
162                };
163                let res = Some(SemanticToken {
164                    delta_line,
165                    delta_start,
166                    length: token.length as u32,
167                    token_type: token.token_type as u32,
168                    token_modifiers_bitset: 0,
169                });
170                pre_line = line;
171                pre_start = start;
172                res
173            });
174            let res = semantic_tokens.collect::<Vec<_>>();
175            // debug!("semantic_tokens: {:#?}", res);
176            Some(SemanticTokensResult::Tokens(SemanticTokens {
177                result_id: None,
178                data: res,
179            }))
180        };
181        Ok(semantic_token())
182    }
183    async fn did_open(&self, params: DidOpenTextDocumentParams) {
184        debug!("file opened");
185        self.on_change(TextDocumentItem {
186            uri: params.text_document.uri,
187            text: params.text_document.text,
188            version: params.text_document.version,
189            language_id: "mimium".to_string(),
190        })
191        .await
192    }
193    async fn did_change(&self, params: DidChangeTextDocumentParams) {
194        self.on_change(TextDocumentItem {
195            text: params.content_changes[0].text.clone(),
196            uri: params.text_document.uri,
197            version: params.text_document.version,
198            language_id: "mimium".to_string(),
199        })
200        .await
201    }
202
203    async fn did_save(&self, params: DidSaveTextDocumentParams) {
204        dbg!(&params.text);
205        if let Some(text) = params.text {
206            let item = TextDocumentItem {
207                uri: params.text_document.uri,
208                text,
209                version: -1,
210                language_id: "mimium".to_string(),
211            };
212            self.on_change(item).await;
213            _ = self.client.semantic_tokens_refresh().await;
214        }
215        debug!("file saved!");
216    }
217    async fn did_close(&self, _: DidCloseTextDocumentParams) {
218        debug!("file closed!");
219    }
220}
221fn diagnostic_from_error(
222    error: Box<dyn ReportableError>,
223    url: Url,
224    rope: &Rope,
225) -> Option<Diagnostic> {
226    let severity = DiagnosticSeverity::ERROR;
227    let main_message = error.get_message();
228    let labels = error.get_labels();
229    let (mainloc, _mainmsg) = labels.first()?;
230
231    let span = &mainloc.span;
232    let start_position = offset_to_position(span.start, rope)?;
233    let end_position = offset_to_position(span.end, rope)?;
234    let related_informations = labels
235        .iter()
236        .filter_map(|(loc, msg)| {
237            let span = &loc.span;
238            let start_position = offset_to_position(span.start, rope)?;
239            let end_position = offset_to_position(span.end, rope)?;
240            let uri = if loc.path.to_string_lossy() != "" {
241                Url::from_file_path(loc.path.clone()).unwrap_or(url.clone())
242            } else {
243                url.clone()
244            };
245            Some(DiagnosticRelatedInformation {
246                location: Location {
247                    uri,
248                    range: Range::new(start_position, end_position),
249                },
250                message: msg.clone(),
251            })
252        })
253        .collect();
254    Some(Diagnostic::new(
255        Range::new(start_position, end_position),
256        Some(severity),
257        None,
258        None,
259        main_message.clone(),
260        Some(related_informations),
261        None,
262    ))
263}
264impl Backend {
265    fn compile(&self, src: &str, url: Url) -> Vec<Diagnostic> {
266        let rope = ropey::Rope::from_str(src);
267
268        let ParseResult {
269            ast,
270            errors,
271            semantic_tokens,
272        } = parse(src, url.as_str());
273        self.semantic_token_map
274            .insert(url.to_string(), semantic_tokens);
275        let errs = {
276            let ast = ast.wrap_to_staged_expr();
277            let (_, _, typeerrs) = mirgen::typecheck(ast, &self.compiler_ctx.builtin_types, None);
278            errors.into_iter().chain(typeerrs).collect::<Vec<_>>()
279        };
280
281        errs.into_iter()
282            .flat_map(|item| diagnostic_from_error(item, url.clone(), &rope))
283            .collect::<Vec<Diagnostic>>()
284    }
285    async fn on_change(&self, params: TextDocumentItem) {
286        debug!("{}", &params.version);
287        let rope = ropey::Rope::from_str(&params.text);
288        self.document_map
289            .insert(params.uri.to_string(), rope.clone());
290        let diagnostics = self.compile(&params.text, params.uri.clone());
291        self.client
292            .publish_diagnostics(params.uri.clone(), diagnostics, Some(params.version))
293            .await;
294    }
295}
296
297pub async fn lib_main() {
298    env_logger::init();
299
300    let stdin = tokio::io::stdin();
301    let stdout = tokio::io::stdout();
302
303    let (service, socket) = LspService::build(|client| Backend {
304        client,
305        compiler_ctx: MimiumCtx::new(),
306        ast_map: DashMap::new(),
307        document_map: DashMap::new(),
308        semantic_token_map: DashMap::new(),
309    })
310    .finish();
311
312    Server::new(stdin, stdout, socket).serve(service).await;
313}
314
315fn offset_to_position(offset: usize, rope: &Rope) -> Option<Position> {
316    let line = rope.try_char_to_line(offset).ok()?;
317    let first_char_of_line = rope.try_line_to_char(line).ok()?;
318    let column = offset - first_char_of_line;
319    Some(Position::new(line as u32, column as u32))
320}
321
322fn position_to_offset(position: Position, rope: &Rope) -> Option<usize> {
323    let line_char_offset = rope.try_line_to_char(position.line as usize).ok()?;
324    let slice = rope.slice(0..line_char_offset + position.character as usize);
325    Some(slice.len_bytes())
326}