More lsp stuff / telemetry-prep (#1694)

* more text document stuff

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* backend for rename and create etc

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates for functions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* js future

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* utils

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup send and sync shit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* save the client

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* store the users privacy settings

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* bump version

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2024-03-12 13:37:47 -07:00
committed by GitHub
parent 73b7d3cc9d
commit 8b2bf00641
21 changed files with 358 additions and 102 deletions

View File

@ -21,14 +21,6 @@ function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})` 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({ function RenameForm({
fileOrDir, fileOrDir,
setIsRenaming, setIsRenaming,
@ -156,7 +148,7 @@ const FileTreeItem = ({
level?: number level?: number
}) => { }) => {
const { send, context } = useFileContext() const { send, context } = useFileContext()
const { lspClients } = useLspContext() const { onFileOpen, onFileClose } = useLspContext()
const navigate = useNavigate() const navigate = useNavigate()
const [isRenaming, setIsRenaming] = useState(false) const [isRenaming, setIsRenaming] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
@ -185,26 +177,8 @@ const FileTreeItem = ({
) )
} else { } else {
// Let the lsp servers know we closed a file. // Let the lsp servers know we closed a file.
const currentFilePath = basename(currentFile?.path || 'main.kcl') onFileClose(currentFile?.path || null, project?.path || null)
lspClients.forEach((lspClient) => { onFileOpen(fileOrDir.path, project?.path || null)
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: '',
},
})
})
// Open kcl files // Open kcl files
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`) navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)

View File

@ -12,24 +12,46 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Extension } from '@codemirror/state' import { Extension } from '@codemirror/state'
import { LanguageSupport } from '@codemirror/language' import { LanguageSupport } from '@codemirror/language'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { basename } from './FileTree'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { FileEntry } from '@tauri-apps/api/fs' import { FileEntry } from '@tauri-apps/api/fs'
import { ProjectWithEntryPointMetadata } from 'lib/types' import { ProjectWithEntryPointMetadata } from 'lib/types'
const DEFAULT_FILE_NAME: string = 'main.kcl'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] 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 = { type LspContext = {
lspClients: LanguageServerClient[] lspClients: LanguageServerClient[]
copilotLSP: Extension | null copilotLSP: Extension | null
kclLSP: LanguageSupport | null kclLSP: LanguageSupport | null
onProjectClose: (file: FileEntry | null, redirect: boolean) => void onProjectClose: (
file: FileEntry | null,
projectPath: string | null,
redirect: boolean
) => void
onProjectOpen: ( onProjectOpen: (
project: ProjectWithEntryPointMetadata | null, project: ProjectWithEntryPointMetadata | null,
file: FileEntry | null file: FileEntry | null
) => void ) => 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) export const LspStateContext = createContext({} as LspContext)
@ -58,7 +80,8 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const client = new Client(fromServer, intoServer) const client = new Client(fromServer, intoServer)
if (!TEST) { if (!TEST) {
Server.initialize(intoServer, fromServer).then((lspServer) => { Server.initialize(intoServer, fromServer).then((lspServer) => {
lspServer.start('kcl') const token = auth?.context?.token
lspServer.start('kcl', token)
setIsKclLspServerReady(true) setIsKclLspServerReady(true)
}) })
} }
@ -77,7 +100,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
if (isKclLspServerReady && !TEST) { if (isKclLspServerReady && !TEST) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = kclLanguage({ const lsp = kclLanguage({
documentUri: `file:///main.kcl`, documentUri: `file:///${DEFAULT_FILE_NAME}`,
workspaceFolders: getWorkspaceFolders(), workspaceFolders: getWorkspaceFolders(),
client: kclLspClient, client: kclLspClient,
}) })
@ -113,7 +136,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
if (isCopilotLspServerReady && !TEST) { if (isCopilotLspServerReady && !TEST) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = copilotPlugin({ const lsp = copilotPlugin({
documentUri: `file:///main.kcl`, documentUri: `file:///${DEFAULT_FILE_NAME}`,
workspaceFolders: getWorkspaceFolders(), workspaceFolders: getWorkspaceFolders(),
client: copilotLspClient, client: copilotLspClient,
allowHTMLContent: true, allowHTMLContent: true,
@ -126,8 +149,15 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const lspClients = [kclLspClient, copilotLspClient] const lspClients = [kclLspClient, copilotLspClient]
const onProjectClose = (file: FileEntry | null, redirect: boolean) => { const onProjectClose = (
const currentFilePath = basename(file?.name || 'main.kcl') file: FileEntry | null,
projectPath: string | null,
redirect: boolean
) => {
const currentFilePath = projectBasename(
file?.path || DEFAULT_FILE_NAME,
projectPath || ''
)
lspClients.forEach((lspClient) => { lspClients.forEach((lspClient) => {
lspClient.textDocumentDidClose({ lspClient.textDocumentDidClose({
textDocument: { textDocument: {
@ -155,7 +185,10 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
}) })
if (file) { if (file) {
// Send that the file was opened. // 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) => { lspClients.forEach((lspClient) => {
lspClient.textDocumentDidOpen({ lspClient.textDocumentDidOpen({
textDocument: { 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 ( return (
<LspStateContext.Provider <LspStateContext.Provider
value={{ value={{
@ -177,6 +286,11 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
kclLSP, kclLSP,
onProjectClose, onProjectClose,
onProjectOpen, onProjectOpen,
onFileOpen,
onFileClose,
onFileCreate,
onFileRename,
onFileDelete,
}} }}
> >
{children} {children}

View File

@ -28,7 +28,7 @@ const ProjectSidebarMenu = ({
<div className="rounded-sm !no-underline h-9 mr-auto max-h-min min-w-max border-0 py-1 px-2 flex items-center gap-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50 dark:hover:bg-chalkboard-90"> <div className="rounded-sm !no-underline h-9 mr-auto max-h-min min-w-max border-0 py-1 px-2 flex items-center gap-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50 dark:hover:bg-chalkboard-90">
<Link <Link
onClick={() => { onClick={() => {
onProjectClose(file || null, false) onProjectClose(file || null, project?.path || null, false)
}} }}
to={paths.HOME} to={paths.HOME}
className="group" className="group"
@ -39,7 +39,7 @@ const ProjectSidebarMenu = ({
<> <>
<Link <Link
onClick={() => { onClick={() => {
onProjectClose(file || null, false) onProjectClose(file || null, project?.path || null, false)
}} }}
to={paths.HOME} to={paths.HOME}
className="!no-underline" className="!no-underline"
@ -163,7 +163,7 @@ function ProjectMenuPopover({
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => { onClick={() => {
onProjectClose(file || null, true) onProjectClose(file || null, project?.path || null, true)
}} }}
icon={{ icon={{
icon: faHome, icon: faHome,

View File

@ -90,6 +90,9 @@ interface LSPNotifyMap {
'textDocument/didOpen': LSP.DidOpenTextDocumentParams 'textDocument/didOpen': LSP.DidOpenTextDocumentParams
'textDocument/didClose': LSP.DidCloseTextDocumentParams 'textDocument/didClose': LSP.DidCloseTextDocumentParams
'workspace/didChangeWorkspaceFolders': LSP.DidChangeWorkspaceFoldersParams 'workspace/didChangeWorkspaceFolders': LSP.DidChangeWorkspaceFoldersParams
'workspace/didCreateFiles': LSP.CreateFilesParams
'workspace/didRenameFiles': LSP.RenameFilesParams
'workspace/didDeleteFiles': LSP.DeleteFilesParams
} }
export interface LanguageServerClientOptions { 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) { async updateSemanticTokens(uri: string) {
const serverCapabilities = this.getServerCapabilities() const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.semanticTokensProvider) { if (!serverCapabilities.semanticTokensProvider) {

View File

@ -39,7 +39,7 @@ export default class Server {
} }
await copilotLspRun(config, token) await copilotLspRun(config, token)
} else if (type_ === 'kcl') { } else if (type_ === 'kcl') {
await kclLspRun(config) await kclLspRun(config, token || '')
} }
} }
} }

View File

@ -20,6 +20,7 @@ import type { Program } from '../wasm-lib/kcl/bindings/Program'
import type { Token } from '../wasm-lib/kcl/bindings/Token' import type { Token } from '../wasm-lib/kcl/bindings/Token'
import { Coords2d } from './std/sketch' import { Coords2d } from './std/sketch'
import { fileSystemManager } from 'lang/std/fileSystemManager' import { fileSystemManager } from 'lang/std/fileSystemManager'
import { DEV } from 'env'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value' 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) { export async function copilotLspRun(config: ServerConfig, token: string) {
try { try {
console.log('starting copilot lsp') console.log('starting copilot lsp')
await copilot_lsp_run(config, token) await copilot_lsp_run(config, token, DEV)
} catch (e: any) { } catch (e: any) {
console.log('copilot lsp failed', e) console.log('copilot lsp failed', e)
// We make it restart recursively so that if it ever dies after like // We can't restart here because a moved value, we should do this another way.
// 8 hours or something it will come back to life.
await copilotLspRun(config, token)
} }
} }
export async function kclLspRun(config: ServerConfig) { export async function kclLspRun(config: ServerConfig, token: string) {
try { try {
console.log('start kcl lsp') console.log('start kcl lsp')
await kcl_lsp_run(config) await kcl_lsp_run(config, token, DEV)
} catch (e: any) { } catch (e: any) {
console.log('kcl lsp failed', e) console.log('kcl lsp failed', e)
// We make it restart recursively so that if it ever dies after like // We can't restart here because a moved value, we should do this another way.
// 8 hours or something it will come back to life.
await kclLspRun(config)
} }
} }

View File

@ -962,6 +962,22 @@ dependencies = [
"syn 2.0.52", "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]] [[package]]
name = "diesel_derives" name = "diesel_derives"
version = "2.1.2" version = "2.1.2"
@ -1891,7 +1907,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.1.44" version = "0.1.45"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -1902,7 +1918,7 @@ dependencies = [
"criterion", "criterion",
"dashmap", "dashmap",
"databake", "databake",
"derive-docs", "derive-docs 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"expectorate", "expectorate",
"futures", "futures",
"gltf-json", "gltf-json",

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.1.44" version = "0.1.45"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" 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 } clap = { version = "4.5.2", features = ["cargo", "derive", "env", "unicode"], optional = true }
dashmap = "5.5.3" dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] } databake = { version = "0.1.7", features = ["derive"] }
#derive-docs = { version = "0.1.10" } derive-docs = { version = "0.1.10" }
derive-docs = { path = "../derive-docs" } #derive-docs = { path = "../derive-docs" }
futures = { version = "0.3.30" } futures = { version = "0.3.30" }
gltf-json = "1.4.0" gltf-json = "1.4.0"
kittycad = { workspace = true } kittycad = { workspace = true }

View File

@ -158,7 +158,7 @@ impl EngineConnection {
} }
} }
#[async_trait::async_trait(?Send)] #[async_trait::async_trait]
impl EngineManager for EngineConnection { impl EngineManager for EngineConnection {
async fn send_modeling_cmd( async fn send_modeling_cmd(
&self, &self,

View File

@ -15,7 +15,7 @@ impl EngineConnection {
} }
} }
#[async_trait::async_trait(?Send)] #[async_trait::async_trait]
impl crate::engine::EngineManager for EngineConnection { impl crate::engine::EngineManager for EngineConnection {
async fn send_modeling_cmd( async fn send_modeling_cmd(
&self, &self,

View File

@ -27,6 +27,10 @@ pub struct EngineConnection {
manager: Arc<EngineCommandManager>, manager: Arc<EngineCommandManager>,
} }
// Safety: WebAssembly will only ever run in a single-threaded context.
unsafe impl Send for EngineConnection {}
unsafe impl Sync for EngineConnection {}
impl EngineConnection { impl EngineConnection {
pub async fn new(manager: EngineCommandManager) -> Result<EngineConnection, JsValue> { pub async fn new(manager: EngineCommandManager) -> Result<EngineConnection, JsValue> {
Ok(EngineConnection { Ok(EngineConnection {
@ -35,7 +39,7 @@ impl EngineConnection {
} }
} }
#[async_trait::async_trait(?Send)] #[async_trait::async_trait]
impl crate::engine::EngineManager for EngineConnection { impl crate::engine::EngineManager for EngineConnection {
async fn send_modeling_cmd( async fn send_modeling_cmd(
&self, &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 { KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from engine: {:?}", e), message: format!("Failed to wait for promise from engine: {:?}", e),
source_ranges: vec![source_range], source_ranges: vec![source_range],

View File

@ -31,7 +31,7 @@ use anyhow::Result;
#[cfg(not(test))] #[cfg(not(test))]
pub use conn_mock::EngineConnection; pub use conn_mock::EngineConnection;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait]
pub trait EngineManager: Clone { pub trait EngineManager: Clone {
/// Send a modeling command and wait for the response message. /// Send a modeling command and wait for the response message.
async fn send_modeling_cmd( async fn send_modeling_cmd(

View File

@ -22,9 +22,9 @@ impl Default for FileManager {
} }
} }
#[async_trait::async_trait(?Send)] #[async_trait::async_trait]
impl FileSystem for FileManager { impl FileSystem for FileManager {
async fn read<P: AsRef<std::path::Path>>( async fn read<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,
path: P, path: P,
source_range: crate::executor::SourceRange, source_range: crate::executor::SourceRange,
@ -37,7 +37,7 @@ impl FileSystem for FileManager {
}) })
} }
async fn exists<P: AsRef<std::path::Path>>( async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,
path: P, path: P,
source_range: crate::executor::SourceRange, source_range: crate::executor::SourceRange,
@ -54,7 +54,7 @@ impl FileSystem for FileManager {
}) })
} }
async fn get_all_files<P: AsRef<std::path::Path>>( async fn get_all_files<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,
path: P, path: P,
source_range: crate::executor::SourceRange, source_range: crate::executor::SourceRange,

View File

@ -13,24 +13,24 @@ use anyhow::Result;
#[cfg(not(test))] #[cfg(not(test))]
pub use wasm::FileManager; pub use wasm::FileManager;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait]
pub trait FileSystem: Clone { pub trait FileSystem: Clone {
/// Read a file from the local file system. /// Read a file from the local file system.
async fn read<P: AsRef<std::path::Path>>( async fn read<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,
path: P, path: P,
source_range: crate::executor::SourceRange, source_range: crate::executor::SourceRange,
) -> Result<Vec<u8>, crate::errors::KclError>; ) -> Result<Vec<u8>, crate::errors::KclError>;
/// Check if a file exists on the local file system. /// Check if a file exists on the local file system.
async fn exists<P: AsRef<std::path::Path>>( async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,
path: P, path: P,
source_range: crate::executor::SourceRange, source_range: crate::executor::SourceRange,
) -> Result<bool, crate::errors::KclError>; ) -> Result<bool, crate::errors::KclError>;
/// Get all the files in a directory recursively. /// Get all the files in a directory recursively.
async fn get_all_files<P: AsRef<std::path::Path>>( async fn get_all_files<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,
path: P, path: P,
source_range: crate::executor::SourceRange, source_range: crate::executor::SourceRange,

View File

@ -6,6 +6,7 @@ use wasm_bindgen::prelude::wasm_bindgen;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
fs::FileSystem, fs::FileSystem,
wasm::JsFuture,
}; };
#[wasm_bindgen(module = "/../../lang/std/fileSystemManager.ts")] #[wasm_bindgen(module = "/../../lang/std/fileSystemManager.ts")]
@ -37,9 +38,9 @@ impl FileManager {
unsafe impl Send for FileManager {} unsafe impl Send for FileManager {}
unsafe impl Sync for FileManager {} unsafe impl Sync for FileManager {}
#[async_trait::async_trait(?Send)] #[async_trait::async_trait]
impl FileSystem for FileManager { impl FileSystem for FileManager {
async fn read<P: AsRef<std::path::Path>>( async fn read<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,
path: P, path: P,
source_range: crate::executor::SourceRange, 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 { KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from engine: {:?}", e), message: format!("Failed to wait for promise from engine: {:?}", e),
source_ranges: vec![source_range], source_ranges: vec![source_range],
@ -77,7 +78,7 @@ impl FileSystem for FileManager {
Ok(bytes) Ok(bytes)
} }
async fn exists<P: AsRef<std::path::Path>>( async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,
path: P, path: P,
source_range: crate::executor::SourceRange, 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 { KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from engine: {:?}", e), message: format!("Failed to wait for promise from engine: {:?}", e),
source_ranges: vec![source_range], source_ranges: vec![source_range],
@ -119,7 +120,7 @@ impl FileSystem for FileManager {
Ok(it_exists) Ok(it_exists)
} }
async fn get_all_files<P: AsRef<std::path::Path>>( async fn get_all_files<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,
path: P, path: P,
source_range: crate::executor::SourceRange, 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 { KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from javascript: {:?}", e), message: format!("Failed to wait for promise from javascript: {:?}", e),
source_ranges: vec![source_range], source_ranges: vec![source_range],

View File

@ -14,3 +14,5 @@ pub mod lsp;
pub mod parser; pub mod parser;
pub mod std; pub mod std;
pub mod token; pub mod token;
#[cfg(target_arch = "wasm32")]
pub mod wasm;

View File

@ -1,5 +1,6 @@
//! A shared backend trait for lsp servers memory and behavior. //! A shared backend trait for lsp servers memory and behavior.
use anyhow::Result;
use dashmap::DashMap; use dashmap::DashMap;
use tower_lsp::lsp_types::{ use tower_lsp::lsp_types::{
CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams, CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,
@ -8,6 +9,8 @@ use tower_lsp::lsp_types::{
TextDocumentItem, WorkspaceFolder, TextDocumentItem, WorkspaceFolder,
}; };
use crate::fs::FileSystem;
/// A trait for the backend of the language server. /// A trait for the backend of the language server.
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait Backend { pub trait Backend {
@ -22,10 +25,13 @@ pub trait Backend {
fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>); fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>);
/// Get the current code map. /// Get the current code map.
fn current_code_map(&self) -> DashMap<String, String>; fn current_code_map(&self) -> DashMap<String, Vec<u8>>;
/// Insert a new code map. /// 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<u8>);
// Remove from code map.
fn remove_from_code_map(&self, uri: String) -> Option<(String, Vec<u8>)>;
/// Clear the current code state. /// Clear the current code state.
fn clear_code_state(&self); fn clear_code_state(&self);
@ -34,8 +40,25 @@ pub trait Backend {
async fn on_change(&self, params: TextDocumentItem); async fn on_change(&self, params: TextDocumentItem);
async fn update_memory(&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.as_bytes().to_vec());
self.insert_current_code_map(params.uri.to_string(), params.text.clone()); }
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_current_code_map(file_path, contents);
}
Ok(())
} }
async fn do_initialized(&self, params: InitializedParams) { async fn do_initialized(&self, params: InitializedParams) {
@ -52,11 +75,23 @@ pub trait Backend {
} }
async fn do_did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { 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); self.remove_workspace_folders(params.event.removed);
// Remove the code from the current code map. // 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. // We do this since it means the user is changing projects so let's refresh the state.
self.clear_code_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) { async fn do_did_change_configuration(&self, params: DidChangeConfigurationParams) {
@ -75,18 +110,36 @@ pub trait Backend {
self.client() self.client()
.log_message(MessageType::INFO, format!("files created: {:?}", params)) .log_message(MessageType::INFO, format!("files created: {:?}", params))
.await; .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) { async fn do_did_rename_files(&self, params: RenameFilesParams) {
self.client() self.client()
.log_message(MessageType::INFO, format!("files renamed: {:?}", params)) .log_message(MessageType::INFO, format!("files renamed: {:?}", params))
.await; .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) { async fn do_did_delete_files(&self, params: DeleteFilesParams) {
self.client() self.client()
.log_message(MessageType::INFO, format!("files deleted: {:?}", params)) .log_message(MessageType::INFO, format!("files deleted: {:?}", params))
.await; .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) { async fn do_did_open(&self, params: DidOpenTextDocumentParams) {

View File

@ -48,9 +48,9 @@ pub struct Backend {
/// The workspace folders. /// The workspace folders.
pub workspace_folders: DashMap<String, WorkspaceFolder>, pub workspace_folders: DashMap<String, WorkspaceFolder>,
/// Current code. /// Current code.
pub current_code_map: DashMap<String, String>, pub current_code_map: DashMap<String, Vec<u8>>,
/// The token is used to authenticate requests to the API server. /// The Zoo API client.
pub token: String, pub zoo_client: kittycad::Client,
/// The editor info is used to store information about the editor. /// The editor info is used to store information about the editor.
pub editor_info: Arc<RwLock<CopilotEditorInfo>>, pub editor_info: Arc<RwLock<CopilotEditorInfo>>,
/// The cache is used to store the results of previous requests. /// 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<String, String> { fn current_code_map(&self) -> DashMap<String, Vec<u8>> {
self.current_code_map.clone() 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<u8>) {
self.current_code_map.insert(uri, text); self.current_code_map.insert(uri, text);
} }
fn remove_from_code_map(&self, uri: String) -> Option<(String, Vec<u8>)> {
self.current_code_map.remove(&uri)
}
fn clear_code_state(&self) { fn clear_code_state(&self) {
self.current_code_map.clear(); self.current_code_map.clear();
} }
@ -125,8 +129,8 @@ impl Backend {
}), }),
}; };
let kc_client = kittycad::Client::new(&self.token); let resp = self
let resp = kc_client .zoo_client
.ai() .ai()
.create_kcl_code_completions(&body) .create_kcl_code_completions(&body)
.await .await

View File

@ -62,13 +62,17 @@ pub struct Backend {
/// AST maps. /// AST maps.
pub ast_map: DashMap<String, crate::ast::types::Program>, pub ast_map: DashMap<String, crate::ast::types::Program>,
/// Current code. /// Current code.
pub current_code_map: DashMap<String, String>, pub current_code_map: DashMap<String, Vec<u8>>,
/// Diagnostics. /// Diagnostics.
pub diagnostics_map: DashMap<String, DocumentDiagnosticReport>, pub diagnostics_map: DashMap<String, DocumentDiagnosticReport>,
/// Symbols map. /// Symbols map.
pub symbols_map: DashMap<String, Vec<DocumentSymbol>>, pub symbols_map: DashMap<String, Vec<DocumentSymbol>>,
/// Semantic tokens map. /// Semantic tokens map.
pub semantic_tokens_map: DashMap<String, Vec<SemanticToken>>, pub semantic_tokens_map: DashMap<String, Vec<SemanticToken>>,
/// The Zoo API client.
pub zoo_client: kittycad::Client,
/// If we can send telemetry for this user.
pub can_send_telemetry: bool,
} }
// Implement the shared backend trait for the language server. // 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<String, String> { fn current_code_map(&self) -> DashMap<String, Vec<u8>> {
self.current_code_map.clone() 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<u8>) {
self.current_code_map.insert(uri, text); self.current_code_map.insert(uri, text);
} }
fn remove_from_code_map(&self, uri: String) -> Option<(String, Vec<u8>)> {
self.current_code_map.remove(&uri)
}
fn clear_code_state(&self) { fn clear_code_state(&self) {
self.current_code_map.clear(); self.current_code_map.clear();
self.token_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 { let Some(current_code) = self.current_code_map.get(&filename) else {
return Ok(None); return Ok(None);
}; };
let Ok(current_code) = std::str::from_utf8(&current_code) else {
return Ok(None);
};
let pos = position_to_char_index(params.text_document_position_params.position, &current_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's iterate over the AST and find the node that contains the cursor.
let Some(ast) = self.ast_map.get(&filename) else { let Some(ast) = self.ast_map.get(&filename) else {
@ -415,7 +426,7 @@ impl LanguageServer for Backend {
return Ok(None); return Ok(None);
}; };
let Some(hover) = value.get_hover_value_for_position(pos, &current_code) else { let Some(hover) = value.get_hover_value_for_position(pos, current_code) else {
return Ok(None); return Ok(None);
}; };
@ -517,8 +528,11 @@ impl LanguageServer for Backend {
let Some(current_code) = self.current_code_map.get(&filename) else { let Some(current_code) = self.current_code_map.get(&filename) else {
return Ok(None); return Ok(None);
}; };
let Ok(current_code) = std::str::from_utf8(&current_code) else {
return Ok(None);
};
let pos = position_to_char_index(params.text_document_position_params.position, &current_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's iterate over the AST and find the node that contains the cursor.
let Some(ast) = self.ast_map.get(&filename) else { let Some(ast) = self.ast_map.get(&filename) else {
@ -529,7 +543,7 @@ impl LanguageServer for Backend {
return Ok(None); return Ok(None);
}; };
let Some(hover) = value.get_hover_value_for_position(pos, &current_code) else { let Some(hover) = value.get_hover_value_for_position(pos, current_code) else {
return Ok(None); return Ok(None);
}; };
@ -595,11 +609,14 @@ impl LanguageServer for Backend {
let Some(current_code) = self.current_code_map.get(&filename) else { let Some(current_code) = self.current_code_map.get(&filename) else {
return Ok(None); return Ok(None);
}; };
let Ok(current_code) = std::str::from_utf8(&current_code) else {
return Ok(None);
};
// Parse the ast. // Parse the ast.
// I don't know if we need to do this again since it should be updated in the context. // 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. // But I figure better safe than sorry since this will write back out to the file.
let tokens = crate::token::lexer(&current_code); let tokens = crate::token::lexer(current_code);
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let Ok(ast) = parser.ast() else { let Ok(ast) = parser.ast() else {
return Ok(None); return Ok(None);
@ -614,7 +631,7 @@ impl LanguageServer for Backend {
0, 0,
); );
let source_range = SourceRange([0, current_code.len() - 1]); let source_range = SourceRange([0, current_code.len() - 1]);
let range = source_range.to_lsp_range(&current_code); let range = source_range.to_lsp_range(current_code);
Ok(Some(vec![TextEdit { Ok(Some(vec![TextEdit {
new_text: recast, new_text: recast,
range, range,
@ -627,24 +644,27 @@ impl LanguageServer for Backend {
let Some(current_code) = self.current_code_map.get(&filename) else { let Some(current_code) = self.current_code_map.get(&filename) else {
return Ok(None); return Ok(None);
}; };
let Ok(current_code) = std::str::from_utf8(&current_code) else {
return Ok(None);
};
// Parse the ast. // Parse the ast.
// I don't know if we need to do this again since it should be updated in the context. // 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. // But I figure better safe than sorry since this will write back out to the file.
let tokens = crate::token::lexer(&current_code); let tokens = crate::token::lexer(current_code);
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let Ok(mut ast) = parser.ast() else { let Ok(mut ast) = parser.ast() else {
return Ok(None); return Ok(None);
}; };
// Let's convert the position to a character index. // Let's convert the position to a character index.
let pos = position_to_char_index(params.text_document_position.position, &current_code); let pos = position_to_char_index(params.text_document_position.position, current_code);
// Now let's perform the rename on the ast. // Now let's perform the rename on the ast.
ast.rename_symbol(&params.new_name, pos); ast.rename_symbol(&params.new_name, pos);
// Now recast it. // Now recast it.
let recast = ast.recast(&Default::default(), 0); let recast = ast.recast(&Default::default(), 0);
let source_range = SourceRange([0, current_code.len() - 1]); let source_range = SourceRange([0, current_code.len() - 1]);
let range = source_range.to_lsp_range(&current_code); let range = source_range.to_lsp_range(current_code);
Ok(Some(WorkspaceEdit { Ok(Some(WorkspaceEdit {
changes: Some(HashMap::from([( changes: Some(HashMap::from([(
params.text_document_position.text_document.uri, params.text_document_position.text_document.uri,

View File

@ -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<wasm_bindgen::JsValue, wasm_bindgen::JsValue>;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let mut pinned: Pin<&mut wasm_bindgen_futures::JsFuture> = Pin::new(&mut self.get_mut().0);
pinned.as_mut().poll(cx)
}
}
impl From<js_sys::Promise> for JsFuture {
fn from(promise: js_sys::Promise) -> JsFuture {
JsFuture(wasm_bindgen_futures::JsFuture::from(promise))
}
}

View File

@ -167,7 +167,7 @@ impl ServerConfig {
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically // NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
#[wasm_bindgen] #[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(); console_error_panic_hook::set_once();
let ServerConfig { let ServerConfig {
@ -183,6 +183,28 @@ pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> {
// we have a test for it. // we have a test for it.
let token_types = kcl_lib::token::TokenType::all_semantic_token_types().unwrap(); 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 { let (service, socket) = LspService::new(|client| kcl_lib::lsp::kcl::Backend {
client, client,
fs: kcl_lib::fs::FileManager::new(fs), 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(), diagnostics_map: Default::default(),
symbols_map: Default::default(), symbols_map: Default::default(),
semantic_tokens_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); 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<Uint8Array, never, void> specifically // NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
#[wasm_bindgen] #[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(); console_error_panic_hook::set_once();
let ServerConfig { let ServerConfig {
@ -235,6 +259,11 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(),
fs, fs,
} = config; } = 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 { let (service, socket) = LspService::build(|client| kcl_lib::lsp::copilot::Backend {
client, client,
fs: kcl_lib::fs::FileManager::new(fs), 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(), current_code_map: Default::default(),
editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())), editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())),
cache: kcl_lib::lsp::copilot::cache::CopilotCache::new(), cache: kcl_lib::lsp::copilot::cache::CopilotCache::new(),
token, zoo_client,
}) })
.custom_method("setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info) .custom_method("setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info)
.custom_method( .custom_method(