diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index 7f3c31934..e5402146b 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -2,7 +2,6 @@ import { test, expect } from '@playwright/test' import { getUtils } from './test-utils' import waitOn from 'wait-on' import { roundOff } from 'lib/utils' -import * as TOML from '@iarna/toml' import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { secrets } from './secrets' import { @@ -11,6 +10,7 @@ import { TEST_SETTINGS_CORRUPTED, TEST_SETTINGS_ONBOARDING, } from './storageStates' +import * as TOML from '@iarna/toml' /* debug helper: unfortunately we do rely on exact coord mouse clicks in a few places diff --git a/package.json b/package.json index 0b13b7fbb..417a5f4a4 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@headlessui/react": "^1.7.18", "@headlessui/tailwindcss": "^0.2.0", - "@iarna/toml": "^2.2.5", "@kittycad/lib": "^0.0.56", "@lezer/javascript": "^1.4.9", "@open-rpc/client-js": "^1.8.1", @@ -55,7 +54,6 @@ "sketch-helpers": "^0.0.4", "swr": "^2.2.5", "three": "^0.163.0", - "toml": "^3.0.0", "ts-node": "^10.9.2", "typescript": "^5.4.5", "ua-parser-js": "^1.0.37", @@ -117,6 +115,7 @@ "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-env": "^7.24.3", + "@iarna/toml": "^2.2.5", "@playwright/test": "^1.43.0", "@tauri-apps/cli": "^2.0.0-beta.13", "@types/crypto-js": "^4.2.2", diff --git a/src/components/LspProvider.tsx b/src/components/LspProvider.tsx index d83a6d183..43bd0b4ed 100644 --- a/src/components/LspProvider.tsx +++ b/src/components/LspProvider.tsx @@ -2,9 +2,8 @@ import { LanguageServerClient } from 'editor/plugins/lsp' import type * as LSP from 'vscode-languageserver-protocol' import React, { createContext, useMemo, useEffect, useContext } from 'react' import { FromServer, IntoServer } from 'editor/plugins/lsp/codec' -import Server from '../editor/plugins/lsp/server' import Client from '../editor/plugins/lsp/client' -import { TEST } from 'env' +import { DEV, TEST } from 'env' import kclLanguage from 'editor/plugins/lsp/kcl/language' import { copilotPlugin } from 'editor/plugins/lsp/copilot' import { useStore } from 'useStore' @@ -15,6 +14,13 @@ import { useNavigate } from 'react-router-dom' import { paths } from 'lib/paths' import { FileEntry } from 'lib/types' import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator' +import Worker from 'editor/plugins/lsp/worker.ts?worker' +import { + LspWorkerEventType, + KclWorkerOptions, + CopilotWorkerOptions, + LspWorker, +} from 'editor/plugins/lsp/types' const DEFAULT_FILE_NAME: string = 'main.kcl' @@ -87,20 +93,34 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { // But the server happens async so we break this into two parts. // Below is the client and server promise. const { lspClient: kclLspClient } = useMemo(() => { - const intoServer: IntoServer = new IntoServer() - const fromServer: FromServer = FromServer.create() - const client = new Client(fromServer, intoServer) - if (!TEST) { - Server.initialize(intoServer, fromServer).then((lspServer) => { - lspServer.start('kcl', token) - setIsKclLspServerReady(true) - }) + if (!token || token === '' || TEST) { + return { lspClient: null } } - const lspClient = new LanguageServerClient({ client, name: 'kcl' }) + const lspWorker = new Worker({ name: 'kcl' }) + const initEvent: KclWorkerOptions = { + token: token, + baseUnit: defaultUnit.current, + devMode: DEV, + } + lspWorker.postMessage({ + worker: LspWorker.Kcl, + eventType: LspWorkerEventType.Init, + eventData: initEvent, + }) + lspWorker.onmessage = function (e) { + fromServer.add(e.data) + } + + const intoServer: IntoServer = new IntoServer(LspWorker.Kcl, lspWorker) + const fromServer: FromServer = FromServer.create() + const client = new Client(fromServer, intoServer) + + setIsKclLspServerReady(true) + + const lspClient = new LanguageServerClient({ client, name: LspWorker.Kcl }) return { lspClient } }, [ - setIsKclLspServerReady, // We need a token for authenticating the server. token, ]) @@ -112,7 +132,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { // We do not want to restart the server, its just wasteful. const kclLSP = useMemo(() => { let plugin = null - if (isKclLspServerReady && !TEST) { + if (isKclLspServerReady && !TEST && kclLspClient) { // Set up the lsp plugin. const lsp = kclLanguage({ documentUri: `file:///${DEFAULT_FILE_NAME}`, @@ -127,10 +147,12 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { // Re-execute the scene when the units change. useEffect(() => { - let plugins = kclLspClient.plugins - for (let plugin of plugins) { - if (plugin.updateUnits && isStreamReady && isNetworkOkay) { - plugin.updateUnits(defaultUnit.current) + if (kclLspClient) { + let plugins = kclLspClient.plugins + for (let plugin of plugins) { + if (plugin.updateUnits && isStreamReady && isNetworkOkay) { + plugin.updateUnits(defaultUnit.current) + } } } }, [ @@ -145,19 +167,36 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { ]) const { lspClient: copilotLspClient } = useMemo(() => { - const intoServer: IntoServer = new IntoServer() - const fromServer: FromServer = FromServer.create() - const client = new Client(fromServer, intoServer) - if (!TEST) { - Server.initialize(intoServer, fromServer).then((lspServer) => { - lspServer.start('copilot', token) - setIsCopilotLspServerReady(true) - }) + if (!token || token === '' || TEST) { + return { lspClient: null } } - const lspClient = new LanguageServerClient({ client, name: 'copilot' }) + const lspWorker = new Worker({ name: 'copilot' }) + const initEvent: CopilotWorkerOptions = { + token: token, + devMode: DEV, + } + lspWorker.postMessage({ + worker: LspWorker.Copilot, + eventType: LspWorkerEventType.Init, + eventData: initEvent, + }) + lspWorker.onmessage = function (e) { + fromServer.add(e.data) + } + + const intoServer: IntoServer = new IntoServer(LspWorker.Copilot, lspWorker) + const fromServer: FromServer = FromServer.create() + const client = new Client(fromServer, intoServer) + + setIsCopilotLspServerReady(true) + + const lspClient = new LanguageServerClient({ + client, + name: LspWorker.Copilot, + }) return { lspClient } - }, [setIsCopilotLspServerReady, token]) + }, [token]) // Here we initialize the plugin which will start the client. // When we have multi-file support the name of the file will be a dep of @@ -166,7 +205,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { // We do not want to restart the server, its just wasteful. const copilotLSP = useMemo(() => { let plugin = null - if (isCopilotLspServerReady && !TEST) { + if (isCopilotLspServerReady && !TEST && copilotLspClient) { // Set up the lsp plugin. const lsp = copilotPlugin({ documentUri: `file:///${DEFAULT_FILE_NAME}`, @@ -180,7 +219,13 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { return plugin }, [copilotLspClient, isCopilotLspServerReady]) - const lspClients = [kclLspClient, copilotLspClient] + let lspClients: LanguageServerClient[] = [] + if (kclLspClient) { + lspClients.push(kclLspClient) + } + if (copilotLspClient) { + lspClients.push(copilotLspClient) + } const onProjectClose = ( file: FileEntry | null, diff --git a/src/editor/plugins/lsp/client.ts b/src/editor/plugins/lsp/client.ts index 9b542faac..fd82f82c1 100644 --- a/src/editor/plugins/lsp/client.ts +++ b/src/editor/plugins/lsp/client.ts @@ -75,7 +75,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient { intoServer.enqueue(encoded) if (null != json.id) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const response = await fromServer.responses.get(json.id)! + const response = await fromServer.responses.get(json.id) this.client.receive(response as jsrpc.JSONRPCResponse) } }) diff --git a/src/editor/plugins/lsp/codec.ts b/src/editor/plugins/lsp/codec.ts index 2a9353d34..e30d6aac4 100644 --- a/src/editor/plugins/lsp/codec.ts +++ b/src/editor/plugins/lsp/codec.ts @@ -6,6 +6,7 @@ import StreamDemuxer from './codec/demuxer' import Headers from './codec/headers' import Queue from './codec/queue' import Tracer from './tracer' +import { LspWorkerEventType, LspWorker } from './types' export const encoder = new TextEncoder() export const decoder = new TextDecoder() @@ -31,9 +32,26 @@ export class IntoServer extends Queue implements AsyncGenerator { + private worker: Worker | null = null + private type_: LspWorker | null = null + constructor(type_?: LspWorker, worker?: Worker) { + super() + if (worker && type_) { + this.worker = worker + this.type_ = type_ + } + } enqueue(item: Uint8Array): void { Tracer.client(Headers.remove(decoder.decode(item))) - super.enqueue(item) + if (this.worker) { + this.worker.postMessage({ + worker: this.type_, + eventType: LspWorkerEventType.Call, + eventData: item, + }) + } else { + super.enqueue(item) + } } } @@ -43,6 +61,8 @@ export interface FromServer extends WritableStream { } readonly notifications: AsyncGenerator readonly requests: AsyncGenerator + + add(item: Uint8Array): void } // eslint-disable-next-line @typescript-eslint/no-namespace diff --git a/src/editor/plugins/lsp/codec/demuxer.ts b/src/editor/plugins/lsp/codec/demuxer.ts index 7920c0a55..d4fcbab0b 100644 --- a/src/editor/plugins/lsp/codec/demuxer.ts +++ b/src/editor/plugins/lsp/codec/demuxer.ts @@ -4,6 +4,7 @@ import Bytes from './bytes' import PromiseMap from './map' import Queue from './queue' import Tracer from '../tracer' +import { Codec } from '../codec' export default class StreamDemuxer extends Queue { readonly responses: PromiseMap = @@ -79,4 +80,20 @@ export default class StreamDemuxer extends Queue { } } } + + add(bytes: Uint8Array): void { + const message = Codec.decode(bytes) as vsrpc.Message + Tracer.server(message) + + // demux the message stream + if (vsrpc.Message.isResponse(message) && null != message.id) { + this.responses.set(message.id, message) + } + if (vsrpc.Message.isNotification(message)) { + this.notifications.enqueue(message) + } + if (vsrpc.Message.isRequest(message)) { + this.requests.enqueue(message) + } + } } diff --git a/src/editor/plugins/lsp/index.ts b/src/editor/plugins/lsp/index.ts index 39f0c7b48..cf1050491 100644 --- a/src/editor/plugins/lsp/index.ts +++ b/src/editor/plugins/lsp/index.ts @@ -10,6 +10,7 @@ import { UpdateUnitsParams } from 'wasm-lib/kcl/bindings/UpdateUnitsParams' import { UpdateCanExecuteParams } from 'wasm-lib/kcl/bindings/UpdateCanExecuteParams' import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse' import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse' +import { LspWorker } from './types' // https://microsoft.github.io/language-server-protocol/specifications/specification-current/ @@ -54,7 +55,7 @@ interface LSPNotifyMap { export interface LanguageServerClientOptions { client: Client - name: string + name: LspWorker } export interface LanguageServerOptions { @@ -217,7 +218,8 @@ export class LanguageServerClient { if (!serverCapabilities.completionProvider) { return } - return await this.request('textDocument/completion', params) + const response = await this.request('textDocument/completion', params) + return response } attachPlugin(plugin: LanguageServerPlugin) { diff --git a/src/editor/plugins/lsp/plugin.ts b/src/editor/plugins/lsp/plugin.ts index 49ba74f5a..05c2ac625 100644 --- a/src/editor/plugins/lsp/plugin.ts +++ b/src/editor/plugins/lsp/plugin.ts @@ -27,7 +27,6 @@ import { posToOffset } from 'editor/plugins/lsp/util' import { Program, ProgramMemory } from 'lang/wasm' import { kclManager } from 'lib/singletons' import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength' -import { lspDiagnosticsToKclErrors } from 'lang/errors' import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse' import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse' @@ -403,7 +402,9 @@ export class LanguageServerPlugin implements PluginValue { // The server has updated the AST, we should update elsewhere. let updatedAst = notification.params as Program console.log('[lsp]: Updated AST', updatedAst) - kclManager.ast = updatedAst + // Since we aren't using the lsp server for executing the program + // we don't update the ast here. + //kclManager.ast = updatedAst // Update the folding ranges, since the AST has changed. // This is a hack since codemirror does not support async foldService. diff --git a/src/editor/plugins/lsp/server.ts b/src/editor/plugins/lsp/server.ts deleted file mode 100644 index 443f75f9e..000000000 --- a/src/editor/plugins/lsp/server.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { InitOutput, ServerConfig } from 'wasm-lib/pkg/wasm_lib' -import { FromServer, IntoServer } from './codec' -import { fileSystemManager } from 'lang/std/fileSystemManager' -import { copilotLspRun, initPromise, kclLspRun } from 'lang/wasm' -import { engineCommandManager } from 'lib/singletons' - -export default class Server { - readonly initOutput: InitOutput - readonly #intoServer: IntoServer - readonly #fromServer: FromServer - - private constructor( - initOutput: InitOutput, - intoServer: IntoServer, - fromServer: FromServer - ) { - this.initOutput = initOutput - this.#intoServer = intoServer - this.#fromServer = fromServer - } - - static async initialize( - intoServer: IntoServer, - fromServer: FromServer - ): Promise { - const initOutput = await initPromise - const server = new Server(initOutput, intoServer, fromServer) - return server - } - - async start(type_: 'kcl' | 'copilot', token?: string): Promise { - const config = new ServerConfig( - this.#intoServer, - this.#fromServer, - fileSystemManager - ) - if (!token || token === '') { - throw new Error( - type_ + ': auth token is required for lsp server to start' - ) - } - if (type_ === 'copilot') { - await copilotLspRun(config, token) - } else if (type_ === 'kcl') { - await kclLspRun(config, engineCommandManager, token || '') - } - } -} diff --git a/src/editor/plugins/lsp/types.ts b/src/editor/plugins/lsp/types.ts new file mode 100644 index 000000000..6f13bd9a0 --- /dev/null +++ b/src/editor/plugins/lsp/types.ts @@ -0,0 +1,27 @@ +import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength' + +export enum LspWorker { + Kcl = 'kcl', + Copilot = 'copilot', +} +export interface KclWorkerOptions { + token: string + baseUnit: UnitLength + devMode: boolean +} + +export interface CopilotWorkerOptions { + token: string + devMode: boolean +} + +export enum LspWorkerEventType { + Init = 'init', + Call = 'call', +} + +export interface LspWorkerEvent { + eventType: LspWorkerEventType + eventData: Uint8Array | KclWorkerOptions | CopilotWorkerOptions + worker: LspWorker +} diff --git a/src/editor/plugins/lsp/worker.ts b/src/editor/plugins/lsp/worker.ts new file mode 100644 index 000000000..cf227fa85 --- /dev/null +++ b/src/editor/plugins/lsp/worker.ts @@ -0,0 +1,142 @@ +import { Codec, FromServer, IntoServer } from 'editor/plugins/lsp/codec' +import { fileSystemManager } from 'lang/std/fileSystemManager' +import init, { + ServerConfig, + copilot_lsp_run, + kcl_lsp_run, +} from 'wasm-lib/pkg/wasm_lib' +import * as jsrpc from 'json-rpc-2.0' +import { + LspWorkerEventType, + LspWorkerEvent, + LspWorker, + KclWorkerOptions, + CopilotWorkerOptions, +} from 'editor/plugins/lsp/types' +import { EngineCommandManager } from 'lang/std/engineConnection' + +const intoServer: IntoServer = new IntoServer() +const fromServer: FromServer = FromServer.create() + +export const wasmUrl = () => { + const baseUrl = + typeof window === 'undefined' + ? 'http://localhost:3000' + : window.location.origin.includes('tauri://localhost') + ? 'tauri://localhost' // custom protocol for macOS + : window.location.origin.includes('tauri.localhost') + ? 'http://tauri.localhost' // fallback for Windows + : window.location.origin.includes('localhost') + ? 'http://localhost:3000' + : window.location.origin && window.location.origin !== 'null' + ? window.location.origin + : 'http://localhost:3000' + const fullUrl = baseUrl + '/wasm_lib_bg.wasm' + console.log(`Worker full URL for WASM: ${fullUrl}`) + + return fullUrl +} + +// Initialise the wasm module. +const initialise = async () => { + const fullUrl = wasmUrl() + const input = await fetch(fullUrl) + const buffer = await input.arrayBuffer() + return init(buffer) +} + +export async function copilotLspRun( + config: ServerConfig, + token: string, + devMode: boolean = false +) { + try { + console.log('starting copilot lsp') + await copilot_lsp_run(config, token, devMode) + } catch (e: any) { + console.log('copilot lsp failed', e) + // We can't restart here because a moved value, we should do this another way. + } +} + +export async function kclLspRun( + config: ServerConfig, + engineCommandManager: EngineCommandManager | null, + token: string, + baseUnit: string, + devMode: boolean = false +) { + try { + console.log('start kcl lsp') + await kcl_lsp_run(config, engineCommandManager, baseUnit, token, devMode) + } catch (e: any) { + console.log('kcl lsp failed', e) + // We can't restart here because a moved value, we should do this another way. + } +} + +onmessage = function (event) { + const { worker, eventType, eventData }: LspWorkerEvent = event.data + + switch (eventType) { + case LspWorkerEventType.Init: + initialise() + .then((instantiatedModule) => { + console.log('Worker: WASM module loaded', worker, instantiatedModule) + const config = new ServerConfig( + intoServer, + fromServer, + fileSystemManager + ) + console.log('Starting worker', worker) + switch (worker) { + case LspWorker.Kcl: + const kclData = eventData as KclWorkerOptions + kclLspRun( + config, + null, + kclData.token, + kclData.baseUnit, + kclData.devMode + ) + break + case LspWorker.Copilot: + let copilotData = eventData as CopilotWorkerOptions + copilotLspRun(config, copilotData.token, copilotData.devMode) + break + } + }) + .catch((error) => { + console.error('Worker: Error loading wasm module', worker, error) + }) + break + case LspWorkerEventType.Call: + const data = eventData as Uint8Array + intoServer.enqueue(data) + const json: jsrpc.JSONRPCRequest = Codec.decode(data) + if (null != json.id) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + fromServer.responses.get(json.id)!.then((response) => { + const encoded = Codec.encode(response as jsrpc.JSONRPCResponse) + postMessage(encoded) + }) + } + break + default: + console.error('Worker: Unknown message type', worker, eventType) + } +} + +new Promise(async (resolve) => { + for await (const requests of fromServer.requests) { + const encoded = Codec.encode(requests as jsrpc.JSONRPCRequest) + postMessage(encoded) + } +}) + +new Promise(async (resolve) => { + for await (const notification of fromServer.notifications) { + const encoded = Codec.encode(notification as jsrpc.JSONRPCRequest) + postMessage(encoded) + } +}) diff --git a/src/lang/std/engineConnection.ts b/src/lang/std/engineConnection.ts index 87353533c..8bc30e0fc 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -611,7 +611,6 @@ class EngineConnection { `Error in response to request ${message.request_id}:\n${errorsString} failed cmd type was ${artifactThatFailed?.commandType}` ) - console.log(artifactThatFailed) } else { console.error(`Error from server:\n${errorsString}`) } @@ -1178,7 +1177,6 @@ export class EngineCommandManager { command?.commandType === 'solid3d_get_extrusion_face_info' && modelingResponse.type === 'solid3d_get_extrusion_face_info' ) { - console.log('modelingResposne', modelingResponse) const parent = this.artifactMap[command?.parentId || ''] modelingResponse.data.faces.forEach((face) => { if (face.cap !== 'none' && face.face_id && parent) { diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index b69a6ed51..32653b2b0 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -7,11 +7,10 @@ import init, { is_points_ccw, get_tangential_arc_to_info, program_memory_init, - ServerConfig, - copilot_lsp_run, - kcl_lsp_run, make_default_planes, coredump, + toml_stringify, + toml_parse, } from '../wasm-lib/pkg/wasm_lib' import { KCLError } from './errors' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' @@ -22,7 +21,6 @@ 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' import { AppInfo } from 'wasm-lib/kcl/bindings/AppInfo' import { CoreDumpManager } from 'lib/coredump' import openWindow from 'lib/openWindow' @@ -76,8 +74,7 @@ export type { ExtrudeGroup } from '../wasm-lib/kcl/bindings/ExtrudeGroup' export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem' export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface' -// Initialise the wasm module. -const initialise = async () => { +export const wasmUrl = () => { const baseUrl = typeof window === 'undefined' ? 'http://127.0.0.1:3000' @@ -92,6 +89,13 @@ const initialise = async () => { : 'http://localhost:3000' const fullUrl = baseUrl + '/wasm_lib_bg.wasm' console.log(`Full URL for WASM: ${fullUrl}`) + + return fullUrl +} + +// Initialise the wasm module. +const initialise = async () => { + const fullUrl = wasmUrl() const input = await fetch(fullUrl) const buffer = await input.arrayBuffer() return init(buffer) @@ -313,32 +317,6 @@ export function programMemoryInit(): ProgramMemory { } } -export async function copilotLspRun(config: ServerConfig, token: string) { - try { - console.log('starting copilot lsp') - await copilot_lsp_run(config, token, DEV) - } catch (e: any) { - console.log('copilot lsp failed', e) - // We can't restart here because a moved value, we should do this another way. - } -} - -export async function kclLspRun( - config: ServerConfig, - engineCommandManager: EngineCommandManager, - token: string -) { - try { - console.log('start kcl lsp') - const baseUnit = - (await getSettingsState)()?.modeling.defaultUnit.current || 'mm' - await kcl_lsp_run(config, engineCommandManager, baseUnit, token, DEV) - } catch (e: any) { - console.log('kcl lsp failed', e) - // We can't restart here because a moved value, we should do this another way. - } -} - export async function coreDump( coreDumpManager: CoreDumpManager, openGithubIssue: boolean = false @@ -353,3 +331,21 @@ export async function coreDump( throw new Error(`Error getting core dump: ${e}`) } } + +export function tomlStringify(toml: any): string { + try { + const s: string = toml_stringify(JSON.stringify(toml)) + return s + } catch (e: any) { + throw new Error(`Error stringifying toml: ${e}`) + } +} + +export function tomlParse(toml: string): any { + try { + const parsed: any = toml_parse(toml) + return parsed + } catch (e: any) { + throw new Error(`Error parsing toml: ${e}`) + } +} diff --git a/src/lib/settings/settingsUtils.ts b/src/lib/settings/settingsUtils.ts index 8fc25f2b0..dfcacff9a 100644 --- a/src/lib/settings/settingsUtils.ts +++ b/src/lib/settings/settingsUtils.ts @@ -6,8 +6,8 @@ import { import { Setting, createSettings, settings } from 'lib/settings/initialSettings' import { SaveSettingsPayload, SettingsLevel } from './settingsTypes' import { isTauri } from 'lib/isTauri' -import * as TOML from '@iarna/toml' import { remove, writeTextFile, exists } from '@tauri-apps/plugin-fs' +import { initPromise, tomlParse, tomlStringify } from 'lang/wasm' /** * We expect the settings to be stored in a TOML file @@ -19,7 +19,7 @@ import { remove, writeTextFile, exists } from '@tauri-apps/plugin-fs' function getSettingsFromStorage(path: string) { return isTauri() ? readSettingsFile(path) - : (TOML.parse(localStorage.getItem(path) ?? '') + : (tomlParse(localStorage.getItem(path) ?? '') .settings as Partial) } @@ -31,6 +31,7 @@ export async function loadAndValidateSettings(projectPath?: string) { // Load the settings from the files if (settingsFilePaths.user) { + await initPromise const userSettings = await getSettingsFromStorage(settingsFilePaths.user) if (userSettings) { setSettingsAtLevel(settings, 'user', userSettings) @@ -77,16 +78,17 @@ async function writeOrClearPersistedSettings( settingsFilePath: string, changedSettings: Partial ) { + await initPromise if (changedSettings && Object.keys(changedSettings).length) { if (isTauri()) { await writeTextFile( settingsFilePath, - TOML.stringify({ settings: changedSettings }) + tomlStringify({ settings: changedSettings }) ) } localStorage.setItem( settingsFilePath, - TOML.stringify({ settings: changedSettings }) + tomlStringify({ settings: changedSettings }) ) } else { if (isTauri() && (await exists(settingsFilePath))) { diff --git a/src/lib/tauriFS.ts b/src/lib/tauriFS.ts index dab1b741f..b190002bf 100644 --- a/src/lib/tauriFS.ts +++ b/src/lib/tauriFS.ts @@ -25,7 +25,7 @@ import { SETTINGS_FILE_EXT, } from 'lib/constants' import { SaveSettingsPayload, SettingsLevel } from './settings/settingsTypes' -import * as TOML from '@iarna/toml' +import { initPromise, tomlParse } from 'lang/wasm' type PathWithPossibleError = { path: string | null @@ -395,9 +395,10 @@ export async function readSettingsFile( } try { + await initPromise const settings = await readTextFile(path) // We expect the settings to be under a top-level [settings] key - return TOML.parse(settings).settings as Partial + return tomlParse(settings).settings as Partial } catch (e) { console.error('Error reading settings file:', e) return {} diff --git a/src/machines/authMachine.ts b/src/machines/authMachine.ts index 7c767ae0b..a3fbc1399 100644 --- a/src/machines/authMachine.ts +++ b/src/machines/authMachine.ts @@ -134,27 +134,14 @@ async function getUser(context: UserContext) { token: context.token, hostname: VITE_KC_API_BASE_URL, }).catch((err) => console.error('error from Tauri getUser', err)) - const tokenPromise = !isTauri() - ? fetch(withBaseURL('/user/api-tokens?limit=1'), { - method: 'GET', - credentials: 'include', - headers, - }) - .then(async (res) => { - const result: Models['ApiTokenResultsPage_type'] = await res.json() - return result.items[0].token - }) - .catch((err) => console.error('error from Browser getUser', err)) - : context.token const user = await userPromise - const token = await tokenPromise if ('error_code' in user) throw new Error(user.message) return { user, - token, + token: context.token, } } diff --git a/src/wasm-lib/Cargo.lock b/src/wasm-lib/Cargo.lock index ca32512f5..7777114f5 100644 --- a/src/wasm-lib/Cargo.lock +++ b/src/wasm-lib/Cargo.lock @@ -4657,6 +4657,7 @@ dependencies = [ "reqwest", "serde_json", "tokio", + "toml", "tower-lsp", "twenty-twenty", "uuid", diff --git a/src/wasm-lib/Cargo.toml b/src/wasm-lib/Cargo.toml index 33667227f..5a1e6773d 100644 --- a/src/wasm-lib/Cargo.toml +++ b/src/wasm-lib/Cargo.toml @@ -16,6 +16,7 @@ kcl-lib = { path = "kcl" } kittycad = { workspace = true } serde_json = "1.0.115" tokio = { version = "1.37.0", features = ["sync"] } +toml = "0.8.12" uuid = { version = "1.8.0", features = ["v4", "js", "serde"] } wasm-bindgen = "0.2.91" wasm-bindgen-futures = "0.4.42" diff --git a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs index f8c531b7e..90824a9ce 100644 --- a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs +++ b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs @@ -240,7 +240,7 @@ impl crate::lsp::backend::Backend for Backend { } // Send the notification to the client that the ast was updated. - if self.can_execute().await { + if self.can_execute().await || self.executor_ctx().await.is_none() { // Only send the notification if we can execute. // Otherwise it confuses the client. self.client diff --git a/src/wasm-lib/src/lib.rs b/src/wasm-lib/src/lib.rs index ca3c34b8e..3c02a56d5 100644 --- a/src/wasm-lib/src/lib.rs +++ b/src/wasm-lib/src/lib.rs @@ -1,7 +1,11 @@ //! Wasm bindings for `kcl`. +#[cfg(target_arch = "wasm32")] +mod toml; #[cfg(target_arch = "wasm32")] mod wasm; +#[cfg(target_arch = "wasm32")] +pub use toml::*; #[cfg(target_arch = "wasm32")] pub use wasm::*; diff --git a/src/wasm-lib/src/toml.rs b/src/wasm-lib/src/toml.rs new file mode 100644 index 000000000..3e609588c --- /dev/null +++ b/src/wasm-lib/src/toml.rs @@ -0,0 +1,25 @@ +//! Functions for interacting with TOML files. +//! We do this in rust because the Javascript TOML libraries are actual trash. + +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + +#[wasm_bindgen] +pub fn toml_parse(s: &str) -> Result { + console_error_panic_hook::set_once(); + + let value: toml::Value = toml::from_str(s).map_err(|e| e.to_string())?; + + // The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the + // gloo-serialize crate instead. + JsValue::from_serde(&value).map_err(|e| e.to_string()) +} + +#[wasm_bindgen] +pub fn toml_stringify(json: &str) -> Result { + console_error_panic_hook::set_once(); + + let value: serde_json::Value = serde_json::from_str(json).map_err(|e| e.to_string())?; + + toml::to_string_pretty(&value).map_err(|e| e.to_string()) +} diff --git a/src/wasm-lib/src/wasm.rs b/src/wasm-lib/src/wasm.rs index 79d812077..0082f8644 100644 --- a/src/wasm-lib/src/wasm.rs +++ b/src/wasm-lib/src/wasm.rs @@ -192,7 +192,7 @@ impl ServerConfig { #[wasm_bindgen] pub async fn kcl_lsp_run( config: ServerConfig, - engine_manager: kcl_lib::engine::conn_wasm::EngineCommandManager, + engine_manager: Option, units: &str, token: String, is_dev: bool, @@ -219,17 +219,20 @@ pub async fn kcl_lsp_run( let file_manager = Arc::new(kcl_lib::fs::FileManager::new(fs)); - let units = kittycad::types::UnitLength::from_str(units).map_err(|e| e.to_string())?; - let engine = kcl_lib::engine::conn_wasm::EngineConnection::new(engine_manager) - .await - .map_err(|e| format!("{:?}", e))?; - // Turn off lsp execute for now - let _executor_ctx = kcl_lib::executor::ExecutorContext { - engine: Arc::new(Box::new(engine)), - fs: file_manager.clone(), - stdlib: std::sync::Arc::new(stdlib), - units, - is_mock: false, + let executor_ctx = if let Some(engine_manager) = engine_manager { + let units = kittycad::types::UnitLength::from_str(units).map_err(|e| e.to_string())?; + let engine = kcl_lib::engine::conn_wasm::EngineConnection::new(engine_manager) + .await + .map_err(|e| format!("{:?}", e))?; + Some(kcl_lib::executor::ExecutorContext { + engine: Arc::new(Box::new(engine)), + fs: file_manager.clone(), + stdlib: std::sync::Arc::new(stdlib), + units, + is_mock: false, + }) + } else { + None }; // Check if we can send telememtry for this user. @@ -266,8 +269,8 @@ pub async fn kcl_lsp_run( semantic_tokens_map: Default::default(), zoo_client, can_send_telemetry: privacy_settings.can_train_on_data, - executor_ctx: Default::default(), - can_execute: Default::default(), + can_execute: Arc::new(tokio::sync::RwLock::new(executor_ctx.is_some())), + executor_ctx: Arc::new(tokio::sync::RwLock::new(executor_ctx)), is_initialized: Default::default(), current_handle: Default::default(), diff --git a/vite.config.ts b/vite.config.ts index eb2d87be9..61927d378 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,15 +5,12 @@ import dns from 'dns' import { defineConfig, configDefaults } from 'vitest/config' import version from 'vite-plugin-package-version' -// Only needed because we run Node < 17 +// Only needed because we run Node < 17 // and we want to open `localhost` not `127.0.0.1` on server start // reference: https://vitejs.dev/config/server-options.html#server-host dns.setDefaultResultOrder('verbatim') const config = defineConfig({ - define: { - global: 'window', - }, server: { open: true, port: 3000, @@ -49,6 +46,11 @@ const config = defineConfig({ eslint(), version(), ], + worker: { + plugins: () => [ + viteTsconfigPaths(), + ], + } }) export default config diff --git a/yarn.lock b/yarn.lock index d3e1f62a0..a518b0038 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8166,7 +8166,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8239,7 +8248,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8488,11 +8504,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toml@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" - integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== - tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -9221,7 +9232,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -9239,6 +9250,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"