diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx index 49331e81b..8c785d442 100644 --- a/src/components/FileTree.tsx +++ b/src/components/FileTree.tsx @@ -21,14 +21,6 @@ function getIndentationCSS(level: number) { return `calc(1rem * ${level + 1})` } -// an OS-agnostic way to get the basename of the path. -export function basename(path: string): string { - // Regular expression to match the last portion of the path, taking into account both POSIX and Windows delimiters - const re = /[^\\/]+$/ - const match = path.match(re) - return match ? match[0] : '' -} - function RenameForm({ fileOrDir, setIsRenaming, @@ -156,7 +148,7 @@ const FileTreeItem = ({ level?: number }) => { const { send, context } = useFileContext() - const { lspClients } = useLspContext() + const { onFileOpen, onFileClose } = useLspContext() const navigate = useNavigate() const [isRenaming, setIsRenaming] = useState(false) const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) @@ -185,26 +177,8 @@ const FileTreeItem = ({ ) } else { // Let the lsp servers know we closed a file. - const currentFilePath = basename(currentFile?.path || 'main.kcl') - lspClients.forEach((lspClient) => { - lspClient.textDocumentDidClose({ - textDocument: { - uri: `file:///${currentFilePath}`, - }, - }) - }) - const newFilePath = basename(fileOrDir.path) - // Then let the clients know we opened a file. - lspClients.forEach((lspClient) => { - lspClient.textDocumentDidOpen({ - textDocument: { - uri: `file:///${newFilePath}`, - languageId: 'kcl', - version: 1, - text: '', - }, - }) - }) + onFileClose(currentFile?.path || null, project?.path || null) + onFileOpen(fileOrDir.path, project?.path || null) // Open kcl files navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`) diff --git a/src/components/LspProvider.tsx b/src/components/LspProvider.tsx index 352fdd160..463496e73 100644 --- a/src/components/LspProvider.tsx +++ b/src/components/LspProvider.tsx @@ -12,24 +12,46 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { Extension } from '@codemirror/state' import { LanguageSupport } from '@codemirror/language' import { useNavigate } from 'react-router-dom' -import { basename } from './FileTree' import { paths } from 'lib/paths' import { FileEntry } from '@tauri-apps/api/fs' import { ProjectWithEntryPointMetadata } from 'lib/types' +const DEFAULT_FILE_NAME: string = 'main.kcl' + function getWorkspaceFolders(): LSP.WorkspaceFolder[] { return [] } +// an OS-agnostic way to get the basename of the path. +export function projectBasename(filePath: string, projectPath: string): string { + const newPath = filePath.replace(projectPath, '') + // Trim any leading slashes. + let trimmedStr = newPath.replace(/^\/+/, '').replace(/^\\+/, '') + return trimmedStr +} + type LspContext = { lspClients: LanguageServerClient[] copilotLSP: Extension | null kclLSP: LanguageSupport | null - onProjectClose: (file: FileEntry | null, redirect: boolean) => void + onProjectClose: ( + file: FileEntry | null, + projectPath: string | null, + redirect: boolean + ) => void onProjectOpen: ( project: ProjectWithEntryPointMetadata | null, file: FileEntry | null ) => void + onFileOpen: (filePath: string | null, projectPath: string | null) => void + onFileClose: (filePath: string | null, projectPath: string | null) => void + onFileCreate: (file: FileEntry, projectPath: string | null) => void + onFileRename: ( + oldFile: FileEntry, + newFile: FileEntry, + projectPath: string | null + ) => void + onFileDelete: (file: FileEntry, projectPath: string | null) => void } export const LspStateContext = createContext({} as LspContext) @@ -58,7 +80,8 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { const client = new Client(fromServer, intoServer) if (!TEST) { Server.initialize(intoServer, fromServer).then((lspServer) => { - lspServer.start('kcl') + const token = auth?.context?.token + lspServer.start('kcl', token) setIsKclLspServerReady(true) }) } @@ -77,7 +100,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { if (isKclLspServerReady && !TEST) { // Set up the lsp plugin. const lsp = kclLanguage({ - documentUri: `file:///main.kcl`, + documentUri: `file:///${DEFAULT_FILE_NAME}`, workspaceFolders: getWorkspaceFolders(), client: kclLspClient, }) @@ -113,7 +136,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { if (isCopilotLspServerReady && !TEST) { // Set up the lsp plugin. const lsp = copilotPlugin({ - documentUri: `file:///main.kcl`, + documentUri: `file:///${DEFAULT_FILE_NAME}`, workspaceFolders: getWorkspaceFolders(), client: copilotLspClient, allowHTMLContent: true, @@ -126,8 +149,15 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { const lspClients = [kclLspClient, copilotLspClient] - const onProjectClose = (file: FileEntry | null, redirect: boolean) => { - const currentFilePath = basename(file?.name || 'main.kcl') + const onProjectClose = ( + file: FileEntry | null, + projectPath: string | null, + redirect: boolean + ) => { + const currentFilePath = projectBasename( + file?.path || DEFAULT_FILE_NAME, + projectPath || '' + ) lspClients.forEach((lspClient) => { lspClient.textDocumentDidClose({ textDocument: { @@ -155,7 +185,10 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { }) if (file) { // Send that the file was opened. - const filename = basename(file?.name || 'main.kcl') + const filename = projectBasename( + file?.path || DEFAULT_FILE_NAME, + project?.path || '' + ) lspClients.forEach((lspClient) => { lspClient.textDocumentDidOpen({ textDocument: { @@ -169,6 +202,82 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { } } + const onFileOpen = (filePath: string | null, projectPath: string | null) => { + const currentFilePath = projectBasename( + filePath || DEFAULT_FILE_NAME, + projectPath || '' + ) + lspClients.forEach((lspClient) => { + lspClient.textDocumentDidOpen({ + textDocument: { + uri: `file:///${currentFilePath}`, + languageId: 'kcl', + version: 1, + text: '', + }, + }) + }) + } + + const onFileClose = (filePath: string | null, projectPath: string | null) => { + const currentFilePath = projectBasename( + filePath || DEFAULT_FILE_NAME, + projectPath || '' + ) + lspClients.forEach((lspClient) => { + lspClient.textDocumentDidClose({ + textDocument: { + uri: `file:///${currentFilePath}`, + }, + }) + }) + } + + const onFileCreate = (file: FileEntry, projectPath: string | null) => { + const currentFilePath = projectBasename(file.path, projectPath || '') + lspClients.forEach((lspClient) => { + lspClient.workspaceDidCreateFiles({ + files: [ + { + uri: `file:///${currentFilePath}`, + }, + ], + }) + }) + } + + const onFileRename = ( + oldFile: FileEntry, + newFile: FileEntry, + projectPath: string | null + ) => { + const oldFilePath = projectBasename(oldFile.path, projectPath || '') + const newFilePath = projectBasename(newFile.path, projectPath || '') + lspClients.forEach((lspClient) => { + lspClient.workspaceDidRenameFiles({ + files: [ + { + oldUri: `file:///${oldFilePath}`, + newUri: `file:///${newFilePath}`, + }, + ], + }) + }) + } + + const onFileDelete = (file: FileEntry, projectPath: string | null) => { + const currentFilePath = projectBasename(file.path, projectPath || '') + lspClients.forEach((lspClient) => { + lspClient.workspaceDidDeleteFiles({ + files: [ + { + uri: `file:///${currentFilePath}`, + }, + ], + }) + }) + } + return ( { kclLSP, onProjectClose, onProjectOpen, + onFileOpen, + onFileClose, + onFileCreate, + onFileRename, + onFileDelete, }} > {children} diff --git a/src/components/ProjectSidebarMenu.tsx b/src/components/ProjectSidebarMenu.tsx index 3a85b2a7e..57808cd8f 100644 --- a/src/components/ProjectSidebarMenu.tsx +++ b/src/components/ProjectSidebarMenu.tsx @@ -28,7 +28,7 @@ const ProjectSidebarMenu = ({
{ - onProjectClose(file || null, false) + onProjectClose(file || null, project?.path || null, false) }} to={paths.HOME} className="group" @@ -39,7 +39,7 @@ const ProjectSidebarMenu = ({ <> { - onProjectClose(file || null, false) + onProjectClose(file || null, project?.path || null, false) }} to={paths.HOME} className="!no-underline" @@ -163,7 +163,7 @@ function ProjectMenuPopover({ { - onProjectClose(file || null, true) + onProjectClose(file || null, project?.path || null, true) }} icon={{ icon: faHome, diff --git a/src/editor/plugins/lsp/index.ts b/src/editor/plugins/lsp/index.ts index 7a88a9609..8ee0e6a6e 100644 --- a/src/editor/plugins/lsp/index.ts +++ b/src/editor/plugins/lsp/index.ts @@ -90,6 +90,9 @@ interface LSPNotifyMap { 'textDocument/didOpen': LSP.DidOpenTextDocumentParams 'textDocument/didClose': LSP.DidCloseTextDocumentParams 'workspace/didChangeWorkspaceFolders': LSP.DidChangeWorkspaceFoldersParams + 'workspace/didCreateFiles': LSP.CreateFilesParams + 'workspace/didRenameFiles': LSP.RenameFilesParams + 'workspace/didDeleteFiles': LSP.DeleteFilesParams } export interface LanguageServerClientOptions { @@ -187,6 +190,18 @@ export class LanguageServerClient { } } + workspaceDidCreateFiles(params: LSP.CreateFilesParams) { + this.notify('workspace/didCreateFiles', params) + } + + workspaceDidRenameFiles(params: LSP.RenameFilesParams) { + this.notify('workspace/didRenameFiles', params) + } + + workspaceDidDeleteFiles(params: LSP.DeleteFilesParams) { + this.notify('workspace/didDeleteFiles', params) + } + async updateSemanticTokens(uri: string) { const serverCapabilities = this.getServerCapabilities() if (!serverCapabilities.semanticTokensProvider) { diff --git a/src/editor/plugins/lsp/server.ts b/src/editor/plugins/lsp/server.ts index 101f0b87f..2be2ac988 100644 --- a/src/editor/plugins/lsp/server.ts +++ b/src/editor/plugins/lsp/server.ts @@ -39,7 +39,7 @@ export default class Server { } await copilotLspRun(config, token) } else if (type_ === 'kcl') { - await kclLspRun(config) + await kclLspRun(config, token || '') } } } diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index 51224db6b..64845ede6 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -20,6 +20,7 @@ import type { Program } from '../wasm-lib/kcl/bindings/Program' import type { Token } from '../wasm-lib/kcl/bindings/Token' import { Coords2d } from './std/sketch' import { fileSystemManager } from 'lang/std/fileSystemManager' +import { DEV } from 'env' export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Value } from '../wasm-lib/kcl/bindings/Value' @@ -286,23 +287,19 @@ export function programMemoryInit(): ProgramMemory { export async function copilotLspRun(config: ServerConfig, token: string) { try { console.log('starting copilot lsp') - await copilot_lsp_run(config, token) + await copilot_lsp_run(config, token, DEV) } catch (e: any) { console.log('copilot lsp failed', e) - // We make it restart recursively so that if it ever dies after like - // 8 hours or something it will come back to life. - await copilotLspRun(config, token) + // We can't restart here because a moved value, we should do this another way. } } -export async function kclLspRun(config: ServerConfig) { +export async function kclLspRun(config: ServerConfig, token: string) { try { console.log('start kcl lsp') - await kcl_lsp_run(config) + await kcl_lsp_run(config, token, DEV) } catch (e: any) { console.log('kcl lsp failed', e) - // We make it restart recursively so that if it ever dies after like - // 8 hours or something it will come back to life. - await kclLspRun(config) + // We can't restart here because a moved value, we should do this another way. } } diff --git a/src/wasm-lib/Cargo.lock b/src/wasm-lib/Cargo.lock index c0ffc8a55..52528b4d7 100644 --- a/src/wasm-lib/Cargo.lock +++ b/src/wasm-lib/Cargo.lock @@ -962,6 +962,22 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "derive-docs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc85a0d10f808387cd56147b520be7efd94b8b198b1453f987b133cd2a90b7e" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "serde", + "serde_tokenstream", + "syn 2.0.52", +] + [[package]] name = "diesel_derives" version = "2.1.2" @@ -1891,7 +1907,7 @@ dependencies = [ [[package]] name = "kcl-lib" -version = "0.1.44" +version = "0.1.45" dependencies = [ "anyhow", "approx 0.5.1", @@ -1902,7 +1918,7 @@ dependencies = [ "criterion", "dashmap", "databake", - "derive-docs", + "derive-docs 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "expectorate", "futures", "gltf-json", diff --git a/src/wasm-lib/kcl/Cargo.toml b/src/wasm-lib/kcl/Cargo.toml index cc122a27f..0f14bcb9f 100644 --- a/src/wasm-lib/kcl/Cargo.toml +++ b/src/wasm-lib/kcl/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "kcl-lib" description = "KittyCAD Language implementation and tools" -version = "0.1.44" +version = "0.1.45" edition = "2021" license = "MIT" repository = "https://github.com/KittyCAD/modeling-app" @@ -17,8 +17,8 @@ async-trait = "0.1.77" clap = { version = "4.5.2", features = ["cargo", "derive", "env", "unicode"], optional = true } dashmap = "5.5.3" databake = { version = "0.1.7", features = ["derive"] } -#derive-docs = { version = "0.1.10" } -derive-docs = { path = "../derive-docs" } +derive-docs = { version = "0.1.10" } +#derive-docs = { path = "../derive-docs" } futures = { version = "0.3.30" } gltf-json = "1.4.0" kittycad = { workspace = true } diff --git a/src/wasm-lib/kcl/src/engine/conn.rs b/src/wasm-lib/kcl/src/engine/conn.rs index 1c2341a2c..37cbef1cb 100644 --- a/src/wasm-lib/kcl/src/engine/conn.rs +++ b/src/wasm-lib/kcl/src/engine/conn.rs @@ -158,7 +158,7 @@ impl EngineConnection { } } -#[async_trait::async_trait(?Send)] +#[async_trait::async_trait] impl EngineManager for EngineConnection { async fn send_modeling_cmd( &self, diff --git a/src/wasm-lib/kcl/src/engine/conn_mock.rs b/src/wasm-lib/kcl/src/engine/conn_mock.rs index b1dc6c1bd..e290eb80b 100644 --- a/src/wasm-lib/kcl/src/engine/conn_mock.rs +++ b/src/wasm-lib/kcl/src/engine/conn_mock.rs @@ -15,7 +15,7 @@ impl EngineConnection { } } -#[async_trait::async_trait(?Send)] +#[async_trait::async_trait] impl crate::engine::EngineManager for EngineConnection { async fn send_modeling_cmd( &self, diff --git a/src/wasm-lib/kcl/src/engine/conn_wasm.rs b/src/wasm-lib/kcl/src/engine/conn_wasm.rs index 6362c9717..7f58fad3f 100644 --- a/src/wasm-lib/kcl/src/engine/conn_wasm.rs +++ b/src/wasm-lib/kcl/src/engine/conn_wasm.rs @@ -27,6 +27,10 @@ pub struct EngineConnection { manager: Arc, } +// Safety: WebAssembly will only ever run in a single-threaded context. +unsafe impl Send for EngineConnection {} +unsafe impl Sync for EngineConnection {} + impl EngineConnection { pub async fn new(manager: EngineCommandManager) -> Result { Ok(EngineConnection { @@ -35,7 +39,7 @@ impl EngineConnection { } } -#[async_trait::async_trait(?Send)] +#[async_trait::async_trait] impl crate::engine::EngineManager for EngineConnection { async fn send_modeling_cmd( &self, @@ -67,7 +71,7 @@ impl crate::engine::EngineManager for EngineConnection { }) })?; - let value = wasm_bindgen_futures::JsFuture::from(promise).await.map_err(|e| { + let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| { KclError::Engine(KclErrorDetails { message: format!("Failed to wait for promise from engine: {:?}", e), source_ranges: vec![source_range], diff --git a/src/wasm-lib/kcl/src/engine/mod.rs b/src/wasm-lib/kcl/src/engine/mod.rs index fdfd43c2d..7faa78555 100644 --- a/src/wasm-lib/kcl/src/engine/mod.rs +++ b/src/wasm-lib/kcl/src/engine/mod.rs @@ -31,7 +31,7 @@ use anyhow::Result; #[cfg(not(test))] pub use conn_mock::EngineConnection; -#[async_trait::async_trait(?Send)] +#[async_trait::async_trait] pub trait EngineManager: Clone { /// Send a modeling command and wait for the response message. async fn send_modeling_cmd( diff --git a/src/wasm-lib/kcl/src/fs/local.rs b/src/wasm-lib/kcl/src/fs/local.rs index 7bfb53f89..c447ff3b9 100644 --- a/src/wasm-lib/kcl/src/fs/local.rs +++ b/src/wasm-lib/kcl/src/fs/local.rs @@ -22,9 +22,9 @@ impl Default for FileManager { } } -#[async_trait::async_trait(?Send)] +#[async_trait::async_trait] impl FileSystem for FileManager { - async fn read>( + async fn read + std::marker::Send + std::marker::Sync>( &self, path: P, source_range: crate::executor::SourceRange, @@ -37,7 +37,7 @@ impl FileSystem for FileManager { }) } - async fn exists>( + async fn exists + std::marker::Send + std::marker::Sync>( &self, path: P, source_range: crate::executor::SourceRange, @@ -54,7 +54,7 @@ impl FileSystem for FileManager { }) } - async fn get_all_files>( + async fn get_all_files + std::marker::Send + std::marker::Sync>( &self, path: P, source_range: crate::executor::SourceRange, diff --git a/src/wasm-lib/kcl/src/fs/mod.rs b/src/wasm-lib/kcl/src/fs/mod.rs index 5cdb9e740..5f75dd406 100644 --- a/src/wasm-lib/kcl/src/fs/mod.rs +++ b/src/wasm-lib/kcl/src/fs/mod.rs @@ -13,24 +13,24 @@ use anyhow::Result; #[cfg(not(test))] pub use wasm::FileManager; -#[async_trait::async_trait(?Send)] +#[async_trait::async_trait] pub trait FileSystem: Clone { /// Read a file from the local file system. - async fn read>( + async fn read + std::marker::Send + std::marker::Sync>( &self, path: P, source_range: crate::executor::SourceRange, ) -> Result, crate::errors::KclError>; /// Check if a file exists on the local file system. - async fn exists>( + async fn exists + std::marker::Send + std::marker::Sync>( &self, path: P, source_range: crate::executor::SourceRange, ) -> Result; /// Get all the files in a directory recursively. - async fn get_all_files>( + async fn get_all_files + std::marker::Send + std::marker::Sync>( &self, path: P, source_range: crate::executor::SourceRange, diff --git a/src/wasm-lib/kcl/src/fs/wasm.rs b/src/wasm-lib/kcl/src/fs/wasm.rs index 16e872d31..b7b4e63a1 100644 --- a/src/wasm-lib/kcl/src/fs/wasm.rs +++ b/src/wasm-lib/kcl/src/fs/wasm.rs @@ -6,6 +6,7 @@ use wasm_bindgen::prelude::wasm_bindgen; use crate::{ errors::{KclError, KclErrorDetails}, fs::FileSystem, + wasm::JsFuture, }; #[wasm_bindgen(module = "/../../lang/std/fileSystemManager.ts")] @@ -37,9 +38,9 @@ impl FileManager { unsafe impl Send for FileManager {} unsafe impl Sync for FileManager {} -#[async_trait::async_trait(?Send)] +#[async_trait::async_trait] impl FileSystem for FileManager { - async fn read>( + async fn read + std::marker::Send + std::marker::Sync>( &self, path: P, source_range: crate::executor::SourceRange, @@ -64,7 +65,7 @@ impl FileSystem for FileManager { }) })?; - let value = wasm_bindgen_futures::JsFuture::from(promise).await.map_err(|e| { + let value = JsFuture::from(promise).await.map_err(|e| { KclError::Engine(KclErrorDetails { message: format!("Failed to wait for promise from engine: {:?}", e), source_ranges: vec![source_range], @@ -77,7 +78,7 @@ impl FileSystem for FileManager { Ok(bytes) } - async fn exists>( + async fn exists + std::marker::Send + std::marker::Sync>( &self, path: P, source_range: crate::executor::SourceRange, @@ -102,7 +103,7 @@ impl FileSystem for FileManager { }) })?; - let value = wasm_bindgen_futures::JsFuture::from(promise).await.map_err(|e| { + let value = JsFuture::from(promise).await.map_err(|e| { KclError::Engine(KclErrorDetails { message: format!("Failed to wait for promise from engine: {:?}", e), source_ranges: vec![source_range], @@ -119,7 +120,7 @@ impl FileSystem for FileManager { Ok(it_exists) } - async fn get_all_files>( + async fn get_all_files + std::marker::Send + std::marker::Sync>( &self, path: P, source_range: crate::executor::SourceRange, @@ -144,7 +145,7 @@ impl FileSystem for FileManager { }) })?; - let value = wasm_bindgen_futures::JsFuture::from(promise).await.map_err(|e| { + let value = JsFuture::from(promise).await.map_err(|e| { KclError::Engine(KclErrorDetails { message: format!("Failed to wait for promise from javascript: {:?}", e), source_ranges: vec![source_range], diff --git a/src/wasm-lib/kcl/src/lib.rs b/src/wasm-lib/kcl/src/lib.rs index 92ff9bb1f..681810697 100644 --- a/src/wasm-lib/kcl/src/lib.rs +++ b/src/wasm-lib/kcl/src/lib.rs @@ -14,3 +14,5 @@ pub mod lsp; pub mod parser; pub mod std; pub mod token; +#[cfg(target_arch = "wasm32")] +pub mod wasm; diff --git a/src/wasm-lib/kcl/src/lsp/backend.rs b/src/wasm-lib/kcl/src/lsp/backend.rs index 6e53e98ad..8da06a364 100644 --- a/src/wasm-lib/kcl/src/lsp/backend.rs +++ b/src/wasm-lib/kcl/src/lsp/backend.rs @@ -1,5 +1,6 @@ //! A shared backend trait for lsp servers memory and behavior. +use anyhow::Result; use dashmap::DashMap; use tower_lsp::lsp_types::{ CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams, @@ -8,6 +9,8 @@ use tower_lsp::lsp_types::{ TextDocumentItem, WorkspaceFolder, }; +use crate::fs::FileSystem; + /// A trait for the backend of the language server. #[async_trait::async_trait] pub trait Backend { @@ -22,10 +25,13 @@ pub trait Backend { fn remove_workspace_folders(&self, folders: Vec); /// Get the current code map. - fn current_code_map(&self) -> DashMap; + fn current_code_map(&self) -> DashMap>; /// Insert a new code map. - fn insert_current_code_map(&self, uri: String, text: String); + fn insert_current_code_map(&self, uri: String, text: Vec); + + // Remove from code map. + fn remove_from_code_map(&self, uri: String) -> Option<(String, Vec)>; /// Clear the current code state. fn clear_code_state(&self); @@ -34,8 +40,25 @@ pub trait Backend { async fn on_change(&self, params: TextDocumentItem); async fn update_memory(&self, params: TextDocumentItem) { - // Lets update the tokens. - self.insert_current_code_map(params.uri.to_string(), params.text.clone()); + self.insert_current_code_map(params.uri.to_string(), params.text.as_bytes().to_vec()); + } + + async fn update_from_disk + 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_current_code_map(file_path, contents); + } + + Ok(()) } async fn do_initialized(&self, params: InitializedParams) { @@ -52,11 +75,23 @@ pub trait Backend { } async fn do_did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { - self.add_workspace_folders(params.event.added); + self.add_workspace_folders(params.event.added.clone()); self.remove_workspace_folders(params.event.removed); // 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. self.clear_code_state(); + 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) { @@ -75,18 +110,36 @@ pub trait Backend { 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_current_code_map(file.uri.to_string(), Default::default()); + } } 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) { + // Rename the file if it exists. + self.insert_current_code_map(file.new_uri.to_string(), value); + } else { + // Otherwise create it. + self.insert_current_code_map(file.new_uri.to_string(), Default::default()); + } + } } 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()); + } } async fn do_did_open(&self, params: DidOpenTextDocumentParams) { diff --git a/src/wasm-lib/kcl/src/lsp/copilot/mod.rs b/src/wasm-lib/kcl/src/lsp/copilot/mod.rs index 1775a6972..c1ad4f851 100644 --- a/src/wasm-lib/kcl/src/lsp/copilot/mod.rs +++ b/src/wasm-lib/kcl/src/lsp/copilot/mod.rs @@ -48,9 +48,9 @@ pub struct Backend { /// The workspace folders. pub workspace_folders: DashMap, /// Current code. - pub current_code_map: DashMap, - /// The token is used to authenticate requests to the API server. - pub token: String, + pub current_code_map: DashMap>, + /// The Zoo API client. + pub zoo_client: kittycad::Client, /// The editor info is used to store information about the editor. pub editor_info: Arc>, /// The cache is used to store the results of previous requests. @@ -84,14 +84,18 @@ impl crate::lsp::backend::Backend for Backend { } } - fn current_code_map(&self) -> DashMap { + fn current_code_map(&self) -> DashMap> { self.current_code_map.clone() } - fn insert_current_code_map(&self, uri: String, text: String) { + fn insert_current_code_map(&self, uri: String, text: Vec) { self.current_code_map.insert(uri, text); } + fn remove_from_code_map(&self, uri: String) -> Option<(String, Vec)> { + self.current_code_map.remove(&uri) + } + fn clear_code_state(&self) { self.current_code_map.clear(); } @@ -125,8 +129,8 @@ impl Backend { }), }; - let kc_client = kittycad::Client::new(&self.token); - let resp = kc_client + let resp = self + .zoo_client .ai() .create_kcl_code_completions(&body) .await diff --git a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs index 803a03229..0ff977500 100644 --- a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs +++ b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs @@ -62,13 +62,17 @@ pub struct Backend { /// AST maps. pub ast_map: DashMap, /// Current code. - pub current_code_map: DashMap, + pub current_code_map: DashMap>, /// Diagnostics. pub diagnostics_map: DashMap, /// Symbols map. pub symbols_map: DashMap>, /// Semantic tokens map. pub semantic_tokens_map: DashMap>, + /// The Zoo API client. + pub zoo_client: kittycad::Client, + /// If we can send telemetry for this user. + pub can_send_telemetry: bool, } // Implement the shared backend trait for the language server. @@ -98,14 +102,18 @@ impl crate::lsp::backend::Backend for Backend { } } - fn current_code_map(&self) -> DashMap { + fn current_code_map(&self) -> DashMap> { self.current_code_map.clone() } - fn insert_current_code_map(&self, uri: String, text: String) { + fn insert_current_code_map(&self, uri: String, text: Vec) { self.current_code_map.insert(uri, text); } + fn remove_from_code_map(&self, uri: String) -> Option<(String, Vec)> { + self.current_code_map.remove(&uri) + } + fn clear_code_state(&self) { self.current_code_map.clear(); self.token_map.clear(); @@ -403,8 +411,11 @@ impl LanguageServer for Backend { let Some(current_code) = self.current_code_map.get(&filename) else { return Ok(None); }; + let Ok(current_code) = std::str::from_utf8(¤t_code) else { + return Ok(None); + }; - let pos = position_to_char_index(params.text_document_position_params.position, ¤t_code); + 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) else { @@ -415,7 +426,7 @@ impl LanguageServer for Backend { return Ok(None); }; - let Some(hover) = value.get_hover_value_for_position(pos, ¤t_code) else { + let Some(hover) = value.get_hover_value_for_position(pos, current_code) else { return Ok(None); }; @@ -517,8 +528,11 @@ impl LanguageServer for Backend { let Some(current_code) = self.current_code_map.get(&filename) else { return Ok(None); }; + let Ok(current_code) = std::str::from_utf8(¤t_code) else { + return Ok(None); + }; - let pos = position_to_char_index(params.text_document_position_params.position, ¤t_code); + 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) else { @@ -529,7 +543,7 @@ impl LanguageServer for Backend { return Ok(None); }; - let Some(hover) = value.get_hover_value_for_position(pos, ¤t_code) else { + let Some(hover) = value.get_hover_value_for_position(pos, current_code) else { return Ok(None); }; @@ -595,11 +609,14 @@ impl LanguageServer for Backend { let Some(current_code) = self.current_code_map.get(&filename) else { return Ok(None); }; + let Ok(current_code) = std::str::from_utf8(¤t_code) else { + return Ok(None); + }; // Parse the ast. // I don't know if we need to do this again since it should be updated in the context. // But I figure better safe than sorry since this will write back out to the file. - let tokens = crate::token::lexer(¤t_code); + let tokens = crate::token::lexer(current_code); let parser = crate::parser::Parser::new(tokens); let Ok(ast) = parser.ast() else { return Ok(None); @@ -614,7 +631,7 @@ impl LanguageServer for Backend { 0, ); let source_range = SourceRange([0, current_code.len() - 1]); - let range = source_range.to_lsp_range(¤t_code); + let range = source_range.to_lsp_range(current_code); Ok(Some(vec![TextEdit { new_text: recast, range, @@ -627,24 +644,27 @@ impl LanguageServer for Backend { let Some(current_code) = self.current_code_map.get(&filename) else { return Ok(None); }; + let Ok(current_code) = std::str::from_utf8(¤t_code) else { + return Ok(None); + }; // Parse the ast. // I don't know if we need to do this again since it should be updated in the context. // But I figure better safe than sorry since this will write back out to the file. - let tokens = crate::token::lexer(¤t_code); + let tokens = crate::token::lexer(current_code); let parser = crate::parser::Parser::new(tokens); let Ok(mut ast) = parser.ast() else { return Ok(None); }; // Let's convert the position to a character index. - let pos = position_to_char_index(params.text_document_position.position, ¤t_code); + let pos = position_to_char_index(params.text_document_position.position, current_code); // Now let's perform the rename on the ast. ast.rename_symbol(¶ms.new_name, pos); // Now recast it. let recast = ast.recast(&Default::default(), 0); let source_range = SourceRange([0, current_code.len() - 1]); - let range = source_range.to_lsp_range(¤t_code); + let range = source_range.to_lsp_range(current_code); Ok(Some(WorkspaceEdit { changes: Some(HashMap::from([( params.text_document_position.text_document.uri, diff --git a/src/wasm-lib/kcl/src/wasm/mod.rs b/src/wasm-lib/kcl/src/wasm/mod.rs new file mode 100644 index 000000000..11ee2e7e3 --- /dev/null +++ b/src/wasm-lib/kcl/src/wasm/mod.rs @@ -0,0 +1,27 @@ +///! Web assembly utils. +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +/// A JsFuture that implements Send and Sync. +pub struct JsFuture(pub wasm_bindgen_futures::JsFuture); + +// Safety: WebAssembly will only ever run in a single-threaded context. +unsafe impl Send for JsFuture {} +unsafe impl Sync for JsFuture {} + +impl std::future::Future for JsFuture { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let mut pinned: Pin<&mut wasm_bindgen_futures::JsFuture> = Pin::new(&mut self.get_mut().0); + pinned.as_mut().poll(cx) + } +} + +impl From for JsFuture { + fn from(promise: js_sys::Promise) -> JsFuture { + JsFuture(wasm_bindgen_futures::JsFuture::from(promise)) + } +} diff --git a/src/wasm-lib/src/wasm.rs b/src/wasm-lib/src/wasm.rs index 1acfb5cbb..49fc18fe9 100644 --- a/src/wasm-lib/src/wasm.rs +++ b/src/wasm-lib/src/wasm.rs @@ -167,7 +167,7 @@ impl ServerConfig { // NOTE: input needs to be an AsyncIterator specifically #[wasm_bindgen] -pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> { +pub async fn kcl_lsp_run(config: ServerConfig, token: String, is_dev: bool) -> Result<(), JsValue> { console_error_panic_hook::set_once(); let ServerConfig { @@ -183,6 +183,28 @@ pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> { // we have a test for it. let token_types = kcl_lib::token::TokenType::all_semantic_token_types().unwrap(); + let mut zoo_client = kittycad::Client::new(token); + if is_dev { + zoo_client.set_base_url("https://api.dev.zoo.dev"); + } + // Check if we can send telememtry for this user. + let privacy_settings = match zoo_client.users().get_privacy_settings().await { + Ok(privacy_settings) => privacy_settings, + Err(err) => { + // In the case of dev we don't always have a sub set, but prod we should. + if err + .to_string() + .contains("The modeling app subscription type is missing.") + { + kittycad::types::PrivacySettings { + can_train_on_data: true, + } + } else { + return Err(err.to_string().into()); + } + } + }; + let (service, socket) = LspService::new(|client| kcl_lib::lsp::kcl::Backend { client, fs: kcl_lib::fs::FileManager::new(fs), @@ -196,6 +218,8 @@ pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> { diagnostics_map: Default::default(), symbols_map: Default::default(), semantic_tokens_map: Default::default(), + zoo_client, + can_send_telemetry: privacy_settings.can_train_on_data, }); let input = wasm_bindgen_futures::stream::JsStream::from(into_server); @@ -226,7 +250,7 @@ pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> { // NOTE: input needs to be an AsyncIterator specifically #[wasm_bindgen] -pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(), JsValue> { +pub async fn copilot_lsp_run(config: ServerConfig, token: String, is_dev: bool) -> Result<(), JsValue> { console_error_panic_hook::set_once(); let ServerConfig { @@ -235,6 +259,11 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(), fs, } = config; + let mut zoo_client = kittycad::Client::new(token); + if is_dev { + zoo_client.set_base_url("https://api.dev.zoo.dev"); + } + let (service, socket) = LspService::build(|client| kcl_lib::lsp::copilot::Backend { client, fs: kcl_lib::fs::FileManager::new(fs), @@ -242,7 +271,7 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(), current_code_map: Default::default(), editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())), cache: kcl_lib::lsp::copilot::cache::CopilotCache::new(), - token, + zoo_client, }) .custom_method("setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info) .custom_method(