Move the wasm lib, and cleanup rust directory and all references (#5585)
* git mv src/wasm-lib rust Signed-off-by: Jess Frazelle <github@jessfraz.com> * mv wasm-lib to workspace Signed-off-by: Jess Frazelle <github@jessfraz.com> * mv kcl-lib Signed-off-by: Jess Frazelle <github@jessfraz.com> * mv derive docs Signed-off-by: Jess Frazelle <github@jessfraz.com> * resolve file paths Signed-off-by: Jess Frazelle <github@jessfraz.com> * clippy Signed-off-by: Jess Frazelle <github@jessfraz.com> * move more shit Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix more paths Signed-off-by: Jess Frazelle <github@jessfraz.com> * make yarn build:wasm work Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix scripts Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixups Signed-off-by: Jess Frazelle <github@jessfraz.com> * better references Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix cargo ci Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix reference Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix more ci Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix tests Signed-off-by: Jess Frazelle <github@jessfraz.com> * cargo sort Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix script Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix Signed-off-by: Jess Frazelle <github@jessfraz.com> * fmt Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix a dep Signed-off-by: Jess Frazelle <github@jessfraz.com> * sort Signed-off-by: Jess Frazelle <github@jessfraz.com> * remove unused deps Signed-off-by: Jess Frazelle <github@jessfraz.com> * Revert "remove unused deps" This reverts commit fbabdb062e275fd5cbc1476f8480a1afee15d972. * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * deps; 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> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
239
rust/kcl-lib/src/lsp/backend.rs
Normal file
239
rust/kcl-lib/src/lsp/backend.rs
Normal file
@ -0,0 +1,239 @@
|
||||
//! A shared backend trait for lsp servers memory and behavior.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use dashmap::DashMap;
|
||||
use tower_lsp::lsp_types::{
|
||||
CreateFilesParams, DeleteFilesParams, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams,
|
||||
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
|
||||
DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializedParams, MessageType, RenameFilesParams,
|
||||
TextDocumentItem, WorkspaceFolder,
|
||||
};
|
||||
|
||||
use crate::fs::FileSystem;
|
||||
|
||||
/// A trait for the backend of the language server.
|
||||
#[async_trait::async_trait]
|
||||
pub trait Backend: Clone + Send + Sync
|
||||
where
|
||||
Self: 'static,
|
||||
{
|
||||
fn client(&self) -> &tower_lsp::Client;
|
||||
|
||||
fn fs(&self) -> &Arc<crate::fs::FileManager>;
|
||||
|
||||
async fn is_initialized(&self) -> bool;
|
||||
|
||||
async fn set_is_initialized(&self, is_initialized: bool);
|
||||
|
||||
async fn workspace_folders(&self) -> Vec<WorkspaceFolder>;
|
||||
|
||||
async fn add_workspace_folders(&self, folders: Vec<WorkspaceFolder>);
|
||||
|
||||
async fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>);
|
||||
|
||||
/// Get the current code map.
|
||||
fn code_map(&self) -> &DashMap<String, Vec<u8>>;
|
||||
|
||||
/// Insert a new code map.
|
||||
async fn insert_code_map(&self, uri: String, text: Vec<u8>);
|
||||
|
||||
// Remove from code map.
|
||||
async fn remove_from_code_map(&self, uri: String) -> Option<Vec<u8>>;
|
||||
|
||||
/// Clear the current code state.
|
||||
async fn clear_code_state(&self);
|
||||
|
||||
/// Get the current diagnostics map.
|
||||
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 {
|
||||
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) {
|
||||
if *current_code == params.text.as_bytes() && !self.has_diagnostics(&filename).await {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.insert_code_map(params.uri.to_string(), params.text.as_bytes().to_vec())
|
||||
.await;
|
||||
self.inner_on_change(params, false).await;
|
||||
}
|
||||
|
||||
async fn update_from_disk<P: AsRef<std::path::Path> + std::marker::Send>(&self, path: P) -> Result<()> {
|
||||
// Read over all the files in the directory and add them to our current code map.
|
||||
let files = self.fs().get_all_files(path.as_ref(), Default::default()).await?;
|
||||
for file in files {
|
||||
// Read the file.
|
||||
let contents = self.fs().read(&file, Default::default()).await?;
|
||||
let file_path = format!(
|
||||
"file://{}",
|
||||
file.as_path()
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not get name of file: {:?}", file))?
|
||||
);
|
||||
self.insert_code_map(file_path, contents).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_initialized(&self, params: InitializedParams) {
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, format!("initialized: {:?}", params))
|
||||
.await;
|
||||
|
||||
self.set_is_initialized(true).await;
|
||||
}
|
||||
|
||||
async fn do_shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, "shutdown".to_string())
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
|
||||
// If we are adding a folder that we were previously on, we should not clear the
|
||||
// state.
|
||||
let should_clear = if !params.event.added.is_empty() {
|
||||
let mut should_clear = false;
|
||||
for folder in params.event.added.iter() {
|
||||
if !self
|
||||
.workspace_folders()
|
||||
.await
|
||||
.iter()
|
||||
.any(|f| f.uri == folder.uri && f.name == folder.name)
|
||||
{
|
||||
should_clear = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
should_clear
|
||||
} else {
|
||||
!(params.event.removed.is_empty() && params.event.added.is_empty())
|
||||
};
|
||||
|
||||
self.add_workspace_folders(params.event.added.clone()).await;
|
||||
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() && should_clear {
|
||||
self.clear_code_state().await;
|
||||
}
|
||||
for added in params.event.added {
|
||||
// Try to read all the files in the project.
|
||||
let project_dir = added.uri.to_string().replace("file://", "");
|
||||
if let Err(err) = self.update_from_disk(&project_dir).await {
|
||||
self.client()
|
||||
.log_message(
|
||||
MessageType::WARNING,
|
||||
format!("updating from disk `{}` failed: {:?}", project_dir, err),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_did_change_configuration(&self, params: DidChangeConfigurationParams) {
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, format!("configuration changed: {:?}", params))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn do_did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, format!("watched files changed: {:?}", params))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn do_did_create_files(&self, params: CreateFilesParams) {
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, format!("files created: {:?}", params))
|
||||
.await;
|
||||
// Create each file in the code map.
|
||||
for file in params.files {
|
||||
self.insert_code_map(file.uri.to_string(), Default::default()).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_did_rename_files(&self, params: RenameFilesParams) {
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, format!("files renamed: {:?}", params))
|
||||
.await;
|
||||
// Rename each file in the code map.
|
||||
for file in params.files {
|
||||
if let Some(value) = self.remove_from_code_map(file.old_uri).await {
|
||||
// Rename the file if it exists.
|
||||
self.insert_code_map(file.new_uri.to_string(), value).await;
|
||||
} else {
|
||||
// Otherwise create it.
|
||||
self.insert_code_map(file.new_uri.to_string(), Default::default()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_did_delete_files(&self, params: DeleteFilesParams) {
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, format!("files deleted: {:?}", params))
|
||||
.await;
|
||||
// Delete each file in the map.
|
||||
for file in params.files {
|
||||
self.remove_from_code_map(file.uri.to_string()).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_did_open(&self, params: DidOpenTextDocumentParams) {
|
||||
let new_params = TextDocumentItem {
|
||||
uri: params.text_document.uri,
|
||||
text: params.text_document.text,
|
||||
version: params.text_document.version,
|
||||
language_id: params.text_document.language_id,
|
||||
};
|
||||
self.on_change(new_params).await;
|
||||
}
|
||||
|
||||
async fn do_did_change(&self, mut params: DidChangeTextDocumentParams) {
|
||||
let new_params = TextDocumentItem {
|
||||
uri: params.text_document.uri,
|
||||
text: std::mem::take(&mut params.content_changes[0].text),
|
||||
version: params.text_document.version,
|
||||
language_id: Default::default(),
|
||||
};
|
||||
self.on_change(new_params).await;
|
||||
}
|
||||
|
||||
async fn do_did_save(&self, params: DidSaveTextDocumentParams) {
|
||||
if let Some(text) = params.text {
|
||||
let new_params = TextDocumentItem {
|
||||
uri: params.text_document.uri,
|
||||
text,
|
||||
version: Default::default(),
|
||||
language_id: Default::default(),
|
||||
};
|
||||
self.on_change(new_params).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_did_close(&self, params: DidCloseTextDocumentParams) {
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, format!("document closed: {:?}", params))
|
||||
.await;
|
||||
}
|
||||
}
|
109
rust/kcl-lib/src/lsp/copilot/cache.rs
Normal file
109
rust/kcl-lib/src/lsp/copilot/cache.rs
Normal file
@ -0,0 +1,109 @@
|
||||
//! The cache.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
sync::{Mutex, RwLock},
|
||||
};
|
||||
|
||||
use crate::lsp::copilot::types::CopilotCompletionResponse;
|
||||
|
||||
// if file changes, keep the cache.
|
||||
// if line number is different for an existing file, clean.
|
||||
#[derive(Debug)]
|
||||
pub struct CopilotCache {
|
||||
inner: RwLock<HashMap<String, Mutex<CopilotCompletionResponse>>>,
|
||||
last_line: RwLock<HashMap<String, Mutex<u32>>>,
|
||||
}
|
||||
|
||||
impl Default for CopilotCache {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotCache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: RwLock::new(HashMap::new()),
|
||||
last_line: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_last_line(&self, uri: &String) -> Option<u32> {
|
||||
let Ok(inner) = self.last_line.read() else {
|
||||
return None;
|
||||
};
|
||||
let last_line = inner.get(uri);
|
||||
match last_line {
|
||||
Some(last_line) => {
|
||||
let Ok(last_line) = last_line.lock() else {
|
||||
return None;
|
||||
};
|
||||
Some(*last_line)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cached_response(&self, uri: &String, _lnum: u32) -> Option<CopilotCompletionResponse> {
|
||||
let Ok(inner) = self.inner.read() else {
|
||||
return None;
|
||||
};
|
||||
let cache = inner.get(uri);
|
||||
match cache {
|
||||
Some(completion_response) => {
|
||||
let Ok(completion_response) = completion_response.lock() else {
|
||||
return None;
|
||||
};
|
||||
Some(completion_response.clone())
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_file_cache(&self, uri: &str, completion_response: CopilotCompletionResponse) {
|
||||
let Ok(mut inner) = self.inner.write() else {
|
||||
return;
|
||||
};
|
||||
inner.insert(uri.to_string(), Mutex::new(completion_response));
|
||||
}
|
||||
|
||||
fn set_last_line(&self, uri: &str, last_line: u32) {
|
||||
let Ok(mut inner) = self.last_line.write() else {
|
||||
return;
|
||||
};
|
||||
inner.insert(uri.to_string(), Mutex::new(last_line));
|
||||
}
|
||||
|
||||
pub fn get_cached_result(&self, uri: &String, last_line: u32) -> Option<CopilotCompletionResponse> {
|
||||
let cached_line = self.get_last_line(uri)?;
|
||||
if last_line != cached_line {
|
||||
return None;
|
||||
};
|
||||
self.get_cached_response(uri, last_line)
|
||||
}
|
||||
|
||||
pub fn set_cached_result(
|
||||
&self,
|
||||
uri: &String,
|
||||
lnum: &u32,
|
||||
completion_response: &CopilotCompletionResponse,
|
||||
) -> Option<CopilotCompletionResponse> {
|
||||
self.set_file_cache(uri, completion_response.clone());
|
||||
self.set_last_line(uri, *lnum);
|
||||
let Ok(inner) = self.inner.write() else {
|
||||
return None;
|
||||
};
|
||||
let cache = inner.get(uri);
|
||||
match cache {
|
||||
Some(completion_response) => {
|
||||
let Ok(completion_response) = completion_response.lock() else {
|
||||
return None;
|
||||
};
|
||||
Some(completion_response.clone())
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
403
rust/kcl-lib/src/lsp/copilot/mod.rs
Normal file
403
rust/kcl-lib/src/lsp/copilot/mod.rs
Normal file
@ -0,0 +1,403 @@
|
||||
//! The copilot lsp server for ghost text.
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod cache;
|
||||
pub mod types;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::Debug,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_lsp::{
|
||||
jsonrpc::{Error, Result},
|
||||
lsp_types::{
|
||||
CreateFilesParams, DeleteFilesParams, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams,
|
||||
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
|
||||
DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializeParams, InitializeResult, InitializedParams,
|
||||
MessageType, OneOf, RenameFilesParams, ServerCapabilities, TextDocumentItem, TextDocumentSyncCapability,
|
||||
TextDocumentSyncKind, TextDocumentSyncOptions, WorkspaceFolder, WorkspaceFoldersServerCapabilities,
|
||||
WorkspaceServerCapabilities,
|
||||
},
|
||||
LanguageServer,
|
||||
};
|
||||
|
||||
use crate::lsp::{
|
||||
backend::Backend as _,
|
||||
copilot::{
|
||||
cache::CopilotCache,
|
||||
types::{
|
||||
CopilotAcceptCompletionParams, CopilotCompletionResponse, CopilotCompletionTelemetry, CopilotEditorInfo,
|
||||
CopilotLspCompletionParams, CopilotRejectCompletionParams, DocParams,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Success {
|
||||
success: bool,
|
||||
}
|
||||
impl Success {
|
||||
pub fn new(success: bool) -> Self {
|
||||
Self { success }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Backend {
|
||||
/// The client is used to send notifications and requests to the client.
|
||||
pub client: tower_lsp::Client,
|
||||
/// The file system client to use.
|
||||
pub fs: Arc<crate::fs::FileManager>,
|
||||
/// The workspace folders.
|
||||
pub workspace_folders: DashMap<String, WorkspaceFolder>,
|
||||
/// Current code.
|
||||
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.
|
||||
pub editor_info: Arc<RwLock<CopilotEditorInfo>>,
|
||||
/// 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: DashMap<uuid::Uuid, CopilotCompletionTelemetry>,
|
||||
/// Diagnostics.
|
||||
pub diagnostics_map: DashMap<String, Vec<Diagnostic>>,
|
||||
|
||||
pub is_initialized: Arc<tokio::sync::RwLock<bool>>,
|
||||
|
||||
/// Are we running in dev mode.
|
||||
pub dev_mode: bool,
|
||||
}
|
||||
|
||||
// Implement the shared backend trait for the language server.
|
||||
#[async_trait::async_trait]
|
||||
impl crate::lsp::backend::Backend for Backend {
|
||||
fn client(&self) -> &tower_lsp::Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
fn fs(&self) -> &Arc<crate::fs::FileManager> {
|
||||
&self.fs
|
||||
}
|
||||
|
||||
async fn is_initialized(&self) -> bool {
|
||||
*self.is_initialized.read().await
|
||||
}
|
||||
|
||||
async fn set_is_initialized(&self, is_initialized: bool) {
|
||||
*self.is_initialized.write().await = is_initialized;
|
||||
}
|
||||
|
||||
async fn workspace_folders(&self) -> Vec<WorkspaceFolder> {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
|
||||
for folder in folders {
|
||||
self.workspace_folders.remove(&folder.name);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async fn remove_from_code_map(&self, uri: String) -> Option<Vec<u8>> {
|
||||
self.code_map.remove(&uri).map(|(_, v)| v)
|
||||
}
|
||||
|
||||
async fn clear_code_state(&self) {
|
||||
self.code_map.clear();
|
||||
}
|
||||
|
||||
fn current_diagnostics_map(&self) -> &DashMap<String, Vec<Diagnostic>> {
|
||||
&self.diagnostics_map
|
||||
}
|
||||
|
||||
async fn inner_on_change(&self, _params: TextDocumentItem, _force: bool) {
|
||||
// We don't need to do anything here.
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn new_wasm(
|
||||
client: tower_lsp::Client,
|
||||
fs: crate::fs::wasm::FileSystemManager,
|
||||
zoo_client: kittycad::Client,
|
||||
dev_mode: bool,
|
||||
) -> Self {
|
||||
Self::new(client, crate::fs::FileManager::new(fs), zoo_client, dev_mode)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
client: tower_lsp::Client,
|
||||
fs: crate::fs::FileManager,
|
||||
zoo_client: kittycad::Client,
|
||||
dev_mode: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
client,
|
||||
fs: Arc::new(fs),
|
||||
workspace_folders: Default::default(),
|
||||
code_map: Default::default(),
|
||||
editor_info: Arc::new(RwLock::new(CopilotEditorInfo::default())),
|
||||
cache: Arc::new(CopilotCache::new()),
|
||||
telemetry: Default::default(),
|
||||
zoo_client,
|
||||
|
||||
is_initialized: Default::default(),
|
||||
diagnostics_map: Default::default(),
|
||||
dev_mode,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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),
|
||||
// We only handle one completion at a time, for now so don't even waste the tokens.
|
||||
n: Some(1),
|
||||
stop: Some(["unset".to_string()].to_vec()),
|
||||
nwo: None,
|
||||
// We haven't implemented streaming yet.
|
||||
stream: false,
|
||||
};
|
||||
|
||||
let resp = self
|
||||
.zoo_client
|
||||
.ml()
|
||||
.create_kcl_code_completions(&body)
|
||||
.await
|
||||
.map_err(|err| Error {
|
||||
code: tower_lsp::jsonrpc::ErrorCode::from(69),
|
||||
data: None,
|
||||
message: Cow::from(format!("Failed to get completions from zoo api: {}", err)),
|
||||
})?;
|
||||
Ok(resp.completions)
|
||||
}
|
||||
|
||||
pub async fn set_editor_info(&self, params: CopilotEditorInfo) -> Result<Success> {
|
||||
self.client.log_message(MessageType::INFO, "setEditorInfo").await;
|
||||
let copy = Arc::clone(&self.editor_info);
|
||||
let mut lock = copy.write().map_err(|err| Error {
|
||||
code: tower_lsp::jsonrpc::ErrorCode::from(69),
|
||||
data: None,
|
||||
message: Cow::from(format!("Failed lock: {}", err)),
|
||||
})?;
|
||||
*lock = params;
|
||||
Ok(Success::new(true))
|
||||
}
|
||||
|
||||
pub fn get_doc_params(&self, params: &CopilotLspCompletionParams) -> Result<DocParams> {
|
||||
let pos = params.doc.position;
|
||||
let uri = params.doc.uri.to_string();
|
||||
let rope = ropey::Rope::from_str(¶ms.doc.source);
|
||||
let offset = crate::lsp::util::position_to_offset(pos.into(), &rope).unwrap_or_default();
|
||||
|
||||
Ok(DocParams {
|
||||
uri: uri.to_string(),
|
||||
pos,
|
||||
language: params.doc.language_id.to_string(),
|
||||
prefix: crate::lsp::util::get_text_before(offset, &rope).unwrap_or_default(),
|
||||
suffix: crate::lsp::util::get_text_after(offset, &rope).unwrap_or_default(),
|
||||
line_before: crate::lsp::util::get_line_before(pos.into(), &rope).unwrap_or_default(),
|
||||
rope,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_completions_cycling(
|
||||
&self,
|
||||
params: CopilotLspCompletionParams,
|
||||
) -> Result<CopilotCompletionResponse> {
|
||||
let doc_params = self.get_doc_params(¶ms)?;
|
||||
let cached_result = self.cache.get_cached_result(&doc_params.uri, doc_params.pos.line);
|
||||
if let Some(cached_result) = cached_result {
|
||||
return Ok(cached_result);
|
||||
}
|
||||
|
||||
let doc_params = self.get_doc_params(¶ms)?;
|
||||
let line_before = doc_params.line_before.to_string();
|
||||
|
||||
// Let's not call it yet since it's not our model.
|
||||
// We will need to wrap in spawn_local like we do in kcl/mod.rs for wasm only.
|
||||
#[cfg(test)]
|
||||
let mut completion_list = self
|
||||
.get_completions(doc_params.language, doc_params.prefix, doc_params.suffix)
|
||||
.await
|
||||
.map_err(|err| Error {
|
||||
code: tower_lsp::jsonrpc::ErrorCode::from(69),
|
||||
data: None,
|
||||
message: Cow::from(format!("Failed to get completions: {}", err)),
|
||||
})?;
|
||||
#[cfg(not(test))]
|
||||
let mut completion_list = vec![];
|
||||
|
||||
// if self.dev_mode
|
||||
if false {
|
||||
completion_list.push(
|
||||
r#"fn cube = (pos, scale) => {
|
||||
const sg = startSketchOn('XY')
|
||||
|> startProfileAt(pos, %)
|
||||
|> line([0, scale], %)
|
||||
|> line([scale, 0], %)
|
||||
|> line([0, -scale], %)
|
||||
return sg
|
||||
}
|
||||
const part001 = cube([0,0], 20)
|
||||
|> close(%)
|
||||
|> extrude(length=20)"#
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let response = CopilotCompletionResponse::from_str_vec(completion_list, line_before, doc_params.pos);
|
||||
// Set the telemetry data for each completion.
|
||||
for completion in response.completions.iter() {
|
||||
let telemetry = CopilotCompletionTelemetry {
|
||||
completion: completion.clone(),
|
||||
params: params.clone(),
|
||||
};
|
||||
self.telemetry.insert(completion.uuid, telemetry);
|
||||
}
|
||||
self.cache
|
||||
.set_cached_result(&doc_params.uri, &doc_params.pos.line, &response);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn accept_completion(&self, params: CopilotAcceptCompletionParams) {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, format!("Accepted completions: {:?}", params))
|
||||
.await;
|
||||
|
||||
// Get the original telemetry data.
|
||||
let Some(original) = self.telemetry.remove(¶ms.uuid) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.client
|
||||
.log_message(MessageType::INFO, format!("Original telemetry: {:?}", original))
|
||||
.await;
|
||||
|
||||
// TODO: Send the telemetry data to the zoo api.
|
||||
}
|
||||
|
||||
pub async fn reject_completions(&self, params: CopilotRejectCompletionParams) {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, format!("Rejected completions: {:?}", params))
|
||||
.await;
|
||||
|
||||
// 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).map(|(_, v)| v) {
|
||||
originals.push(original);
|
||||
}
|
||||
}
|
||||
|
||||
self.client
|
||||
.log_message(MessageType::INFO, format!("Original telemetry: {:?}", originals))
|
||||
.await;
|
||||
|
||||
// TODO: Send the telemetry data to the zoo api.
|
||||
}
|
||||
}
|
||||
|
||||
#[tower_lsp::async_trait]
|
||||
impl LanguageServer for Backend {
|
||||
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
|
||||
Ok(InitializeResult {
|
||||
capabilities: ServerCapabilities {
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
|
||||
open_close: Some(true),
|
||||
change: Some(TextDocumentSyncKind::FULL),
|
||||
..Default::default()
|
||||
})),
|
||||
workspace: Some(WorkspaceServerCapabilities {
|
||||
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
|
||||
supported: Some(true),
|
||||
change_notifications: Some(OneOf::Left(true)),
|
||||
}),
|
||||
file_operations: None,
|
||||
}),
|
||||
..ServerCapabilities::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
async fn initialized(&self, params: InitializedParams) {
|
||||
self.do_initialized(params).await
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
|
||||
self.do_shutdown().await
|
||||
}
|
||||
|
||||
async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
|
||||
self.do_did_change_workspace_folders(params).await
|
||||
}
|
||||
|
||||
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
|
||||
self.do_did_change_configuration(params).await
|
||||
}
|
||||
|
||||
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
|
||||
self.do_did_change_watched_files(params).await
|
||||
}
|
||||
|
||||
async fn did_create_files(&self, params: CreateFilesParams) {
|
||||
self.do_did_create_files(params).await
|
||||
}
|
||||
|
||||
async fn did_rename_files(&self, params: RenameFilesParams) {
|
||||
self.do_did_rename_files(params).await
|
||||
}
|
||||
|
||||
async fn did_delete_files(&self, params: DeleteFilesParams) {
|
||||
self.do_did_delete_files(params).await
|
||||
}
|
||||
|
||||
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
||||
self.do_did_open(params).await
|
||||
}
|
||||
|
||||
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||
self.do_did_change(params).await;
|
||||
}
|
||||
|
||||
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
||||
self.do_did_save(params).await
|
||||
}
|
||||
|
||||
async fn did_close(&self, params: DidCloseTextDocumentParams) {
|
||||
self.do_did_close(params).await
|
||||
}
|
||||
}
|
205
rust/kcl-lib/src/lsp/copilot/types.rs
Normal file
205
rust/kcl-lib/src/lsp/copilot/types.rs
Normal file
@ -0,0 +1,205 @@
|
||||
//! Types we need for communication with the server.
|
||||
|
||||
use ropey::Rope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Position in a text document expressed as zero-based line and character offset.
|
||||
/// A position is between two characters like an 'insert' cursor in a editor.
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default, Deserialize, Serialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CopilotPosition {
|
||||
/// Line position in a document (zero-based).
|
||||
pub line: u32,
|
||||
/// Character offset on a line in a document (zero-based). The meaning of this
|
||||
/// offset is determined by the negotiated `PositionEncodingKind`.
|
||||
///
|
||||
/// If the character value is greater than the line length it defaults back
|
||||
/// to the line length.
|
||||
pub character: u32,
|
||||
}
|
||||
|
||||
impl From<CopilotPosition> for tower_lsp::lsp_types::Position {
|
||||
fn from(position: CopilotPosition) -> Self {
|
||||
tower_lsp::lsp_types::Position {
|
||||
line: position.line,
|
||||
character: position.character,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A range in a text document expressed as (zero-based) start and end positions.
|
||||
/// A range is comparable to a selection in an editor. Therefore the end position is exclusive.
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Deserialize, Serialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CopilotRange {
|
||||
/// The range's start position.
|
||||
pub start: CopilotPosition,
|
||||
/// The range's end position.
|
||||
pub end: CopilotPosition,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CopilotCyclingCompletion {
|
||||
pub uuid: uuid::Uuid, // unique id we use for tracking accepted or rejected completions
|
||||
pub display_text: String, // partial text
|
||||
pub text: String, // fulltext
|
||||
pub range: CopilotRange, // start char always 0
|
||||
pub position: CopilotPosition,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct Choices {
|
||||
pub text: String,
|
||||
pub index: i16,
|
||||
pub finish_reason: Option<String>,
|
||||
pub logprobs: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CopilotCompletionResponse {
|
||||
pub completions: Vec<CopilotCyclingCompletion>,
|
||||
pub cancellation_reason: Option<String>,
|
||||
}
|
||||
|
||||
impl CopilotCompletionResponse {
|
||||
pub fn from_str_vec(str_vec: Vec<String>, line_before: String, pos: CopilotPosition) -> Self {
|
||||
let completions = str_vec
|
||||
.iter()
|
||||
.map(|x| CopilotCyclingCompletion::new(x.to_string(), line_before.to_string(), pos))
|
||||
.collect();
|
||||
Self {
|
||||
completions,
|
||||
cancellation_reason: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotCyclingCompletion {
|
||||
pub fn new(text: String, line_before: String, position: CopilotPosition) -> Self {
|
||||
let display_text = text.clone();
|
||||
let text = format!("{}{}", line_before, text);
|
||||
let end_char = text.find('\n').unwrap_or(text.len()) as u32;
|
||||
Self {
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
display_text, // partial text
|
||||
text, // fulltext
|
||||
range: CopilotRange {
|
||||
start: CopilotPosition {
|
||||
character: 0,
|
||||
line: position.line,
|
||||
},
|
||||
end: CopilotPosition {
|
||||
character: end_char,
|
||||
line: position.line,
|
||||
},
|
||||
}, // start char always 0
|
||||
position,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct LanguageEntry {
|
||||
pub language_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct EditorConfiguration {
|
||||
pub disabled_languages: Vec<LanguageEntry>,
|
||||
pub enable_auto_completions: bool,
|
||||
}
|
||||
|
||||
impl Default for EditorConfiguration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
disabled_languages: vec![],
|
||||
enable_auto_completions: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct EditorInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CopilotEditorInfo {
|
||||
pub editor_configuration: EditorConfiguration,
|
||||
pub editor_info: EditorInfo,
|
||||
pub editor_plugin_info: EditorInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct DocParams {
|
||||
#[serde(skip)]
|
||||
pub rope: Rope,
|
||||
pub uri: String,
|
||||
pub pos: CopilotPosition,
|
||||
pub language: String,
|
||||
pub line_before: String,
|
||||
pub prefix: String,
|
||||
pub suffix: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CopilotLspCompletionParams {
|
||||
pub doc: CopilotDocParams,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CopilotDocParams {
|
||||
pub indent_size: u32,
|
||||
pub insert_spaces: bool,
|
||||
pub language_id: String,
|
||||
pub path: String,
|
||||
pub position: CopilotPosition,
|
||||
pub relative_path: String,
|
||||
pub source: String,
|
||||
pub tab_size: u32,
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CopilotAcceptCompletionParams {
|
||||
pub uuid: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CopilotRejectCompletionParams {
|
||||
pub uuids: Vec<uuid::Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CopilotCompletionTelemetry {
|
||||
pub completion: CopilotCyclingCompletion,
|
||||
pub params: CopilotLspCompletionParams,
|
||||
}
|
49
rust/kcl-lib/src/lsp/kcl/custom_notifications.rs
Normal file
49
rust/kcl-lib/src/lsp/kcl/custom_notifications.rs
Normal file
@ -0,0 +1,49 @@
|
||||
//! Custom notifications for the KCL LSP server that are not part of the LSP specification.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_lsp::lsp_types::notification::Notification;
|
||||
|
||||
use crate::{parsing::ast::types::Node, settings::types::UnitLength};
|
||||
|
||||
/// A notification that the AST has changed.
|
||||
#[derive(Debug)]
|
||||
pub enum AstUpdated {}
|
||||
|
||||
impl Notification for AstUpdated {
|
||||
type Params = Node<crate::parsing::ast::types::Program>;
|
||||
const METHOD: &'static str = "kcl/astUpdated";
|
||||
}
|
||||
|
||||
/// Text documents are identified using a URI. On the protocol level, URIs are passed as strings.
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct TextDocumentIdentifier {
|
||||
/// The text document's URI.
|
||||
pub uri: url::Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct UpdateUnitsParams {
|
||||
pub text_document: TextDocumentIdentifier,
|
||||
/// The content of the text document.
|
||||
pub text: String,
|
||||
pub units: UnitLength,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, ts_rs::TS)]
|
||||
#[ts(export)]
|
||||
pub struct UpdateUnitsResponse {}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct UpdateCanExecuteParams {
|
||||
pub can_execute: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, ts_rs::TS)]
|
||||
#[ts(export)]
|
||||
pub struct UpdateCanExecuteResponse {}
|
1469
rust/kcl-lib/src/lsp/kcl/mod.rs
Normal file
1469
rust/kcl-lib/src/lsp/kcl/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
56
rust/kcl-lib/src/lsp/mod.rs
Normal file
56
rust/kcl-lib/src/lsp/mod.rs
Normal file
@ -0,0 +1,56 @@
|
||||
//! The servers that power the text editor.
|
||||
|
||||
pub mod backend;
|
||||
pub mod copilot;
|
||||
pub mod kcl;
|
||||
#[cfg(any(test, feature = "lsp-test-util"))]
|
||||
pub mod test_util;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod util;
|
||||
|
||||
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, DiagnosticTag};
|
||||
pub use util::IntoDiagnostic;
|
||||
|
||||
use crate::{
|
||||
errors::{Severity, Tag},
|
||||
CompilationError,
|
||||
};
|
||||
|
||||
impl IntoDiagnostic for CompilationError {
|
||||
fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
|
||||
let edit = self.suggestion.as_ref().map(|s| {
|
||||
let range = s.source_range.to_lsp_range(code);
|
||||
serde_json::to_value((s, range)).unwrap()
|
||||
});
|
||||
|
||||
vec![Diagnostic {
|
||||
range: self.source_range.to_lsp_range(code),
|
||||
severity: Some(self.severity()),
|
||||
code: None,
|
||||
code_description: None,
|
||||
source: Some("kcl".to_string()),
|
||||
message: self.message.clone(),
|
||||
related_information: None,
|
||||
tags: self.tag.to_lsp_tags(),
|
||||
data: edit,
|
||||
}]
|
||||
}
|
||||
|
||||
fn severity(&self) -> DiagnosticSeverity {
|
||||
match self.severity {
|
||||
Severity::Warning => DiagnosticSeverity::WARNING,
|
||||
_ => DiagnosticSeverity::ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
fn to_lsp_tags(self) -> Option<Vec<DiagnosticTag>> {
|
||||
match self {
|
||||
Tag::Deprecated => Some(vec![DiagnosticTag::DEPRECATED]),
|
||||
Tag::Unnecessary => Some(vec![DiagnosticTag::UNNECESSARY]),
|
||||
Tag::None => None,
|
||||
}
|
||||
}
|
||||
}
|
85
rust/kcl-lib/src/lsp/test_util.rs
Normal file
85
rust/kcl-lib/src/lsp/test_util.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
use tower_lsp::LanguageServer;
|
||||
|
||||
// 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 kcl_std = crate::docs::kcl_doc::walk_prelude();
|
||||
let stdlib_completions = crate::lsp::kcl::get_completions_from_stdlib(&stdlib, &kcl_std)?;
|
||||
let stdlib_signatures = crate::lsp::kcl::get_signatures_from_stdlib(&stdlib, &kcl_std);
|
||||
|
||||
let zoo_client = crate::engine::new_zoo_client(None, None)?;
|
||||
|
||||
let executor_ctx = if execute {
|
||||
Some(crate::execution::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(),
|
||||
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(),
|
||||
dev_mode: 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())
|
||||
}
|
3547
rust/kcl-lib/src/lsp/tests.rs
Normal file
3547
rust/kcl-lib/src/lsp/tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
43
rust/kcl-lib/src/lsp/util.rs
Normal file
43
rust/kcl-lib/src/lsp/util.rs
Normal file
@ -0,0 +1,43 @@
|
||||
//! Utility functions for working with ropes and positions.
|
||||
|
||||
use ropey::Rope;
|
||||
use tower_lsp::lsp_types::{Diagnostic, Position};
|
||||
|
||||
pub fn position_to_offset(position: Position, rope: &Rope) -> Option<usize> {
|
||||
Some(rope.try_line_to_char(position.line as usize).ok()? + position.character as usize)
|
||||
}
|
||||
|
||||
pub fn get_text_before(offset: usize, rope: &Rope) -> Option<String> {
|
||||
if offset == 0 {
|
||||
return Some("".to_string());
|
||||
}
|
||||
Some(rope.slice(0..offset).to_string())
|
||||
}
|
||||
|
||||
pub fn get_text_after(offset: usize, rope: &Rope) -> Option<String> {
|
||||
let end_idx = rope.len_chars();
|
||||
if offset == end_idx {
|
||||
return Some("".to_string());
|
||||
}
|
||||
Some(rope.slice(offset..end_idx).to_string())
|
||||
}
|
||||
|
||||
pub fn get_line_before(pos: Position, rope: &Rope) -> Option<String> {
|
||||
let char_offset = pos.character as usize;
|
||||
let offset = position_to_offset(pos, rope).unwrap();
|
||||
if char_offset == 0 {
|
||||
return Some("".to_string());
|
||||
}
|
||||
let line_start = offset - char_offset;
|
||||
Some(rope.slice(line_start..offset).to_string())
|
||||
}
|
||||
|
||||
/// Convert an object into a [lsp_types::Diagnostic] given the
|
||||
/// [TextDocumentItem]'s `.text` field.
|
||||
pub trait IntoDiagnostic {
|
||||
/// Convert the traited object to a vector of [lsp_types::Diagnostic].
|
||||
fn to_lsp_diagnostics(&self, text: &str) -> Vec<Diagnostic>;
|
||||
|
||||
/// Get the severity of the diagnostic.
|
||||
fn severity(&self) -> tower_lsp::lsp_types::DiagnosticSeverity;
|
||||
}
|
Reference in New Issue
Block a user