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:
@ -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)}`)
|
||||
|
@ -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 (
|
||||
<LspStateContext.Provider
|
||||
value={{
|
||||
@ -177,6 +286,11 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
kclLSP,
|
||||
onProjectClose,
|
||||
onProjectOpen,
|
||||
onFileOpen,
|
||||
onFileClose,
|
||||
onFileCreate,
|
||||
onFileRename,
|
||||
onFileDelete,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -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">
|
||||
<Link
|
||||
onClick={() => {
|
||||
onProjectClose(file || null, false)
|
||||
onProjectClose(file || null, project?.path || null, false)
|
||||
}}
|
||||
to={paths.HOME}
|
||||
className="group"
|
||||
@ -39,7 +39,7 @@ const ProjectSidebarMenu = ({
|
||||
<>
|
||||
<Link
|
||||
onClick={() => {
|
||||
onProjectClose(file || null, false)
|
||||
onProjectClose(file || null, project?.path || null, false)
|
||||
}}
|
||||
to={paths.HOME}
|
||||
className="!no-underline"
|
||||
@ -163,7 +163,7 @@ function ProjectMenuPopover({
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
onProjectClose(file || null, true)
|
||||
onProjectClose(file || null, project?.path || null, true)
|
||||
}}
|
||||
icon={{
|
||||
icon: faHome,
|
||||
|
@ -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) {
|
||||
|
@ -39,7 +39,7 @@ export default class Server {
|
||||
}
|
||||
await copilotLspRun(config, token)
|
||||
} else if (type_ === 'kcl') {
|
||||
await kclLspRun(config)
|
||||
await kclLspRun(config, token || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
20
src/wasm-lib/Cargo.lock
generated
20
src/wasm-lib/Cargo.lock
generated
@ -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",
|
||||
|
@ -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 }
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -27,6 +27,10 @@ pub struct EngineConnection {
|
||||
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 {
|
||||
pub async fn new(manager: EngineCommandManager) -> Result<EngineConnection, JsValue> {
|
||||
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],
|
||||
|
@ -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(
|
||||
|
@ -22,9 +22,9 @@ impl Default for FileManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
#[async_trait::async_trait]
|
||||
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,
|
||||
path: P,
|
||||
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,
|
||||
path: P,
|
||||
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,
|
||||
path: P,
|
||||
source_range: crate::executor::SourceRange,
|
||||
|
@ -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<P: AsRef<std::path::Path>>(
|
||||
async fn read<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
|
||||
&self,
|
||||
path: P,
|
||||
source_range: crate::executor::SourceRange,
|
||||
) -> Result<Vec<u8>, crate::errors::KclError>;
|
||||
|
||||
/// 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,
|
||||
path: P,
|
||||
source_range: crate::executor::SourceRange,
|
||||
) -> Result<bool, crate::errors::KclError>;
|
||||
|
||||
/// 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,
|
||||
path: P,
|
||||
source_range: crate::executor::SourceRange,
|
||||
|
@ -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<P: AsRef<std::path::Path>>(
|
||||
async fn read<P: AsRef<std::path::Path> + 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<P: AsRef<std::path::Path>>(
|
||||
async fn exists<P: AsRef<std::path::Path> + 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<P: AsRef<std::path::Path>>(
|
||||
async fn get_all_files<P: AsRef<std::path::Path> + 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],
|
||||
|
@ -14,3 +14,5 @@ pub mod lsp;
|
||||
pub mod parser;
|
||||
pub mod std;
|
||||
pub mod token;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm;
|
||||
|
@ -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<WorkspaceFolder>);
|
||||
|
||||
/// 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.
|
||||
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.
|
||||
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<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) {
|
||||
@ -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) {
|
||||
|
@ -48,9 +48,9 @@ pub struct Backend {
|
||||
/// The workspace folders.
|
||||
pub workspace_folders: DashMap<String, WorkspaceFolder>,
|
||||
/// Current code.
|
||||
pub current_code_map: DashMap<String, String>,
|
||||
/// The token is used to authenticate requests to the API server.
|
||||
pub token: String,
|
||||
pub current_code_map: DashMap<String, Vec<u8>>,
|
||||
/// The Zoo API client.
|
||||
pub zoo_client: kittycad::Client,
|
||||
/// The editor info is used to store information about the editor.
|
||||
pub editor_info: Arc<RwLock<CopilotEditorInfo>>,
|
||||
/// The cache is used to store the results of previous requests.
|
||||
@ -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()
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
fn remove_from_code_map(&self, uri: String) -> Option<(String, Vec<u8>)> {
|
||||
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
|
||||
|
@ -62,13 +62,17 @@ pub struct Backend {
|
||||
/// AST maps.
|
||||
pub ast_map: DashMap<String, crate::ast::types::Program>,
|
||||
/// Current code.
|
||||
pub current_code_map: DashMap<String, String>,
|
||||
pub current_code_map: DashMap<String, Vec<u8>>,
|
||||
/// Diagnostics.
|
||||
pub diagnostics_map: DashMap<String, DocumentDiagnosticReport>,
|
||||
/// Symbols map.
|
||||
pub symbols_map: DashMap<String, Vec<DocumentSymbol>>,
|
||||
/// Semantic tokens map.
|
||||
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.
|
||||
@ -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()
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
fn remove_from_code_map(&self, uri: String) -> Option<(String, Vec<u8>)> {
|
||||
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,
|
||||
|
27
src/wasm-lib/kcl/src/wasm/mod.rs
Normal file
27
src/wasm-lib/kcl/src/wasm/mod.rs
Normal 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))
|
||||
}
|
||||
}
|
@ -167,7 +167,7 @@ impl ServerConfig {
|
||||
|
||||
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> 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<Uint8Array, never, void> 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(
|
||||
|
Reference in New Issue
Block a user