move back to using dashmap and cleanup heaps of code (#2834)
* more Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixups Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * everything pre mutex locks Signed-off-by: Jess Frazelle <github@jessfraz.com> * remove clones Signed-off-by: Jess Frazelle <github@jessfraz.com> * another clone Signed-off-by: Jess Frazelle <github@jessfraz.com> * iupdates Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * progress Signed-off-by: Jess Frazelle <github@jessfraz.com> * more fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * test-utils Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * all features Signed-off-by: Jess Frazelle <github@jessfraz.com> * better naming Signed-off-by: Jess Frazelle <github@jessfraz.com> * upates Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
4
.github/workflows/cargo-bench.yml
vendored
4
.github/workflows/cargo-bench.yml
vendored
@ -38,5 +38,7 @@ jobs:
|
||||
- name: Benchmark kcl library
|
||||
shell: bash
|
||||
run: |-
|
||||
cd src/wasm-lib/kcl; cargo bench -- iai
|
||||
cd src/wasm-lib/kcl; cargo bench --all-features -- iai
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
|
||||
|
2
src/wasm-lib/Cargo.lock
generated
2
src/wasm-lib/Cargo.lock
generated
@ -533,6 +533,7 @@ dependencies = [
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"futures",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
@ -545,6 +546,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"tokio",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
@ -19,7 +19,7 @@ chrono = "0.4.38"
|
||||
clap = { version = "4.5.7", default-features = false, optional = true }
|
||||
dashmap = "6.0.1"
|
||||
databake = { version = "0.1.8", features = ["derive"] }
|
||||
derive-docs = { version = "0.1.19", path = "../derive-docs" }
|
||||
derive-docs = { version = "0.1.19", path = "../derive-docs" }
|
||||
form_urlencoded = "1.2.1"
|
||||
futures = { version = "0.3.30" }
|
||||
git_rev = "0.1.0"
|
||||
@ -28,7 +28,7 @@ kittycad = { workspace = true, features = ["clap"] }
|
||||
lazy_static = "1.5.0"
|
||||
mime_guess = "2.0.4"
|
||||
parse-display = "0.9.1"
|
||||
pyo3 = {version = "0.22.0", optional = true}
|
||||
pyo3 = { version = "0.22.0", optional = true }
|
||||
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
|
||||
ropey = "1.6.1"
|
||||
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
|
||||
@ -67,6 +67,8 @@ cli = ["dep:clap"]
|
||||
disable-println = []
|
||||
engine = []
|
||||
pyo3 = ["dep:pyo3"]
|
||||
# Helper functions also used in benchmarks.
|
||||
lsp-test-util = []
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
@ -78,10 +80,10 @@ debug = true # Flamegraphs of benchmarks require accurate debug symbols
|
||||
[dev-dependencies]
|
||||
base64 = "0.22.1"
|
||||
convert_case = "0.6.0"
|
||||
criterion = "0.5.1"
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
expectorate = "1.1.0"
|
||||
iai = "0.1"
|
||||
image = {version = "0.25.1", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25.1", default-features = false, features = ["png"] }
|
||||
insta = { version = "1.38.0", features = ["json"] }
|
||||
itertools = "0.13.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
@ -95,3 +97,13 @@ harness = false
|
||||
[[bench]]
|
||||
name = "compiler_benchmark_iai"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "lsp_semantic_tokens_benchmark_criterion"
|
||||
harness = false
|
||||
required-features = ["lsp-test-util"]
|
||||
|
||||
[[bench]]
|
||||
name = "lsp_semantic_tokens_benchmark_iai"
|
||||
harness = false
|
||||
required-features = ["lsp-test-util"]
|
||||
|
@ -0,0 +1,65 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use kcl_lib::lsp::test_util::kcl_lsp_server;
|
||||
use tokio::runtime::Runtime;
|
||||
use tower_lsp::LanguageServer;
|
||||
|
||||
async fn kcl_lsp_semantic_tokens(code: &str) {
|
||||
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: code.to_string(),
|
||||
},
|
||||
})
|
||||
.await;
|
||||
|
||||
// Send semantic tokens request.
|
||||
black_box(
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
fn bench_kcl_lsp_semantic_tokens(c: &mut Criterion) {
|
||||
for (name, code) in [
|
||||
("pipes_on_pipes", PIPES_PROGRAM),
|
||||
("big_kitt", KITT_PROGRAM),
|
||||
("cube", CUBE_PROGRAM),
|
||||
("math", MATH_PROGRAM),
|
||||
("mike_stress_test", MIKE_STRESS_TEST_PROGRAM),
|
||||
("global_tags", GLOBAL_TAGS_FILE),
|
||||
] {
|
||||
c.bench_with_input(BenchmarkId::new("semantic_tokens_", name), &code, |b, &s| {
|
||||
let rt = Runtime::new().unwrap();
|
||||
|
||||
// Spawn a future onto the runtime
|
||||
b.iter(|| {
|
||||
rt.block_on(kcl_lsp_semantic_tokens(s));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_kcl_lsp_semantic_tokens);
|
||||
criterion_main!(benches);
|
||||
|
||||
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
|
||||
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
||||
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
|
||||
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
|
||||
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../../tests/executor/inputs/mike_stress_test.kcl");
|
||||
const GLOBAL_TAGS_FILE: &str = include_str!("../../tests/executor/inputs/global-tags.kcl");
|
@ -0,0 +1,45 @@
|
||||
use iai::black_box;
|
||||
use kcl_lib::lsp::test_util::kcl_lsp_server;
|
||||
use tower_lsp::LanguageServer;
|
||||
|
||||
async fn kcl_lsp_semantic_tokens(code: &str) {
|
||||
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: code.to_string(),
|
||||
},
|
||||
})
|
||||
.await;
|
||||
|
||||
// Send semantic tokens request.
|
||||
black_box(
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
async fn semantic_tokens_global_tags() {
|
||||
let code = GLOBAL_TAGS_FILE;
|
||||
kcl_lsp_semantic_tokens(code).await;
|
||||
}
|
||||
|
||||
iai::main! {
|
||||
semantic_tokens_global_tags,
|
||||
}
|
||||
|
||||
const GLOBAL_TAGS_FILE: &str = include_str!("../../tests/executor/inputs/global-tags.kcl");
|
@ -828,7 +828,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
some_function,
|
||||
crate::ast::types::Function::StdLib {
|
||||
func: Box::new(crate::std::sketch::Line),
|
||||
func: Box::new(crate::std::sketch::Line)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ impl IntoDiagnostic for KclError {
|
||||
|
||||
Diagnostic {
|
||||
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
severity: Some(self.severity()),
|
||||
code: None,
|
||||
// TODO: this is neat we can pass a URL to a help page here for this specific error.
|
||||
code_description: None,
|
||||
@ -153,6 +153,10 @@ impl IntoDiagnostic for KclError {
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn severity(&self) -> DiagnosticSeverity {
|
||||
DiagnosticSeverity::ERROR
|
||||
}
|
||||
}
|
||||
|
||||
/// This is different than to_string() in that it will serialize the Error
|
||||
|
@ -65,7 +65,11 @@ mod tests {
|
||||
assert_finding!(lint_variables, Z0001, "const thicc_nes = 0.5");
|
||||
}
|
||||
|
||||
test_finding!(z0001_full_bad, lint_variables, Z0001, "\
|
||||
test_finding!(
|
||||
z0001_full_bad,
|
||||
lint_variables,
|
||||
Z0001,
|
||||
"\
|
||||
// Define constants
|
||||
const pipeLength = 40
|
||||
const pipeSmallDia = 10
|
||||
@ -94,9 +98,14 @@ const Part001 = startSketchOn('XY')
|
||||
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|
||||
|> close(%)
|
||||
|> revolve({ axis: 'y' }, %)
|
||||
");
|
||||
"
|
||||
);
|
||||
|
||||
test_no_finding!(z0001_full_good, lint_variables, Z0001, "\
|
||||
test_no_finding!(
|
||||
z0001_full_good,
|
||||
lint_variables,
|
||||
Z0001,
|
||||
"\
|
||||
// Define constants
|
||||
const pipeLength = 40
|
||||
const pipeSmallDia = 10
|
||||
@ -125,5 +134,6 @@ const part001 = startSketchOn('XY')
|
||||
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|
||||
|> close(%)
|
||||
|> revolve({ axis: 'y' }, %)
|
||||
");
|
||||
"
|
||||
);
|
||||
}
|
||||
|
@ -70,6 +70,10 @@ impl IntoDiagnostic for Discovered {
|
||||
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
(&self).to_lsp_diagnostic(code)
|
||||
}
|
||||
|
||||
fn severity(&self) -> DiagnosticSeverity {
|
||||
(&self).severity()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for &Discovered {
|
||||
@ -79,7 +83,7 @@ impl IntoDiagnostic for &Discovered {
|
||||
|
||||
Diagnostic {
|
||||
range: source_range.to_lsp_range(code),
|
||||
severity: Some(DiagnosticSeverity::INFORMATION),
|
||||
severity: Some(self.severity()),
|
||||
code: None,
|
||||
// TODO: this is neat we can pass a URL to a help page here for this specific error.
|
||||
code_description: None,
|
||||
@ -90,6 +94,10 @@ impl IntoDiagnostic for &Discovered {
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn severity(&self) -> DiagnosticSeverity {
|
||||
DiagnosticSeverity::INFORMATION
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract lint problem type.
|
||||
|
@ -3,59 +3,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::sync::RwLock;
|
||||
use dashmap::DashMap;
|
||||
use tower_lsp::lsp_types::{
|
||||
CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,
|
||||
CreateFilesParams, DeleteFilesParams, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams,
|
||||
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
|
||||
DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentDiagnosticReport, InitializedParams, MessageType,
|
||||
RenameFilesParams, TextDocumentItem, WorkspaceFolder,
|
||||
DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializedParams, MessageType, RenameFilesParams,
|
||||
TextDocumentItem, WorkspaceFolder,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
fs::FileSystem,
|
||||
lsp::safemap::SafeMap,
|
||||
thread::{JoinHandle, Thread},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InnerHandle(Arc<JoinHandle>);
|
||||
|
||||
impl InnerHandle {
|
||||
pub fn new(handle: JoinHandle) -> Self {
|
||||
Self(Arc::new(handle))
|
||||
}
|
||||
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.0.is_finished()
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.0.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UpdateHandle(Arc<RwLock<Option<InnerHandle>>>);
|
||||
|
||||
impl UpdateHandle {
|
||||
pub fn new(handle: InnerHandle) -> Self {
|
||||
Self(Arc::new(RwLock::new(Some(handle))))
|
||||
}
|
||||
|
||||
pub async fn read(&self) -> Option<InnerHandle> {
|
||||
self.0.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn write(&self, handle: Option<InnerHandle>) {
|
||||
*self.0.write().await = handle;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UpdateHandle {
|
||||
fn default() -> Self {
|
||||
Self(Arc::new(RwLock::new(None)))
|
||||
}
|
||||
}
|
||||
use crate::fs::FileSystem;
|
||||
|
||||
/// A trait for the backend of the language server.
|
||||
#[async_trait::async_trait]
|
||||
@ -71,10 +27,6 @@ where
|
||||
|
||||
async fn set_is_initialized(&self, is_initialized: bool);
|
||||
|
||||
async fn current_handle(&self) -> Option<InnerHandle>;
|
||||
|
||||
async fn set_current_handle(&self, handle: Option<InnerHandle>);
|
||||
|
||||
async fn workspace_folders(&self) -> Vec<WorkspaceFolder>;
|
||||
|
||||
async fn add_workspace_folders(&self, folders: Vec<WorkspaceFolder>);
|
||||
@ -82,7 +34,7 @@ where
|
||||
async fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>);
|
||||
|
||||
/// Get the current code map.
|
||||
fn code_map(&self) -> &SafeMap<String, Vec<u8>>;
|
||||
fn code_map(&self) -> &DashMap<String, Vec<u8>>;
|
||||
|
||||
/// Insert a new code map.
|
||||
async fn insert_code_map(&self, uri: String, text: Vec<u8>);
|
||||
@ -94,62 +46,36 @@ where
|
||||
async fn clear_code_state(&self);
|
||||
|
||||
/// Get the current diagnostics map.
|
||||
fn current_diagnostics_map(&self) -> &SafeMap<String, DocumentDiagnosticReport>;
|
||||
fn current_diagnostics_map(&self) -> &DashMap<String, Vec<Diagnostic>>;
|
||||
|
||||
/// On change event.
|
||||
async fn inner_on_change(&self, params: TextDocumentItem, force: bool);
|
||||
|
||||
/// Check if the file has diagnostics.
|
||||
async fn has_diagnostics(&self, uri: &str) -> bool {
|
||||
if let Some(tower_lsp::lsp_types::DocumentDiagnosticReport::Full(diagnostics)) =
|
||||
self.current_diagnostics_map().get(uri).await
|
||||
{
|
||||
!diagnostics.full_document_diagnostic_report.items.is_empty()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
let Some(diagnostics) = self.current_diagnostics_map().get(uri) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
!diagnostics.is_empty()
|
||||
}
|
||||
|
||||
async fn on_change(&self, params: TextDocumentItem) {
|
||||
// Check if the document is in the current code map and if it is the same as what we have
|
||||
// stored.
|
||||
let filename = params.uri.to_string();
|
||||
if let Some(current_code) = self.code_map().get(&filename).await {
|
||||
if current_code == params.text.as_bytes() && !self.has_diagnostics(&filename).await {
|
||||
if let Some(current_code) = self.code_map().get(&filename) {
|
||||
if *current_code == params.text.as_bytes() && !self.has_diagnostics(&filename).await {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we already have a handle running.
|
||||
if let Some(current_handle) = self.current_handle().await {
|
||||
self.set_current_handle(None).await;
|
||||
// Drop that handle to cancel it.
|
||||
current_handle.cancel();
|
||||
}
|
||||
println!("on_change after check: {:?}", params);
|
||||
|
||||
let cloned = self.clone();
|
||||
let task = JoinHandle::new(async move {
|
||||
cloned
|
||||
.insert_code_map(params.uri.to_string(), params.text.as_bytes().to_vec())
|
||||
.await;
|
||||
cloned.inner_on_change(params, false).await;
|
||||
cloned.set_current_handle(None).await;
|
||||
});
|
||||
let update_handle = InnerHandle::new(task);
|
||||
|
||||
// Set our new handle.
|
||||
self.set_current_handle(Some(update_handle.clone())).await;
|
||||
}
|
||||
|
||||
async fn wait_on_handle(&self) {
|
||||
while let Some(handle) = self.current_handle().await {
|
||||
if !handle.is_finished() {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.set_current_handle(None).await;
|
||||
self.insert_code_map(params.uri.to_string(), params.text.as_bytes().to_vec())
|
||||
.await;
|
||||
println!("on_change after insert: {:?}", params);
|
||||
self.inner_on_change(params, false).await;
|
||||
}
|
||||
|
||||
async fn update_from_disk<P: AsRef<std::path::Path> + std::marker::Send>(&self, path: P) -> Result<()> {
|
||||
@ -211,7 +137,7 @@ where
|
||||
self.remove_workspace_folders(params.event.removed).await;
|
||||
// Remove the code from the current code map.
|
||||
// We do this since it means the user is changing projects so let's refresh the state.
|
||||
if !self.code_map().is_empty().await && should_clear {
|
||||
if !self.code_map().is_empty() && should_clear {
|
||||
self.clear_code_state().await;
|
||||
}
|
||||
for added in params.event.added {
|
||||
|
@ -9,28 +9,27 @@ use std::{
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_lsp::{
|
||||
jsonrpc::{Error, Result},
|
||||
lsp_types::{
|
||||
CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,
|
||||
CreateFilesParams, DeleteFilesParams, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams,
|
||||
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
|
||||
DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentDiagnosticReport, InitializeParams,
|
||||
InitializeResult, InitializedParams, MessageType, OneOf, RenameFilesParams, ServerCapabilities,
|
||||
TextDocumentItem, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, WorkspaceFolder,
|
||||
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
|
||||
DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializeParams, InitializeResult, InitializedParams,
|
||||
MessageType, OneOf, RenameFilesParams, ServerCapabilities, TextDocumentItem, TextDocumentSyncCapability,
|
||||
TextDocumentSyncKind, TextDocumentSyncOptions, WorkspaceFolder, WorkspaceFoldersServerCapabilities,
|
||||
WorkspaceServerCapabilities,
|
||||
},
|
||||
LanguageServer,
|
||||
};
|
||||
|
||||
use super::backend::{InnerHandle, UpdateHandle};
|
||||
use crate::lsp::{
|
||||
backend::Backend as _,
|
||||
copilot::types::{
|
||||
CopilotAcceptCompletionParams, CopilotCompletionResponse, CopilotCompletionTelemetry, CopilotEditorInfo,
|
||||
CopilotLspCompletionParams, CopilotRejectCompletionParams, DocParams,
|
||||
},
|
||||
safemap::SafeMap,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
@ -50,9 +49,9 @@ pub struct Backend {
|
||||
/// The file system client to use.
|
||||
pub fs: Arc<crate::fs::FileManager>,
|
||||
/// The workspace folders.
|
||||
pub workspace_folders: SafeMap<String, WorkspaceFolder>,
|
||||
pub workspace_folders: DashMap<String, WorkspaceFolder>,
|
||||
/// Current code.
|
||||
pub code_map: SafeMap<String, Vec<u8>>,
|
||||
pub code_map: DashMap<String, Vec<u8>>,
|
||||
/// The Zoo API client.
|
||||
pub zoo_client: kittycad::Client,
|
||||
/// The editor info is used to store information about the editor.
|
||||
@ -60,12 +59,11 @@ pub struct Backend {
|
||||
/// The cache is used to store the results of previous requests.
|
||||
pub cache: Arc<cache::CopilotCache>,
|
||||
/// Storage so we can send telemetry data back out.
|
||||
pub telemetry: SafeMap<uuid::Uuid, CopilotCompletionTelemetry>,
|
||||
pub telemetry: DashMap<uuid::Uuid, CopilotCompletionTelemetry>,
|
||||
/// Diagnostics.
|
||||
pub diagnostics_map: SafeMap<String, DocumentDiagnosticReport>,
|
||||
pub diagnostics_map: DashMap<String, Vec<Diagnostic>>,
|
||||
|
||||
pub is_initialized: Arc<tokio::sync::RwLock<bool>>,
|
||||
pub current_handle: UpdateHandle,
|
||||
}
|
||||
|
||||
// Implement the shared backend trait for the language server.
|
||||
@ -87,47 +85,40 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
*self.is_initialized.write().await = is_initialized;
|
||||
}
|
||||
|
||||
async fn current_handle(&self) -> Option<InnerHandle> {
|
||||
self.current_handle.read().await
|
||||
}
|
||||
|
||||
async fn set_current_handle(&self, handle: Option<InnerHandle>) {
|
||||
self.current_handle.write(handle).await;
|
||||
}
|
||||
|
||||
async fn workspace_folders(&self) -> Vec<WorkspaceFolder> {
|
||||
self.workspace_folders.inner().await.values().cloned().collect()
|
||||
// TODO: fix clone
|
||||
self.workspace_folders.iter().map(|i| i.clone()).collect()
|
||||
}
|
||||
|
||||
async fn add_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
|
||||
for folder in folders {
|
||||
self.workspace_folders.insert(folder.name.to_string(), folder).await;
|
||||
self.workspace_folders.insert(folder.name.to_string(), folder);
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
|
||||
for folder in folders {
|
||||
self.workspace_folders.remove(&folder.name).await;
|
||||
self.workspace_folders.remove(&folder.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn code_map(&self) -> &SafeMap<String, Vec<u8>> {
|
||||
fn code_map(&self) -> &DashMap<String, Vec<u8>> {
|
||||
&self.code_map
|
||||
}
|
||||
|
||||
async fn insert_code_map(&self, uri: String, text: Vec<u8>) {
|
||||
self.code_map.insert(uri, text).await;
|
||||
self.code_map.insert(uri, text);
|
||||
}
|
||||
|
||||
async fn remove_from_code_map(&self, uri: String) -> Option<Vec<u8>> {
|
||||
self.code_map.remove(&uri).await
|
||||
self.code_map.remove(&uri).map(|(_, v)| v)
|
||||
}
|
||||
|
||||
async fn clear_code_state(&self) {
|
||||
self.code_map.clear().await;
|
||||
self.code_map.clear();
|
||||
}
|
||||
|
||||
fn current_diagnostics_map(&self) -> &SafeMap<String, DocumentDiagnosticReport> {
|
||||
fn current_diagnostics_map(&self) -> &DashMap<String, Vec<Diagnostic>> {
|
||||
&self.diagnostics_map
|
||||
}
|
||||
|
||||
@ -140,8 +131,15 @@ impl Backend {
|
||||
/// Get completions from the kittycad api.
|
||||
pub async fn get_completions(&self, language: String, prompt: String, suffix: String) -> Result<Vec<String>> {
|
||||
let body = kittycad::types::KclCodeCompletionRequest {
|
||||
prompt: Some(prompt.clone()),
|
||||
suffix: Some(suffix.clone()),
|
||||
extra: Some(kittycad::types::KclCodeCompletionParams {
|
||||
language: Some(language.to_string()),
|
||||
next_indent: None,
|
||||
trim_by_indentation: true,
|
||||
prompt_tokens: Some(prompt.len() as u32),
|
||||
suffix_tokens: Some(suffix.len() as u32),
|
||||
}),
|
||||
prompt: Some(prompt),
|
||||
suffix: Some(suffix),
|
||||
max_tokens: Some(500),
|
||||
temperature: Some(1.0),
|
||||
top_p: Some(1.0),
|
||||
@ -151,13 +149,6 @@ impl Backend {
|
||||
nwo: None,
|
||||
// We haven't implemented streaming yet.
|
||||
stream: false,
|
||||
extra: Some(kittycad::types::KclCodeCompletionParams {
|
||||
language: Some(language.to_string()),
|
||||
next_indent: None,
|
||||
trim_by_indentation: true,
|
||||
prompt_tokens: Some(prompt.len() as u32),
|
||||
suffix_tokens: Some(suffix.len() as u32),
|
||||
}),
|
||||
};
|
||||
|
||||
let resp = self
|
||||
@ -236,7 +227,7 @@ impl Backend {
|
||||
completion: completion.clone(),
|
||||
params: params.clone(),
|
||||
};
|
||||
self.telemetry.insert(completion.uuid, telemetry).await;
|
||||
self.telemetry.insert(completion.uuid, telemetry);
|
||||
}
|
||||
self.cache
|
||||
.set_cached_result(&doc_params.uri, &doc_params.pos.line, &response);
|
||||
@ -250,7 +241,7 @@ impl Backend {
|
||||
.await;
|
||||
|
||||
// Get the original telemetry data.
|
||||
let Some(original) = self.telemetry.remove(¶ms.uuid).await else {
|
||||
let Some(original) = self.telemetry.remove(¶ms.uuid) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@ -269,7 +260,7 @@ impl Backend {
|
||||
// Get the original telemetry data.
|
||||
let mut originals: Vec<CopilotCompletionTelemetry> = Default::default();
|
||||
for uuid in params.uuids {
|
||||
if let Some(original) = self.telemetry.remove(&uuid).await {
|
||||
if let Some(original) = self.telemetry.remove(&uuid).map(|(_, v)| v) {
|
||||
originals.push(original);
|
||||
}
|
||||
}
|
||||
@ -342,7 +333,7 @@ impl LanguageServer for Backend {
|
||||
}
|
||||
|
||||
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||
self.do_did_change(params.clone()).await;
|
||||
self.do_did_change(params).await;
|
||||
}
|
||||
|
||||
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
||||
|
@ -14,12 +14,13 @@ pub mod custom_notifications;
|
||||
use anyhow::Result;
|
||||
#[cfg(feature = "cli")]
|
||||
use clap::Parser;
|
||||
use dashmap::DashMap;
|
||||
use sha2::Digest;
|
||||
use tower_lsp::{
|
||||
jsonrpc::Result as RpcResult,
|
||||
lsp_types::{
|
||||
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, CreateFilesParams,
|
||||
DeleteFilesParams, DiagnosticOptions, DiagnosticServerCapabilities, DiagnosticSeverity,
|
||||
DeleteFilesParams, Diagnostic, DiagnosticOptions, DiagnosticServerCapabilities, DiagnosticSeverity,
|
||||
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
|
||||
DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
|
||||
DidSaveTextDocumentParams, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportResult,
|
||||
@ -43,11 +44,7 @@ use crate::lint::checks;
|
||||
use crate::{
|
||||
ast::types::{Value, VariableKind},
|
||||
executor::SourceRange,
|
||||
lsp::{
|
||||
backend::{Backend as _, InnerHandle, UpdateHandle},
|
||||
safemap::SafeMap,
|
||||
util::IntoDiagnostic,
|
||||
},
|
||||
lsp::{backend::Backend as _, util::IntoDiagnostic},
|
||||
parser::PIPE_OPERATOR,
|
||||
token::TokenType,
|
||||
};
|
||||
@ -96,25 +93,25 @@ pub struct Backend {
|
||||
/// The file system client to use.
|
||||
pub fs: Arc<crate::fs::FileManager>,
|
||||
/// The workspace folders.
|
||||
pub workspace_folders: SafeMap<String, WorkspaceFolder>,
|
||||
pub workspace_folders: DashMap<String, WorkspaceFolder>,
|
||||
/// The stdlib completions for the language.
|
||||
pub stdlib_completions: HashMap<String, CompletionItem>,
|
||||
/// The stdlib signatures for the language.
|
||||
pub stdlib_signatures: HashMap<String, SignatureHelp>,
|
||||
/// Token maps.
|
||||
pub token_map: SafeMap<String, Vec<crate::token::Token>>,
|
||||
pub token_map: DashMap<String, Vec<crate::token::Token>>,
|
||||
/// AST maps.
|
||||
pub ast_map: SafeMap<String, crate::ast::types::Program>,
|
||||
pub ast_map: DashMap<String, crate::ast::types::Program>,
|
||||
/// Memory maps.
|
||||
pub memory_map: SafeMap<String, crate::executor::ProgramMemory>,
|
||||
pub memory_map: DashMap<String, crate::executor::ProgramMemory>,
|
||||
/// Current code.
|
||||
pub code_map: SafeMap<String, Vec<u8>>,
|
||||
pub code_map: DashMap<String, Vec<u8>>,
|
||||
/// Diagnostics.
|
||||
pub diagnostics_map: SafeMap<String, DocumentDiagnosticReport>,
|
||||
pub diagnostics_map: DashMap<String, Vec<Diagnostic>>,
|
||||
/// Symbols map.
|
||||
pub symbols_map: SafeMap<String, Vec<DocumentSymbol>>,
|
||||
pub symbols_map: DashMap<String, Vec<DocumentSymbol>>,
|
||||
/// Semantic tokens map.
|
||||
pub semantic_tokens_map: SafeMap<String, Vec<SemanticToken>>,
|
||||
pub semantic_tokens_map: DashMap<String, Vec<SemanticToken>>,
|
||||
/// The Zoo API client.
|
||||
pub zoo_client: kittycad::Client,
|
||||
/// If we can send telemetry for this user.
|
||||
@ -125,7 +122,6 @@ pub struct Backend {
|
||||
pub can_execute: Arc<RwLock<bool>>,
|
||||
|
||||
pub is_initialized: Arc<RwLock<bool>>,
|
||||
pub current_handle: UpdateHandle,
|
||||
}
|
||||
|
||||
// Implement the shared backend trait for the language server.
|
||||
@ -147,83 +143,75 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
*self.is_initialized.write().await = is_initialized;
|
||||
}
|
||||
|
||||
async fn current_handle(&self) -> Option<InnerHandle> {
|
||||
self.current_handle.read().await
|
||||
}
|
||||
|
||||
async fn set_current_handle(&self, handle: Option<InnerHandle>) {
|
||||
self.current_handle.write(handle).await;
|
||||
}
|
||||
|
||||
async fn workspace_folders(&self) -> Vec<WorkspaceFolder> {
|
||||
self.workspace_folders.inner().await.values().cloned().collect()
|
||||
// TODO: fix clone
|
||||
self.workspace_folders.iter().map(|i| i.clone()).collect()
|
||||
}
|
||||
|
||||
async fn add_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
|
||||
for folder in folders {
|
||||
self.workspace_folders.insert(folder.name.to_string(), folder).await;
|
||||
self.workspace_folders.insert(folder.name.to_string(), folder);
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
|
||||
for folder in folders {
|
||||
self.workspace_folders.remove(&folder.name).await;
|
||||
self.workspace_folders.remove(&folder.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn code_map(&self) -> &SafeMap<String, Vec<u8>> {
|
||||
fn code_map(&self) -> &DashMap<String, Vec<u8>> {
|
||||
&self.code_map
|
||||
}
|
||||
|
||||
async fn insert_code_map(&self, uri: String, text: Vec<u8>) {
|
||||
self.code_map.insert(uri, text).await;
|
||||
self.code_map.insert(uri, text);
|
||||
}
|
||||
|
||||
async fn remove_from_code_map(&self, uri: String) -> Option<Vec<u8>> {
|
||||
self.code_map.remove(&uri).await
|
||||
self.code_map.remove(&uri).map(|x| x.1)
|
||||
}
|
||||
|
||||
async fn clear_code_state(&self) {
|
||||
self.code_map.clear().await;
|
||||
self.token_map.clear().await;
|
||||
self.ast_map.clear().await;
|
||||
self.diagnostics_map.clear().await;
|
||||
self.symbols_map.clear().await;
|
||||
self.semantic_tokens_map.clear().await;
|
||||
self.code_map.clear();
|
||||
self.token_map.clear();
|
||||
self.ast_map.clear();
|
||||
self.diagnostics_map.clear();
|
||||
self.symbols_map.clear();
|
||||
self.semantic_tokens_map.clear();
|
||||
}
|
||||
|
||||
fn current_diagnostics_map(&self) -> &SafeMap<String, DocumentDiagnosticReport> {
|
||||
fn current_diagnostics_map(&self) -> &DashMap<String, Vec<Diagnostic>> {
|
||||
&self.diagnostics_map
|
||||
}
|
||||
|
||||
async fn inner_on_change(&self, params: TextDocumentItem, force: bool) {
|
||||
let filename = params.uri.to_string();
|
||||
// We already updated the code map in the shared backend.
|
||||
|
||||
// Lets update the tokens.
|
||||
let tokens = match crate::token::lexer(¶ms.text) {
|
||||
Ok(tokens) => tokens,
|
||||
Err(err) => {
|
||||
self.add_to_diagnostics(¶ms, err, true).await;
|
||||
self.token_map.remove(¶ms.uri.to_string()).await;
|
||||
self.ast_map.remove(¶ms.uri.to_string()).await;
|
||||
self.symbols_map.remove(¶ms.uri.to_string()).await;
|
||||
self.semantic_tokens_map.remove(¶ms.uri.to_string()).await;
|
||||
self.memory_map.remove(¶ms.uri.to_string()).await;
|
||||
self.add_to_diagnostics(¶ms, &[err], true).await;
|
||||
self.token_map.remove(&filename);
|
||||
self.ast_map.remove(&filename);
|
||||
self.symbols_map.remove(&filename);
|
||||
self.semantic_tokens_map.remove(&filename);
|
||||
self.memory_map.remove(&filename);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the previous tokens.
|
||||
let previous_tokens = self.token_map.get(¶ms.uri.to_string()).await;
|
||||
|
||||
// Try to get the memory for the current code.
|
||||
let has_memory = if let Some(memory) = self.memory_map.get(¶ms.uri.to_string()).await {
|
||||
memory != crate::executor::ProgramMemory::default()
|
||||
let has_memory = if let Some(memory) = self.memory_map.get(&filename) {
|
||||
*memory != crate::executor::ProgramMemory::default()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let tokens_changed = if let Some(previous_tokens) = &previous_tokens {
|
||||
// Get the previous tokens.
|
||||
let tokens_changed = if let Some(previous_tokens) = self.token_map.get(&filename) {
|
||||
*previous_tokens != tokens
|
||||
} else {
|
||||
true
|
||||
@ -237,7 +225,7 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
|
||||
if tokens_changed {
|
||||
// Update our token map.
|
||||
self.token_map.insert(params.uri.to_string(), tokens.clone()).await;
|
||||
self.token_map.insert(params.uri.to_string(), tokens.clone());
|
||||
// Update our semantic tokens.
|
||||
self.update_semantic_tokens(&tokens, ¶ms).await;
|
||||
}
|
||||
@ -248,19 +236,19 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
let ast = match result {
|
||||
Ok(ast) => ast,
|
||||
Err(err) => {
|
||||
self.add_to_diagnostics(¶ms, err, true).await;
|
||||
self.ast_map.remove(¶ms.uri.to_string()).await;
|
||||
self.symbols_map.remove(¶ms.uri.to_string()).await;
|
||||
self.memory_map.remove(¶ms.uri.to_string()).await;
|
||||
self.add_to_diagnostics(¶ms, &[err], true).await;
|
||||
self.ast_map.remove(&filename);
|
||||
self.symbols_map.remove(&filename);
|
||||
self.memory_map.remove(&filename);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Check if the ast changed.
|
||||
let ast_changed = match self.ast_map.get(¶ms.uri.to_string()).await {
|
||||
let ast_changed = match self.ast_map.get(&filename) {
|
||||
Some(old_ast) => {
|
||||
// Check if the ast changed.
|
||||
old_ast != ast
|
||||
*old_ast != ast
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
@ -271,14 +259,12 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
}
|
||||
|
||||
if ast_changed {
|
||||
self.ast_map.insert(params.uri.to_string(), ast.clone()).await;
|
||||
self.ast_map.insert(params.uri.to_string(), ast.clone());
|
||||
// Update the symbols map.
|
||||
self.symbols_map
|
||||
.insert(
|
||||
params.uri.to_string(),
|
||||
ast.get_lsp_symbols(¶ms.text).unwrap_or_default(),
|
||||
)
|
||||
.await;
|
||||
self.symbols_map.insert(
|
||||
params.uri.to_string(),
|
||||
ast.get_lsp_symbols(¶ms.text).unwrap_or_default(),
|
||||
);
|
||||
|
||||
// Update our semantic tokens.
|
||||
self.update_semantic_tokens(&tokens, ¶ms).await;
|
||||
@ -290,12 +276,7 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
// Clear the lints before we lint.
|
||||
self.clear_diagnostics_map(¶ms.uri, Some(DiagnosticSeverity::INFORMATION))
|
||||
.await;
|
||||
for discovered_finding in &discovered_findings {
|
||||
self.add_to_diagnostics(¶ms, discovered_finding, false).await;
|
||||
}
|
||||
self.add_to_diagnostics(¶ms, &discovered_findings, false).await;
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,16 +307,8 @@ impl Backend {
|
||||
*self.can_execute.read().await
|
||||
}
|
||||
|
||||
async fn set_can_execute(&self, can_execute: bool) {
|
||||
*self.can_execute.write().await = can_execute;
|
||||
}
|
||||
|
||||
pub async fn executor_ctx(&self) -> Option<crate::executor::ExecutorContext> {
|
||||
self.executor_ctx.read().await.clone()
|
||||
}
|
||||
|
||||
async fn set_executor_ctx(&self, executor_ctx: crate::executor::ExecutorContext) {
|
||||
*self.executor_ctx.write().await = Some(executor_ctx);
|
||||
pub async fn executor_ctx(&self) -> tokio::sync::RwLockReadGuard<'_, Option<crate::executor::ExecutorContext>> {
|
||||
self.executor_ctx.read().await
|
||||
}
|
||||
|
||||
async fn update_semantic_tokens(&self, tokens: &[crate::token::Token], params: &TextDocumentItem) {
|
||||
@ -369,7 +342,7 @@ impl Backend {
|
||||
|
||||
// Calculate the token modifiers.
|
||||
// Get the value at the current position.
|
||||
let token_modifiers_bitset = if let Some(ast) = self.ast_map.get(¶ms.uri.to_string()).await {
|
||||
let token_modifiers_bitset = if let Some(ast) = self.ast_map.get(params.uri.as_str()) {
|
||||
let token_index = Arc::new(Mutex::new(token_type_index));
|
||||
let modifier_index: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
|
||||
crate::walk::walk(&ast, &|node: crate::walk::Node| {
|
||||
@ -519,15 +492,12 @@ impl Backend {
|
||||
|
||||
last_position = position;
|
||||
}
|
||||
self.semantic_tokens_map
|
||||
.insert(params.uri.to_string(), semantic_tokens)
|
||||
.await;
|
||||
self.semantic_tokens_map.insert(params.uri.to_string(), semantic_tokens);
|
||||
}
|
||||
|
||||
async fn clear_diagnostics_map(&self, uri: &url::Url, severity: Option<DiagnosticSeverity>) {
|
||||
let mut items = match self.diagnostics_map.get(uri.as_str()).await {
|
||||
Some(DocumentDiagnosticReport::Full(report)) => report.full_document_diagnostic_report.items,
|
||||
_ => vec![],
|
||||
let Some(mut items) = self.diagnostics_map.get_mut(uri.as_str()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// If we only want to clear a specific severity, do that.
|
||||
@ -537,84 +507,72 @@ impl Backend {
|
||||
items.clear();
|
||||
}
|
||||
|
||||
self.diagnostics_map
|
||||
.insert(
|
||||
uri.to_string(),
|
||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
items: items.clone(),
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
if items.is_empty() {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
self.client.publish_diagnostics(uri.clone(), items.clone(), None).await;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
self.client.publish_diagnostics(uri.clone(), items, None).await;
|
||||
// We need to drop the items here.
|
||||
drop(items);
|
||||
|
||||
self.diagnostics_map.remove(uri.as_str());
|
||||
} else {
|
||||
// We don't need to update the map since we used get_mut.
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
self.client.publish_diagnostics(uri.clone(), items.clone(), None).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_to_diagnostics<DiagT: IntoDiagnostic + std::fmt::Debug>(
|
||||
&self,
|
||||
params: &TextDocumentItem,
|
||||
diagnostic: DiagT,
|
||||
diagnostics: &[DiagT],
|
||||
clear_all_before_add: bool,
|
||||
) {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, format!("adding {:?} to diag", diagnostic))
|
||||
.log_message(MessageType::INFO, format!("adding {:?} to diag", diagnostics))
|
||||
.await;
|
||||
|
||||
let diagnostic = diagnostic.to_lsp_diagnostic(¶ms.text);
|
||||
|
||||
if clear_all_before_add {
|
||||
self.clear_diagnostics_map(¶ms.uri, None).await;
|
||||
} else if diagnostic.severity == Some(DiagnosticSeverity::ERROR) {
|
||||
} else if diagnostics.iter().all(|x| x.severity() == DiagnosticSeverity::ERROR) {
|
||||
// If the diagnostic is an error, it will be the only error we get since that halts
|
||||
// execution.
|
||||
// Clear the diagnostics before we add a new one.
|
||||
self.clear_diagnostics_map(¶ms.uri, Some(DiagnosticSeverity::ERROR))
|
||||
.await;
|
||||
} else if diagnostics
|
||||
.iter()
|
||||
.all(|x| x.severity() == DiagnosticSeverity::INFORMATION)
|
||||
{
|
||||
// If the diagnostic is a lint, we will pass them all to add at once so we need to
|
||||
// clear the old ones.
|
||||
self.clear_diagnostics_map(¶ms.uri, Some(DiagnosticSeverity::INFORMATION))
|
||||
.await;
|
||||
}
|
||||
|
||||
let DocumentDiagnosticReport::Full(mut report) =
|
||||
self.diagnostics_map
|
||||
.get(params.uri.as_str())
|
||||
.await
|
||||
.unwrap_or(DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
items: vec![],
|
||||
},
|
||||
}))
|
||||
else {
|
||||
unreachable!();
|
||||
let mut items = if let Some(items) = self.diagnostics_map.get(params.uri.as_str()) {
|
||||
// TODO: Would be awesome to fix the clone here.
|
||||
items.clone()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Ensure we don't already have this diagnostic.
|
||||
if report
|
||||
.full_document_diagnostic_report
|
||||
.items
|
||||
.iter()
|
||||
.any(|x| x == &diagnostic)
|
||||
{
|
||||
self.client
|
||||
.publish_diagnostics(params.uri.clone(), report.full_document_diagnostic_report.items, None)
|
||||
.await;
|
||||
return;
|
||||
for diagnostic in diagnostics {
|
||||
let d = diagnostic.to_lsp_diagnostic(¶ms.text);
|
||||
// Make sure we don't duplicate diagnostics.
|
||||
if !items.iter().any(|x| x == &d) {
|
||||
items.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
report.full_document_diagnostic_report.items.push(diagnostic);
|
||||
self.diagnostics_map.insert(params.uri.to_string(), items.clone());
|
||||
|
||||
self.diagnostics_map
|
||||
.insert(params.uri.to_string(), DocumentDiagnosticReport::Full(report.clone()))
|
||||
.await;
|
||||
|
||||
self.client
|
||||
.publish_diagnostics(params.uri.clone(), report.full_document_diagnostic_report.items, None)
|
||||
.await;
|
||||
self.client.publish_diagnostics(params.uri.clone(), items, None).await;
|
||||
}
|
||||
|
||||
async fn execute(&self, params: &TextDocumentItem, ast: &crate::ast::types::Program) -> Result<()> {
|
||||
@ -624,7 +582,8 @@ impl Backend {
|
||||
}
|
||||
|
||||
// Execute the code if we have an executor context.
|
||||
let Some(executor_ctx) = self.executor_ctx().await else {
|
||||
let ctx = self.executor_ctx().await;
|
||||
let Some(ref executor_ctx) = *ctx else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@ -639,17 +598,16 @@ impl Backend {
|
||||
let memory = match executor_ctx.run(ast, None).await {
|
||||
Ok(memory) => memory,
|
||||
Err(err) => {
|
||||
self.memory_map.remove(¶ms.uri.to_string()).await;
|
||||
self.add_to_diagnostics(params, err, false).await;
|
||||
self.memory_map.remove(params.uri.as_str());
|
||||
self.add_to_diagnostics(params, &[err], false).await;
|
||||
|
||||
// Since we already published the diagnostics we don't really care about the error
|
||||
// string.
|
||||
return Err(anyhow::anyhow!("failed to execute code"));
|
||||
}
|
||||
};
|
||||
drop(executor_ctx);
|
||||
|
||||
self.memory_map.insert(params.uri.to_string(), memory.clone()).await;
|
||||
self.memory_map.insert(params.uri.to_string(), memory.clone());
|
||||
|
||||
// Send the notification to the client that the memory was updated.
|
||||
self.client
|
||||
@ -688,7 +646,7 @@ impl Backend {
|
||||
}
|
||||
|
||||
async fn completions_get_variables_from_ast(&self, file_name: &str) -> Vec<CompletionItem> {
|
||||
let ast = match self.ast_map.get(file_name).await {
|
||||
let ast = match self.ast_map.get(file_name) {
|
||||
Some(ast) => ast,
|
||||
None => return vec![],
|
||||
};
|
||||
@ -705,7 +663,9 @@ impl Backend {
|
||||
// Collect all the file data we know.
|
||||
let mut buf = vec![];
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
|
||||
for (entry, value) in self.code_map.inner().await.iter() {
|
||||
for code in self.code_map.iter() {
|
||||
let entry = code.key();
|
||||
let value = code.value();
|
||||
let file_name = entry.replace("file://", "").to_string();
|
||||
|
||||
let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
@ -741,7 +701,7 @@ impl Backend {
|
||||
// Get the workspace folders.
|
||||
// The key of the workspace folder is the project name.
|
||||
let workspace_folders = self.workspace_folders().await;
|
||||
let project_names: Vec<String> = workspace_folders.iter().map(|v| v.name.clone()).collect::<Vec<_>>();
|
||||
let project_names: Vec<&str> = workspace_folders.iter().map(|v| v.name.as_str()).collect::<Vec<_>>();
|
||||
// Get the first name.
|
||||
let project_name = project_names
|
||||
.first()
|
||||
@ -788,7 +748,9 @@ impl Backend {
|
||||
let filename = params.text_document.uri.to_string();
|
||||
|
||||
{
|
||||
let Some(mut executor_ctx) = self.executor_ctx().await else {
|
||||
let mut ctx = self.executor_ctx.write().await;
|
||||
// Borrow the executor context mutably.
|
||||
let Some(ref mut executor_ctx) = *ctx else {
|
||||
self.client
|
||||
.log_message(MessageType::ERROR, "no executor context set to update units for")
|
||||
.await;
|
||||
@ -800,8 +762,8 @@ impl Backend {
|
||||
.await;
|
||||
|
||||
// Try to get the memory for the current code.
|
||||
let has_memory = if let Some(memory) = self.memory_map.get(&filename).await {
|
||||
memory != crate::executor::ProgramMemory::default()
|
||||
let has_memory = if let Some(memory) = self.memory_map.get(&filename) {
|
||||
*memory != crate::executor::ProgramMemory::default()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@ -816,10 +778,6 @@ impl Backend {
|
||||
|
||||
// Set the engine units.
|
||||
executor_ctx.update_units(params.units);
|
||||
|
||||
// Update the locked executor context.
|
||||
self.set_executor_ctx(executor_ctx.clone()).await;
|
||||
drop(executor_ctx);
|
||||
}
|
||||
// Lock is dropped here since nested.
|
||||
// This is IMPORTANT.
|
||||
@ -847,20 +805,13 @@ impl Backend {
|
||||
&self,
|
||||
params: custom_notifications::UpdateCanExecuteParams,
|
||||
) -> RpcResult<custom_notifications::UpdateCanExecuteResponse> {
|
||||
let can_execute = self.can_execute().await;
|
||||
let mut can_execute = self.can_execute.write().await;
|
||||
|
||||
if can_execute == params.can_execute {
|
||||
if *can_execute == params.can_execute {
|
||||
return Ok(custom_notifications::UpdateCanExecuteResponse {});
|
||||
}
|
||||
|
||||
if !params.can_execute {
|
||||
// Kill any in progress executions.
|
||||
if let Some(current_handle) = self.current_handle().await {
|
||||
current_handle.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
self.set_can_execute(params.can_execute).await;
|
||||
*can_execute = params.can_execute;
|
||||
|
||||
Ok(custom_notifications::UpdateCanExecuteResponse {})
|
||||
}
|
||||
@ -973,7 +924,7 @@ impl LanguageServer for Backend {
|
||||
}
|
||||
|
||||
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||
self.do_did_change(params.clone()).await;
|
||||
self.do_did_change(params).await;
|
||||
}
|
||||
|
||||
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
||||
@ -1012,7 +963,7 @@ impl LanguageServer for Backend {
|
||||
async fn hover(&self, params: HoverParams) -> RpcResult<Option<Hover>> {
|
||||
let filename = params.text_document_position_params.text_document.uri.to_string();
|
||||
|
||||
let Some(current_code) = self.code_map.get(&filename).await else {
|
||||
let Some(current_code) = self.code_map.get(&filename) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Ok(current_code) = std::str::from_utf8(¤t_code) else {
|
||||
@ -1022,7 +973,7 @@ impl LanguageServer for Backend {
|
||||
let pos = position_to_char_index(params.text_document_position_params.position, current_code);
|
||||
|
||||
// Let's iterate over the AST and find the node that contains the cursor.
|
||||
let Some(ast) = self.ast_map.get(&filename).await else {
|
||||
let Some(ast) = self.ast_map.get(&filename) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@ -1055,7 +1006,11 @@ impl LanguageServer for Backend {
|
||||
value: format!(
|
||||
"```{}{}```\n{}",
|
||||
name,
|
||||
label_details.detail.clone().unwrap_or_default(),
|
||||
if let Some(detail) = &label_details.detail {
|
||||
detail
|
||||
} else {
|
||||
""
|
||||
},
|
||||
docs
|
||||
),
|
||||
}),
|
||||
@ -1114,7 +1069,7 @@ impl LanguageServer for Backend {
|
||||
let filename = params.text_document.uri.to_string();
|
||||
|
||||
// Get the current diagnostics for this file.
|
||||
let Some(diagnostic) = self.diagnostics_map.get(&filename).await else {
|
||||
let Some(items) = self.diagnostics_map.get(&filename) else {
|
||||
// Send an empty report.
|
||||
return Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
|
||||
RelatedFullDocumentDiagnosticReport {
|
||||
@ -1127,13 +1082,21 @@ impl LanguageServer for Backend {
|
||||
)));
|
||||
};
|
||||
|
||||
Ok(DocumentDiagnosticReportResult::Report(diagnostic.clone()))
|
||||
Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
|
||||
RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
items: items.clone(),
|
||||
},
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
async fn signature_help(&self, params: SignatureHelpParams) -> RpcResult<Option<SignatureHelp>> {
|
||||
let filename = params.text_document_position_params.text_document.uri.to_string();
|
||||
|
||||
let Some(current_code) = self.code_map.get(&filename).await else {
|
||||
let Some(current_code) = self.code_map.get(&filename) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Ok(current_code) = std::str::from_utf8(¤t_code) else {
|
||||
@ -1143,7 +1106,7 @@ impl LanguageServer for Backend {
|
||||
let pos = position_to_char_index(params.text_document_position_params.position, current_code);
|
||||
|
||||
// Let's iterate over the AST and find the node that contains the cursor.
|
||||
let Some(ast) = self.ast_map.get(&filename).await else {
|
||||
let Some(ast) = self.ast_map.get(&filename) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@ -1177,7 +1140,7 @@ impl LanguageServer for Backend {
|
||||
|
||||
signature.active_parameter = Some(parameter_index);
|
||||
|
||||
Ok(Some(signature.clone()))
|
||||
Ok(Some(signature))
|
||||
}
|
||||
crate::ast::types::Hover::Comment { value: _, range: _ } => {
|
||||
return Ok(None);
|
||||
@ -1194,7 +1157,7 @@ impl LanguageServer for Backend {
|
||||
async fn semantic_tokens_full(&self, params: SemanticTokensParams) -> RpcResult<Option<SemanticTokensResult>> {
|
||||
let filename = params.text_document.uri.to_string();
|
||||
|
||||
let Some(semantic_tokens) = self.semantic_tokens_map.get(&filename).await else {
|
||||
let Some(semantic_tokens) = self.semantic_tokens_map.get(&filename) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@ -1207,7 +1170,7 @@ impl LanguageServer for Backend {
|
||||
async fn document_symbol(&self, params: DocumentSymbolParams) -> RpcResult<Option<DocumentSymbolResponse>> {
|
||||
let filename = params.text_document.uri.to_string();
|
||||
|
||||
let Some(symbols) = self.symbols_map.get(&filename).await else {
|
||||
let Some(symbols) = self.symbols_map.get(&filename) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@ -1217,7 +1180,7 @@ impl LanguageServer for Backend {
|
||||
async fn formatting(&self, params: DocumentFormattingParams) -> RpcResult<Option<Vec<TextEdit>>> {
|
||||
let filename = params.text_document.uri.to_string();
|
||||
|
||||
let Some(current_code) = self.code_map.get(&filename).await else {
|
||||
let Some(current_code) = self.code_map.get(&filename) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Ok(current_code) = std::str::from_utf8(¤t_code) else {
|
||||
@ -1254,7 +1217,7 @@ impl LanguageServer for Backend {
|
||||
async fn rename(&self, params: RenameParams) -> RpcResult<Option<WorkspaceEdit>> {
|
||||
let filename = params.text_document_position.text_document.uri.to_string();
|
||||
|
||||
let Some(current_code) = self.code_map.get(&filename).await else {
|
||||
let Some(current_code) = self.code_map.get(&filename) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Ok(current_code) = std::str::from_utf8(¤t_code) else {
|
||||
@ -1297,7 +1260,7 @@ impl LanguageServer for Backend {
|
||||
let filename = params.text_document.uri.to_string();
|
||||
|
||||
// Get the ast.
|
||||
let Some(ast) = self.ast_map.get(&filename).await else {
|
||||
let Some(ast) = self.ast_map.get(&filename) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,8 @@
|
||||
pub mod backend;
|
||||
pub mod copilot;
|
||||
pub mod kcl;
|
||||
mod safemap;
|
||||
#[cfg(any(test, feature = "lsp-test-util"))]
|
||||
pub mod test_util;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod util;
|
||||
|
@ -1,60 +0,0 @@
|
||||
//! A map type that is safe to use in a concurrent environment.
|
||||
//! But also in wasm.
|
||||
//! Previously, we used `dashmap::DashMap` for this purpose, but it doesn't work in wasm.
|
||||
|
||||
use std::{borrow::Borrow, collections::HashMap, hash::Hash, sync::Arc};
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// A thread-safe map type.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SafeMap<K: Eq + Hash + Clone, V: Clone>(Arc<RwLock<HashMap<K, V>>>);
|
||||
|
||||
impl<K: Eq + Hash + Clone, V: Clone> SafeMap<K, V> {
|
||||
/// Create a new empty map.
|
||||
pub fn new() -> Self {
|
||||
SafeMap(Arc::new(RwLock::new(HashMap::new())))
|
||||
}
|
||||
|
||||
pub async fn len(&self) -> usize {
|
||||
self.0.read().await.len()
|
||||
}
|
||||
|
||||
pub async fn is_empty(&self) -> bool {
|
||||
self.0.read().await.is_empty()
|
||||
}
|
||||
|
||||
pub async fn clear(&self) {
|
||||
self.0.write().await.clear();
|
||||
}
|
||||
|
||||
/// Insert a key-value pair into the map.
|
||||
pub async fn insert(&self, key: K, value: V) {
|
||||
self.0.write().await.insert(key, value);
|
||||
}
|
||||
|
||||
/// Get a reference to the value associated with the given key.
|
||||
pub async fn get<Q>(&self, key: &Q) -> Option<V>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq + ?Sized,
|
||||
{
|
||||
self.0.read().await.get(key).cloned()
|
||||
}
|
||||
|
||||
/// Remove the key-value pair associated with the given key.
|
||||
pub async fn remove(&self, key: &K) -> Option<V> {
|
||||
self.0.write().await.remove(key)
|
||||
}
|
||||
|
||||
/// Get a reference to the underlying map.
|
||||
pub async fn inner(&self) -> HashMap<K, V> {
|
||||
self.0.read().await.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Eq + Hash + Clone, V: Clone> Default for SafeMap<K, V> {
|
||||
fn default() -> Self {
|
||||
SafeMap::new()
|
||||
}
|
||||
}
|
112
src/wasm-lib/kcl/src/lsp/test_util.rs
Normal file
112
src/wasm-lib/kcl/src/lsp/test_util.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
use tower_lsp::LanguageServer;
|
||||
|
||||
fn new_zoo_client() -> kittycad::Client {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60));
|
||||
let ws_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.connection_verbose(true)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||
.http1_only();
|
||||
|
||||
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
|
||||
|
||||
// Create the client.
|
||||
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
// Set a local engine address if it's set.
|
||||
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
// Create a fake kcl lsp server for testing.
|
||||
pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
|
||||
let stdlib = crate::std::StdLib::new();
|
||||
let stdlib_completions = crate::lsp::kcl::get_completions_from_stdlib(&stdlib)?;
|
||||
let stdlib_signatures = crate::lsp::kcl::get_signatures_from_stdlib(&stdlib)?;
|
||||
|
||||
let zoo_client = new_zoo_client();
|
||||
|
||||
let executor_ctx = if execute {
|
||||
Some(crate::executor::ExecutorContext::new(&zoo_client, Default::default()).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let can_execute = executor_ctx.is_some();
|
||||
|
||||
// Create the backend.
|
||||
let (service, _) = tower_lsp::LspService::build(|client| crate::lsp::kcl::Backend {
|
||||
client,
|
||||
fs: Arc::new(crate::fs::FileManager::new()),
|
||||
workspace_folders: Default::default(),
|
||||
stdlib_completions,
|
||||
stdlib_signatures,
|
||||
token_map: Default::default(),
|
||||
ast_map: Default::default(),
|
||||
memory_map: Default::default(),
|
||||
code_map: Default::default(),
|
||||
diagnostics_map: Default::default(),
|
||||
symbols_map: Default::default(),
|
||||
semantic_tokens_map: Default::default(),
|
||||
zoo_client,
|
||||
can_send_telemetry: true,
|
||||
executor_ctx: Arc::new(tokio::sync::RwLock::new(executor_ctx)),
|
||||
can_execute: Arc::new(tokio::sync::RwLock::new(can_execute)),
|
||||
is_initialized: Default::default(),
|
||||
})
|
||||
.custom_method("kcl/updateUnits", crate::lsp::kcl::Backend::update_units)
|
||||
.custom_method("kcl/updateCanExecute", crate::lsp::kcl::Backend::update_can_execute)
|
||||
.finish();
|
||||
|
||||
let server = service.inner();
|
||||
|
||||
server
|
||||
.initialize(tower_lsp::lsp_types::InitializeParams::default())
|
||||
.await?;
|
||||
|
||||
server.initialized(tower_lsp::lsp_types::InitializedParams {}).await;
|
||||
|
||||
Ok(server.clone())
|
||||
}
|
||||
|
||||
// Create a fake copilot lsp server for testing.
|
||||
pub async fn copilot_lsp_server() -> Result<crate::lsp::copilot::Backend> {
|
||||
// We don't actually need to authenticate to the backend for this test.
|
||||
let zoo_client = kittycad::Client::new_from_env();
|
||||
|
||||
// Create the backend.
|
||||
let (service, _) = tower_lsp::LspService::new(|client| crate::lsp::copilot::Backend {
|
||||
client,
|
||||
fs: Arc::new(crate::fs::FileManager::new()),
|
||||
workspace_folders: Default::default(),
|
||||
code_map: Default::default(),
|
||||
zoo_client,
|
||||
editor_info: Arc::new(RwLock::new(crate::lsp::copilot::types::CopilotEditorInfo::default())),
|
||||
cache: Arc::new(crate::lsp::copilot::cache::CopilotCache::new()),
|
||||
telemetry: Default::default(),
|
||||
is_initialized: Default::default(),
|
||||
diagnostics_map: Default::default(),
|
||||
});
|
||||
let server = service.inner();
|
||||
|
||||
server
|
||||
.initialize(tower_lsp::lsp_types::InitializeParams::default())
|
||||
.await?;
|
||||
|
||||
server.initialized(tower_lsp::lsp_types::InitializedParams {}).await;
|
||||
|
||||
Ok(server.clone())
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -37,4 +37,7 @@ pub fn get_line_before(pos: Position, rope: &Rope) -> Option<String> {
|
||||
pub trait IntoDiagnostic {
|
||||
/// Convert the traited object to a [lsp_types::Diagnostic].
|
||||
fn to_lsp_diagnostic(&self, text: &str) -> Diagnostic;
|
||||
|
||||
/// Get the severity of the diagnostic.
|
||||
fn severity(&self) -> tower_lsp::lsp_types::DiagnosticSeverity;
|
||||
}
|
||||
|
@ -474,11 +474,7 @@ fn integer_range(i: TokenSlice) -> PResult<Vec<Value>> {
|
||||
}
|
||||
|
||||
fn object_property(i: TokenSlice) -> PResult<ObjectProperty> {
|
||||
let key = identifier
|
||||
.context(expected(
|
||||
"the property's key (the name or identifier of the property), e.g. in 'height: 4', 'height' is the property key",
|
||||
))
|
||||
.parse_next(i)?;
|
||||
let key = identifier.context(expected("the property's key (the name or identifier of the property), e.g. in 'height: 4', 'height' is the property key")).parse_next(i)?;
|
||||
colon
|
||||
.context(expected(
|
||||
"a colon, which separates the property's key from the value you're setting it to, e.g. 'height: 4'",
|
||||
@ -588,12 +584,9 @@ fn member_expression_subscript(i: TokenSlice) -> PResult<(LiteralIdentifier, usi
|
||||
fn member_expression(i: TokenSlice) -> PResult<MemberExpression> {
|
||||
// This is an identifier, followed by a sequence of members (aka properties)
|
||||
// First, the identifier.
|
||||
let id = identifier
|
||||
.context(expected("the identifier of the object whose property you're trying to access, e.g. in 'shape.size.width', 'shape' is the identifier"))
|
||||
.parse_next(i)?;
|
||||
let id = identifier.context(expected("the identifier of the object whose property you're trying to access, e.g. in 'shape.size.width', 'shape' is the identifier")).parse_next(i)?;
|
||||
// Now a sequence of members.
|
||||
let member = alt((member_expression_dot, member_expression_subscript))
|
||||
.context(expected("a member/property, e.g. size.x and size['height'] and size[0] are all different ways to access a member/property of 'size'"));
|
||||
let member = alt((member_expression_dot, member_expression_subscript)).context(expected("a member/property, e.g. size.x and size['height'] and size[0] are all different ways to access a member/property of 'size'"));
|
||||
let mut members: Vec<_> = repeat(1.., member)
|
||||
.context(expected("a sequence of at least one members/properties"))
|
||||
.parse_next(i)?;
|
||||
@ -1111,19 +1104,9 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> {
|
||||
// TODO: negation. Original parser doesn't support `not` yet.
|
||||
TokenType::Operator => Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: token.as_source_ranges(),
|
||||
message: format!(
|
||||
"{EXPECTED} but found {} which is an operator, but not a unary one (unary operators apply to just a single operand, your operator applies to two or more operands)",
|
||||
token.value.as_str(),
|
||||
),
|
||||
})),
|
||||
other => Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: token.as_source_ranges(),
|
||||
message: format!(
|
||||
"{EXPECTED} but found {} which is {}",
|
||||
token.value.as_str(),
|
||||
other,
|
||||
),
|
||||
message: format!("{EXPECTED} but found {} which is an operator, but not a unary one (unary operators apply to just a single operand, your operator applies to two or more operands)", token.value.as_str(),),
|
||||
})),
|
||||
other => Err(KclError::Syntax(KclErrorDetails { source_ranges: token.as_source_ranges(), message: format!("{EXPECTED} but found {} which is {}", token.value.as_str(), other,) })),
|
||||
})
|
||||
.context(expected("a unary expression, e.g. -x or -3"))
|
||||
.parse_next(i)?;
|
||||
@ -1691,7 +1674,7 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
start0.value,
|
||||
NonCodeValue::BlockComment {
|
||||
value: "comment at start".to_owned(),
|
||||
style: CommentStyle::Block,
|
||||
style: CommentStyle::Block
|
||||
}
|
||||
);
|
||||
assert_eq!(start1.value, NonCodeValue::NewLine);
|
||||
@ -1756,8 +1739,8 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
start: 32,
|
||||
end: 33,
|
||||
value: 2u32.into(),
|
||||
raw: "2".to_owned(),
|
||||
})),
|
||||
raw: "2".to_owned()
|
||||
}))
|
||||
})],
|
||||
non_code_meta: NonCodeMeta {
|
||||
non_code_nodes: Default::default(),
|
||||
@ -1765,7 +1748,7 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
start: 7,
|
||||
end: 25,
|
||||
value: NonCodeValue::NewLine
|
||||
}],
|
||||
}]
|
||||
},
|
||||
},
|
||||
return_type: None,
|
||||
@ -1790,7 +1773,7 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
non_code_meta.non_code_nodes.get(&2).unwrap()[0].value,
|
||||
NonCodeValue::InlineComment {
|
||||
value: "inline-comment".to_owned(),
|
||||
style: CommentStyle::Line,
|
||||
style: CommentStyle::Line
|
||||
}
|
||||
);
|
||||
assert_eq!(body.len(), 4);
|
||||
@ -1815,8 +1798,8 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
end: 20,
|
||||
value: NonCodeValue::BlockComment {
|
||||
value: "this is a comment".to_owned(),
|
||||
style: CommentStyle::Line,
|
||||
},
|
||||
style: CommentStyle::Line
|
||||
}
|
||||
}],
|
||||
non_code_meta.start,
|
||||
);
|
||||
@ -1827,13 +1810,13 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
end: 82,
|
||||
value: NonCodeValue::InlineComment {
|
||||
value: "block\n comment".to_owned(),
|
||||
style: CommentStyle::Block,
|
||||
},
|
||||
style: CommentStyle::Block
|
||||
}
|
||||
},
|
||||
NonCodeNode {
|
||||
start: 82,
|
||||
end: 86,
|
||||
value: NonCodeValue::NewLine,
|
||||
value: NonCodeValue::NewLine
|
||||
},
|
||||
]),
|
||||
non_code_meta.non_code_nodes.get(&0),
|
||||
@ -1844,8 +1827,8 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
end: 129,
|
||||
value: NonCodeValue::BlockComment {
|
||||
value: "this is also a comment".to_owned(),
|
||||
style: CommentStyle::Line,
|
||||
},
|
||||
style: CommentStyle::Line
|
||||
}
|
||||
}]),
|
||||
non_code_meta.non_code_nodes.get(&1),
|
||||
);
|
||||
@ -1864,7 +1847,7 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
actual.non_code_meta.non_code_nodes.get(&0).unwrap()[0].value,
|
||||
NonCodeValue::InlineComment {
|
||||
value: "block\n comment".to_owned(),
|
||||
style: CommentStyle::Block,
|
||||
style: CommentStyle::Block
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -1912,7 +1895,7 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
start: 9,
|
||||
end: 10,
|
||||
value: 3u32.into(),
|
||||
raw: "3".to_owned(),
|
||||
raw: "3".to_owned()
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
@ -567,7 +567,7 @@ mod tests {
|
||||
project_name: Some("assembly".to_string()),
|
||||
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None,
|
||||
current_file_path: None
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -586,7 +586,7 @@ mod tests {
|
||||
project_name: None,
|
||||
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None,
|
||||
current_file_path: None
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -624,7 +624,7 @@ mod tests {
|
||||
project_name: Some("modeling-app".to_string()),
|
||||
project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None,
|
||||
current_file_path: None
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -642,7 +642,7 @@ mod tests {
|
||||
project_name: Some("browser".to_string()),
|
||||
project_path: "/browser".to_string(),
|
||||
current_file_name: Some("main.kcl".to_string()),
|
||||
current_file_path: Some("/browser/main.kcl".to_string()),
|
||||
current_file_path: Some("/browser/main.kcl".to_string())
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -660,7 +660,7 @@ mod tests {
|
||||
project_name: Some("browser".to_string()),
|
||||
project_path: "/browser".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None,
|
||||
current_file_path: None
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -1046,13 +1046,7 @@ const model = import("model.obj")"#
|
||||
let result = super::ProjectState::new_from_path(tmp_project_dir.join("settings.toml")).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
format!(
|
||||
"File type (toml) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl",
|
||||
tmp_project_dir.join("settings.toml").display()
|
||||
)
|
||||
);
|
||||
assert_eq!(result.unwrap_err().to_string(), format!("File type (toml) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl", tmp_project_dir.join("settings.toml").display()));
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
@ -1067,13 +1061,7 @@ const model = import("model.obj")"#
|
||||
let result = super::ProjectState::new_from_path(tmp_project_dir.join("settings.docx")).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
format!(
|
||||
"File type (docx) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl",
|
||||
tmp_project_dir.join("settings.docx").display()
|
||||
)
|
||||
);
|
||||
assert_eq!(result.unwrap_err().to_string(), format!("File type (docx) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl", tmp_project_dir.join("settings.docx").display()));
|
||||
|
||||
std::fs::remove_dir_all(tmp_project_dir).unwrap();
|
||||
}
|
||||
|
@ -640,7 +640,7 @@ textWrapping = true
|
||||
app: AppSettings {
|
||||
appearance: AppearanceSettings {
|
||||
theme: AppTheme::Dark,
|
||||
color: Default::default(),
|
||||
color: Default::default()
|
||||
},
|
||||
onboarding_status: OnboardingStatus::Dismissed,
|
||||
project_directory: None,
|
||||
@ -654,15 +654,15 @@ textWrapping = true
|
||||
mouse_controls: Default::default(),
|
||||
highlight_edges: Default::default(),
|
||||
show_debug_panel: true,
|
||||
enable_ssao: false.into(),
|
||||
enable_ssao: false.into()
|
||||
},
|
||||
text_editor: TextEditorSettings {
|
||||
text_wrapping: true.into(),
|
||||
blinking_cursor: true.into(),
|
||||
blinking_cursor: true.into()
|
||||
},
|
||||
project: Default::default(),
|
||||
command_bar: CommandBarSettings {
|
||||
include_settings: true.into(),
|
||||
include_settings: true.into()
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -698,7 +698,7 @@ includeSettings = false
|
||||
app: AppSettings {
|
||||
appearance: AppearanceSettings {
|
||||
theme: AppTheme::Dark,
|
||||
color: 138.0.into(),
|
||||
color: 138.0.into()
|
||||
},
|
||||
onboarding_status: Default::default(),
|
||||
project_directory: None,
|
||||
@ -712,15 +712,15 @@ includeSettings = false
|
||||
mouse_controls: Default::default(),
|
||||
highlight_edges: Default::default(),
|
||||
show_debug_panel: true,
|
||||
enable_ssao: true.into(),
|
||||
enable_ssao: true.into()
|
||||
},
|
||||
text_editor: TextEditorSettings {
|
||||
text_wrapping: false.into(),
|
||||
blinking_cursor: false.into(),
|
||||
blinking_cursor: false.into()
|
||||
},
|
||||
project: Default::default(),
|
||||
command_bar: CommandBarSettings {
|
||||
include_settings: false.into(),
|
||||
include_settings: false.into()
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -761,7 +761,7 @@ defaultProjectName = "projects-$nnn"
|
||||
app: AppSettings {
|
||||
appearance: AppearanceSettings {
|
||||
theme: AppTheme::Dark,
|
||||
color: 138.0.into(),
|
||||
color: 138.0.into()
|
||||
},
|
||||
onboarding_status: OnboardingStatus::Dismissed,
|
||||
project_directory: None,
|
||||
@ -775,18 +775,18 @@ defaultProjectName = "projects-$nnn"
|
||||
mouse_controls: Default::default(),
|
||||
highlight_edges: Default::default(),
|
||||
show_debug_panel: true,
|
||||
enable_ssao: true.into(),
|
||||
enable_ssao: true.into()
|
||||
},
|
||||
text_editor: TextEditorSettings {
|
||||
text_wrapping: false.into(),
|
||||
blinking_cursor: false.into(),
|
||||
blinking_cursor: false.into()
|
||||
},
|
||||
project: ProjectSettings {
|
||||
directory: "/Users/macinatormax/Documents/kittycad-modeling-projects".into(),
|
||||
default_project_name: "projects-$nnn".to_string().into(),
|
||||
default_project_name: "projects-$nnn".to_string().into()
|
||||
},
|
||||
command_bar: CommandBarSettings {
|
||||
include_settings: false.into(),
|
||||
include_settings: false.into()
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -836,7 +836,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
|
||||
app: AppSettings {
|
||||
appearance: AppearanceSettings {
|
||||
theme: AppTheme::System,
|
||||
color: Default::default(),
|
||||
color: Default::default()
|
||||
},
|
||||
onboarding_status: OnboardingStatus::Dismissed,
|
||||
project_directory: None,
|
||||
@ -850,15 +850,15 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
|
||||
mouse_controls: Default::default(),
|
||||
highlight_edges: true.into(),
|
||||
show_debug_panel: false,
|
||||
enable_ssao: true.into(),
|
||||
enable_ssao: true.into()
|
||||
},
|
||||
text_editor: TextEditorSettings {
|
||||
text_wrapping: true.into(),
|
||||
blinking_cursor: true.into(),
|
||||
blinking_cursor: true.into()
|
||||
},
|
||||
project: ProjectSettings {
|
||||
directory: "/Users/macinatormax/Documents/kittycad-modeling-projects".into(),
|
||||
default_project_name: "project-$nnn".to_string().into(),
|
||||
default_project_name: "project-$nnn".to_string().into()
|
||||
},
|
||||
command_bar: CommandBarSettings {
|
||||
include_settings: true.into()
|
||||
|
@ -115,7 +115,7 @@ includeSettings = false
|
||||
app: AppSettings {
|
||||
appearance: AppearanceSettings {
|
||||
theme: AppTheme::Dark,
|
||||
color: 138.0.into(),
|
||||
color: 138.0.into()
|
||||
},
|
||||
onboarding_status: Default::default(),
|
||||
project_directory: None,
|
||||
@ -129,14 +129,14 @@ includeSettings = false
|
||||
mouse_controls: Default::default(),
|
||||
highlight_edges: Default::default(),
|
||||
show_debug_panel: true,
|
||||
enable_ssao: true.into(),
|
||||
enable_ssao: true.into()
|
||||
},
|
||||
text_editor: TextEditorSettings {
|
||||
text_wrapping: false.into(),
|
||||
blinking_cursor: false.into(),
|
||||
blinking_cursor: false.into()
|
||||
},
|
||||
command_bar: CommandBarSettings {
|
||||
include_settings: false.into(),
|
||||
include_settings: false.into()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -85,9 +85,9 @@ async fn inner_chamfer(
|
||||
// error to the user that they can only tag one edge at a time.
|
||||
if tag.is_some() && data.tags.len() > 1 {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: "You can only tag one edge at a time with a tagged chamfer. Either delete the tag for the chamfer fn if you don't need it OR separate into individual chamfer functions for each tag.".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
message: "You can only tag one edge at a time with a tagged chamfer. Either delete the tag for the chamfer fn if you don't need it OR separate into individual chamfer functions for each tag.".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
let mut fillet_or_chamfers = Vec::new();
|
||||
|
@ -314,10 +314,7 @@ fn get_import_format_from_extension(ext: &str) -> Result<kittycad::types::InputF
|
||||
} else if ext == "glb" {
|
||||
kittycad::types::FileImportFormat::Gltf
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"unknown source format for file extension: {}. Try setting the `--src-format` flag explicitly or use a valid format.",
|
||||
ext
|
||||
)
|
||||
anyhow::bail!("unknown source format for file extension: {}. Try setting the `--src-format` flag explicitly or use a valid format.", ext)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -431,7 +431,7 @@ mod tests {
|
||||
);
|
||||
|
||||
if let Err(err) = result {
|
||||
assert!(err.to_string().contains( "Point Point2d { x: 0.0, y: 5.0 } is not on the circumference of the circle with center Point2d { x: 10.0, y: -10.0 } and radius 10."));
|
||||
assert!(err.to_string().contains("Point Point2d { x: 0.0, y: 5.0 } is not on the circumference of the circle with center Point2d { x: 10.0, y: -10.0 } and radius 10."));
|
||||
} else {
|
||||
panic!("Expected error");
|
||||
}
|
||||
|
@ -297,7 +297,6 @@ pub async fn kcl_lsp_run(
|
||||
executor_ctx: Arc::new(tokio::sync::RwLock::new(executor_ctx)),
|
||||
|
||||
is_initialized: Default::default(),
|
||||
current_handle: Default::default(),
|
||||
})
|
||||
.custom_method("kcl/updateUnits", kcl_lib::lsp::kcl::Backend::update_units)
|
||||
.custom_method("kcl/updateCanExecute", kcl_lib::lsp::kcl::Backend::update_can_execute)
|
||||
@ -356,7 +355,6 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, baseurl: Strin
|
||||
zoo_client,
|
||||
|
||||
is_initialized: Default::default(),
|
||||
current_handle: Default::default(),
|
||||
diagnostics_map: Default::default(),
|
||||
})
|
||||
.custom_method("copilot/setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info)
|
||||
|
Reference in New Issue
Block a user