Skip to content

Commit 4c7726d

Browse files
committed
Run spxls in Web Worker
1 parent 55bc295 commit 4c7726d

File tree

5 files changed

+130
-38
lines changed

5 files changed

+130
-38
lines changed

spx-gui/src/components/editor/code-editor/lsp/index.ts

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import { timeout, until, untilNotNull } from '@/utils/utils'
55
import { extname } from '@/utils/path'
66
import { toText } from '@/models/common/file'
77
import type { Project } from '@/models/project'
8-
import wasmExecScriptUrl from '@/assets/wasm_exec.js?url'
9-
import spxlsWasmUrl from '@/assets/spxls.wasm?url'
108
import {
119
fromLSPRange,
1210
type DefinitionIdentifier,
@@ -15,38 +13,45 @@ import {
1513
type TextDocumentIdentifier,
1614
containsPosition
1715
} from '../common'
18-
import { Spxlc } from './spxls/client'
19-
import type { Files as SpxlsFiles } from './spxls'
16+
import { Spxlc, type IConnection } from './spxls/client'
17+
import type { NotificationMessage, RequestMessage, ResponseMessage, Files as SpxlsFiles } from './spxls'
2018
import { spxGetDefinitions, spxRenameResources } from './spxls/commands'
2119
import {
2220
type CompletionItem,
2321
isDocumentLinkForResourceReference,
2422
parseDocumentLinkForDefinition
2523
} from './spxls/methods'
24+
import type { IWorkerHandler } from './worker'
2625

27-
function loadScript(url: string) {
28-
return new Promise((resolve, reject) => {
29-
const script = document.createElement('script')
30-
script.src = url
31-
script.onload = resolve
32-
script.onerror = reject
33-
document.body.appendChild(script)
34-
})
35-
}
36-
37-
async function loadGoWasm(wasmUrl: string) {
38-
await loadScript(wasmExecScriptUrl)
39-
const go = new Go()
40-
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), go.importObject)
41-
go.run(instance)
26+
/** Connection between LS client and server when the server runs in a Web Worker. */
27+
class WorkerConnection implements IConnection {
28+
private worker: IWorkerHandler
29+
constructor() {
30+
this.worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })
31+
}
32+
sendMessage(message: RequestMessage | NotificationMessage) {
33+
this.worker.postMessage({ type: 'lsp', message })
34+
}
35+
onMessage(handler: (message: ResponseMessage | NotificationMessage) => void) {
36+
this.worker.addEventListener('message', (event) => {
37+
const message = event.data
38+
handler(message.message)
39+
})
40+
}
41+
sendFiles(files: SpxlsFiles): void {
42+
this.worker.postMessage({ type: 'files', files })
43+
}
44+
dispose() {
45+
this.worker.terminate()
46+
}
4247
}
4348

4449
export class SpxLSPClient extends Disposable {
4550
constructor(private project: Project) {
4651
super()
4752
}
4853

49-
private files: SpxlsFiles = {}
54+
private connection = new WorkerConnection()
5055
private isFilesStale = shallowRef(true)
5156
private spxlcRef = shallowRef<Spxlc | null>(null)
5257

@@ -71,7 +76,7 @@ export class SpxLSPClient extends Disposable {
7176
})
7277
)
7378
signal.throwIfAborted()
74-
this.files = loadedFiles
79+
this.connection.sendFiles(loadedFiles)
7580
this.isFilesStale.value = false
7681
}
7782

@@ -87,10 +92,15 @@ export class SpxLSPClient extends Disposable {
8792
return spxlc
8893
}
8994

90-
async init() {
95+
init() {
9196
this.addDisposer(watchEffect((cleanUp) => this.loadFiles(getCleanupSignal(cleanUp))))
92-
await loadGoWasm(spxlsWasmUrl)
93-
this.spxlcRef.value = new Spxlc(() => this.files)
97+
this.spxlcRef.value = new Spxlc(this.connection)
98+
}
99+
100+
dispose() {
101+
this.spxlcRef.value?.dispose()
102+
this.connection.dispose()
103+
super.dispose()
94104
}
95105

96106
private async executeCommand<A extends any[], R>(command: string, ...args: A): Promise<R> {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* This file is worker script for spxls (spx language server).
3+
* It runs in a Web Worker.
4+
*/
5+
6+
declare const self: DedicatedWorkerGlobalScope
7+
8+
import '@/assets/wasm_exec.js'
9+
import spxlsWasmUrl from '@/assets/spxls.wasm?url'
10+
import type { Files, Message, NotificationMessage, RequestMessage, ResponseMessage, Spxls } from './spxls'
11+
12+
export type FilesMessage = {
13+
type: 'files'
14+
files: Files
15+
}
16+
17+
export type LSPMessage<M extends Message> = {
18+
type: 'lsp'
19+
message: M
20+
}
21+
22+
/** Message that worker send to main thread. */
23+
export type WorkerMessage = LSPMessage<ResponseMessage | NotificationMessage>
24+
25+
/** Message that main thread send to worker. */
26+
export type MainMessage = LSPMessage<RequestMessage | NotificationMessage> | FilesMessage
27+
28+
interface IWorkerScope {
29+
postMessage(message: WorkerMessage): void
30+
addEventListener(type: 'message', listener: (event: MessageEvent<MainMessage>) => void): void
31+
}
32+
33+
export interface IWorkerHandler {
34+
postMessage(message: MainMessage): void
35+
addEventListener(type: 'message', listener: (event: MessageEvent<WorkerMessage>) => void): void
36+
terminate(): void
37+
}
38+
39+
const scope: IWorkerScope = self
40+
const lsIniting = initLS()
41+
let files: Files = {}
42+
43+
async function initLS(): Promise<Spxls> {
44+
const go = new Go()
45+
const { instance } = await WebAssembly.instantiateStreaming(fetch(spxlsWasmUrl), go.importObject)
46+
go.run(instance)
47+
const ls = NewSpxls(
48+
() => files,
49+
(message) => {
50+
scope.postMessage({ type: 'lsp', message })
51+
}
52+
)
53+
if (ls instanceof Error) throw ls
54+
return ls
55+
}
56+
57+
function main() {
58+
scope.addEventListener('message', async (event) => {
59+
const message = event.data
60+
switch (message.type) {
61+
case 'lsp': {
62+
const ls = await lsIniting
63+
const error = ls.handleMessage(message.message)
64+
if (error != null) throw error
65+
break
66+
}
67+
case 'files':
68+
files = message.files
69+
return
70+
}
71+
})
72+
}
73+
74+
main()

spx-gui/tsconfig.app.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
"@/*": [
1616
"src/*"
1717
]
18-
}
18+
},
19+
"lib": [
20+
"es2023",
21+
"dom",
22+
"dom.iterable",
23+
"webworker"
24+
]
1925
}
2026
}

tools/spxls/client.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import { type Files, type NotificationMessage, type RequestMessage, type ResponseMessage, type ResponseError as ResponseErrorObj, type Spxls } from '.'
1+
import { type NotificationMessage, type RequestMessage, type ResponseMessage, type ResponseError as ResponseErrorObj } from '.'
2+
3+
/** Connection between the client and the language server. */
4+
export interface IConnection {
5+
sendMessage(message: RequestMessage | NotificationMessage): void
6+
onMessage(handler: (message: ResponseMessage | NotificationMessage) => void): void
7+
}
28

39
/**
410
* Client wrapper for the spxls.
511
*/
612
export class Spxlc {
7-
private ls: Spxls
813
private nextRequestId: number = 1
914
private pendingRequests = new Map<number, {
1015
resolve: (response: any) => void
@@ -14,12 +19,10 @@ export class Spxlc {
1419

1520
/**
1621
* Creates a new client instance.
17-
* @param filesProvider Function that provides access to workspace files.
22+
* @param connection The connection to the language server.
1823
*/
19-
constructor(filesProvider: () => Files) {
20-
const ls = NewSpxls(filesProvider, this.handleMessage.bind(this))
21-
if (ls instanceof Error) throw ls
22-
this.ls = ls
24+
constructor(private connection: IConnection) {
25+
connection.onMessage(m => this.handleMessage(m))
2326
}
2427

2528
/**
@@ -68,7 +71,7 @@ export class Spxlc {
6871
}
6972

7073
/**
71-
* Sends a request to the language server and waits for response.
74+
* Sends a request to the language server and waits for response. TODO: support signal for cancellation.
7275
* @param method LSP method name.
7376
* @param params Method parameters.
7477
* @returns Promise that resolves with the response.
@@ -87,8 +90,9 @@ export class Spxlc {
8790
params
8891
}
8992
this.pendingRequests.set(id, { resolve, reject })
90-
const err = this.ls.handleMessage(message)
91-
if (err != null) {
93+
try {
94+
this.connection.sendMessage(message)
95+
} catch (err) {
9296
reject(err)
9397
this.pendingRequests.delete(id)
9498
}
@@ -119,8 +123,7 @@ export class Spxlc {
119123
method,
120124
params
121125
}
122-
const err = this.ls.handleMessage(message)
123-
if (err != null) throw err
126+
this.connection.sendMessage(message)
124127
}
125128

126129
/**

tools/spxls/index.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export interface Spxls {
1010
handleMessage(message: RequestMessage | NotificationMessage): Error | null
1111
}
1212

13-
1413
declare global {
1514
/**
1615
* Creates a new instance of the spx language server.

0 commit comments

Comments
 (0)