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