Skip to content

Commit 739b438

Browse files
committed
Run spxls in Web Worker
1 parent d5d5f13 commit 739b438

File tree

5 files changed

+158
-35
lines changed

5 files changed

+158
-35
lines changed

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

Lines changed: 28 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/index.ts', import.meta.url), { type: 'module' })
31+
}
32+
setFiles(files: SpxlsFiles): void {
33+
this.worker.postMessage({ type: 'files', files })
34+
}
35+
sendMessage(message: RequestMessage | NotificationMessage) {
36+
this.worker.postMessage({ type: 'lsp', message })
37+
}
38+
onMessage(handler: (message: ResponseMessage | NotificationMessage) => void) {
39+
this.worker.addEventListener('message', (event) => {
40+
const message = event.data
41+
handler(message.message)
42+
})
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.setFiles(loadedFiles)
7580
this.isFilesStale.value = false
7681
}
7782

@@ -87,10 +92,9 @@ 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)
9498
}
9599

96100
private async executeCommand<A extends any[], R>(command: string, ...args: A): Promise<R> {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
class SpxlsWorker {
40+
private lsInited: Promise<Spxls>
41+
42+
constructor(private scope: IWorkerScope) {
43+
this.lsInited = this.initLS()
44+
45+
scope.addEventListener('message', async (event) => {
46+
const message = event.data
47+
switch (message.type) {
48+
case 'lsp': {
49+
const ls = await this.lsInited
50+
const error = ls.handleMessage(message.message)
51+
if (error != null) throw error
52+
break
53+
}
54+
case 'files':
55+
this.setFiles(message.files)
56+
return
57+
}
58+
})
59+
}
60+
61+
private files: Files = {}
62+
setFiles(files: Files): void {
63+
this.files = files
64+
}
65+
66+
private async initLS(): Promise<Spxls> {
67+
const go = new Go()
68+
const { instance } = await WebAssembly.instantiateStreaming(fetch(spxlsWasmUrl), go.importObject)
69+
go.run(instance)
70+
const ls = NewSpxls(
71+
() => this.files,
72+
(message) => {
73+
this.scope.postMessage({ type: 'lsp', message })
74+
}
75+
)
76+
if (ls instanceof Error) throw ls
77+
return ls
78+
}
79+
}
80+
81+
new SpxlsWorker(self)

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: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,45 @@
11
import { type Files, type NotificationMessage, type RequestMessage, type ResponseMessage, type Spxls } from '.'
22

3+
/** Connection between the client and the language server. */
4+
export interface IConnection {
5+
setFiles(files: Files): void
6+
sendMessage(message: RequestMessage | NotificationMessage): void
7+
onMessage(handler: (message: ResponseMessage | NotificationMessage) => void): void
8+
dispose(): void
9+
}
10+
11+
/** Direct connection between LS client and server when the two run in the same thread. */
12+
export class DirectConnection implements IConnection {
13+
private ls: Spxls
14+
constructor() {
15+
const ls = NewSpxls(() => this.files, message => this.handleServerMessage(message))
16+
if (ls instanceof Error) throw ls
17+
this.ls = ls
18+
}
19+
private serverMessageHandlers: ((message: ResponseMessage | NotificationMessage) => void)[] = []
20+
private handleServerMessage(message: ResponseMessage | NotificationMessage): void {
21+
for (const handler of this.serverMessageHandlers) handler(message)
22+
}
23+
private files: Files = {}
24+
setFiles(files: Files) {
25+
this.files = files
26+
}
27+
sendMessage(message: RequestMessage | NotificationMessage) {
28+
const error = this.ls.handleMessage(message)
29+
if (error != null) throw error
30+
}
31+
onMessage(handler: (message: ResponseMessage | NotificationMessage) => void) {
32+
this.serverMessageHandlers.push(handler)
33+
}
34+
dispose() {
35+
this.serverMessageHandlers = []
36+
}
37+
}
38+
339
/**
440
* Client wrapper for the spxls.
541
*/
642
export class Spxlc {
7-
private ls: Spxls
843
private nextRequestId: number = 1
944
private pendingRequests = new Map<number, {
1045
resolve: (response: any) => void
@@ -16,10 +51,8 @@ export class Spxlc {
1651
* Creates a new client instance.
1752
* @param filesProvider Function that provides access to workspace files.
1853
*/
19-
constructor(filesProvider: () => Files) {
20-
const ls = NewSpxls(filesProvider, this.handleMessage.bind(this))
21-
if (ls instanceof Error) throw ls
22-
this.ls = ls
54+
constructor(private connection: IConnection) {
55+
connection.onMessage(m => this.handleMessage(m))
2356
}
2457

2558
/**
@@ -85,8 +118,9 @@ export class Spxlc {
85118
params
86119
}
87120
this.pendingRequests.set(id, { resolve, reject })
88-
const err = this.ls.handleMessage(message)
89-
if (err != null) {
121+
try {
122+
this.connection.sendMessage(message)
123+
} catch (err) {
90124
reject(err)
91125
this.pendingRequests.delete(id)
92126
}
@@ -117,8 +151,7 @@ export class Spxlc {
117151
method,
118152
params
119153
}
120-
const err = this.ls.handleMessage(message)
121-
if (err != null) throw err
154+
this.connection.sendMessage(message)
122155
}
123156

124157
/**

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)