Skip to content

Commit

Permalink
Run spxls in Web Worker
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca committed Dec 31, 2024
1 parent d5d5f13 commit c6628af
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 37 deletions.
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)
})
}
setFiles(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.setFiles(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 Spxls } from '.'
import { type NotificationMessage, type RequestMessage, type ResponseMessage } 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 @@ -85,8 +88,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 @@ -117,8 +121,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

0 comments on commit c6628af

Please sign in to comment.