Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run spxls in Web Worker #1189

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 34 additions & 24 deletions spx-gui/src/components/editor/code-editor/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { timeout, until, untilNotNull } from '@/utils/utils'
import { extname } from '@/utils/path'
import { toText } from '@/models/common/file'
import type { Project } from '@/models/project'
import wasmExecScriptUrl from '@/assets/wasm_exec.js?url'
import spxlsWasmUrl from '@/assets/spxls.wasm?url'
import {
fromLSPRange,
type DefinitionIdentifier,
Expand All @@ -15,38 +13,45 @@ import {
type TextDocumentIdentifier,
containsPosition
} from '../common'
import { Spxlc } from './spxls/client'
import type { Files as SpxlsFiles } from './spxls'
import { Spxlc, type IConnection } from './spxls/client'
import type { NotificationMessage, RequestMessage, ResponseMessage, Files as SpxlsFiles } from './spxls'
import { spxGetDefinitions, spxRenameResources } from './spxls/commands'
import {
type CompletionItem,
isDocumentLinkForResourceReference,
parseDocumentLinkForDefinition
} from './spxls/methods'
import type { IWorkerHandler } from './worker'

function loadScript(url: string) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.onload = resolve
script.onerror = reject
document.body.appendChild(script)
})
}

async function loadGoWasm(wasmUrl: string) {
await loadScript(wasmExecScriptUrl)
const go = new Go()
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), go.importObject)
go.run(instance)
/** Connection between LS client and server when the server runs in a Web Worker. */
class WorkerConnection implements IConnection {
private worker: IWorkerHandler
constructor() {
this.worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })
}
sendMessage(message: RequestMessage | NotificationMessage) {
this.worker.postMessage({ type: 'lsp', message })
}
onMessage(handler: (message: ResponseMessage | NotificationMessage) => void) {
this.worker.addEventListener('message', (event) => {
const message = event.data
handler(message.message)
})
}
sendFiles(files: SpxlsFiles): void {
this.worker.postMessage({ type: 'files', files })
}
dispose() {
this.worker.terminate()
}
}

export class SpxLSPClient extends Disposable {
constructor(private project: Project) {
super()
}

private files: SpxlsFiles = {}
private connection = new WorkerConnection()
private isFilesStale = shallowRef(true)
private spxlcRef = shallowRef<Spxlc | null>(null)

Expand All @@ -71,7 +76,7 @@ export class SpxLSPClient extends Disposable {
})
)
signal.throwIfAborted()
this.files = loadedFiles
this.connection.sendFiles(loadedFiles)
this.isFilesStale.value = false
}

Expand All @@ -87,10 +92,15 @@ export class SpxLSPClient extends Disposable {
return spxlc
}

async init() {
init() {
this.addDisposer(watchEffect((cleanUp) => this.loadFiles(getCleanupSignal(cleanUp))))
await loadGoWasm(spxlsWasmUrl)
this.spxlcRef.value = new Spxlc(() => this.files)
this.spxlcRef.value = new Spxlc(this.connection)
}

dispose() {
this.spxlcRef.value?.dispose()
this.connection.dispose()
super.dispose()
}

private async executeCommand<A extends any[], R>(command: string, ...args: A): Promise<R> {
Expand Down
74 changes: 74 additions & 0 deletions spx-gui/src/components/editor/code-editor/lsp/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* This file is worker script for spxls (spx language server).
* It runs in a Web Worker.
*/

declare const self: DedicatedWorkerGlobalScope

import '@/assets/wasm_exec.js'
import spxlsWasmUrl from '@/assets/spxls.wasm?url'
import type { Files, Message, NotificationMessage, RequestMessage, ResponseMessage, Spxls } from './spxls'

export type FilesMessage = {
type: 'files'
files: Files
}

export type LSPMessage<M extends Message> = {
type: 'lsp'
message: M
}

/** Message that worker send to main thread. */
export type WorkerMessage = LSPMessage<ResponseMessage | NotificationMessage>

/** Message that main thread send to worker. */
export type MainMessage = LSPMessage<RequestMessage | NotificationMessage> | FilesMessage

interface IWorkerScope {
postMessage(message: WorkerMessage): void
addEventListener(type: 'message', listener: (event: MessageEvent<MainMessage>) => void): void
}

export interface IWorkerHandler {
postMessage(message: MainMessage): void
addEventListener(type: 'message', listener: (event: MessageEvent<WorkerMessage>) => void): void
terminate(): void
}

const scope: IWorkerScope = self
const lsIniting = initLS()
let files: Files = {}

async function initLS(): Promise<Spxls> {
const go = new Go()
const { instance } = await WebAssembly.instantiateStreaming(fetch(spxlsWasmUrl), go.importObject)
go.run(instance)
const ls = NewSpxls(
() => files,
(message) => {
scope.postMessage({ type: 'lsp', message })
}
)
if (ls instanceof Error) throw ls
return ls
}

function main() {
scope.addEventListener('message', async (event) => {
const message = event.data
switch (message.type) {
case 'lsp': {
const ls = await lsIniting
const error = ls.handleMessage(message.message)
if (error != null) throw error
break
}
case 'files':
files = message.files
return
}
})
}

main()
8 changes: 7 additions & 1 deletion spx-gui/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
"@/*": [
"src/*"
]
}
},
"lib": [
"es2023",
"dom",
"dom.iterable",
"webworker"
]
}
}
25 changes: 14 additions & 11 deletions tools/spxls/client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { type Files, type NotificationMessage, type RequestMessage, type ResponseMessage, type ResponseError as ResponseErrorObj, type Spxls } from '.'
import { type NotificationMessage, type RequestMessage, type ResponseMessage, type ResponseError as ResponseErrorObj } from '.'

/** Connection between the client and the language server. */
export interface IConnection {
sendMessage(message: RequestMessage | NotificationMessage): void
onMessage(handler: (message: ResponseMessage | NotificationMessage) => void): void
}

/**
* Client wrapper for the spxls.
*/
export class Spxlc {
private ls: Spxls
private nextRequestId: number = 1
private pendingRequests = new Map<number, {
resolve: (response: any) => void
Expand All @@ -14,12 +19,10 @@ export class Spxlc {

/**
* Creates a new client instance.
* @param filesProvider Function that provides access to workspace files.
* @param connection The connection to the language server.
*/
constructor(filesProvider: () => Files) {
const ls = NewSpxls(filesProvider, this.handleMessage.bind(this))
if (ls instanceof Error) throw ls
this.ls = ls
constructor(private connection: IConnection) {
connection.onMessage(m => this.handleMessage(m))
}

/**
Expand Down Expand Up @@ -87,8 +90,9 @@ export class Spxlc {
params
}
this.pendingRequests.set(id, { resolve, reject })
const err = this.ls.handleMessage(message)
if (err != null) {
try {
this.connection.sendMessage(message)
} catch (err) {
reject(err)
this.pendingRequests.delete(id)
}
Expand Down Expand Up @@ -119,8 +123,7 @@ export class Spxlc {
method,
params
}
const err = this.ls.handleMessage(message)
if (err != null) throw err
this.connection.sendMessage(message)
}

/**
Expand Down
1 change: 0 additions & 1 deletion tools/spxls/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export interface Spxls {
handleMessage(message: RequestMessage | NotificationMessage): Error | null
}


declare global {
/**
* Creates a new instance of the spx language server.
Expand Down
Loading