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})`
}
// 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)}`)

View File

@ -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}

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">
<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,

View File

@ -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) {

View File

@ -39,7 +39,7 @@ export default class Server {
}
await copilotLspRun(config, token)
} 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 { 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.
}
}

View File

@ -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",

View File

@ -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 }

View File

@ -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,

View File

@ -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,

View File

@ -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],

View File

@ -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(

View File

@ -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,

View File

@ -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,

View File

@ -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],

View File

@ -14,3 +14,5 @@ pub mod lsp;
pub mod parser;
pub mod std;
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.
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) {

View File

@ -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

View File

@ -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(&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 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, &current_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(&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 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, &current_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(&current_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(&current_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(&current_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(&current_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(&current_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, &current_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(&params.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(&current_code);
let range = source_range.to_lsp_range(current_code);
Ok(Some(WorkspaceEdit {
changes: Some(HashMap::from([(
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
#[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(