Web workers for the lsp servers (#2136)
* put the lsps into a web worker Signed-off-by: Jess Frazelle <github@jessfraz.com> * remove extraneous logs Signed-off-by: Jess Frazelle <github@jessfraz.com> * remove trash toml lib Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * less logs Signed-off-by: Jess Frazelle <github@jessfraz.com> * less logs Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixups Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes for tests Signed-off-by: Jess Frazelle <github@jessfraz.com> * for playwright go back to the shitty lib Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
@ -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<Uint8Array>
|
||||
implements AsyncGenerator<Uint8Array, never, void>
|
||||
{
|
||||
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<Uint8Array> {
|
||||
}
|
||||
readonly notifications: AsyncGenerator<vsrpc.NotificationMessage, never, void>
|
||||
readonly requests: AsyncGenerator<vsrpc.RequestMessage, never, void>
|
||||
|
||||
add(item: Uint8Array): void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
|
@ -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<Uint8Array> {
|
||||
readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> =
|
||||
@ -79,4 +80,20 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
@ -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<Server> {
|
||||
const initOutput = await initPromise
|
||||
const server = new Server(initOutput, intoServer, fromServer)
|
||||
return server
|
||||
}
|
||||
|
||||
async start(type_: 'kcl' | 'copilot', token?: string): Promise<void> {
|
||||
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 || '')
|
||||
}
|
||||
}
|
||||
}
|
27
src/editor/plugins/lsp/types.ts
Normal file
27
src/editor/plugins/lsp/types.ts
Normal file
@ -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
|
||||
}
|
142
src/editor/plugins/lsp/worker.ts
Normal file
142
src/editor/plugins/lsp/worker.ts
Normal file
@ -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<void>(async (resolve) => {
|
||||
for await (const requests of fromServer.requests) {
|
||||
const encoded = Codec.encode(requests as jsrpc.JSONRPCRequest)
|
||||
postMessage(encoded)
|
||||
}
|
||||
})
|
||||
|
||||
new Promise<void>(async (resolve) => {
|
||||
for await (const notification of fromServer.notifications) {
|
||||
const encoded = Codec.encode(notification as jsrpc.JSONRPCRequest)
|
||||
postMessage(encoded)
|
||||
}
|
||||
})
|
@ -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) {
|
||||
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
@ -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<SaveSettingsPayload>)
|
||||
}
|
||||
|
||||
@ -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<SaveSettingsPayload>
|
||||
) {
|
||||
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))) {
|
||||
|
@ -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<SaveSettingsPayload>
|
||||
return tomlParse(settings).settings as Partial<SaveSettingsPayload>
|
||||
} catch (e) {
|
||||
console.error('Error reading settings file:', e)
|
||||
return {}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
1
src/wasm-lib/Cargo.lock
generated
1
src/wasm-lib/Cargo.lock
generated
@ -4657,6 +4657,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tower-lsp",
|
||||
"twenty-twenty",
|
||||
"uuid",
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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::*;
|
||||
|
25
src/wasm-lib/src/toml.rs
Normal file
25
src/wasm-lib/src/toml.rs
Normal file
@ -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<JsValue, String> {
|
||||
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<String, String> {
|
||||
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())
|
||||
}
|
@ -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<kcl_lib::engine::conn_wasm::EngineCommandManager>,
|
||||
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(),
|
||||
|
@ -11,9 +11,6 @@ import version from 'vite-plugin-package-version'
|
||||
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
|
||||
|
36
yarn.lock
36
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"
|
||||
|
Reference in New Issue
Block a user