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:
Jess Frazelle
2024-04-16 21:36:19 -07:00
committed by GitHub
parent 35c3103186
commit f0b9de2c1c
24 changed files with 412 additions and 167 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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)
}
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4657,6 +4657,7 @@ dependencies = [
"reqwest",
"serde_json",
"tokio",
"toml",
"tower-lsp",
"twenty-twenty",
"uuid",

View File

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

View File

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

View File

@ -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
View 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())
}

View File

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

View File

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

View File

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