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(), 11); // 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(), 12); 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(), 12); 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(), 13); 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(), 13); 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(), 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() ); // 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(), 14); 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(), 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() ); // 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(), 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() ); // 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(), 11); // 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(), 11); // 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(), 12); 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(), 12); 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(), 13); 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(), 13); 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(), 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() ); // 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(), 14); 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(), 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() ); // 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(), 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() ); // 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(), 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() ); // 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(), 11); } #[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(), 11); // 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(), 12); 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(), 12); 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, // Blank lines to check that we get completions even in an AST newline thing. 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: 0 }, }, 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); assert!(completions.iter().any(|c| c.label == "@settings")); } 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(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#"startSketchOn(XY) foo = 42 foo fn bar(x: string): string { return x } bar("an arg") startSketchOn(XY) |> startProfileAt([0, 0], %) |> line(end = [10, 0]) |> line(end = [0, 10]) "# .to_string(), }, }) .await; // Std lib call 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(); match hover.unwrap().contents { tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { assert!(value.contains("startSketchOn")); assert!(value.contains(": SketchSurface")); assert!(value.contains("Start a new 2-dimensional sketch on a specific")); } _ => unreachable!(), } // Variable use 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: 2, character: 1 }, }, work_done_progress_params: Default::default(), }) .await .unwrap(); match hover.unwrap().contents { tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { assert!(value.contains("foo: number = 42")); } _ => unreachable!(), } // User-defined function call. 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: 8, character: 1 }, }, work_done_progress_params: Default::default(), }) .await .unwrap(); match hover.unwrap().contents { tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { assert!(value.contains("bar(x: string): string")); } _ => unreachable!(), } // Variable inside a function 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: 5, character: 9 }, }, work_done_progress_params: Default::default(), }) .await .unwrap(); match hover.unwrap().contents { tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { assert!(value.contains("x: string")); } _ => unreachable!(), } // std function KwArg 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: 12, character: 11, }, }, work_done_progress_params: Default::default(), }) .await .unwrap(); match hover.unwrap().contents { tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { assert!(value.contains("end?: [number]")); assert!(value.contains("How far away (along the X and Y axes) should this line go?")); } _ => unreachable!(), } } #[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, 2); 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::VARIABLE) .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!("../../e2e/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.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, "Unexpected 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.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.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.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.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(), crate::Program { ast: Default::default(), original_file_contents: Default::default(), }, ); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); assert_eq!(ast.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.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.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.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.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.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.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.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.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.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.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.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: 17, start_character: None, end_line: 65, 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.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.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.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.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.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.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.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.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.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); } #[tokio::test(flavor = "multi_thread")] async fn kcl_test_kcl_lsp_multi_file_error() { let server = kcl_lsp_server(true).await.unwrap(); let cwd = std::env::current_dir().unwrap(); let joined = cwd.join("tests/import_file_parse_error/"); // Change the current directory. std::env::set_current_dir(joined).unwrap(); let code = std::fs::read_to_string("input.kcl").unwrap(); // Send open file. server .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { text_document: tower_lsp::lsp_types::TextDocumentItem { uri: "file:///input.kcl".try_into().unwrap(), language_id: "kcl".to_string(), version: 1, text: code.clone(), }, }) .await; // Send diagnostics request. let diagnostics = server .diagnostic(tower_lsp::lsp_types::DocumentDiagnosticParams { text_document: tower_lsp::lsp_types::TextDocumentIdentifier { uri: "file:///input.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); let item = diagnostics.full_document_diagnostic_report.items.first().unwrap(); assert_eq!(item.message, "syntax: Unexpected token: }"); assert_eq!( Some(vec![tower_lsp::lsp_types::DiagnosticRelatedInformation { location: tower_lsp::lsp_types::Location { uri: "file:///parse-failure.kcl".try_into().unwrap(), range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 1, character: 9 }, end: tower_lsp::lsp_types::Position { line: 2, character: 1 }, }, }, message: "syntax: Unexpected token: }".to_string(), }]), item.related_information ); } else { panic!("Expected full diagnostics"); } } else { panic!("Expected diagnostics"); } }