use std::collections::BTreeMap; use pretty_assertions::assert_eq; use tower_lsp::{ lsp_types::{Diagnostic, SemanticTokenModifier, SemanticTokenType}, LanguageServer, }; use crate::{ lsp::test_util::{copilot_lsp_server, kcl_lsp_server}, parsing::ast::types::{Node, Program}, }; #[track_caller] fn assert_diagnostic_count(diagnostics: Option<&Vec>, n: usize) { let Some(diagnostics) = diagnostics else { assert_eq!(n, 0, "No diagnostics"); return; }; assert_eq!( diagnostics .iter() .filter(|d| d.severity.as_ref().unwrap() != &tower_lsp::lsp_types::DiagnosticSeverity::WARNING) .count(), n, "expected {n} errors, found {diagnostics:#?}" ); } #[tokio::test(flavor = "multi_thread", worker_threads = 12)] async fn test_updating_kcl_lsp_files() { let server = kcl_lsp_server(false).await.unwrap(); assert_eq!(server.code_map.len(), 0); // Get the path to the current file. let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src").join("lsp"); let string_path = format!("file://{}", path.display()); // Run workspace folders change. server .did_change_workspace_folders(tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams { event: tower_lsp::lsp_types::WorkspaceFoldersChangeEvent { added: vec![tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string(), }], removed: vec![], }, }) .await; // Get the workspace folders. assert_eq!(server.workspace_folders.len(), 1); assert_eq!( server.workspace_folders.get("my-project").unwrap().clone(), tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string() } ); assert_eq!(server.code_map.len(), 10); // Run open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: "test".to_string(), }, }) .await; // Check the code map. assert_eq!(server.code_map.len(), 11); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); // Close the file. server .did_close(tower_lsp::lsp_types::DidCloseTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, }) .await; // Check the code map. assert_eq!(server.code_map.len(), 11); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); // Open another file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test2.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: "test2".to_string(), }, }) .await; // Check the code map. assert_eq!(server.code_map.len(), 12); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test2.kcl").unwrap().clone(), "test2".as_bytes() ); // Run on change. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test2.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: "changed".to_string(), }], }) .await; // Check the code map. assert_eq!(server.code_map.len(), 12); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test2.kcl").unwrap().clone(), "changed".as_bytes() ); // Rename a file. server .did_rename_files(tower_lsp::lsp_types::RenameFilesParams { files: vec![tower_lsp::lsp_types::FileRename { old_uri: "file:///test2.kcl".into(), new_uri: "file:///test3.kcl".into(), }], }) .await; // Check the code map. assert_eq!(server.code_map.len(), 12); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test3.kcl").unwrap().clone(), "changed".as_bytes() ); // Create a file. server .did_create_files(tower_lsp::lsp_types::CreateFilesParams { files: vec![tower_lsp::lsp_types::FileCreate { uri: "file:///test4.kcl".into(), }], }) .await; // Check the code map. assert_eq!(server.code_map.len(), 13); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test3.kcl").unwrap().clone(), "changed".as_bytes() ); assert_eq!(server.code_map.get("file:///test4.kcl").unwrap().clone(), "".as_bytes()); // Delete a file. server .did_delete_files(tower_lsp::lsp_types::DeleteFilesParams { files: vec![tower_lsp::lsp_types::FileDelete { uri: "file:///test4.kcl".into(), }], }) .await; // Check the code map. assert_eq!(server.code_map.len(), 12); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test3.kcl").unwrap().clone(), "changed".as_bytes() ); // If we are adding the same folder we already had we should not nuke the code_map. server .did_change_workspace_folders(tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams { event: tower_lsp::lsp_types::WorkspaceFoldersChangeEvent { added: vec![tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string(), }], removed: vec![], }, }) .await; // Get the workspace folders. assert_eq!(server.workspace_folders.len(), 1); assert_eq!( server.workspace_folders.get("my-project").unwrap().clone(), tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string() } ); // Check the code map. assert_eq!(server.code_map.len(), 12); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test3.kcl").unwrap().clone(), "changed".as_bytes() ); // Remove folders. // Run workspace folders change. server .did_change_workspace_folders(tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams { event: tower_lsp::lsp_types::WorkspaceFoldersChangeEvent { added: vec![tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project2".to_string(), }], removed: vec![tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string(), }], }, }) .await; // Get the workspace folders. assert_eq!(server.workspace_folders.len(), 1); assert_eq!( server.workspace_folders.get("my-project2").unwrap().clone(), tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project2".to_string() } ); assert_eq!(server.code_map.len(), 10); // Just make sure that one of the current files read from disk is accurate. assert_eq!( server .code_map .get(&format!("{}/util.rs", string_path)) .unwrap() .clone(), include_str!("util.rs").as_bytes() ); } #[tokio::test(flavor = "multi_thread")] async fn test_updating_copilot_lsp_files() { let server = copilot_lsp_server().await.unwrap(); assert_eq!(server.code_map.len(), 0); // Get the path to the current file. let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src").join("lsp"); let string_path = format!("file://{}", path.display()); // Run workspace folders change. server .did_change_workspace_folders(tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams { event: tower_lsp::lsp_types::WorkspaceFoldersChangeEvent { added: vec![tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string(), }], removed: vec![], }, }) .await; // Get the workspace folders. assert_eq!(server.workspace_folders.len(), 1); assert_eq!( server.workspace_folders.get("my-project").unwrap().clone(), tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string() } ); assert_eq!(server.code_map.len(), 10); // Run open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: "test".to_string(), }, }) .await; // Check the code map. assert_eq!(server.code_map.len(), 11); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); // Close the file. server .did_close(tower_lsp::lsp_types::DidCloseTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, }) .await; // Check the code map. assert_eq!(server.code_map.len(), 11); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); // Open another file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test2.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: "test2".to_string(), }, }) .await; // Check the code map. assert_eq!(server.code_map.len(), 12); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test2.kcl").unwrap().clone(), "test2".as_bytes() ); // Run on change. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test2.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: "changed".to_string(), }], }) .await; // Check the code map. assert_eq!(server.code_map.len(), 12); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test2.kcl").unwrap().clone(), "changed".as_bytes() ); // Rename a file. server .did_rename_files(tower_lsp::lsp_types::RenameFilesParams { files: vec![tower_lsp::lsp_types::FileRename { old_uri: "file:///test2.kcl".into(), new_uri: "file:///test3.kcl".into(), }], }) .await; // Check the code map. assert_eq!(server.code_map.len(), 12); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test3.kcl").unwrap().clone(), "changed".as_bytes() ); // Create a file. server .did_create_files(tower_lsp::lsp_types::CreateFilesParams { files: vec![tower_lsp::lsp_types::FileCreate { uri: "file:///test4.kcl".into(), }], }) .await; // Check the code map. assert_eq!(server.code_map.len(), 13); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test3.kcl").unwrap().clone(), "changed".as_bytes() ); assert_eq!(server.code_map.get("file:///test4.kcl").unwrap().clone(), "".as_bytes()); // Delete a file. server .did_delete_files(tower_lsp::lsp_types::DeleteFilesParams { files: vec![tower_lsp::lsp_types::FileDelete { uri: "file:///test4.kcl".into(), }], }) .await; // Check the code map. assert_eq!(server.code_map.len(), 12); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test3.kcl").unwrap().clone(), "changed".as_bytes() ); // If we are adding the same folder we already had we should not nuke the code_map. server .did_change_workspace_folders(tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams { event: tower_lsp::lsp_types::WorkspaceFoldersChangeEvent { added: vec![tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string(), }], removed: vec![], }, }) .await; // Get the workspace folders. assert_eq!(server.workspace_folders.len(), 1); assert_eq!( server.workspace_folders.get("my-project").unwrap().clone(), tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string() } ); // Check the code map. assert_eq!(server.code_map.len(), 12); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test3.kcl").unwrap().clone(), "changed".as_bytes() ); // If we change nothing it should not change the current code map. server .did_change_workspace_folders(tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams { event: tower_lsp::lsp_types::WorkspaceFoldersChangeEvent { added: vec![], removed: vec![], }, }) .await; // Get the workspace folders. assert_eq!(server.workspace_folders.len(), 1); assert_eq!( server.workspace_folders.get("my-project").unwrap().clone(), tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string() } ); // Check the code map. assert_eq!(server.code_map.len(), 12); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); assert_eq!( server.code_map.get("file:///test3.kcl").unwrap().clone(), "changed".as_bytes() ); // Remove folders. // Run workspace folders change. server .did_change_workspace_folders(tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams { event: tower_lsp::lsp_types::WorkspaceFoldersChangeEvent { added: vec![tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project2".to_string(), }], removed: vec![tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string(), }], }, }) .await; // Get the workspace folders. assert_eq!(server.workspace_folders.len(), 1); assert_eq!( server.workspace_folders.get("my-project2").unwrap().clone(), tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project2".to_string() } ); assert_eq!(server.code_map.len(), 10); } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_create_zip() { let server = kcl_lsp_server(false).await.unwrap(); assert_eq!(server.code_map.len(), 0); // Get the path to the current file. let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src").join("lsp"); let string_path = format!("file://{}", path.display()); // Run workspace folders change. server .did_change_workspace_folders(tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams { event: tower_lsp::lsp_types::WorkspaceFoldersChangeEvent { added: vec![tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string(), }], removed: vec![], }, }) .await; // Get the workspace folders. assert_eq!(server.workspace_folders.len(), 1); assert_eq!( server.workspace_folders.get("my-project").unwrap().clone(), tower_lsp::lsp_types::WorkspaceFolder { uri: string_path.as_str().try_into().unwrap(), name: "my-project".to_string() } ); assert_eq!(server.code_map.len(), 10); // Run open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: "test".to_string(), }, }) .await; // Check the code map. assert_eq!(server.code_map.len(), 11); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "test".as_bytes() ); // Create a zip. let bytes = server.create_zip().await.unwrap(); // Write the bytes to a tmp file. let tmp_dir = std::env::temp_dir(); let filename = format!("test-{}.zip", chrono::Utc::now().timestamp()); let tmp_file = tmp_dir.join(filename); std::fs::write(&tmp_file, bytes).unwrap(); // Try to unzip the file. let mut archive = zip::ZipArchive::new(std::fs::File::open(&tmp_file).unwrap()).unwrap(); // Check the files in the zip. let mut files = BTreeMap::new(); for i in 0..archive.len() { let file = archive.by_index(i).unwrap(); files.insert(file.name().to_string(), file.size()); } assert_eq!(files.len(), 11); let util_path = format!("{}/util.rs", string_path).replace("file://", ""); assert!(files.contains_key(&util_path)); assert_eq!(files.get("/test.kcl"), Some(&4)); } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_completions() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"thing= 1 st"# .to_string(), }, }) .await; // Send completion request. let completions = server .completion(tower_lsp::lsp_types::CompletionParams { text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, position: tower_lsp::lsp_types::Position { line: 0, character: 16 }, }, context: None, partial_result_params: Default::default(), work_done_progress_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the completions. if let tower_lsp::lsp_types::CompletionResponse::Array(completions) = completions { assert!(completions.len() > 10); } else { panic!("Expected array of completions"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_completions_empty_in_comment() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"thing= 1 // st"#.to_string(), }, }) .await; // Send completion request. let completions = server .completion(tower_lsp::lsp_types::CompletionParams { text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, position: tower_lsp::lsp_types::Position { line: 0, character: 13 }, }, context: None, partial_result_params: Default::default(), work_done_progress_params: Default::default(), }) .await .unwrap(); assert!(completions.is_none()); } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_completions_tags() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"part001 = startSketchOn('XY') |> startProfileAt([11.19, 28.35], %) |> line(end = [28.67, -13.25], tag = $here) |> line(end = [-4.12, -22.81]) |> line(end = [-33.24, 14.55]) |> close() |> extrude(length = 5)"# .to_string(), }, }) .await; // Send completion request. let completions = server .completion(tower_lsp::lsp_types::CompletionParams { text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, position: tower_lsp::lsp_types::Position { line: 0, character: 198, }, }, context: None, partial_result_params: Default::default(), work_done_progress_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the completions. if let tower_lsp::lsp_types::CompletionResponse::Array(completions) = completions { assert!(completions.len() > 10); // Make sure that `here` is in the completions. let const_completion = completions .iter() .find(|completion| completion.label == "here") .unwrap(); assert_eq!( const_completion.kind, Some(tower_lsp::lsp_types::CompletionItemKind::REFERENCE) ); } else { panic!("Expected array of completions"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_completions_const_raw() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"con"#.to_string(), }, }) .await; // Send completion request. let completions = server .completion(tower_lsp::lsp_types::CompletionParams { text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, position: tower_lsp::lsp_types::Position { line: 0, character: 2 }, }, context: None, partial_result_params: Default::default(), work_done_progress_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the completions. if let tower_lsp::lsp_types::CompletionResponse::Array(completions) = completions { assert!(completions.len() > 10); // Find the one with label "fn". let const_completion = completions.iter().find(|completion| completion.label == "fn").unwrap(); assert_eq!( const_completion.kind, Some(tower_lsp::lsp_types::CompletionItemKind::KEYWORD) ); } else { panic!("Expected array of completions"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_completions_import() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"import boo, baz as bux from 'bar.kcl' //import 'bar.kcl' x = b"# .to_string(), }, }) .await; // Send completion request. let completions = server .completion(tower_lsp::lsp_types::CompletionParams { text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, position: tower_lsp::lsp_types::Position { line: 2, character: 5 }, }, context: None, partial_result_params: Default::default(), work_done_progress_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the completions. if let tower_lsp::lsp_types::CompletionResponse::Array(completions) = completions { assert!(completions.len() > 10); // Find the one with label "foo". completions.iter().find(|completion| completion.label == "boo").unwrap(); // completions // .iter() // .find(|completion| completion.label == "bar") // .unwrap(); completions.iter().find(|completion| completion.label == "bux").unwrap(); assert!(!completions.iter().any(|completion| completion.label == "baz")); // Find the one with label "bar". } else { panic!("Expected array of completions"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_on_hover() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: "startSketchOn()".to_string(), }, }) .await; // Send hover request. let hover = server .hover(tower_lsp::lsp_types::HoverParams { text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, position: tower_lsp::lsp_types::Position { line: 0, character: 2 }, }, work_done_progress_params: Default::default(), }) .await .unwrap(); // Check the hover. if let Some(hover) = hover { assert_eq!(hover.contents, tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { kind: tower_lsp::lsp_types::MarkupKind::Markdown, value: "```startSketchOn(data: SketchData, tag?: FaceTag) -> SketchSurface```\nStart a new 2-dimensional sketch on a specific plane or face.\n\n### Sketch on Face Behavior\n\nThere are some important behaviors to understand when sketching on a face:\n\nThe resulting sketch will _include_ the face and thus Solid that was sketched on. So say you were to export the resulting Sketch / Solid from a sketch on a face, you would get both the artifact of the sketch on the face and the parent face / Solid itself.\n\nThis is important to understand because if you were to then sketch on the resulting Solid, it would again include the face and parent Solid that was sketched on. This could go on indefinitely.\n\nThe point is if you want to export the result of a sketch on a face, you only need to export the final Solid that was created from the sketch on the face, since it will include all the parent faces and Solids.".to_string() })); } else { panic!("Expected hover"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_on_hover_shebang() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"#!/usr/bin/env zoo kcl view startSketchOn()"# .to_string(), }, }) .await; // Send hover request. let hover = server .hover(tower_lsp::lsp_types::HoverParams { text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, position: tower_lsp::lsp_types::Position { line: 0, character: 2 }, }, work_done_progress_params: Default::default(), }) .await .unwrap(); // Check the hover. if let Some(hover) = hover { assert_eq!( hover.contents, tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { kind: tower_lsp::lsp_types::MarkupKind::Markdown, value: "The `#!` at the start of a script, known as a shebang, specifies the path to the interpreter that should execute the script. This line is not necessary for your `kcl` to run in the modeling-app. You can safely delete it. If you wish to learn more about what you _can_ do with a shebang, read this doc: [zoo.dev/docs/faq/shebang](https://zoo.dev/docs/faq/shebang).".to_string() }) ); } else { panic!("Expected hover"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_signature_help() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 1, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: "startSketchOn('XY')".to_string(), }], }) .await; // Send signature help request. let signature_help = server .signature_help(tower_lsp::lsp_types::SignatureHelpParams { text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, position: tower_lsp::lsp_types::Position { line: 0, character: 2 }, }, context: None, work_done_progress_params: Default::default(), }) .await .unwrap(); // Check the signature help. if let Some(signature_help) = signature_help { assert_eq!( signature_help.signatures.len(), 1, "Expected one signature, got {:?}", signature_help.signatures ); assert_eq!(signature_help.signatures[0].label, "startSketchOn"); } else { panic!("Expected signature help"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_semantic_tokens() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: "startSketchOn('XY')".to_string(), }, }) .await; // Send semantic tokens request. let semantic_tokens = server .semantic_tokens_full(tower_lsp::lsp_types::SemanticTokensParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, partial_result_params: Default::default(), work_done_progress_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the semantic tokens. if let tower_lsp::lsp_types::SemanticTokensResult::Tokens(semantic_tokens) = semantic_tokens { assert_eq!(semantic_tokens.data.len(), 2); assert_eq!(semantic_tokens.data[0].length, 13); assert_eq!(semantic_tokens.data[0].delta_start, 0); assert_eq!(semantic_tokens.data[0].delta_line, 0); assert_eq!( semantic_tokens.data[0].token_type, server .get_semantic_token_type_index(&SemanticTokenType::FUNCTION) .unwrap() ); assert_eq!(semantic_tokens.data[1].length, 4); assert_eq!(semantic_tokens.data[1].delta_start, 14); assert_eq!(semantic_tokens.data[1].delta_line, 0); assert_eq!( semantic_tokens.data[1].token_type, server .get_semantic_token_type_index(&SemanticTokenType::STRING) .unwrap() ); } else { panic!("Expected semantic tokens"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_semantic_tokens_large_file() { let server = kcl_lsp_server(false).await.unwrap(); let code = include_str!("../../../tests/executor/inputs/global-tags.kcl"); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Send semantic tokens request. let semantic_tokens = server .semantic_tokens_full(tower_lsp::lsp_types::SemanticTokensParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, partial_result_params: Default::default(), work_done_progress_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the semantic tokens. if let tower_lsp::lsp_types::SemanticTokensResult::Tokens(semantic_tokens) = semantic_tokens { assert!(!semantic_tokens.data.is_empty()); } else { panic!("Expected semantic tokens"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_semantic_tokens_with_modifiers() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20], tag = $seg01) |> line(end = [-20, 0]) |> close() |> extrude(length = 3.14) thing = {blah: "foo"} bar = thing.blah fn myFn = (param1) => { return param1 }"# .to_string(), }, }) .await; // Assure we have no errors. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); // Get the token map. let token_map = server.token_map.get("file:///test.kcl").unwrap().clone(); assert!(!token_map.is_empty()); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Send semantic tokens request. let semantic_tokens = server .semantic_tokens_full(tower_lsp::lsp_types::SemanticTokensParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, partial_result_params: Default::default(), work_done_progress_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the semantic tokens. if let tower_lsp::lsp_types::SemanticTokensResult::Tokens(semantic_tokens) = semantic_tokens { let function_index = server .get_semantic_token_type_index(&SemanticTokenType::FUNCTION) .unwrap(); let property_index = server .get_semantic_token_type_index(&SemanticTokenType::PROPERTY) .unwrap(); let parameter_index = server .get_semantic_token_type_index(&SemanticTokenType::PARAMETER) .unwrap(); let variable_index = server .get_semantic_token_type_index(&SemanticTokenType::VARIABLE) .unwrap(); let declaration_index = server .get_semantic_token_modifier_index(vec![SemanticTokenModifier::DECLARATION]) .unwrap(); let definition_index = server .get_semantic_token_modifier_index(vec![SemanticTokenModifier::DEFINITION]) .unwrap(); let default_library_index = server .get_semantic_token_modifier_index(vec![SemanticTokenModifier::DEFAULT_LIBRARY]) .unwrap(); let variable_modifiers = server .get_semantic_token_modifier_index(vec![ SemanticTokenModifier::DECLARATION, SemanticTokenModifier::READONLY, ]) .unwrap(); let tag_modifiers = server .get_semantic_token_modifier_index(vec![SemanticTokenModifier::DEFINITION, SemanticTokenModifier::STATIC]) .unwrap(); // Iterate over the tokens and check the token types. let mut found_parameter = false; let mut found_property = false; let mut found_function_declaration = false; let mut found_variable_declaration = false; let mut found_property_declaration = false; let mut found_tag_declaration = false; let mut found_default_library = false; for token in semantic_tokens.data { if token.token_type == function_index && token.token_modifiers_bitset == default_library_index { found_default_library = true; } if token.token_type == parameter_index { found_parameter = true; } else if token.token_type == property_index { found_property = true; } if token.token_type == definition_index && token.token_modifiers_bitset == tag_modifiers { found_tag_declaration = true; } if token.token_type == function_index && token.token_modifiers_bitset == variable_modifiers { found_function_declaration = true; } if token.token_type == variable_index && token.token_modifiers_bitset == variable_modifiers { found_variable_declaration = true; } if token.token_type == property_index && token.token_modifiers_bitset == declaration_index { found_property_declaration = true; } if found_parameter && found_property && found_function_declaration && found_variable_declaration && found_property_declaration && found_tag_declaration && found_default_library { break; } } if !found_parameter { panic!("Expected parameter token"); } if !found_property { panic!("Expected property token"); } if !found_function_declaration { panic!("Expected function declaration token"); } if !found_variable_declaration { panic!("Expected variable declaration token"); } if !found_property_declaration { panic!("Expected property declaration token"); } if !found_tag_declaration { panic!("Expected tag declaration token"); } if !found_default_library { panic!("Expected default library token"); } } else { panic!("Expected semantic tokens"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_semantic_tokens_multiple_comments() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"// Ball Bearing // A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads. // Define constants like ball diameter, inside diameter, overhange length, and thickness sphereDia = 0.5"# .to_string(), }, }) .await; // Send semantic tokens request. let semantic_tokens = server .semantic_tokens_full(tower_lsp::lsp_types::SemanticTokensParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, partial_result_params: Default::default(), work_done_progress_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the semantic tokens. if let tower_lsp::lsp_types::SemanticTokensResult::Tokens(semantic_tokens) = semantic_tokens { assert_eq!(semantic_tokens.data.len(), 6); assert_eq!(semantic_tokens.data[0].length, 15); assert_eq!(semantic_tokens.data[0].delta_start, 0); assert_eq!(semantic_tokens.data[0].delta_line, 0); assert_eq!( semantic_tokens.data[0].token_type, server .get_semantic_token_type_index(&SemanticTokenType::COMMENT) .unwrap() ); assert_eq!(semantic_tokens.data[1].length, 232); assert_eq!(semantic_tokens.data[1].delta_start, 0); assert_eq!(semantic_tokens.data[1].delta_line, 1); assert_eq!( semantic_tokens.data[1].token_type, server .get_semantic_token_type_index(&SemanticTokenType::COMMENT) .unwrap() ); assert_eq!(semantic_tokens.data[2].length, 88); assert_eq!(semantic_tokens.data[2].delta_start, 0); assert_eq!(semantic_tokens.data[2].delta_line, 2); assert_eq!( semantic_tokens.data[2].token_type, server .get_semantic_token_type_index(&SemanticTokenType::COMMENT) .unwrap() ); assert_eq!(semantic_tokens.data[3].length, 9); assert_eq!(semantic_tokens.data[3].delta_start, 0); assert_eq!(semantic_tokens.data[3].delta_line, 1); assert_eq!( semantic_tokens.data[3].token_type, server .get_semantic_token_type_index(&SemanticTokenType::VARIABLE) .unwrap() ); assert_eq!(semantic_tokens.data[4].length, 1); assert_eq!(semantic_tokens.data[4].delta_start, 10); assert_eq!( semantic_tokens.data[4].token_type, server .get_semantic_token_type_index(&SemanticTokenType::OPERATOR) .unwrap() ); assert_eq!(semantic_tokens.data[5].length, 3); assert_eq!(semantic_tokens.data[5].delta_start, 2); assert_eq!( semantic_tokens.data[5].token_type, server .get_semantic_token_type_index(&SemanticTokenType::NUMBER) .unwrap() ); } else { panic!("Expected semantic tokens"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_document_symbol() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"myVar = 1 startSketchOn('XY')"# .to_string(), }, }) .await; // Send document symbol request. let document_symbol = server .document_symbol(tower_lsp::lsp_types::DocumentSymbolParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the document symbol. if let tower_lsp::lsp_types::DocumentSymbolResponse::Nested(document_symbol) = document_symbol { assert_eq!(document_symbol.len(), 1); assert_eq!(document_symbol[0].name, "myVar"); } else { panic!("Expected document symbol"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_document_symbol_tag() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"part001 = startSketchOn('XY') |> startProfileAt([11.19, 28.35], %) |> line(end = [28.67, -13.25], tag = $here) |> line(end = [-4.12, -22.81]) |> line(end = [-33.24, 14.55]) |> close() |> extrude(length = 5)"# .to_string(), }, }) .await; // Send document symbol request. let document_symbol = server .document_symbol(tower_lsp::lsp_types::DocumentSymbolParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the document symbol. if let tower_lsp::lsp_types::DocumentSymbolResponse::Nested(document_symbol) = document_symbol { assert_eq!(document_symbol.len(), 2); assert_eq!(document_symbol[0].name, "part001"); assert_eq!(document_symbol[1].name, "here"); } else { panic!("Expected document symbol"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_formatting() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"startSketchOn('XY') |> startProfileAt([0,0], %)"# .to_string(), }, }) .await; // Send formatting request. let formatting = server .formatting(tower_lsp::lsp_types::DocumentFormattingParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, options: tower_lsp::lsp_types::FormattingOptions { tab_size: 4, insert_spaces: true, properties: Default::default(), trim_trailing_whitespace: None, insert_final_newline: None, trim_final_newlines: None, }, work_done_progress_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the formatting. assert_eq!(formatting.len(), 1); assert_eq!( formatting[0].new_text, r#"startSketchOn('XY') |> startProfileAt([0, 0], %)"# ); } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_formatting_extra_parens() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"// Ball Bearing // A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads. // Define constants like ball diameter, inside diameter, overhange length, and thickness sphereDia = 0.5 insideDia = 1 thickness = 0.25 overHangLength = .4 // Sketch and revolve the inside bearing piece insideRevolve = startSketchOn('XZ') |> startProfileAt([insideDia / 2, 0], %) |> line(end = [0, thickness + sphereDia / 2]) |> line(end = [overHangLength, 0]) |> line(end = [0, -thickness]) |> line(end = [-overHangLength + thickness, 0]) |> line(end = [0, -sphereDia]) |> line(end = [overHangLength - thickness, 0]) |> line(end = [0, -thickness]) |> line(end = [-overHangLength, 0]) |> close() |> revolve({ axis: 'y' }, %) // Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis) sphere = startSketchOn('XZ') |> startProfileAt([ 0.05 + insideDia / 2 + thickness, 0 - 0.05 ], %) |> line(end = [sphereDia - 0.1, 0]) |> arc({ angle_start: 0, angle_end: -180, radius: sphereDia / 2 - 0.05 }, %) |> close() |> revolve({ axis: 'x' }, %) |> patternCircular3d( axis = [0, 0, 1], center = [0, 0, 0], repetitions = 10, arcDegrees = 360, rotateDuplicates = true, ) // Sketch and revolve the outside bearing outsideRevolve = startSketchOn('XZ') |> startProfileAt([ insideDia / 2 + thickness + sphereDia, 0 ], %) |> line(end = [0, sphereDia / 2]) |> line(end = [-overHangLength + thickness, 0]) |> line(end = [0, thickness]) |> line(end = [overHangLength, 0]) |> line(end = [0, -2 * thickness - sphereDia]) |> line(end = [-overHangLength, 0]) |> line(end = [0, thickness]) |> line(end = [overHangLength - thickness, 0]) |> close() |> revolve({ axis: 'y' }, %)"# .to_string(), }, }) .await; // Send formatting request. let formatting = server .formatting(tower_lsp::lsp_types::DocumentFormattingParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, options: tower_lsp::lsp_types::FormattingOptions { tab_size: 2, insert_spaces: true, properties: Default::default(), trim_trailing_whitespace: None, insert_final_newline: None, trim_final_newlines: None, }, work_done_progress_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the formatting. assert_eq!(formatting.len(), 1); assert_eq!( formatting[0].range, tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 0, character: 0 }, end: tower_lsp::lsp_types::Position { line: 60, character: 30 } } ); assert_eq!( formatting[0].new_text, r#"// Ball Bearing // A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads. // Define constants like ball diameter, inside diameter, overhange length, and thickness sphereDia = 0.5 insideDia = 1 thickness = 0.25 overHangLength = .4 // Sketch and revolve the inside bearing piece insideRevolve = startSketchOn('XZ') |> startProfileAt([insideDia / 2, 0], %) |> line(end = [0, thickness + sphereDia / 2]) |> line(end = [overHangLength, 0]) |> line(end = [0, -thickness]) |> line(end = [-overHangLength + thickness, 0]) |> line(end = [0, -sphereDia]) |> line(end = [overHangLength - thickness, 0]) |> line(end = [0, -thickness]) |> line(end = [-overHangLength, 0]) |> close() |> revolve({ axis = 'y' }, %) // Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis) sphere = startSketchOn('XZ') |> startProfileAt([ 0.05 + insideDia / 2 + thickness, 0 - 0.05 ], %) |> line(end = [sphereDia - 0.1, 0]) |> arc({ angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05 }, %) |> close() |> revolve({ axis = 'x' }, %) |> patternCircular3d( axis = [0, 0, 1], center = [0, 0, 0], repetitions = 10, arcDegrees = 360, rotateDuplicates = true, ) // Sketch and revolve the outside bearing outsideRevolve = startSketchOn('XZ') |> startProfileAt([ insideDia / 2 + thickness + sphereDia, 0 ], %) |> line(end = [0, sphereDia / 2]) |> line(end = [-overHangLength + thickness, 0]) |> line(end = [0, thickness]) |> line(end = [overHangLength, 0]) |> line(end = [0, -2 * thickness - sphereDia]) |> line(end = [-overHangLength, 0]) |> line(end = [0, thickness]) |> line(end = [overHangLength - thickness, 0]) |> close() |> revolve({ axis = 'y' }, %)"# ); } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_rename() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"thing= 1"#.to_string(), }, }) .await; // Send rename request. let rename = server .rename(tower_lsp::lsp_types::RenameParams { text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, position: tower_lsp::lsp_types::Position { line: 0, character: 2 }, }, new_name: "newName".to_string(), work_done_progress_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the rename. let changes = rename.changes.unwrap(); let u: tower_lsp::lsp_types::Url = "file:///test.kcl".try_into().unwrap(); assert_eq!( changes.get(&u).unwrap().clone(), vec![tower_lsp::lsp_types::TextEdit { range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 0, character: 0 }, end: tower_lsp::lsp_types::Position { line: 0, character: 7 } }, new_text: "newName = 1\n".to_string() }] ); } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_diagnostic_no_errors() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"thing= 1"#.to_string(), }, }) .await; // Send diagnostics request. let diagnostics = server .diagnostic(tower_lsp::lsp_types::DocumentDiagnosticParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, partial_result_params: Default::default(), work_done_progress_params: Default::default(), identifier: None, previous_result_id: None, }) .await .unwrap(); // Check the diagnostics. if let tower_lsp::lsp_types::DocumentDiagnosticReportResult::Report(diagnostics) = diagnostics { if let tower_lsp::lsp_types::DocumentDiagnosticReport::Full(diagnostics) = diagnostics { assert_eq!(diagnostics.full_document_diagnostic_report.items.len(), 0); } else { panic!("Expected full diagnostics"); } } else { panic!("Expected diagnostics"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_diagnostic_has_errors() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"k;ajsndasd thing= 1"#.to_string(), }, }) .await; // Send diagnostics request. let diagnostics = server .diagnostic(tower_lsp::lsp_types::DocumentDiagnosticParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, partial_result_params: Default::default(), work_done_progress_params: Default::default(), identifier: None, previous_result_id: None, }) .await .unwrap(); // Check the diagnostics. if let tower_lsp::lsp_types::DocumentDiagnosticReportResult::Report(diagnostics) = diagnostics { if let tower_lsp::lsp_types::DocumentDiagnosticReport::Full(diagnostics) = diagnostics { assert_eq!(diagnostics.full_document_diagnostic_report.items.len(), 1); assert_eq!( diagnostics.full_document_diagnostic_report.items[0].message, "lexical: found unknown token ';'" ); } else { panic!("Expected full diagnostics"); } } else { panic!("Expected diagnostics"); } } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_diagnostic_has_lints() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///testlint.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"THING = 10"#.to_string(), }, }) .await; // Send diagnostics request. let diagnostics = server .diagnostic(tower_lsp::lsp_types::DocumentDiagnosticParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///testlint.kcl".try_into().unwrap(), }, partial_result_params: Default::default(), work_done_progress_params: Default::default(), identifier: None, previous_result_id: None, }) .await .unwrap(); // Check the diagnostics. if let tower_lsp::lsp_types::DocumentDiagnosticReportResult::Report(diagnostics) = diagnostics { if let tower_lsp::lsp_types::DocumentDiagnosticReport::Full(diagnostics) = diagnostics { assert_eq!(diagnostics.full_document_diagnostic_report.items.len(), 1); assert_eq!( diagnostics.full_document_diagnostic_report.items[0].message, "Identifiers must be lowerCamelCase" ); } else { panic!("Expected full diagnostics"); } } else { panic!("Expected diagnostics"); } } #[tokio::test(flavor = "multi_thread")] async fn test_copilot_lsp_set_editor_info() { let server = copilot_lsp_server().await.unwrap(); // Send set editor info request. server .set_editor_info(crate::lsp::copilot::types::CopilotEditorInfo { editor_info: crate::lsp::copilot::types::EditorInfo { name: "vscode".to_string(), version: "1.0.0".to_string(), }, editor_configuration: crate::lsp::copilot::types::EditorConfiguration { disabled_languages: vec![], enable_auto_completions: true, }, editor_plugin_info: crate::lsp::copilot::types::EditorInfo { name: "copilot".to_string(), version: "1.0.0".to_string(), }, }) .await .unwrap(); // Check the editor info. // Acquire the lock. let editor_info = server.editor_info.read().unwrap(); assert_eq!(editor_info.editor_info.name, "vscode"); assert_eq!(editor_info.editor_info.version, "1.0.0"); } #[tokio::test(flavor = "multi_thread")] #[ignore] // Ignore til hosted model is faster (@jessfraz working on). async fn test_copilot_lsp_completions_raw() { let server = copilot_lsp_server().await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.copilot".try_into().unwrap(), language_id: "copilot".to_string(), version: 1, text: "st".to_string(), }, }) .await; // Send completion request. let completions = server .get_completions( "kcl".to_string(), r#"bracket = startSketchOn('XY') |> startProfileAt([0, 0], %) "# .to_string(), r#" |> close() |> extrude(length = 10)"# .to_string(), ) .await .unwrap(); // Check the completions. assert_eq!(completions.len(), 1); println!("got completion:\n```\n{}\n```", completions[0]); // Test the cache. let completions_hit_cache = server .get_completions( "kcl".to_string(), r#"bracket = startSketchOn('XY') |> startProfileAt([0, 0], %) "# .to_string(), r#" |> close() |> extrude(length = 10)"# .to_string(), ) .await .unwrap(); assert_eq!(completions, completions_hit_cache); } #[tokio::test(flavor = "multi_thread")] #[ignore] // Ignore til hosted model is faster (@jessfraz working on). async fn test_copilot_lsp_completions() { let server = copilot_lsp_server().await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.copilot".try_into().unwrap(), language_id: "copilot".to_string(), version: 1, text: "st".to_string(), }, }) .await; // Send completion request. let params = crate::lsp::copilot::types::CopilotLspCompletionParams { doc: crate::lsp::copilot::types::CopilotDocParams { indent_size: 4, insert_spaces: true, language_id: "kcl".to_string(), path: "file:///test.copilot".to_string(), position: crate::lsp::copilot::types::CopilotPosition { line: 3, character: 3 }, relative_path: "test.copilot".to_string(), source: r#"bracket = startSketchOn('XY') |> startProfileAt([0, 0], %) |> close() |> extrude(length = 10) "# .to_string(), tab_size: 4, uri: "file:///test.copilot".into(), }, }; let completions = server.get_completions_cycling(params.clone()).await.unwrap(); // Check the completions. assert_eq!(completions.completions.len(), 1); // Accept the completion. let completion = completions.completions.first().unwrap(); // Send completion accept request. server .accept_completion(crate::lsp::copilot::types::CopilotAcceptCompletionParams { uuid: completion.uuid }) .await; // Test the cache. let completions_hit_cache = server.get_completions_cycling(params).await.unwrap(); assert_eq!(completions.completions, completions_hit_cache.completions); // Reject the completion. let completion = completions.completions.first().unwrap(); // Send completion reject request. server .reject_completions(crate::lsp::copilot::types::CopilotRejectCompletionParams { uuids: vec![completion.uuid], }) .await; } #[tokio::test(flavor = "multi_thread")] async fn test_copilot_on_save() { let server = copilot_lsp_server().await.unwrap(); // Send save file. server .did_save(tower_lsp::lsp_types::DidSaveTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.copilot".try_into().unwrap(), }, text: Some("my file".to_string()), }) .await; // Check the code map. assert_eq!(server.code_map.len(), 1); assert_eq!( server.code_map.get("file:///test.copilot").unwrap().clone(), "my file".as_bytes() ); } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_on_save() { let server = kcl_lsp_server(false).await.unwrap(); // Send save file. server .did_save(tower_lsp::lsp_types::DidSaveTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, text: Some("my file".to_string()), }) .await; // Check the code map. assert_eq!(server.code_map.len(), 1); assert_eq!( server.code_map.get("file:///test.kcl").unwrap().clone(), "my file".as_bytes() ); } #[tokio::test(flavor = "multi_thread")] async fn test_copilot_rename_not_exists() { let server = copilot_lsp_server().await.unwrap(); // Send rename request. server .did_rename_files(tower_lsp::lsp_types::RenameFilesParams { files: vec![tower_lsp::lsp_types::FileRename { old_uri: "file:///test.copilot".into(), new_uri: "file:///test2.copilot".into(), }], }) .await; // Check the code map. assert_eq!(server.code_map.len(), 1); assert_eq!( server.code_map.get("file:///test2.copilot").unwrap().clone(), "".as_bytes() ); } #[tokio::test(flavor = "multi_thread")] async fn test_lsp_initialized() { let copilot_server = copilot_lsp_server().await.unwrap(); // Send initialize request. copilot_server .initialize(tower_lsp::lsp_types::InitializeParams::default()) .await .unwrap(); // Send initialized request. copilot_server .initialized(tower_lsp::lsp_types::InitializedParams {}) .await; // Check the code map. assert_eq!(copilot_server.code_map.len(), 0); // Now do the same for kcl. let kcl_server = kcl_lsp_server(false).await.unwrap(); // Send initialize request. kcl_server .initialize(tower_lsp::lsp_types::InitializeParams::default()) .await .unwrap(); // Send initialized request. kcl_server.initialized(tower_lsp::lsp_types::InitializedParams {}).await; // Check the code map. assert_eq!(kcl_server.code_map.len(), 0); // Now shut them down. copilot_server.shutdown().await.unwrap(); kcl_server.shutdown().await.unwrap(); } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_on_change_update_ast() { let server = kcl_lsp_server(false).await.unwrap(); let same_text = r#"thing = 1"#.to_string(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: same_text.clone(), }, }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); // Send change file. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 1, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: same_text.clone(), }], }) .await; // Make sure the ast is the same. assert_eq!(ast, server.ast_map.get("file:///test.kcl").unwrap().clone()); // Update the text. let new_text = r#"thing = 2"#.to_string(); // Send change file. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: new_text.clone(), }], }) .await; assert!(ast != server.ast_map.get("file:///test.kcl").unwrap().clone()); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_on_change_update_memory() { let server = kcl_lsp_server(true).await.unwrap(); let same_text = r#"thing = 1"#.to_string(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: same_text.clone(), }, }) .await; // Send change file. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 1, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: same_text.clone(), }], }) .await; // Update the text. let new_text = r#"thing = 2"#.to_string(); // Send change file. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: new_text.clone(), }], }) .await; } #[tokio::test(flavor = "multi_thread", worker_threads = 10)] async fn kcl_test_kcl_lsp_update_units() { let server = kcl_lsp_server(true).await.unwrap(); let same_text = r#"fn cube = (pos, scale) => { sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line(end = [0, scale]) |> line(end = [scale, 0]) |> line(end = [0, -scale]) return sg } part001 = cube([0,0], 20) |> close() |> extrude(length = 20)"# .to_string(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: same_text.clone(), }, }) .await; // Get the tokens. let tokens = server.token_map.get("file:///test.kcl").unwrap().clone(); assert_eq!(tokens.as_slice().len(), 123); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert_eq!(ast.body.len(), 2); // Send change file. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 1, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: same_text.clone(), }], }) .await; let units = server.executor_ctx.read().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::Mm); // Update the units. server .update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams { text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, units: crate::settings::types::UnitLength::M, text: same_text.clone(), }) .await .unwrap(); let units = server.executor_ctx().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::M); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_empty_file_execute_ok() { let server = kcl_lsp_server(true).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: "".to_string(), }, }) .await; } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_diagnostics_on_parse_error() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: "asdasd asdasd asda!d".to_string(), }, }) .await; // Get the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); // Update the text. let new_text = r#"thing = 2"#.to_string(); // Send change file. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: new_text.clone(), }], }) .await; // Get the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_diagnostics_on_execution_error() { let server = kcl_lsp_server(true).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> extrude(length = 3.14) |> fillet({ radius: 3.14, tags: ["tag_or_edge_fn"], }, %)"# .to_string(), }, }) .await; // Get the diagnostics. // TODO warnings being stomped by execution errors? assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); // Update the text. let new_text = r#"part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> extrude(length = 3.14)"# .to_string(); // Send change file. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: new_text.clone(), }], }) .await; // Get the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_full_to_empty_file_updates_ast_and_memory() { let server = kcl_lsp_server(true).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> extrude(length = 3.14)"# .to_string(), }, }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Send change file. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: "".to_string(), }], }) .await; let mut default_hashed = Node::::default(); default_hashed.compute_digest(); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert_eq!(ast, default_hashed); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_unchanged_but_has_diagnostics_reexecute() { let server = kcl_lsp_server(true).await.unwrap(); let code = r#"part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> extrude(length = 3.14)"#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); // Add some fake diagnostics. server.diagnostics_map.insert( "file:///test.kcl".to_string(), vec![tower_lsp::lsp_types::Diagnostic { range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 0, character: 0 }, end: tower_lsp::lsp_types::Position { line: 0, character: 0 }, }, message: "fake diagnostic".to_string(), severity: Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR), code: None, source: None, related_information: None, tags: None, data: None, code_description: None, }], ); // Assure we have one diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); // Clear the ast and memory. server .ast_map .insert("file:///test.kcl".to_string(), Node::::default()); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert_eq!(ast, Node::::default()); // Send change file, but the code is the same. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: code.to_string(), }], }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_and_ast_unchanged_but_has_diagnostics_reexecute() { let server = kcl_lsp_server(true).await.unwrap(); let code = r#"part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> extrude(length = 3.14)"#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); // Add some fake diagnostics. server.diagnostics_map.insert( "file:///test.kcl".to_string(), vec![tower_lsp::lsp_types::Diagnostic { range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 0, character: 0 }, end: tower_lsp::lsp_types::Position { line: 0, character: 0 }, }, message: "fake diagnostic".to_string(), severity: Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR), code: None, source: None, related_information: None, tags: None, data: None, code_description: None, }], ); // Assure we have one diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); // Send change file, but the code is the same. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: code.to_string(), }], }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_and_ast_units_unchanged_but_has_diagnostics_reexecute_on_unit_change() { let server = kcl_lsp_server(true).await.unwrap(); let code = r#"part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> extrude(length = 3.14)"#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); // Add some fake diagnostics. server.diagnostics_map.insert( "file:///test.kcl".to_string(), vec![tower_lsp::lsp_types::Diagnostic { range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 0, character: 0 }, end: tower_lsp::lsp_types::Position { line: 0, character: 0 }, }, message: "fake diagnostic".to_string(), severity: Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR), code: None, source: None, related_information: None, tags: None, data: None, code_description: None, }], ); // Assure we have one diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); let units = server.executor_ctx().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::Mm); // Update the units to the _same_ units. server .update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams { text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, units: crate::settings::types::UnitLength::Mm, text: code.to_string(), }) .await .unwrap(); let units = server.executor_ctx().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::Mm); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_and_ast_units_unchanged_but_has_memory_reexecute_on_unit_change() { let server = kcl_lsp_server(true).await.unwrap(); let code = r#"part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> extrude(length = 3.14)"#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); let units = server.executor_ctx().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::Mm); // Update the units to the _same_ units. server .update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams { text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, units: crate::settings::types::UnitLength::Mm, text: code.to_string(), }) .await .unwrap(); let units = server.executor_ctx().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::Mm); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_cant_execute_set() { let server = kcl_lsp_server(true).await.unwrap(); let code = r#"part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> extrude(length = 3.14)"#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); // Update the units to the _same_ units. let units = server.executor_ctx().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::Mm); server .update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams { text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, units: crate::settings::types::UnitLength::Mm, text: code.to_string(), }) .await .unwrap(); let units = server.executor_ctx().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::Mm); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); assert_eq!(server.can_execute().await, true); // Set that we cannot execute. server .update_can_execute(crate::lsp::kcl::custom_notifications::UpdateCanExecuteParams { can_execute: false }) .await .unwrap(); assert_eq!(server.can_execute().await, false); // Update the units to the _same_ units. let units = server.executor_ctx().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::Mm); server .update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams { text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, units: crate::settings::types::UnitLength::Mm, text: code.to_string(), }) .await .unwrap(); let units = server.executor_ctx().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::Mm); let mut default_hashed = Node::::default(); default_hashed.compute_digest(); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != default_hashed); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); // Set that we CAN execute. server .update_can_execute(crate::lsp::kcl::custom_notifications::UpdateCanExecuteParams { can_execute: true }) .await .unwrap(); assert_eq!(server.can_execute().await, true); // Update the units to the _same_ units. let units = server.executor_ctx.read().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::Mm); server .update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams { text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, units: crate::settings::types::UnitLength::Mm, text: code.to_string(), }) .await .unwrap(); let units = server.executor_ctx.read().await.clone().unwrap().settings.units; assert_eq!(units, crate::settings::types::UnitLength::Mm); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have no diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0); } #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_folding() { let server = kcl_lsp_server(false).await.unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: r#"startSketchOn('XY') |> startProfileAt([0,0], %)"# .to_string(), }, }) .await; // Send folding request. let folding = server .folding_range(tower_lsp::lsp_types::FoldingRangeParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), }) .await .unwrap() .unwrap(); // Check the folding. assert_eq!(folding.len(), 1); assert_eq!( folding.first().unwrap().clone(), tower_lsp::lsp_types::FoldingRange { start_line: 19, start_character: None, end_line: 67, end_character: None, kind: Some(tower_lsp::lsp_types::FoldingRangeKind::Region), collapsed_text: Some("startSketchOn('XY')".to_string()) } ); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_with_parse_error_and_ast_unchanged_but_has_diagnostics_reparse() { let server = kcl_lsp_server(false).await.unwrap(); let code = r#"part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> ^^^things(3.14, %)"#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl"); assert!(ast.is_none()); // Assure we have one diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); // Send change file, but the code is the same. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: code.to_string(), }], }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl"); assert!(ast.is_none()); // Assure we have one diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_with_lint_and_ast_unchanged_but_has_diagnostics_reparse() { let server = kcl_lsp_server(false).await.unwrap(); let code = r#"LINT = 1 part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> extrude(length = 3.14)"#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have one diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); // Send change file, but the code is the same. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: code.to_string(), }], }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have one diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_with_lint_and_parse_error_and_ast_unchanged_but_has_diagnostics_reparse() { let server = kcl_lsp_server(false).await.unwrap(); let code = r#"LINT = 1 part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> ^^^^thing(3.14, %)"#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl"); assert!(ast.is_none()); // Assure we have diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); // Send change file, but the code is the same. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: code.to_string(), }], }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl"); assert!(ast.is_none()); // Assure we have one diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_lint_and_ast_unchanged_but_has_diagnostics_reexecute() { let server = kcl_lsp_server(true).await.unwrap(); let code = r#"LINT = 1 part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20], tag = $seg01) |> line(end = [-20, 0], tag = $seg01) |> close() |> extrude(length = 3.14)"#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Assure we have diagnostics. // Check the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 2); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Send change file, but the code is the same. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: code.to_string(), }], }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have diagnostics. // Check the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 2); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_lint_reexecute_new_lint() { let server = kcl_lsp_server(true).await.unwrap(); let code = r#"LINT = 1 part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20], tag = $seg01) |> line(end = [-20, 0], tag = $seg01) |> close() |> extrude(length = 3.14)"#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Assure we have diagnostics. // Check the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 2); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Send change file, but the code is the same. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: r#"part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20], tag = $seg01) |> line(end = [-20, 0], tag = $seg01) |> close() |> extrude(length = 3.14) NEW_LINT = 1"# .to_string(), }], }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Assure we have diagnostics. // Check the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 2); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_lint_reexecute_new_ast_error() { let server = kcl_lsp_server(true).await.unwrap(); let code = r#"LINT = 1 part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20], tag = $seg01) |> line(end = [-20, 0], tag = $seg01) |> close() |> ^^^extrude(length = 3.14)"#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Assure we have diagnostics. // Check the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); // Get the ast. let ast = server.ast_map.get("file:///test.kcl"); assert!(ast.is_none()); // Send change file, but the code is the same. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: r#"part001 = startSketchOn('XY') |> ^^^^startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20], tag = $seg01) |> line(end = [-20, 0], tag = $seg01) |> close() |> extrude(length = 3.14) NEW_LINT = 1"# .to_string(), }], }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl"); assert!(ast.is_none()); // Assure we have diagnostics. // Check the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_lint_reexecute_had_lint_new_parse_error() { let server = kcl_lsp_server(true).await.unwrap(); let code = r#"LINT = 1 part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() "#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Assure we have diagnostics. // Check the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Get the symbols map. let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone(); assert!(!symbols_map.is_empty()); // Get the semantic tokens map. let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); assert!(!semantic_tokens_map.is_empty()); // Send change file, but the code is the same. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: r#"part001 = startSketchOn('XY') |> ^^^^startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() |> extrude(length = 3.14) NEW_LINT = 1"# .to_string(), }], }) .await; // Get the ast. let ast = server.ast_map.get("file:///test.kcl"); assert!(ast.is_none()); // Get the symbols map. let symbols_map = server.symbols_map.get("file:///test.kcl"); assert!(symbols_map.is_none()); // Get the semantic tokens map. let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); assert!(!semantic_tokens_map.is_empty()); // Assure we have diagnostics. // Check the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_code_lint_reexecute_had_lint_new_execution_error() { let server = kcl_lsp_server(true).await.unwrap(); let code = r#"LINT = 1 part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0]) |> line(end = [0, 20]) |> line(end = [-20, 0]) |> close() "#; // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.to_string(), }, }) .await; // Assure we have diagnostics. // Check the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1); // Get the token map. let token_map = server.token_map.get("file:///test.kcl").unwrap().clone(); assert!(!token_map.is_empty()); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Get the symbols map. let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone(); assert!(!symbols_map.is_empty()); // Get the semantic tokens map. let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); assert!(!semantic_tokens_map.is_empty()); // Send change file, but the code is the same. server .did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams { text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), version: 2, }, content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent { range: None, range_length: None, text: r#"LINT = 1 part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line(end = [20, 0], tag = $seg01) |> line(end = [0, 20], tag = $seg01) |> line(end = [-20, 0]) |> close() "# .to_string(), }], }) .await; // Get the token map. let token_map = server.token_map.get("file:///test.kcl").unwrap().clone(); assert!(!token_map.is_empty()); // Get the ast. let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert!(ast != Node::::default()); // Get the symbols map. let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone(); assert!(!symbols_map.is_empty()); // Get the semantic tokens map. let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); assert!(!semantic_tokens_map.is_empty()); // Assure we have diagnostics. // Check the diagnostics. assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 2); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_completions_number_literal() { let server = kcl_lsp_server(false).await.unwrap(); server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///test.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: "thing = 10".to_string(), }, }) .await; let completions = server .completion(tower_lsp::lsp_types::CompletionParams { text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///test.kcl".try_into().unwrap(), }, position: tower_lsp::lsp_types::Position { line: 0, character: 10 }, }, context: None, partial_result_params: Default::default(), work_done_progress_params: Default::default(), }) .await .unwrap(); assert_eq!(completions.is_none(), true); }