Skip to main content

kcl_lib/lsp/copilot/
mod.rs

1//! The copilot lsp server for ghost text.
2#![allow(dead_code)]
3
4pub mod cache;
5pub mod types;
6
7use std::borrow::Cow;
8use std::fmt::Debug;
9use std::sync::Arc;
10use std::sync::RwLock;
11
12use dashmap::DashMap;
13use serde::Deserialize;
14use serde::Serialize;
15use tower_lsp::LanguageServer;
16use tower_lsp::jsonrpc::Error;
17use tower_lsp::jsonrpc::Result;
18use tower_lsp::lsp_types::CreateFilesParams;
19use tower_lsp::lsp_types::DeleteFilesParams;
20use tower_lsp::lsp_types::Diagnostic;
21use tower_lsp::lsp_types::DidChangeConfigurationParams;
22use tower_lsp::lsp_types::DidChangeTextDocumentParams;
23use tower_lsp::lsp_types::DidChangeWatchedFilesParams;
24use tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams;
25use tower_lsp::lsp_types::DidCloseTextDocumentParams;
26use tower_lsp::lsp_types::DidOpenTextDocumentParams;
27use tower_lsp::lsp_types::DidSaveTextDocumentParams;
28use tower_lsp::lsp_types::InitializeParams;
29use tower_lsp::lsp_types::InitializeResult;
30use tower_lsp::lsp_types::InitializedParams;
31use tower_lsp::lsp_types::MessageType;
32use tower_lsp::lsp_types::OneOf;
33use tower_lsp::lsp_types::RenameFilesParams;
34use tower_lsp::lsp_types::ServerCapabilities;
35use tower_lsp::lsp_types::TextDocumentItem;
36use tower_lsp::lsp_types::TextDocumentSyncCapability;
37use tower_lsp::lsp_types::TextDocumentSyncKind;
38use tower_lsp::lsp_types::TextDocumentSyncOptions;
39use tower_lsp::lsp_types::WorkspaceFolder;
40use tower_lsp::lsp_types::WorkspaceFoldersServerCapabilities;
41use tower_lsp::lsp_types::WorkspaceServerCapabilities;
42
43use crate::lsp::backend::Backend as _;
44use crate::lsp::copilot::cache::CopilotCache;
45use crate::lsp::copilot::types::CopilotAcceptCompletionParams;
46use crate::lsp::copilot::types::CopilotCompletionResponse;
47use crate::lsp::copilot::types::CopilotCompletionTelemetry;
48use crate::lsp::copilot::types::CopilotEditorInfo;
49use crate::lsp::copilot::types::CopilotLspCompletionParams;
50use crate::lsp::copilot::types::CopilotRejectCompletionParams;
51use crate::lsp::copilot::types::DocParams;
52
53#[derive(Deserialize, Serialize, Debug)]
54pub struct Success {
55    success: bool,
56}
57impl Success {
58    pub fn new(success: bool) -> Self {
59        Self { success }
60    }
61}
62
63#[derive(Clone)]
64pub struct Backend {
65    /// The client is used to send notifications and requests to the client.
66    pub client: tower_lsp::Client,
67    /// The file system client to use.
68    pub fs: crate::fs::FileSystemHandle,
69    /// The workspace folders.
70    pub workspace_folders: DashMap<String, WorkspaceFolder>,
71    /// Current code.
72    pub code_map: DashMap<String, Vec<u8>>,
73    /// The Zoo API client.
74    pub zoo_client: kittycad::Client,
75    /// The editor info is used to store information about the editor.
76    pub editor_info: Arc<RwLock<CopilotEditorInfo>>,
77    /// The cache is used to store the results of previous requests.
78    pub cache: Arc<cache::CopilotCache>,
79    /// Storage so we can send telemetry data back out.
80    pub telemetry: DashMap<uuid::Uuid, CopilotCompletionTelemetry>,
81    /// Diagnostics.
82    pub diagnostics_map: DashMap<String, Vec<Diagnostic>>,
83
84    pub is_initialized: Arc<tokio::sync::RwLock<bool>>,
85
86    /// Are we running in dev mode.
87    pub dev_mode: bool,
88}
89
90// Implement the shared backend trait for the language server.
91#[async_trait::async_trait]
92impl crate::lsp::backend::Backend for Backend {
93    fn client(&self) -> &tower_lsp::Client {
94        &self.client
95    }
96
97    fn fs(&self) -> &crate::fs::FileSystemHandle {
98        &self.fs
99    }
100
101    async fn is_initialized(&self) -> bool {
102        *self.is_initialized.read().await
103    }
104
105    async fn set_is_initialized(&self, is_initialized: bool) {
106        *self.is_initialized.write().await = is_initialized;
107    }
108
109    async fn workspace_folders(&self) -> Vec<WorkspaceFolder> {
110        // TODO: fix clone
111        self.workspace_folders.iter().map(|i| i.clone()).collect()
112    }
113
114    async fn add_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
115        for folder in folders {
116            self.workspace_folders.insert(folder.name.to_string(), folder);
117        }
118    }
119
120    async fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
121        for folder in folders {
122            self.workspace_folders.remove(&folder.name);
123        }
124    }
125
126    fn code_map(&self) -> &DashMap<String, Vec<u8>> {
127        &self.code_map
128    }
129
130    async fn insert_code_map(&self, uri: String, text: Vec<u8>) {
131        self.code_map.insert(uri, text);
132    }
133
134    async fn remove_from_code_map(&self, uri: String) -> Option<Vec<u8>> {
135        self.code_map.remove(&uri).map(|(_, v)| v)
136    }
137
138    async fn clear_code_state(&self) {
139        self.code_map.clear();
140    }
141
142    fn current_diagnostics_map(&self) -> &DashMap<String, Vec<Diagnostic>> {
143        &self.diagnostics_map
144    }
145
146    async fn inner_on_change(&self, _params: TextDocumentItem, _force: bool) {
147        // We don't need to do anything here.
148    }
149}
150
151impl Backend {
152    #[cfg(target_arch = "wasm32")]
153    pub fn new_wasm(
154        client: tower_lsp::Client,
155        fs: crate::fs::wasm::FileSystemManager,
156        zoo_client: kittycad::Client,
157        dev_mode: bool,
158    ) -> Self {
159        Self::new(
160            client,
161            crate::fs::new_file_system_handle(crate::fs::FileManager::new(fs)),
162            zoo_client,
163            dev_mode,
164        )
165    }
166
167    pub fn new(
168        client: tower_lsp::Client,
169        fs: crate::fs::FileSystemHandle,
170        zoo_client: kittycad::Client,
171        dev_mode: bool,
172    ) -> Self {
173        Self {
174            client,
175            fs,
176            workspace_folders: Default::default(),
177            code_map: Default::default(),
178            editor_info: Arc::new(RwLock::new(CopilotEditorInfo::default())),
179            cache: Arc::new(CopilotCache::new()),
180            telemetry: Default::default(),
181            zoo_client,
182
183            is_initialized: Default::default(),
184            diagnostics_map: Default::default(),
185            dev_mode,
186        }
187    }
188
189    /// Get completions from the kittycad api.
190    pub async fn get_completions(&self, language: String, prompt: String, suffix: String) -> Result<Vec<String>> {
191        let body = kittycad::types::KclCodeCompletionRequest {
192            extra: Some(kittycad::types::KclCodeCompletionParams {
193                language: Some(language.to_string()),
194                next_indent: None,
195                trim_by_indentation: true,
196                prompt_tokens: Some(prompt.len() as u32),
197                suffix_tokens: Some(suffix.len() as u32),
198            }),
199            prompt: Some(prompt),
200            suffix: Some(suffix),
201            max_tokens: Some(500),
202            temperature: Some(1.0),
203            top_p: Some(1.0),
204            // We only handle one completion at a time, for now so don't even waste the tokens.
205            n: Some(1),
206            stop: Some(["unset".to_string()].to_vec()),
207            nwo: None,
208            // We haven't implemented streaming yet.
209            stream: false,
210            model_version: None,
211        };
212
213        let resp = self
214            .zoo_client
215            .ml()
216            .create_kcl_code_completions(&body)
217            .await
218            .map_err(|err| Error {
219                code: tower_lsp::jsonrpc::ErrorCode::from(69),
220                data: None,
221                message: Cow::from(format!("Failed to get completions from zoo api: {err}")),
222            })?;
223        Ok(resp.completions)
224    }
225
226    pub async fn set_editor_info(&self, params: CopilotEditorInfo) -> Result<Success> {
227        self.client.log_message(MessageType::INFO, "setEditorInfo").await;
228        let copy = Arc::clone(&self.editor_info);
229        let mut lock = copy.write().map_err(|err| Error {
230            code: tower_lsp::jsonrpc::ErrorCode::from(69),
231            data: None,
232            message: Cow::from(format!("Failed lock: {err}")),
233        })?;
234        *lock = params;
235        Ok(Success::new(true))
236    }
237
238    pub fn get_doc_params(&self, params: &CopilotLspCompletionParams) -> Result<DocParams> {
239        let pos = params.doc.position;
240        let uri = params.doc.uri.to_string();
241        let rope = ropey::Rope::from_str(&params.doc.source);
242        let offset = crate::lsp::util::position_to_offset(pos.into(), &rope).unwrap_or_default();
243
244        Ok(DocParams {
245            uri,
246            pos,
247            language: params.doc.language_id.to_string(),
248            prefix: crate::lsp::util::get_text_before(offset, &rope).unwrap_or_default(),
249            suffix: crate::lsp::util::get_text_after(offset, &rope).unwrap_or_default(),
250            line_before: crate::lsp::util::get_line_before(pos.into(), &rope).unwrap_or_default(),
251            rope,
252        })
253    }
254
255    pub async fn get_completions_cycling(
256        &self,
257        params: CopilotLspCompletionParams,
258    ) -> Result<CopilotCompletionResponse> {
259        let doc_params = self.get_doc_params(&params)?;
260        let cached_result = self.cache.get_cached_result(&doc_params.uri, doc_params.pos.line);
261        if let Some(cached_result) = cached_result {
262            return Ok(cached_result);
263        }
264
265        let doc_params = self.get_doc_params(&params)?;
266        let line_before = doc_params.line_before.to_string();
267
268        // Let's not call it yet since it's not our model.
269        // We will need to wrap in spawn_local like we do in kcl/mod.rs for wasm only.
270        #[cfg(test)]
271        let mut completion_list = self
272            .get_completions(doc_params.language, doc_params.prefix, doc_params.suffix)
273            .await
274            .map_err(|err| Error {
275                code: tower_lsp::jsonrpc::ErrorCode::from(69),
276                data: None,
277                message: Cow::from(format!("Failed to get completions: {err}")),
278            })?;
279        #[cfg(not(test))]
280        let mut completion_list = vec![];
281
282        // if self.dev_mode
283        if false {
284            completion_list.push(
285                r#"fn cube(pos, scale) {
286  sg = startSketchOn(XY)
287    |> startProfile(at = pos)
288    |> line(end = [0, scale])
289    |> line(end = [scale, 0])
290    |> line(end = [0, -scale])
291  return sg
292}
293part001 = cube(pos = [0,0], scale = 20)
294    |> close()
295    |> extrude(length=20)"#
296                    .to_string(),
297            );
298        }
299
300        let response = CopilotCompletionResponse::from_str_vec(completion_list, line_before, doc_params.pos);
301        // Set the telemetry data for each completion.
302        for completion in response.completions.iter() {
303            let telemetry = CopilotCompletionTelemetry {
304                completion: completion.clone(),
305                params: params.clone(),
306            };
307            self.telemetry.insert(completion.uuid, telemetry);
308        }
309        self.cache
310            .set_cached_result(&doc_params.uri, &doc_params.pos.line, &response);
311
312        Ok(response)
313    }
314
315    pub async fn accept_completion(&self, params: CopilotAcceptCompletionParams) {
316        self.client
317            .log_message(MessageType::INFO, format!("Accepted completions: {params:?}"))
318            .await;
319
320        // Get the original telemetry data.
321        let Some(original) = self.telemetry.remove(&params.uuid) else {
322            return;
323        };
324
325        self.client
326            .log_message(MessageType::INFO, format!("Original telemetry: {original:?}"))
327            .await;
328
329        // TODO: Send the telemetry data to the zoo api.
330    }
331
332    pub async fn reject_completions(&self, params: CopilotRejectCompletionParams) {
333        self.client
334            .log_message(MessageType::INFO, format!("Rejected completions: {params:?}"))
335            .await;
336
337        // Get the original telemetry data.
338        let mut originals: Vec<CopilotCompletionTelemetry> = Default::default();
339        for uuid in params.uuids {
340            if let Some(original) = self.telemetry.remove(&uuid).map(|(_, v)| v) {
341                originals.push(original);
342            }
343        }
344
345        self.client
346            .log_message(MessageType::INFO, format!("Original telemetry: {originals:?}"))
347            .await;
348
349        // TODO: Send the telemetry data to the zoo api.
350    }
351}
352
353#[tower_lsp::async_trait]
354impl LanguageServer for Backend {
355    async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
356        Ok(InitializeResult {
357            capabilities: ServerCapabilities {
358                text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
359                    open_close: Some(true),
360                    change: Some(TextDocumentSyncKind::FULL),
361                    ..Default::default()
362                })),
363                workspace: Some(WorkspaceServerCapabilities {
364                    workspace_folders: Some(WorkspaceFoldersServerCapabilities {
365                        supported: Some(true),
366                        change_notifications: Some(OneOf::Left(true)),
367                    }),
368                    file_operations: None,
369                }),
370                ..ServerCapabilities::default()
371            },
372            ..Default::default()
373        })
374    }
375
376    async fn initialized(&self, params: InitializedParams) {
377        self.do_initialized(params).await
378    }
379
380    async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
381        self.do_shutdown().await
382    }
383
384    async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
385        self.do_did_change_workspace_folders(params).await
386    }
387
388    async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
389        self.do_did_change_configuration(params).await
390    }
391
392    async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
393        self.do_did_change_watched_files(params).await
394    }
395
396    async fn did_create_files(&self, params: CreateFilesParams) {
397        self.do_did_create_files(params).await
398    }
399
400    async fn did_rename_files(&self, params: RenameFilesParams) {
401        self.do_did_rename_files(params).await
402    }
403
404    async fn did_delete_files(&self, params: DeleteFilesParams) {
405        self.do_did_delete_files(params).await
406    }
407
408    async fn did_open(&self, params: DidOpenTextDocumentParams) {
409        self.do_did_open(params).await
410    }
411
412    async fn did_change(&self, params: DidChangeTextDocumentParams) {
413        self.do_did_change(params).await;
414    }
415
416    async fn did_save(&self, params: DidSaveTextDocumentParams) {
417        self.do_did_save(params).await
418    }
419
420    async fn did_close(&self, params: DidCloseTextDocumentParams) {
421        self.do_did_close(params).await
422    }
423}