Skip to main content

kcl_lib/lsp/kcl/
mod.rs

1//! Functions for the `kcl` lsp server.
2#![allow(dead_code)]
3
4use std::cell::Cell;
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::sync::Mutex;
8
9use anyhow::Result;
10#[cfg(feature = "cli")]
11use clap::Parser;
12use dashmap::DashMap;
13use tokio::sync::RwLock;
14use tower_lsp::Client;
15use tower_lsp::LanguageServer;
16use tower_lsp::jsonrpc::Result as RpcResult;
17use tower_lsp::lsp_types::CodeAction;
18use tower_lsp::lsp_types::CodeActionKind;
19use tower_lsp::lsp_types::CodeActionOptions;
20use tower_lsp::lsp_types::CodeActionOrCommand;
21use tower_lsp::lsp_types::CodeActionParams;
22use tower_lsp::lsp_types::CodeActionProviderCapability;
23use tower_lsp::lsp_types::CodeActionResponse;
24use tower_lsp::lsp_types::ColorInformation;
25use tower_lsp::lsp_types::ColorPresentation;
26use tower_lsp::lsp_types::ColorPresentationParams;
27use tower_lsp::lsp_types::ColorProviderCapability;
28use tower_lsp::lsp_types::CompletionItem;
29use tower_lsp::lsp_types::CompletionItemKind;
30use tower_lsp::lsp_types::CompletionOptions;
31use tower_lsp::lsp_types::CompletionParams;
32use tower_lsp::lsp_types::CompletionResponse;
33use tower_lsp::lsp_types::CreateFilesParams;
34use tower_lsp::lsp_types::DeleteFilesParams;
35use tower_lsp::lsp_types::Diagnostic;
36use tower_lsp::lsp_types::DiagnosticOptions;
37use tower_lsp::lsp_types::DiagnosticServerCapabilities;
38use tower_lsp::lsp_types::DiagnosticSeverity;
39use tower_lsp::lsp_types::DidChangeConfigurationParams;
40use tower_lsp::lsp_types::DidChangeTextDocumentParams;
41use tower_lsp::lsp_types::DidChangeWatchedFilesParams;
42use tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams;
43use tower_lsp::lsp_types::DidCloseTextDocumentParams;
44use tower_lsp::lsp_types::DidOpenTextDocumentParams;
45use tower_lsp::lsp_types::DidSaveTextDocumentParams;
46use tower_lsp::lsp_types::DocumentColorParams;
47use tower_lsp::lsp_types::DocumentDiagnosticParams;
48use tower_lsp::lsp_types::DocumentDiagnosticReport;
49use tower_lsp::lsp_types::DocumentDiagnosticReportResult;
50use tower_lsp::lsp_types::DocumentFilter;
51use tower_lsp::lsp_types::DocumentFormattingParams;
52use tower_lsp::lsp_types::DocumentSymbol;
53use tower_lsp::lsp_types::DocumentSymbolParams;
54use tower_lsp::lsp_types::DocumentSymbolResponse;
55use tower_lsp::lsp_types::Documentation;
56use tower_lsp::lsp_types::FoldingRange;
57use tower_lsp::lsp_types::FoldingRangeParams;
58use tower_lsp::lsp_types::FoldingRangeProviderCapability;
59use tower_lsp::lsp_types::FullDocumentDiagnosticReport;
60use tower_lsp::lsp_types::Hover as LspHover;
61use tower_lsp::lsp_types::HoverContents;
62use tower_lsp::lsp_types::HoverParams;
63use tower_lsp::lsp_types::HoverProviderCapability;
64use tower_lsp::lsp_types::InitializeParams;
65use tower_lsp::lsp_types::InitializeResult;
66use tower_lsp::lsp_types::InitializedParams;
67use tower_lsp::lsp_types::InlayHint;
68use tower_lsp::lsp_types::InlayHintParams;
69use tower_lsp::lsp_types::InsertTextFormat;
70use tower_lsp::lsp_types::MarkupContent;
71use tower_lsp::lsp_types::MarkupKind;
72use tower_lsp::lsp_types::MessageType;
73use tower_lsp::lsp_types::OneOf;
74use tower_lsp::lsp_types::Position;
75use tower_lsp::lsp_types::PrepareRenameResponse;
76use tower_lsp::lsp_types::RelatedFullDocumentDiagnosticReport;
77use tower_lsp::lsp_types::RenameFilesParams;
78use tower_lsp::lsp_types::RenameParams;
79use tower_lsp::lsp_types::SemanticToken;
80use tower_lsp::lsp_types::SemanticTokenModifier;
81use tower_lsp::lsp_types::SemanticTokenType;
82use tower_lsp::lsp_types::SemanticTokens;
83use tower_lsp::lsp_types::SemanticTokensFullOptions;
84use tower_lsp::lsp_types::SemanticTokensLegend;
85use tower_lsp::lsp_types::SemanticTokensOptions;
86use tower_lsp::lsp_types::SemanticTokensParams;
87use tower_lsp::lsp_types::SemanticTokensRegistrationOptions;
88use tower_lsp::lsp_types::SemanticTokensResult;
89use tower_lsp::lsp_types::SemanticTokensServerCapabilities;
90use tower_lsp::lsp_types::ServerCapabilities;
91use tower_lsp::lsp_types::SignatureHelp;
92use tower_lsp::lsp_types::SignatureHelpOptions;
93use tower_lsp::lsp_types::SignatureHelpParams;
94use tower_lsp::lsp_types::StaticRegistrationOptions;
95use tower_lsp::lsp_types::TextDocumentItem;
96use tower_lsp::lsp_types::TextDocumentPositionParams;
97use tower_lsp::lsp_types::TextDocumentRegistrationOptions;
98use tower_lsp::lsp_types::TextDocumentSyncCapability;
99use tower_lsp::lsp_types::TextDocumentSyncKind;
100use tower_lsp::lsp_types::TextDocumentSyncOptions;
101use tower_lsp::lsp_types::TextEdit;
102use tower_lsp::lsp_types::WorkDoneProgressOptions;
103use tower_lsp::lsp_types::WorkspaceEdit;
104use tower_lsp::lsp_types::WorkspaceFolder;
105use tower_lsp::lsp_types::WorkspaceFoldersServerCapabilities;
106use tower_lsp::lsp_types::WorkspaceServerCapabilities;
107
108use crate::ModuleId;
109use crate::Program;
110use crate::SourceRange;
111use crate::docs::kcl_doc::ArgData;
112use crate::docs::kcl_doc::ModData;
113use crate::exec::KclValue;
114use crate::execution::cache;
115use crate::lsp::LspSuggestion;
116use crate::lsp::ToLspRange;
117use crate::lsp::backend::Backend as _;
118use crate::lsp::kcl::hover::Hover;
119use crate::lsp::kcl::hover::HoverOpts;
120use crate::lsp::util::IntoDiagnostic;
121use crate::parsing::PIPE_OPERATOR;
122use crate::parsing::ast::types::Expr;
123use crate::parsing::ast::types::VariableKind;
124use crate::parsing::token::LexerMode;
125use crate::parsing::token::RESERVED_WORDS;
126use crate::parsing::token::TokenStream;
127use crate::parsing::token::adapter;
128use crate::parsing::token::lex;
129
130pub mod custom_notifications;
131mod hover;
132
133const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 10] = [
134    SemanticTokenType::NUMBER,
135    SemanticTokenType::VARIABLE,
136    SemanticTokenType::KEYWORD,
137    SemanticTokenType::TYPE,
138    SemanticTokenType::STRING,
139    SemanticTokenType::OPERATOR,
140    SemanticTokenType::COMMENT,
141    SemanticTokenType::FUNCTION,
142    SemanticTokenType::PARAMETER,
143    SemanticTokenType::PROPERTY,
144];
145
146const SEMANTIC_TOKEN_MODIFIERS: [SemanticTokenModifier; 5] = [
147    SemanticTokenModifier::DECLARATION,
148    SemanticTokenModifier::DEFINITION,
149    SemanticTokenModifier::DEFAULT_LIBRARY,
150    SemanticTokenModifier::READONLY,
151    SemanticTokenModifier::STATIC,
152];
153
154/// A subcommand for running the server.
155#[derive(Clone, Debug)]
156#[cfg_attr(feature = "cli", derive(Parser))]
157pub struct Server {
158    /// Port that the server should listen
159    #[cfg_attr(feature = "cli", clap(long, default_value = "8080"))]
160    pub socket: i32,
161
162    /// Listen over stdin and stdout instead of a tcp socket.
163    #[cfg_attr(feature = "cli", clap(short, long, default_value = "false"))]
164    pub stdio: bool,
165}
166
167/// The lsp server backend.
168#[derive(Clone)]
169pub struct Backend {
170    /// The client for the backend.
171    pub client: Client,
172    /// The file system client to use.
173    pub fs: crate::fs::FileSystemHandle,
174    /// The workspace folders.
175    pub workspace_folders: DashMap<String, WorkspaceFolder>,
176    /// The stdlib completions for the language.
177    pub stdlib_completions: HashMap<String, CompletionItem>,
178    /// The stdlib completions inside a sketch block, where `solver::*`
179    /// shadows the regular sketch stdlib.
180    pub sketch_block_stdlib_completions: HashMap<String, CompletionItem>,
181    /// The stdlib signatures for the language.
182    pub stdlib_signatures: HashMap<String, SignatureHelp>,
183    /// The stdlib signatures inside a sketch block.
184    pub sketch_block_stdlib_signatures: HashMap<String, SignatureHelp>,
185    /// For all KwArg functions in std, a map from their arg names to arg help snippets (markdown format).
186    pub stdlib_args: HashMap<String, HashMap<String, LspArgData>>,
187    /// KwArg docs inside a sketch block.
188    pub sketch_block_stdlib_args: HashMap<String, HashMap<String, LspArgData>>,
189    /// KCL keywords
190    pub kcl_keywords: HashMap<String, CompletionItem>,
191    /// Token maps.
192    pub(super) token_map: DashMap<String, TokenStream>,
193    /// AST maps.
194    pub ast_map: DashMap<String, crate::Program>,
195    /// Current code.
196    pub code_map: DashMap<String, Vec<u8>>,
197    /// Diagnostics.
198    pub diagnostics_map: DashMap<String, Vec<Diagnostic>>,
199    /// Symbols map.
200    pub symbols_map: DashMap<String, Vec<DocumentSymbol>>,
201    /// Semantic tokens map.
202    pub semantic_tokens_map: DashMap<String, Vec<SemanticToken>>,
203    /// The Zoo API client.
204    pub zoo_client: kittycad::Client,
205    /// Optional executor context to use if we want to execute the code.
206    pub executor_ctx: Arc<RwLock<Option<crate::execution::ExecutorContext>>>,
207    /// If we are currently allowed to execute the ast.
208    pub can_execute: Arc<RwLock<bool>>,
209
210    pub is_initialized: Arc<RwLock<bool>>,
211}
212
213impl Backend {
214    #[cfg(target_arch = "wasm32")]
215    pub fn new_wasm(
216        client: Client,
217        executor_ctx: Option<crate::execution::ExecutorContext>,
218        fs: crate::fs::wasm::FileSystemManager,
219        zoo_client: kittycad::Client,
220    ) -> Result<Self, String> {
221        Self::with_file_system(
222            client,
223            executor_ctx,
224            crate::fs::new_file_system_handle(crate::fs::FileManager::new(fs)),
225            zoo_client,
226        )
227    }
228
229    #[cfg(not(target_arch = "wasm32"))]
230    pub fn new(
231        client: Client,
232        executor_ctx: Option<crate::execution::ExecutorContext>,
233        zoo_client: kittycad::Client,
234    ) -> Result<Self, String> {
235        Self::with_file_system(
236            client,
237            executor_ctx,
238            crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
239            zoo_client,
240        )
241    }
242
243    fn with_file_system(
244        client: Client,
245        executor_ctx: Option<crate::execution::ExecutorContext>,
246        fs: crate::fs::FileSystemHandle,
247        zoo_client: kittycad::Client,
248    ) -> Result<Self, String> {
249        let kcl_std = crate::docs::kcl_doc::walk_stdlib();
250        let stdlib_completions = get_completions_from_stdlib(&kcl_std).map_err(|e| e.to_string())?;
251        let sketch_block_stdlib_completions =
252            get_completions_from_stdlib_for_sketch_block(&kcl_std).map_err(|e| e.to_string())?;
253        let stdlib_signatures = get_signatures_from_stdlib(&kcl_std);
254        let sketch_block_stdlib_signatures = get_signatures_from_stdlib_for_sketch_block(&kcl_std);
255        let stdlib_args = get_arg_maps_from_stdlib(&kcl_std);
256        let sketch_block_stdlib_args = get_arg_maps_from_stdlib_for_sketch_block(&kcl_std);
257        let kcl_keywords = get_keywords();
258
259        Ok(Self {
260            client,
261            fs,
262            stdlib_completions,
263            sketch_block_stdlib_completions,
264            stdlib_signatures,
265            sketch_block_stdlib_signatures,
266            stdlib_args,
267            sketch_block_stdlib_args,
268            kcl_keywords,
269            zoo_client,
270            can_execute: Arc::new(RwLock::new(executor_ctx.is_some())),
271            executor_ctx: Arc::new(RwLock::new(executor_ctx)),
272            workspace_folders: Default::default(),
273            token_map: Default::default(),
274            ast_map: Default::default(),
275            code_map: Default::default(),
276            diagnostics_map: Default::default(),
277            symbols_map: Default::default(),
278            semantic_tokens_map: Default::default(),
279            is_initialized: Default::default(),
280        })
281    }
282
283    fn is_in_sketch_block(ast: &crate::Program, position: usize) -> bool {
284        let in_sketch_block = Cell::new(false);
285        let _ = crate::walk::walk(&ast.ast, |node| {
286            if let crate::walk::Node::SketchBlock(sketch_block) = node
287                && SourceRange::from(&sketch_block.body).contains(position)
288            {
289                in_sketch_block.set(true);
290                return Ok::<bool, anyhow::Error>(false);
291            }
292
293            Ok::<bool, anyhow::Error>(true)
294        });
295
296        in_sketch_block.get()
297    }
298
299    fn stdlib_completions_for_position<'a>(
300        &'a self,
301        ast: &crate::Program,
302        position: usize,
303    ) -> &'a HashMap<String, CompletionItem> {
304        if Self::is_in_sketch_block(ast, position) {
305            &self.sketch_block_stdlib_completions
306        } else {
307            &self.stdlib_completions
308        }
309    }
310
311    fn stdlib_signatures_for_position<'a>(
312        &'a self,
313        ast: &crate::Program,
314        position: usize,
315    ) -> &'a HashMap<String, SignatureHelp> {
316        if Self::is_in_sketch_block(ast, position) {
317            &self.sketch_block_stdlib_signatures
318        } else {
319            &self.stdlib_signatures
320        }
321    }
322
323    fn stdlib_args_for_position<'a>(
324        &'a self,
325        ast: &crate::Program,
326        position: usize,
327    ) -> &'a HashMap<String, HashMap<String, LspArgData>> {
328        if Self::is_in_sketch_block(ast, position) {
329            &self.sketch_block_stdlib_args
330        } else {
331            &self.stdlib_args
332        }
333    }
334
335    fn remove_from_ast_maps(&self, filename: &str) {
336        self.ast_map.remove(filename);
337        self.symbols_map.remove(filename);
338    }
339
340    fn try_arg_completions(
341        &self,
342        program: &crate::Program,
343        position: usize,
344        current_code: &str,
345    ) -> Option<impl Iterator<Item = CompletionItem>> {
346        let curr_expr = program.ast.get_expr_for_position(position)?;
347        let hover =
348            curr_expr.get_hover_value_for_position(position, current_code, &HoverOpts::default_for_signature_help())?;
349
350        // Now we can tell if the user's cursor is inside a callable function.
351        // If so, get its name (the function name being called.)
352        let maybe_callee = match hover {
353            Hover::Function { name, range: _ } => Some(name),
354            Hover::Signature {
355                name,
356                parameter_index: _,
357                range: _,
358            } => Some(name),
359            Hover::Comment { .. } => None,
360            Hover::Variable { .. } => None,
361            Hover::KwArg {
362                callee_name,
363                name: _,
364                range: _,
365            } => Some(callee_name),
366            Hover::Type { .. } => None,
367        };
368        let stdlib_args = self.stdlib_args_for_position(program, position);
369        let callee_args = maybe_callee.and_then(|fn_name| stdlib_args.get(&fn_name))?;
370
371        let arg_label_completions = callee_args
372            .iter()
373            // Don't suggest labels for unlabelled args!
374            .filter(|(_arg_name, arg_data)| arg_data.props.is_labelled())
375            .map(|(arg_name, arg_data)| CompletionItem {
376                label: arg_name.to_owned(),
377                label_details: None,
378                kind: Some(CompletionItemKind::PROPERTY),
379                detail: arg_data.props.ty.clone(),
380                documentation: arg_data.props.docs.clone().map(|docs| {
381                    Documentation::MarkupContent(MarkupContent {
382                        kind: MarkupKind::Markdown,
383                        value: docs,
384                    })
385                }),
386                deprecated: None,
387                preselect: None,
388                sort_text: Some(arg_name.to_owned()),
389                filter_text: Some(arg_name.to_owned()),
390                insert_text: {
391                    // let snippet = "${0:x}";
392                    if let Some(snippet) = arg_data.props.get_autocomplete_snippet(0).map(|(_i, snippet)| snippet) {
393                        Some(snippet)
394                    } else {
395                        Some(format!("{arg_name} = "))
396                    }
397                },
398                insert_text_format: Some(InsertTextFormat::SNIPPET),
399                insert_text_mode: None,
400                text_edit: None,
401                additional_text_edits: None,
402                command: None,
403                commit_characters: None,
404                data: None,
405                tags: None,
406            });
407        Some(arg_label_completions)
408    }
409}
410
411// Implement the shared backend trait for the language server.
412#[async_trait::async_trait]
413impl crate::lsp::backend::Backend for Backend {
414    fn client(&self) -> &Client {
415        &self.client
416    }
417
418    fn fs(&self) -> &crate::fs::FileSystemHandle {
419        &self.fs
420    }
421
422    async fn is_initialized(&self) -> bool {
423        *self.is_initialized.read().await
424    }
425
426    async fn set_is_initialized(&self, is_initialized: bool) {
427        *self.is_initialized.write().await = is_initialized;
428    }
429
430    async fn workspace_folders(&self) -> Vec<WorkspaceFolder> {
431        // TODO: fix clone
432        self.workspace_folders.iter().map(|i| i.clone()).collect()
433    }
434
435    async fn add_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
436        for folder in folders {
437            self.workspace_folders.insert(folder.name.to_string(), folder);
438        }
439    }
440
441    async fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
442        for folder in folders {
443            self.workspace_folders.remove(&folder.name);
444        }
445    }
446
447    fn code_map(&self) -> &DashMap<String, Vec<u8>> {
448        &self.code_map
449    }
450
451    async fn insert_code_map(&self, uri: String, text: Vec<u8>) {
452        self.code_map.insert(uri, text);
453    }
454
455    async fn remove_from_code_map(&self, uri: String) -> Option<Vec<u8>> {
456        self.code_map.remove(&uri).map(|x| x.1)
457    }
458
459    async fn clear_code_state(&self) {
460        self.code_map.clear();
461        self.token_map.clear();
462        self.ast_map.clear();
463        self.diagnostics_map.clear();
464        self.symbols_map.clear();
465        self.semantic_tokens_map.clear();
466    }
467
468    fn current_diagnostics_map(&self) -> &DashMap<String, Vec<Diagnostic>> {
469        &self.diagnostics_map
470    }
471
472    async fn inner_on_change(&self, params: TextDocumentItem, force: bool) {
473        if force {
474            crate::bust_cache().await;
475        }
476
477        let filename = params.uri.to_string();
478        // We already updated the code map in the shared backend.
479
480        // Lets update the tokens.
481        let module_id = ModuleId::default();
482        // Mode-aware lex. The old lexer keeps its exact bail-on-error behavior.
483        // The new lexer always yields a token stream so semantic highlighting
484        // survives a lexical error; any lexical error is carried in `lex_error`
485        // and reported below, after semantic tokens are computed.
486        let (tokens, lex_error) = match LexerMode::resolve() {
487            LexerMode::Old => match lex(&params.text, module_id) {
488                Ok(tokens) => (tokens, None),
489                Err(err) => {
490                    self.add_to_diagnostics(&params, &[err], true).await;
491                    self.token_map.remove(&filename);
492                    self.remove_from_ast_maps(&filename);
493                    self.semantic_tokens_map.remove(&filename);
494                    return;
495                }
496            },
497            LexerMode::New => {
498                let result = adapter::lex_with_diagnostics(&params.text, module_id);
499                let lex_error = result.to_lexical_error();
500                (result.tokens, lex_error)
501            }
502        };
503
504        // Get the previous tokens.
505        let tokens_changed = match self.token_map.get(&filename) {
506            Some(previous_tokens) => *previous_tokens != tokens,
507            _ => true,
508        };
509
510        let had_diagnostics = self.has_diagnostics(params.uri.as_ref()).await;
511
512        // Check if the tokens are the same.
513        if !tokens_changed && !force && !had_diagnostics {
514            // We return early here because the tokens are the same.
515            return;
516        }
517
518        if tokens_changed {
519            // Update our token map.
520            self.token_map.insert(params.uri.to_string(), tokens.clone());
521            // Update our semantic tokens.
522            self.update_semantic_tokens(&tokens, &params).await;
523        }
524
525        // With the new lexer a lexical error is surfaced as a diagnostic, but the
526        // token stream and semantic tokens (computed above) are retained so the
527        // editor keeps highlighting. No AST is produced (mirrors the parse-error
528        // path below).
529        if let Some(err) = lex_error {
530            self.add_to_diagnostics(&params, &[err], true).await;
531            self.remove_from_ast_maps(&filename);
532            return;
533        }
534
535        // Lets update the ast.
536
537        let (ast, errs) = match crate::parsing::parse_tokens(tokens.clone()).0 {
538            Ok(result) => result,
539            Err(err) => {
540                self.add_to_diagnostics(&params, &[err], true).await;
541                self.remove_from_ast_maps(&filename);
542                return;
543            }
544        };
545
546        self.add_to_diagnostics(&params, &errs, true).await;
547
548        if errs.iter().any(|e| e.severity == crate::errors::Severity::Fatal) {
549            self.remove_from_ast_maps(&filename);
550            return;
551        }
552
553        let Some(mut ast) = ast else {
554            self.remove_from_ast_maps(&filename);
555            return;
556        };
557
558        // Here we will want to store the digest and compare, but for now
559        // we're doing this in a non-load-bearing capacity so we can remove
560        // this if it backfires and only hork the LSP.
561        ast.compute_digest();
562
563        // Save it as a program.
564        let ast = crate::Program {
565            ast,
566            original_file_contents: params.text.clone(),
567        };
568
569        // Check if the ast changed.
570        let ast_changed = match self.ast_map.get(&filename) {
571            Some(old_ast) => {
572                // Check if the ast changed.
573                *old_ast.ast != *ast.ast
574            }
575            None => true,
576        };
577
578        if !ast_changed && !force && !had_diagnostics {
579            // Return early if the ast did not change and we don't need to force.
580            return;
581        }
582
583        if ast_changed {
584            self.ast_map.insert(params.uri.to_string(), ast.clone());
585            // Update the symbols map.
586            self.symbols_map.insert(
587                params.uri.to_string(),
588                ast.ast.get_lsp_symbols(&params.text).unwrap_or_default(),
589            );
590
591            // Update our semantic tokens.
592            self.update_semantic_tokens(&tokens, &params).await;
593
594            let mut discovered_findings: Vec<_> = ast.lint_all().into_iter().flatten().collect();
595            // Filter out Z0005 (old sketch syntax) from LSP diagnostics
596            // TODO: Remove this filter once the transpiler is complete and all tests are updated
597            discovered_findings.retain(|finding| finding.finding.code != "Z0005");
598            self.add_to_diagnostics(&params, &discovered_findings, false).await;
599        }
600
601        // Send the notification to the client that the ast was updated.
602        if self.can_execute().await || self.executor_ctx().await.is_none() {
603            // Only send the notification if we can execute.
604            // Otherwise it confuses the client.
605            self.client
606                .send_notification::<custom_notifications::AstUpdated>(ast.ast.clone())
607                .await;
608        }
609
610        // Execute the code if we have an executor context.
611        // This function automatically executes if we should & updates the diagnostics if we got
612        // errors.
613        if self.execute(&params, &ast).await.is_err() {
614            return;
615        }
616
617        // If we made it here we can clear the diagnostics.
618        self.clear_diagnostics_map(&params.uri, Some(DiagnosticSeverity::ERROR))
619            .await;
620    }
621}
622
623impl Backend {
624    pub async fn can_execute(&self) -> bool {
625        *self.can_execute.read().await
626    }
627
628    pub async fn executor_ctx(&self) -> tokio::sync::RwLockReadGuard<'_, Option<crate::execution::ExecutorContext>> {
629        self.executor_ctx.read().await
630    }
631
632    async fn update_semantic_tokens(&self, tokens: &TokenStream, params: &TextDocumentItem) {
633        // Update the semantic tokens map.
634        let mut semantic_tokens = vec![];
635        let mut last_position = Position::new(0, 0);
636        for token in tokens.as_slice() {
637            let Ok(token_type) = SemanticTokenType::try_from(token.token_type) else {
638                // We continue here because not all tokens can be converted this way, we will get
639                // the rest from the ast.
640                continue;
641            };
642
643            let mut token_type_index = match self.get_semantic_token_type_index(&token_type) {
644                Some(index) => index,
645                // This is actually bad this should not fail.
646                // The test for listing all semantic token types should make this never happen.
647                None => {
648                    self.client
649                        .log_message(
650                            MessageType::ERROR,
651                            format!("token type `{token_type:?}` not accounted for"),
652                        )
653                        .await;
654                    continue;
655                }
656            };
657
658            let source_range: SourceRange = token.into();
659            let position = source_range.start_to_lsp_position(&params.text);
660
661            // Calculate the token modifiers.
662            // Get the value at the current position.
663            let token_modifiers_bitset = match self.ast_map.get(params.uri.as_str()) {
664                Some(ast) => {
665                    let token_index = Arc::new(Mutex::new(token_type_index));
666                    let modifier_index: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
667                    crate::walk::walk(&ast.ast, |node: crate::walk::Node| {
668                        let Ok(node_range): Result<SourceRange, _> = (&node).try_into() else {
669                            return Ok(true);
670                        };
671
672                        if !node_range.contains(source_range.start()) {
673                            return Ok(true);
674                        }
675
676                        let get_modifier = |modifier: Vec<SemanticTokenModifier>| -> Result<bool> {
677                            let mut mods = modifier_index.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
678                            let Some(token_modifier_index) = self.get_semantic_token_modifier_index(modifier) else {
679                                return Ok(true);
680                            };
681                            if *mods == 0 {
682                                *mods = token_modifier_index;
683                            } else {
684                                *mods |= token_modifier_index;
685                            }
686                            Ok(false)
687                        };
688
689                        match node {
690                            crate::walk::Node::TagDeclarator(_) => {
691                                return get_modifier(vec![
692                                    SemanticTokenModifier::DEFINITION,
693                                    SemanticTokenModifier::STATIC,
694                                ]);
695                            }
696                            crate::walk::Node::VariableDeclarator(variable) => {
697                                let sr: SourceRange = (&variable.id).into();
698                                if sr.contains(source_range.start()) {
699                                    if let Expr::FunctionExpression(_) = &variable.init {
700                                        let mut ti = token_index.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
701                                        *ti = match self.get_semantic_token_type_index(&SemanticTokenType::FUNCTION) {
702                                            Some(index) => index,
703                                            None => token_type_index,
704                                        };
705                                    }
706
707                                    return get_modifier(vec![
708                                        SemanticTokenModifier::DECLARATION,
709                                        SemanticTokenModifier::READONLY,
710                                    ]);
711                                }
712                            }
713                            crate::walk::Node::Parameter(_) => {
714                                let mut ti = token_index.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
715                                *ti = match self.get_semantic_token_type_index(&SemanticTokenType::PARAMETER) {
716                                    Some(index) => index,
717                                    None => token_type_index,
718                                };
719                                return Ok(false);
720                            }
721                            crate::walk::Node::MemberExpression(member_expression) => {
722                                let sr: SourceRange = (&member_expression.property).into();
723                                if sr.contains(source_range.start()) {
724                                    let mut ti = token_index.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
725                                    *ti = match self.get_semantic_token_type_index(&SemanticTokenType::PROPERTY) {
726                                        Some(index) => index,
727                                        None => token_type_index,
728                                    };
729                                    return Ok(false);
730                                }
731                            }
732                            crate::walk::Node::ObjectProperty(object_property) => {
733                                let sr: SourceRange = (&object_property.key).into();
734                                if sr.contains(source_range.start()) {
735                                    let mut ti = token_index.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
736                                    *ti = match self.get_semantic_token_type_index(&SemanticTokenType::PROPERTY) {
737                                        Some(index) => index,
738                                        None => token_type_index,
739                                    };
740                                }
741                                return get_modifier(vec![SemanticTokenModifier::DECLARATION]);
742                            }
743                            crate::walk::Node::CallExpressionKw(call_expr) => {
744                                let sr: SourceRange = (&call_expr.callee).into();
745                                if sr.contains(source_range.start()) {
746                                    let mut ti = token_index.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
747                                    *ti = match self.get_semantic_token_type_index(&SemanticTokenType::FUNCTION) {
748                                        Some(index) => index,
749                                        None => token_type_index,
750                                    };
751
752                                    if self.stdlib_completions.contains_key(&call_expr.callee.name.name) {
753                                        // This is a stdlib function.
754                                        return get_modifier(vec![SemanticTokenModifier::DEFAULT_LIBRARY]);
755                                    }
756
757                                    return Ok(false);
758                                }
759                            }
760                            _ => {}
761                        }
762                        Ok(true)
763                    })
764                    .unwrap_or_default();
765
766                    let t = match token_index.lock() {
767                        Ok(guard) => *guard,
768                        _ => 0,
769                    };
770                    token_type_index = t;
771
772                    match modifier_index.lock() {
773                        Ok(guard) => *guard,
774                        _ => 0,
775                    }
776                }
777                _ => 0,
778            };
779
780            // We need to check if we are on the last token of the line.
781            // If we are starting from the end of the last line just add 1 to the line.
782            // Check if we are on the last token of the line.
783            if let Some(line) = params.text.lines().nth(position.line as usize)
784                && line.len() == position.character as usize
785            {
786                // We are on the last token of the line.
787                // We need to add a new line.
788                let semantic_token = SemanticToken {
789                    delta_line: position.line - last_position.line + 1,
790                    delta_start: 0,
791                    length: (token.end - token.start) as u32,
792                    token_type: token_type_index,
793                    token_modifiers_bitset,
794                };
795
796                semantic_tokens.push(semantic_token);
797
798                last_position = Position::new(position.line + 1, 0);
799                continue;
800            }
801
802            let semantic_token = SemanticToken {
803                delta_line: position.line - last_position.line,
804                delta_start: if position.line != last_position.line {
805                    position.character
806                } else {
807                    position.character - last_position.character
808                },
809                length: (token.end - token.start) as u32,
810                token_type: token_type_index,
811                token_modifiers_bitset,
812            };
813
814            semantic_tokens.push(semantic_token);
815
816            last_position = position;
817        }
818        self.semantic_tokens_map.insert(params.uri.to_string(), semantic_tokens);
819    }
820
821    async fn clear_diagnostics_map(&self, uri: &url::Url, severity: Option<DiagnosticSeverity>) {
822        let Some(mut items) = self.diagnostics_map.get_mut(uri.as_str()) else {
823            return;
824        };
825
826        // If we only want to clear a specific severity, do that.
827        if let Some(severity) = severity {
828            items.retain(|x| x.severity != Some(severity));
829        } else {
830            items.clear();
831        }
832
833        if items.is_empty() {
834            #[cfg(not(target_arch = "wasm32"))]
835            {
836                self.client.publish_diagnostics(uri.clone(), items.clone(), None).await;
837            }
838
839            // We need to drop the items here.
840            drop(items);
841
842            self.diagnostics_map.remove(uri.as_str());
843        } else {
844            // We don't need to update the map since we used get_mut.
845
846            #[cfg(not(target_arch = "wasm32"))]
847            {
848                self.client.publish_diagnostics(uri.clone(), items.clone(), None).await;
849            }
850        }
851    }
852
853    async fn add_to_diagnostics<DiagT: IntoDiagnostic + std::fmt::Debug>(
854        &self,
855        params: &TextDocumentItem,
856        diagnostics: &[DiagT],
857        clear_all_before_add: bool,
858    ) {
859        if diagnostics.is_empty() {
860            return;
861        }
862
863        if clear_all_before_add {
864            self.clear_diagnostics_map(&params.uri, None).await;
865        } else if diagnostics.iter().all(|x| x.severity() == DiagnosticSeverity::ERROR) {
866            // If the diagnostic is an error, it will be the only error we get since that halts
867            // execution.
868            // Clear the diagnostics before we add a new one.
869            self.clear_diagnostics_map(&params.uri, Some(DiagnosticSeverity::ERROR))
870                .await;
871        } else if diagnostics
872            .iter()
873            .all(|x| x.severity() == DiagnosticSeverity::INFORMATION)
874        {
875            // If the diagnostic is a lint, we will pass them all to add at once so we need to
876            // clear the old ones.
877            self.clear_diagnostics_map(&params.uri, Some(DiagnosticSeverity::INFORMATION))
878                .await;
879        }
880
881        let mut items = match self.diagnostics_map.get(params.uri.as_str()) {
882            Some(items) => {
883                // TODO: Would be awesome to fix the clone here.
884                items.clone()
885            }
886            _ => {
887                vec![]
888            }
889        };
890
891        for diagnostic in diagnostics {
892            let lsp_d = diagnostic.to_lsp_diagnostics(&params.text);
893            // Make sure we don't duplicate diagnostics.
894            for d in lsp_d {
895                if !items.iter().any(|x| x == &d) {
896                    items.push(d);
897                }
898            }
899        }
900
901        self.diagnostics_map.insert(params.uri.to_string(), items.clone());
902
903        self.client.publish_diagnostics(params.uri.clone(), items, None).await;
904    }
905
906    async fn execute(&self, params: &TextDocumentItem, ast: &Program) -> Result<()> {
907        // Check if we can execute.
908        if !self.can_execute().await {
909            return Ok(());
910        }
911
912        // Execute the code if we have an executor context.
913        let ctx = self.executor_ctx().await;
914        let Some(ref executor_ctx) = *ctx else {
915            return Ok(());
916        };
917
918        if !self.is_initialized().await {
919            // We are not initialized yet.
920            return Ok(());
921        }
922
923        // Use run_mock for mock contexts, run_with_caching for live contexts
924        let result = if executor_ctx.is_mock() {
925            executor_ctx
926                .run_mock(ast, &crate::execution::MockConfig::default())
927                .await
928        } else {
929            executor_ctx.run_with_caching(ast.clone()).await
930        };
931
932        match result {
933            Err(err) => {
934                self.add_to_diagnostics(params, &[err], false).await;
935
936                // Since we already published the diagnostics we don't really care about the error
937                // string.
938                Err(anyhow::anyhow!("failed to execute code"))
939            }
940            Ok(_) => Ok(()),
941        }
942    }
943
944    pub fn get_semantic_token_type_index(&self, token_type: &SemanticTokenType) -> Option<u32> {
945        SEMANTIC_TOKEN_TYPES
946            .iter()
947            .position(|x| *x == *token_type)
948            .map(|y| y as u32)
949    }
950
951    pub fn get_semantic_token_modifier_index(&self, token_types: Vec<SemanticTokenModifier>) -> Option<u32> {
952        if token_types.is_empty() {
953            return None;
954        }
955
956        let mut modifier = None;
957        for token_type in token_types {
958            if let Some(index) = SEMANTIC_TOKEN_MODIFIERS
959                .iter()
960                .position(|x| *x == token_type)
961                .map(|y| y as u32)
962            {
963                modifier = match modifier {
964                    Some(modifier) => Some(modifier | index),
965                    None => Some(index),
966                };
967            }
968        }
969        modifier
970    }
971
972    pub async fn update_can_execute(
973        &self,
974        params: custom_notifications::UpdateCanExecuteParams,
975    ) -> RpcResult<custom_notifications::UpdateCanExecuteResponse> {
976        let mut can_execute = self.can_execute.write().await;
977
978        if *can_execute == params.can_execute {
979            return Ok(custom_notifications::UpdateCanExecuteResponse {});
980        }
981
982        *can_execute = params.can_execute;
983
984        Ok(custom_notifications::UpdateCanExecuteResponse {})
985    }
986
987    /// Returns the new string for the code after rename.
988    pub fn inner_prepare_rename(
989        &self,
990        params: &TextDocumentPositionParams,
991        new_name: &str,
992    ) -> RpcResult<Option<(String, String)>> {
993        let filename = params.text_document.uri.to_string();
994
995        let Some(current_code) = self.code_map.get(&filename) else {
996            return Ok(None);
997        };
998        let Ok(current_code) = std::str::from_utf8(&current_code) else {
999            return Ok(None);
1000        };
1001
1002        // Parse the ast.
1003        // I don't know if we need to do this again since it should be updated in the context.
1004        // But I figure better safe than sorry since this will write back out to the file.
1005        let module_id = ModuleId::default();
1006        let Ok(mut ast) = crate::parsing::parse_str(current_code, module_id).parse_errs_as_err() else {
1007            return Ok(None);
1008        };
1009
1010        // Let's convert the position to a character index.
1011        let pos = position_to_char_index(params.position, current_code);
1012        // Now let's perform the rename on the ast.
1013        ast.rename_symbol(new_name, pos);
1014        // Now recast it.
1015        let recast = ast.recast_top(&Default::default(), 0);
1016
1017        Ok(Some((current_code.to_string(), recast)))
1018    }
1019}
1020
1021#[tower_lsp::async_trait]
1022impl LanguageServer for Backend {
1023    async fn initialize(&self, params: InitializeParams) -> RpcResult<InitializeResult> {
1024        self.client
1025            .log_message(MessageType::INFO, format!("initialize: {params:?}"))
1026            .await;
1027
1028        Ok(InitializeResult {
1029            capabilities: ServerCapabilities {
1030                color_provider: Some(ColorProviderCapability::Simple(true)),
1031                code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
1032                    code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
1033                    resolve_provider: Some(false),
1034                    work_done_progress_options: WorkDoneProgressOptions::default(),
1035                })),
1036                completion_provider: Some(CompletionOptions {
1037                    resolve_provider: Some(false),
1038                    trigger_characters: Some(vec![".".to_string()]),
1039                    work_done_progress_options: Default::default(),
1040                    all_commit_characters: None,
1041                    ..Default::default()
1042                }),
1043                diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
1044                    ..Default::default()
1045                })),
1046                document_formatting_provider: Some(OneOf::Left(true)),
1047                folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
1048                hover_provider: Some(HoverProviderCapability::Simple(true)),
1049                inlay_hint_provider: Some(OneOf::Left(true)),
1050                rename_provider: Some(OneOf::Left(true)),
1051                semantic_tokens_provider: Some(SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
1052                    SemanticTokensRegistrationOptions {
1053                        text_document_registration_options: {
1054                            TextDocumentRegistrationOptions {
1055                                document_selector: Some(vec![DocumentFilter {
1056                                    language: Some("kcl".to_string()),
1057                                    scheme: Some("file".to_string()),
1058                                    pattern: None,
1059                                }]),
1060                            }
1061                        },
1062                        semantic_tokens_options: SemanticTokensOptions {
1063                            work_done_progress_options: WorkDoneProgressOptions::default(),
1064                            legend: SemanticTokensLegend {
1065                                token_types: SEMANTIC_TOKEN_TYPES.to_vec(),
1066                                token_modifiers: SEMANTIC_TOKEN_MODIFIERS.to_vec(),
1067                            },
1068                            range: Some(false),
1069                            full: Some(SemanticTokensFullOptions::Bool(true)),
1070                        },
1071                        static_registration_options: StaticRegistrationOptions::default(),
1072                    },
1073                )),
1074                signature_help_provider: Some(SignatureHelpOptions {
1075                    trigger_characters: None,
1076                    retrigger_characters: None,
1077                    ..Default::default()
1078                }),
1079                text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
1080                    open_close: Some(true),
1081                    change: Some(TextDocumentSyncKind::FULL),
1082                    ..Default::default()
1083                })),
1084                workspace: Some(WorkspaceServerCapabilities {
1085                    workspace_folders: Some(WorkspaceFoldersServerCapabilities {
1086                        supported: Some(true),
1087                        change_notifications: Some(OneOf::Left(true)),
1088                    }),
1089                    file_operations: None,
1090                }),
1091                ..Default::default()
1092            },
1093            ..Default::default()
1094        })
1095    }
1096
1097    async fn initialized(&self, params: InitializedParams) {
1098        self.do_initialized(params).await
1099    }
1100
1101    async fn shutdown(&self) -> RpcResult<()> {
1102        self.do_shutdown().await
1103    }
1104
1105    async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
1106        self.do_did_change_workspace_folders(params).await
1107    }
1108
1109    async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
1110        self.do_did_change_configuration(params).await
1111    }
1112
1113    async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
1114        self.do_did_change_watched_files(params).await
1115    }
1116
1117    async fn did_create_files(&self, params: CreateFilesParams) {
1118        self.do_did_create_files(params).await
1119    }
1120
1121    async fn did_rename_files(&self, params: RenameFilesParams) {
1122        self.do_did_rename_files(params).await
1123    }
1124
1125    async fn did_delete_files(&self, params: DeleteFilesParams) {
1126        self.do_did_delete_files(params).await
1127    }
1128
1129    async fn did_open(&self, params: DidOpenTextDocumentParams) {
1130        self.do_did_open(params).await
1131    }
1132
1133    async fn did_change(&self, params: DidChangeTextDocumentParams) {
1134        self.do_did_change(params).await;
1135    }
1136
1137    async fn did_save(&self, params: DidSaveTextDocumentParams) {
1138        self.do_did_save(params).await
1139    }
1140
1141    async fn did_close(&self, params: DidCloseTextDocumentParams) {
1142        self.do_did_close(params).await;
1143    }
1144
1145    async fn hover(&self, params: HoverParams) -> RpcResult<Option<LspHover>> {
1146        let filename = params.text_document_position_params.text_document.uri.to_string();
1147
1148        let Some(current_code) = self.code_map.get(&filename) else {
1149            return Ok(None);
1150        };
1151        let Ok(current_code) = std::str::from_utf8(&current_code) else {
1152            return Ok(None);
1153        };
1154
1155        let pos = position_to_char_index(params.text_document_position_params.position, current_code);
1156
1157        // Let's iterate over the AST and find the node that contains the cursor.
1158        let Some(ast) = self.ast_map.get(&filename) else {
1159            return Ok(None);
1160        };
1161
1162        let Some(hover) = ast
1163            .ast
1164            .get_hover_value_for_position(pos, current_code, &HoverOpts::default_for_hover())
1165        else {
1166            return Ok(None);
1167        };
1168        let stdlib_completions = self.stdlib_completions_for_position(&ast, pos);
1169        let stdlib_args = self.stdlib_args_for_position(&ast, pos);
1170
1171        match hover {
1172            Hover::Function { name, range } => {
1173                let (sig, docs) = if let Some(Some(result)) = with_cached_var(&name, |value| {
1174                    match value {
1175                        // User-defined function
1176                        KclValue::Function { value, .. } if !value.is_std() => {
1177                            // TODO get docs from comments
1178                            Some((value.ast.signature(), ""))
1179                        }
1180                        _ => None,
1181                    }
1182                })
1183                .await
1184                {
1185                    result
1186                } else {
1187                    // Get the docs for this function.
1188                    let Some(completion) = stdlib_completions.get(&name) else {
1189                        return Ok(None);
1190                    };
1191                    let Some(docs) = &completion.documentation else {
1192                        return Ok(None);
1193                    };
1194
1195                    let docs = match docs {
1196                        Documentation::String(docs) => docs,
1197                        Documentation::MarkupContent(MarkupContent { value, .. }) => value,
1198                    };
1199
1200                    let docs = if docs.len() > 320 {
1201                        let end = docs.find("\n\n").or_else(|| docs.find("\n\r\n")).unwrap_or(320);
1202                        &docs[..end]
1203                    } else {
1204                        &**docs
1205                    };
1206
1207                    let Some(label_details) = &completion.label_details else {
1208                        return Ok(None);
1209                    };
1210
1211                    let sig = if let Some(detail) = &label_details.detail {
1212                        detail.clone()
1213                    } else {
1214                        String::new()
1215                    };
1216
1217                    (sig, docs)
1218                };
1219
1220                Ok(Some(LspHover {
1221                    contents: HoverContents::Markup(MarkupContent {
1222                        kind: MarkupKind::Markdown,
1223                        value: format!("```\n{name}{sig}\n```\n\n{docs}"),
1224                    }),
1225                    range: Some(range),
1226                }))
1227            }
1228            Hover::Type { name, range } => {
1229                let Some(completion) = stdlib_completions.get(&name) else {
1230                    return Ok(None);
1231                };
1232                let Some(docs) = &completion.documentation else {
1233                    return Ok(None);
1234                };
1235
1236                let docs = match docs {
1237                    Documentation::String(docs) => docs,
1238                    Documentation::MarkupContent(MarkupContent { value, .. }) => value,
1239                };
1240
1241                let docs = if docs.len() > 320 {
1242                    let end = docs.find("\n\n").or_else(|| docs.find("\n\r\n")).unwrap_or(320);
1243                    &docs[..end]
1244                } else {
1245                    &**docs
1246                };
1247
1248                Ok(Some(LspHover {
1249                    contents: HoverContents::Markup(MarkupContent {
1250                        kind: MarkupKind::Markdown,
1251                        value: format!("```\n{name}\n```\n\n{docs}"),
1252                    }),
1253                    range: Some(range),
1254                }))
1255            }
1256            Hover::KwArg {
1257                name,
1258                callee_name,
1259                range,
1260            } => {
1261                // TODO handle user-defined functions too
1262
1263                let Some(arg_map) = stdlib_args.get(&callee_name) else {
1264                    return Ok(None);
1265                };
1266
1267                let Some(arg_entry) = arg_map.get(&name) else {
1268                    return Ok(None);
1269                };
1270
1271                Ok(Some(LspHover {
1272                    contents: HoverContents::Markup(MarkupContent {
1273                        kind: MarkupKind::Markdown,
1274                        value: arg_entry.tip.clone(),
1275                    }),
1276                    range: Some(range),
1277                }))
1278            }
1279            Hover::Variable {
1280                name,
1281                ty: Some(ty),
1282                range,
1283            } => Ok(Some(LspHover {
1284                contents: HoverContents::Markup(MarkupContent {
1285                    kind: MarkupKind::Markdown,
1286                    value: format!("```\n{name}: {ty}\n```"),
1287                }),
1288                range: Some(range),
1289            })),
1290            Hover::Variable { name, ty: None, range } => Ok(with_cached_var(&name, |value| {
1291                let mut text: String = format!("```\n{name}");
1292                if let Some(ty) = value.principal_type() {
1293                    text.push_str(&format!(": {}", ty.human_friendly_type()));
1294                }
1295                if let Some(v) = value.value_str() {
1296                    text.push_str(&format!(" = {v}"));
1297                }
1298                text.push_str("\n```");
1299
1300                LspHover {
1301                    contents: HoverContents::Markup(MarkupContent {
1302                        kind: MarkupKind::Markdown,
1303                        value: text,
1304                    }),
1305                    range: Some(range),
1306                }
1307            })
1308            .await),
1309            Hover::Signature { .. } => Ok(None),
1310            Hover::Comment { value, range } => Ok(Some(LspHover {
1311                contents: HoverContents::Markup(MarkupContent {
1312                    kind: MarkupKind::Markdown,
1313                    value,
1314                }),
1315                range: Some(range),
1316            })),
1317        }
1318    }
1319
1320    async fn completion(&self, params: CompletionParams) -> RpcResult<Option<CompletionResponse>> {
1321        let mut completions = vec![CompletionItem {
1322            label: PIPE_OPERATOR.to_string(),
1323            label_details: None,
1324            kind: Some(CompletionItemKind::OPERATOR),
1325            detail: Some("A pipe operator.".to_string()),
1326            documentation: Some(Documentation::MarkupContent(MarkupContent {
1327                kind: MarkupKind::Markdown,
1328                value: "A pipe operator.".to_string(),
1329            })),
1330            deprecated: Some(false),
1331            preselect: None,
1332            sort_text: None,
1333            filter_text: None,
1334            insert_text: Some("|> ".to_string()),
1335            insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
1336            insert_text_mode: None,
1337            text_edit: None,
1338            additional_text_edits: None,
1339            command: None,
1340            commit_characters: None,
1341            data: None,
1342            tags: None,
1343        }];
1344
1345        // Get the current line up to cursor
1346        let Some(current_code) = self
1347            .code_map
1348            .get(params.text_document_position.text_document.uri.as_ref())
1349        else {
1350            return Ok(Some(CompletionResponse::Array(completions)));
1351        };
1352        let Ok(current_code) = std::str::from_utf8(&current_code) else {
1353            return Ok(Some(CompletionResponse::Array(completions)));
1354        };
1355
1356        // Get the current line up to cursor, with bounds checking
1357        if let Some(line) = current_code
1358            .lines()
1359            .nth(params.text_document_position.position.line as usize)
1360        {
1361            let char_pos = params.text_document_position.position.character as usize;
1362            if char_pos <= line.len() {
1363                let line_prefix = &line[..char_pos];
1364                // Get last word
1365                let last_word = line_prefix
1366                    .split(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
1367                    .next_back()
1368                    .unwrap_or("");
1369
1370                // If the last word starts with a digit, return no completions
1371                if !last_word.is_empty() && last_word.chars().next().unwrap().is_ascii_digit() {
1372                    return Ok(None);
1373                }
1374            }
1375        }
1376
1377        // Add more to the completions if we have more.
1378        let Some(ast) = self
1379            .ast_map
1380            .get(params.text_document_position.text_document.uri.as_ref())
1381        else {
1382            completions.extend(self.stdlib_completions.values().cloned());
1383            completions.extend(self.kcl_keywords.values().cloned());
1384            return Ok(Some(CompletionResponse::Array(completions)));
1385        };
1386
1387        let position = position_to_char_index(params.text_document_position.position, current_code);
1388        if ast.ast.in_comment(position) {
1389            // If we are in a code comment we don't want to show completions.
1390            return Ok(None);
1391        }
1392
1393        completions.extend(self.stdlib_completions_for_position(&ast, position).values().cloned());
1394        completions.extend(self.kcl_keywords.values().cloned());
1395
1396        // If we're inside a CallExpression or something where a function parameter label could be completed,
1397        // then complete it.
1398        // Let's find the AST node that the user's cursor is in.
1399        if let Some(arg_label_completions) = self.try_arg_completions(&ast, position, current_code) {
1400            completions.extend(arg_label_completions);
1401        }
1402
1403        // Get the completion items for the ast.
1404        let Ok(variables) = ast.ast.completion_items(position) else {
1405            return Ok(Some(CompletionResponse::Array(completions)));
1406        };
1407
1408        // Get our variables from our AST to include in our completions.
1409        completions.extend(variables);
1410
1411        Ok(Some(CompletionResponse::Array(completions)))
1412    }
1413
1414    async fn diagnostic(&self, params: DocumentDiagnosticParams) -> RpcResult<DocumentDiagnosticReportResult> {
1415        let filename = params.text_document.uri.to_string();
1416
1417        // Get the current diagnostics for this file.
1418        let Some(items) = self.diagnostics_map.get(&filename) else {
1419            // Send an empty report.
1420            return Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1421                RelatedFullDocumentDiagnosticReport {
1422                    related_documents: None,
1423                    full_document_diagnostic_report: FullDocumentDiagnosticReport {
1424                        result_id: None,
1425                        items: vec![],
1426                    },
1427                },
1428            )));
1429        };
1430
1431        Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1432            RelatedFullDocumentDiagnosticReport {
1433                related_documents: None,
1434                full_document_diagnostic_report: FullDocumentDiagnosticReport {
1435                    result_id: None,
1436                    items: items.clone(),
1437                },
1438            },
1439        )))
1440    }
1441
1442    async fn signature_help(&self, params: SignatureHelpParams) -> RpcResult<Option<SignatureHelp>> {
1443        let filename = params.text_document_position_params.text_document.uri.to_string();
1444
1445        let Some(current_code) = self.code_map.get(&filename) else {
1446            return Ok(None);
1447        };
1448        let Ok(current_code) = std::str::from_utf8(&current_code) else {
1449            return Ok(None);
1450        };
1451
1452        let pos = position_to_char_index(params.text_document_position_params.position, current_code);
1453
1454        let ast = self.ast_map.get(&filename);
1455        let stdlib_signatures = ast
1456            .as_ref()
1457            .map(|ast| self.stdlib_signatures_for_position(ast, pos))
1458            .unwrap_or(&self.stdlib_signatures);
1459
1460        // Get the character at the position.
1461        let Some(ch) = current_code.chars().nth(pos) else {
1462            return Ok(None);
1463        };
1464
1465        let check_char = |ch: char| {
1466            // If  we are on a (, then get the string in front of the (
1467            // and try to get the signature.
1468            // We do these before the ast check because we might not have a valid ast.
1469            if ch == '(' {
1470                // If the current character is not a " " then get the next space after
1471                // our position so we can split on that.
1472                // Find the next space after the current position.
1473                let next_space = if ch != ' ' {
1474                    if let Some(next_space) = current_code[pos..].find(' ') {
1475                        pos + next_space
1476                    } else if let Some(next_space) = current_code[pos..].find('(') {
1477                        pos + next_space
1478                    } else {
1479                        pos
1480                    }
1481                } else {
1482                    pos
1483                };
1484                let p2 = std::cmp::max(pos, next_space);
1485
1486                let last_word = current_code[..p2].split_whitespace().last()?;
1487
1488                // Get the function name.
1489                return stdlib_signatures.get(last_word);
1490            } else if ch == ',' {
1491                // If we have a comma, then get the string in front of
1492                // the closest ( and try to get the signature.
1493
1494                // Find the last ( before the comma.
1495                let last_paren = current_code[..pos].rfind('(')?;
1496                // Get the string in front of the (.
1497                let last_word = current_code[..last_paren].split_whitespace().last()?;
1498                // Get the function name.
1499                return stdlib_signatures.get(last_word);
1500            }
1501
1502            None
1503        };
1504
1505        if let Some(signature) = check_char(ch) {
1506            return Ok(Some(signature.clone()));
1507        }
1508
1509        // Check if we have context.
1510        if let Some(context) = params.context
1511            && let Some(character) = context.trigger_character
1512        {
1513            for character in character.chars() {
1514                // Check if we are on a ( or a ,.
1515                if (character == '(' || character == ',')
1516                    && let Some(signature) = check_char(character)
1517                {
1518                    return Ok(Some(signature.clone()));
1519                }
1520            }
1521        }
1522
1523        let Some(ast) = ast else {
1524            return Ok(None);
1525        };
1526
1527        let Some(value) = ast.ast.get_expr_for_position(pos) else {
1528            return Ok(None);
1529        };
1530
1531        let Some(hover) =
1532            value.get_hover_value_for_position(pos, current_code, &HoverOpts::default_for_signature_help())
1533        else {
1534            return Ok(None);
1535        };
1536
1537        match hover {
1538            Hover::Function { name, range: _ } => {
1539                // Get the docs for this function.
1540                let Some(signature) = stdlib_signatures.get(&name) else {
1541                    return Ok(None);
1542                };
1543
1544                Ok(Some(signature.clone()))
1545            }
1546            Hover::Signature {
1547                name,
1548                parameter_index,
1549                range: _,
1550            } => {
1551                let Some(signature) = stdlib_signatures.get(&name) else {
1552                    return Ok(None);
1553                };
1554
1555                let mut signature = signature.clone();
1556
1557                signature.active_parameter = Some(parameter_index);
1558
1559                Ok(Some(signature))
1560            }
1561            _ => {
1562                return Ok(None);
1563            }
1564        }
1565    }
1566
1567    async fn inlay_hint(&self, _params: InlayHintParams) -> RpcResult<Option<Vec<InlayHint>>> {
1568        // TODO: do this
1569
1570        Ok(None)
1571    }
1572
1573    async fn semantic_tokens_full(&self, params: SemanticTokensParams) -> RpcResult<Option<SemanticTokensResult>> {
1574        let filename = params.text_document.uri.to_string();
1575
1576        let Some(semantic_tokens) = self.semantic_tokens_map.get(&filename) else {
1577            return Ok(None);
1578        };
1579
1580        Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
1581            result_id: None,
1582            data: semantic_tokens.clone(),
1583        })))
1584    }
1585
1586    async fn document_symbol(&self, params: DocumentSymbolParams) -> RpcResult<Option<DocumentSymbolResponse>> {
1587        let filename = params.text_document.uri.to_string();
1588
1589        let Some(symbols) = self.symbols_map.get(&filename) else {
1590            return Ok(None);
1591        };
1592
1593        Ok(Some(DocumentSymbolResponse::Nested(symbols.clone())))
1594    }
1595
1596    async fn formatting(&self, params: DocumentFormattingParams) -> RpcResult<Option<Vec<TextEdit>>> {
1597        let filename = params.text_document.uri.to_string();
1598
1599        let Some(current_code) = self.code_map.get(&filename) else {
1600            return Ok(None);
1601        };
1602        let Ok(current_code) = std::str::from_utf8(&current_code) else {
1603            return Ok(None);
1604        };
1605
1606        // Parse the ast.
1607        // I don't know if we need to do this again since it should be updated in the context.
1608        // But I figure better safe than sorry since this will write back out to the file.
1609        let module_id = ModuleId::default();
1610        let Ok(ast) = crate::parsing::parse_str(current_code, module_id).parse_errs_as_err() else {
1611            return Ok(None);
1612        };
1613        // Now recast it.
1614        let recast = ast.recast_top(
1615            &crate::parsing::ast::types::FormatOptions {
1616                tab_size: params.options.tab_size as usize,
1617                insert_final_newline: params.options.insert_final_newline.unwrap_or(false),
1618                use_tabs: !params.options.insert_spaces,
1619            },
1620            0,
1621        );
1622        let source_range = SourceRange::new(0, current_code.len(), module_id);
1623        let range = source_range.to_lsp_range(current_code);
1624        Ok(Some(vec![TextEdit {
1625            new_text: recast,
1626            range,
1627        }]))
1628    }
1629
1630    async fn rename(&self, params: RenameParams) -> RpcResult<Option<WorkspaceEdit>> {
1631        let Some((current_code, new_code)) =
1632            self.inner_prepare_rename(&params.text_document_position, &params.new_name)?
1633        else {
1634            return Ok(None);
1635        };
1636
1637        let source_range = SourceRange::new(0, current_code.len(), ModuleId::default());
1638        let range = source_range.to_lsp_range(&current_code);
1639        Ok(Some(WorkspaceEdit {
1640            changes: Some(HashMap::from([(
1641                params.text_document_position.text_document.uri,
1642                vec![TextEdit {
1643                    new_text: new_code,
1644                    range,
1645                }],
1646            )])),
1647            document_changes: None,
1648            change_annotations: None,
1649        }))
1650    }
1651
1652    async fn prepare_rename(&self, params: TextDocumentPositionParams) -> RpcResult<Option<PrepareRenameResponse>> {
1653        if self
1654            .inner_prepare_rename(&params, "someNameNoOneInTheirRightMindWouldEverUseForTesting")?
1655            .is_none()
1656        {
1657            return Ok(None);
1658        }
1659
1660        // Return back to the client, that it is safe to use the rename behavior.
1661        Ok(Some(PrepareRenameResponse::DefaultBehavior { default_behavior: true }))
1662    }
1663
1664    async fn folding_range(&self, params: FoldingRangeParams) -> RpcResult<Option<Vec<FoldingRange>>> {
1665        let filename = params.text_document.uri.to_string();
1666
1667        // Get the ast.
1668        let Some(ast) = self.ast_map.get(&filename) else {
1669            return Ok(None);
1670        };
1671
1672        // Get the folding ranges.
1673        let folding_ranges = ast.ast.get_lsp_folding_ranges();
1674
1675        if folding_ranges.is_empty() {
1676            return Ok(None);
1677        }
1678
1679        Ok(Some(folding_ranges))
1680    }
1681
1682    async fn code_action(&self, params: CodeActionParams) -> RpcResult<Option<CodeActionResponse>> {
1683        // Get the actual diagnostics from our map to ensure we have the full diagnostic info
1684        let filename = params.text_document.uri.to_string();
1685        let stored_diagnostics = self
1686            .diagnostics_map
1687            .get(&filename)
1688            .map(|d| d.clone())
1689            .unwrap_or_default();
1690
1691        // Use stored diagnostics if available, otherwise fall back to params.context.diagnostics
1692        let diagnostics_to_check: Vec<_> = if stored_diagnostics.is_empty() {
1693            params.context.diagnostics.clone()
1694        } else {
1695            // Match params.context.diagnostics with stored diagnostics by range
1696            params
1697                .context
1698                .diagnostics
1699                .iter()
1700                .filter_map(|param_diag| {
1701                    stored_diagnostics
1702                        .iter()
1703                        .find(|stored_diag| stored_diag.range == param_diag.range)
1704                        .cloned()
1705                })
1706                .collect()
1707        };
1708
1709        let actions = diagnostics_to_check
1710            .into_iter()
1711            .filter_map(|diagnostic| {
1712                // Handle regular suggestions (like camelCase)
1713                let (suggestion, range) = diagnostic
1714                    .data
1715                    .as_ref()
1716                    .and_then(|data| serde_json::from_value::<LspSuggestion>(data.clone()).ok())?;
1717                let edit = TextEdit {
1718                    range,
1719                    new_text: suggestion.insert,
1720                };
1721                let changes = HashMap::from([(params.text_document.uri.clone(), vec![edit])]);
1722
1723                // If you add more code action kinds, make sure you add it to the server
1724                // capabilities on initialization!
1725                Some(CodeActionOrCommand::CodeAction(CodeAction {
1726                    title: suggestion.title,
1727                    kind: Some(CodeActionKind::QUICKFIX),
1728                    diagnostics: Some(vec![diagnostic]),
1729                    edit: Some(WorkspaceEdit {
1730                        changes: Some(changes),
1731                        document_changes: None,
1732                        change_annotations: None,
1733                    }),
1734                    command: None,
1735                    is_preferred: Some(true),
1736                    disabled: None,
1737                    data: None,
1738                }))
1739            })
1740            .collect();
1741
1742        Ok(Some(actions))
1743    }
1744
1745    async fn document_color(&self, params: DocumentColorParams) -> RpcResult<Vec<ColorInformation>> {
1746        let filename = params.text_document.uri.to_string();
1747
1748        let Some(current_code) = self.code_map.get(&filename) else {
1749            return Ok(vec![]);
1750        };
1751        let Ok(current_code) = std::str::from_utf8(&current_code) else {
1752            return Ok(vec![]);
1753        };
1754
1755        // Get the ast from our map.
1756        let Some(ast) = self.ast_map.get(&filename) else {
1757            return Ok(vec![]);
1758        };
1759
1760        // Get the colors from the ast.
1761        let Ok(colors) = ast.ast.document_color(current_code) else {
1762            return Ok(vec![]);
1763        };
1764
1765        Ok(colors)
1766    }
1767
1768    async fn color_presentation(&self, params: ColorPresentationParams) -> RpcResult<Vec<ColorPresentation>> {
1769        let filename = params.text_document.uri.to_string();
1770
1771        let Some(current_code) = self.code_map.get(&filename) else {
1772            return Ok(vec![]);
1773        };
1774        let Ok(current_code) = std::str::from_utf8(&current_code) else {
1775            return Ok(vec![]);
1776        };
1777
1778        // Get the ast from our map.
1779        let Some(ast) = self.ast_map.get(&filename) else {
1780            return Ok(vec![]);
1781        };
1782
1783        let pos_start = position_to_char_index(params.range.start, current_code);
1784        let pos_end = position_to_char_index(params.range.end, current_code);
1785
1786        // Get the colors from the ast.
1787        let Ok(Some(presentation)) = ast.ast.color_presentation(&params.color, pos_start, pos_end) else {
1788            return Ok(vec![]);
1789        };
1790
1791        Ok(vec![presentation])
1792    }
1793}
1794
1795#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1796enum StdlibCompletionContext {
1797    Default,
1798    SketchBlock,
1799}
1800
1801fn is_sketch2_doc(doc: &crate::docs::kcl_doc::DocData) -> bool {
1802    doc.qual_name().starts_with("std::solver::")
1803}
1804
1805fn strip_sketch2_prefix(value: &str) -> String {
1806    value.strip_prefix("solver::").unwrap_or(value).to_owned()
1807}
1808
1809fn rewrite_completion_for_sketch_block(mut completion: CompletionItem) -> CompletionItem {
1810    completion.label = strip_sketch2_prefix(&completion.label);
1811    completion.insert_text = completion.insert_text.map(|text| strip_sketch2_prefix(&text));
1812    completion
1813}
1814
1815fn rewrite_signature_for_sketch_block(mut signature: SignatureHelp) -> SignatureHelp {
1816    for info in &mut signature.signatures {
1817        info.label = strip_sketch2_prefix(&info.label);
1818    }
1819    signature
1820}
1821
1822fn should_skip_stdlib_doc(
1823    doc: &crate::docs::kcl_doc::DocData,
1824    has_existing: bool,
1825    context: StdlibCompletionContext,
1826) -> bool {
1827    match context {
1828        StdlibCompletionContext::Default => is_sketch2_doc(doc) || (has_existing && doc.is_experimental()),
1829        StdlibCompletionContext::SketchBlock => has_existing && doc.is_experimental() && !is_sketch2_doc(doc),
1830    }
1831}
1832
1833/// Get completions from our stdlib.
1834pub fn get_completions_from_stdlib(kcl_std: &ModData) -> Result<HashMap<String, CompletionItem>> {
1835    get_completions_from_stdlib_in_context(kcl_std, StdlibCompletionContext::Default)
1836}
1837
1838pub fn get_completions_from_stdlib_for_sketch_block(kcl_std: &ModData) -> Result<HashMap<String, CompletionItem>> {
1839    get_completions_from_stdlib_in_context(kcl_std, StdlibCompletionContext::SketchBlock)
1840}
1841
1842fn get_completions_from_stdlib_in_context(
1843    kcl_std: &ModData,
1844    context: StdlibCompletionContext,
1845) -> Result<HashMap<String, CompletionItem>> {
1846    let mut completions = HashMap::new();
1847
1848    for d in kcl_std.all_docs() {
1849        if let Some(mut ci) = d.to_completion_item() {
1850            let preferred = d.preferred_name();
1851            if should_skip_stdlib_doc(d, completions.contains_key(preferred), context) {
1852                continue;
1853            }
1854            let key = if context == StdlibCompletionContext::SketchBlock && is_sketch2_doc(d) {
1855                ci = rewrite_completion_for_sketch_block(ci);
1856                strip_sketch2_prefix(preferred)
1857            } else {
1858                preferred.to_owned()
1859            };
1860            completions.insert(key, ci);
1861        }
1862    }
1863
1864    let variable_kinds = VariableKind::to_completion_items();
1865    for variable_kind in variable_kinds {
1866        completions.insert(variable_kind.label.clone(), variable_kind);
1867    }
1868
1869    Ok(completions)
1870}
1871
1872/// Get signatures from our stdlib.
1873pub fn get_signatures_from_stdlib(kcl_std: &ModData) -> HashMap<String, SignatureHelp> {
1874    get_signatures_from_stdlib_in_context(kcl_std, StdlibCompletionContext::Default)
1875}
1876
1877pub fn get_signatures_from_stdlib_for_sketch_block(kcl_std: &ModData) -> HashMap<String, SignatureHelp> {
1878    get_signatures_from_stdlib_in_context(kcl_std, StdlibCompletionContext::SketchBlock)
1879}
1880
1881fn get_signatures_from_stdlib_in_context(
1882    kcl_std: &ModData,
1883    context: StdlibCompletionContext,
1884) -> HashMap<String, SignatureHelp> {
1885    let mut signatures = HashMap::new();
1886
1887    for d in kcl_std.all_docs() {
1888        if let Some(mut sig) = d.to_signature_help() {
1889            let preferred = d.preferred_name();
1890            if should_skip_stdlib_doc(d, signatures.contains_key(preferred), context) {
1891                continue;
1892            }
1893            let key = if context == StdlibCompletionContext::SketchBlock && is_sketch2_doc(d) {
1894                sig = rewrite_signature_for_sketch_block(sig);
1895                strip_sketch2_prefix(preferred)
1896            } else {
1897                preferred.to_owned()
1898            };
1899            signatures.insert(key, sig);
1900        }
1901    }
1902
1903    signatures
1904}
1905
1906/// Get KCL keywords
1907pub fn get_keywords() -> HashMap<String, CompletionItem> {
1908    RESERVED_WORDS
1909        .keys()
1910        .map(|k| (k.to_string(), keyword_to_completion(k.to_string())))
1911        .collect()
1912}
1913
1914fn keyword_to_completion(kw: String) -> CompletionItem {
1915    CompletionItem {
1916        label: kw,
1917        label_details: None,
1918        kind: Some(CompletionItemKind::KEYWORD),
1919        detail: None,
1920        documentation: None,
1921        deprecated: Some(false),
1922        preselect: None,
1923        sort_text: None,
1924        filter_text: None,
1925        insert_text: None,
1926        insert_text_format: None,
1927        insert_text_mode: None,
1928        text_edit: None,
1929        additional_text_edits: None,
1930        command: None,
1931        commit_characters: None,
1932        data: None,
1933        tags: None,
1934    }
1935}
1936
1937#[derive(Clone, Debug)]
1938pub struct LspArgData {
1939    pub tip: String,
1940    pub props: ArgData,
1941}
1942
1943/// Get signatures from our stdlib.
1944pub fn get_arg_maps_from_stdlib(kcl_std: &ModData) -> HashMap<String, HashMap<String, LspArgData>> {
1945    get_arg_maps_from_stdlib_in_context(kcl_std, StdlibCompletionContext::Default)
1946}
1947
1948pub fn get_arg_maps_from_stdlib_for_sketch_block(kcl_std: &ModData) -> HashMap<String, HashMap<String, LspArgData>> {
1949    get_arg_maps_from_stdlib_in_context(kcl_std, StdlibCompletionContext::SketchBlock)
1950}
1951
1952fn get_arg_maps_from_stdlib_in_context(
1953    kcl_std: &ModData,
1954    context: StdlibCompletionContext,
1955) -> HashMap<String, HashMap<String, LspArgData>> {
1956    let mut result = HashMap::new();
1957
1958    for d in kcl_std.all_docs() {
1959        let crate::docs::kcl_doc::DocData::Fn(f) = d else {
1960            continue;
1961        };
1962        let arg_map: HashMap<String, _> = f
1963            .args
1964            .iter()
1965            .map(|data| {
1966                let mut tip = "```\n".to_owned();
1967                tip.push_str(&data.to_string());
1968                tip.push_str("\n```");
1969                if let Some(docs) = &data.docs {
1970                    tip.push_str("\n\n");
1971                    tip.push_str(docs);
1972                }
1973                let arg_data = LspArgData {
1974                    tip,
1975                    props: data.clone(),
1976                };
1977                (data.name.clone(), arg_data)
1978            })
1979            .collect();
1980        if !arg_map.is_empty() {
1981            let key = if context == StdlibCompletionContext::SketchBlock && is_sketch2_doc(d) {
1982                strip_sketch2_prefix(&f.preferred_name)
1983            } else {
1984                f.preferred_name.clone()
1985            };
1986            if should_skip_stdlib_doc(d, result.contains_key(&key), context) {
1987                continue;
1988            }
1989            result.insert(key, arg_map);
1990        }
1991    }
1992
1993    result
1994}
1995
1996/// Convert a position to a character index from the start of the file.
1997fn position_to_char_index(position: Position, code: &str) -> usize {
1998    // Get the character position from the start of the file.
1999    let mut char_position = 0;
2000    for (index, line) in code.lines().enumerate() {
2001        if index == position.line as usize {
2002            char_position += position.character as usize;
2003            break;
2004        } else {
2005            char_position += line.len() + 1;
2006        }
2007    }
2008
2009    let end_of_file = if code.is_empty() { 0 } else { code.len() - 1 };
2010    std::cmp::min(char_position, end_of_file)
2011}
2012
2013async fn with_cached_var<T>(name: &str, f: impl Fn(&KclValue) -> T) -> Option<T> {
2014    let mem = cache::read_old_memory().await?;
2015    let value = mem.stack.get(name, SourceRange::default()).ok()?;
2016
2017    Some(f(&value))
2018}
2019
2020#[cfg(test)]
2021mod tests {
2022    use pretty_assertions::assert_eq;
2023
2024    use super::*;
2025
2026    #[test]
2027    fn test_position_to_char_index_first_line() {
2028        let code = r#"def foo():
2029return 42"#;
2030        let position = Position::new(0, 3);
2031        let index = position_to_char_index(position, code);
2032        assert_eq!(index, 3);
2033    }
2034
2035    #[test]
2036    fn test_position_to_char_index() {
2037        let code = r#"def foo():
2038return 42"#;
2039        let position = Position::new(1, 4);
2040        let index = position_to_char_index(position, code);
2041        assert_eq!(index, 15);
2042    }
2043
2044    #[test]
2045    fn test_position_to_char_index_with_newline() {
2046        let code = r#"def foo():
2047
2048return 42"#;
2049        let position = Position::new(2, 0);
2050        let index = position_to_char_index(position, code);
2051        assert_eq!(index, 12);
2052    }
2053
2054    #[test]
2055    fn test_position_to_char_at_end() {
2056        let code = r#"def foo():
2057return 42"#;
2058
2059        let position = Position::new(1, 8);
2060        let index = position_to_char_index(position, code);
2061        assert_eq!(index, 19);
2062    }
2063
2064    #[test]
2065    fn test_position_to_char_empty() {
2066        let code = r#""#;
2067        let position = Position::new(0, 0);
2068        let index = position_to_char_index(position, code);
2069        assert_eq!(index, 0);
2070    }
2071
2072    #[test]
2073    fn stdlib_completions_keyed_by_preferred_name() {
2074        let stdlib = crate::docs::kcl_doc::walk_stdlib();
2075        let completions = get_completions_from_stdlib(&stdlib).unwrap();
2076
2077        assert!(
2078            completions.contains_key("clone"),
2079            "prelude fn should be keyed by short name"
2080        );
2081        assert!(
2082            completions.contains_key("units::toRadians"),
2083            "module fn should be keyed by preferred name with module prefix"
2084        );
2085        assert!(
2086            !completions.contains_key("toRadians"),
2087            "module fn should NOT be keyed by bare short name"
2088        );
2089    }
2090
2091    #[test]
2092    fn stdlib_signatures_keyed_by_preferred_name() {
2093        let stdlib = crate::docs::kcl_doc::walk_stdlib();
2094        let signatures = get_signatures_from_stdlib(&stdlib);
2095
2096        assert!(
2097            signatures.contains_key("clone"),
2098            "prelude fn should be keyed by short name"
2099        );
2100        assert!(
2101            signatures.contains_key("units::toRadians"),
2102            "module fn should be keyed by preferred name with module prefix"
2103        );
2104        assert!(
2105            !signatures.contains_key("toRadians"),
2106            "module fn should NOT be keyed by bare short name"
2107        );
2108    }
2109
2110    #[test]
2111    fn stdlib_args_keyed_by_preferred_name() {
2112        let stdlib = crate::docs::kcl_doc::walk_stdlib();
2113        let args = get_arg_maps_from_stdlib(&stdlib);
2114
2115        assert!(args.contains_key("clone"), "prelude fn should be keyed by short name");
2116        assert!(
2117            args.contains_key("units::toRadians"),
2118            "module fn should be keyed by preferred name with module prefix"
2119        );
2120        assert!(
2121            !args.contains_key("toRadians"),
2122            "module fn should NOT be keyed by bare short name"
2123        );
2124    }
2125
2126    #[test]
2127    fn default_completions_exclude_solver() {
2128        let stdlib = crate::docs::kcl_doc::walk_stdlib();
2129        let completions = get_completions_from_stdlib(&stdlib).unwrap();
2130
2131        assert!(
2132            completions.contains_key("line"),
2133            "prelude line should be present in default context"
2134        );
2135        assert!(
2136            !completions.contains_key("solver::line"),
2137            "solver::line should NOT appear in default context"
2138        );
2139        assert!(
2140            !completions.contains_key("solver::coincident"),
2141            "solver-only functions should NOT appear in default context"
2142        );
2143    }
2144
2145    #[test]
2146    fn sketch1_block_completions_exclude_solver() {
2147        // Sketch v1 (startSketchOn()) blocks use the Default completions map (is_in_sketch_block
2148        // returns false for sketch v1 syntax);  solver functions must be absent.
2149        let stdlib = crate::docs::kcl_doc::walk_stdlib();
2150        let completions = get_completions_from_stdlib(&stdlib).unwrap();
2151
2152        assert!(
2153            completions.contains_key("line"),
2154            "prelude line should be present for old sketch blocks"
2155        );
2156        assert!(
2157            !completions.contains_key("solver::line"),
2158            "solver::line should NOT appear for old sketch blocks"
2159        );
2160        assert!(
2161            !completions.contains_key("solver::coincident"),
2162            "solver-only functions should NOT appear for old sketch blocks"
2163        );
2164    }
2165
2166    #[test]
2167    fn sketch2_completions_include_solver() {
2168        let stdlib = crate::docs::kcl_doc::walk_stdlib();
2169        let completions = get_completions_from_stdlib_for_sketch_block(&stdlib).unwrap();
2170
2171        // Solver functions should shadow prelude functions under their bare name
2172        assert!(
2173            completions.contains_key("line"),
2174            "solver line should be present under bare name in sketch2 context"
2175        );
2176        let line = &completions["line"];
2177        assert!(
2178            line.insert_text
2179                .as_ref()
2180                .is_some_and(|t| t.starts_with("line(start = ")),
2181            "sketch2 line should use solver signature (start =), not prelude signature"
2182        );
2183
2184        // Solver-only functions should also be present under bare name
2185        assert!(
2186            completions.contains_key("coincident"),
2187            "solver-only functions should be present under bare name in sketch2 context"
2188        );
2189        assert!(
2190            !completions.contains_key("solver::coincident"),
2191            "solver functions should NOT keep solver:: prefix in sketch2 context"
2192        );
2193    }
2194}