Skip to content

Commit

Permalink
feat(tools/spxls): implement initial LSP features
Browse files Browse the repository at this point in the history
- Add WASM adapter for spxls.
- Implement spxlc (JavaScript client for spxls).
- Add `MapFS` implementation for file system operations.
- Implement `textDocument/formatting` LSP capability.
- Fix multiple `var` blocks handling in spx source code. (Fixes goplus#752)
- Fix declaration order requirements (vars -> funcs -> others). (Fixes
  goplus#591)
- Set up basic project layout.

Updates goplus#1059

Signed-off-by: Aofei Sheng <aofei@aofeisheng.com>
  • Loading branch information
aofei committed Nov 19, 2024
1 parent c8b100f commit 9a51995
Show file tree
Hide file tree
Showing 15 changed files with 1,749 additions and 26 deletions.
69 changes: 43 additions & 26 deletions tools/spxls/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@

A lightweight Go+ language server for [spx](https://github.com/goplus/spx) that runs in the browser using WebAssembly.

## LSP methods
This project follows the [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/)
using [JSON-RPC 2.0](https://www.jsonrpc.org/specification) for message exchange. However, unlike traditional LSP
implementations that require a network transport layer, this project operates directly in the browser's memory space
through its API interfaces.

## Usage

This project is a standard Go WebAssembly module. You can use it like any other Go WASM modules in your web applications.

For detailed API references, please check the [index.d.ts](index.d.ts) file.

## Supported LSP methods

| Category | Method | Purpose & Explanation |
|----------|--------|-----------------------|
Expand All @@ -29,6 +40,8 @@ A lightweight Go+ language server for [spx](https://github.com/goplus/spx) that
|| [`textDocument/typeDefinition`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_typeDefinition) | Navigates to type definitions of variables/fields. |
|| [`textDocument/implementation`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_implementation) | Locates implementations. |
|| [`textDocument/references`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_references) | Finds all references of a symbol. |
|| [`textDocument/documentLink`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_documentLink) | Provides clickable links within document content. |
|| [`documentLink/resolve`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#documentLink_resolve) | Provides detailed target information for selected document links. |
|| [`textDocument/documentSymbol`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_documentSymbol) | Provides document symbols for outline/navigation. |
|| [`textDocument/documentHighlight`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_documentHighlight) | Highlights other occurrences of selected symbol. |
|| [`workspace/symbol`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#workspace_symbol) | Provides workspace-wide symbol search with name matching patterns. |
Expand Down Expand Up @@ -61,44 +74,48 @@ defined as follows:

```typescript
interface ExecuteCommandParams {
/**
* The identifier of the actual command handler.
*/
command: 'spx.renameResources'

/**
* Arguments that the command should be invoked with.
*/
arguments: RenameResourceParams[]
/**
* The identifier of the actual command handler.
*/
command: 'spx.renameResources'

/**
* Arguments that the command should be invoked with.
*/
arguments: SpxRenameResourceParams[]
}
```

```typescript
/**
* Parameters to rename a resource in the workspace.
* Parameters to rename a spx resource in the workspace.
*/
interface RenameResourceParams {
/**
* The resource.
*/
resource: ResourceIdentifier

/**
* The new name of the resource.
*/
newName: string
interface SpxRenameResourceParams {
/**
* The spx resource.
*/
resource: SpxResourceIdentifier

/**
* The new name of the spx resource.
*/
newName: string
}
```

```typescript
interface ResourceIdentifier {
/**
* The resource's URI.
*/
uri: URI
interface SpxResourceIdentifier {
/**
* The spx resource's URI.
*/
uri: SpxResourceUri
}
```

```typescript
type SpxResourceUri = string
```
*Response:*
- result: [`WorkspaceEdit`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#workspaceEdit)
Expand Down
111 changes: 111 additions & 0 deletions tools/spxls/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Files, NewSpxls, NotificationMessage, RequestMessage, ResponseMessage, Spxls } from '.'

/**
* Client wrapper for the spxls.
*/
export class Spxlc {
private ls: Spxls
private nextRequestId: number = 1
private pendingRequests = new Map<number, {
resolve: (response: any) => void
reject: (error: any) => void
}>()
private notificationHandlers = new Map<string, (params: any) => void>()

/**
* Creates a new client instance.
* @param filesProvider Function that provides access to workspace files.
*/
constructor(filesProvider: () => Files) {
this.ls = NewSpxls(filesProvider, this.handleMessage.bind(this))
}

/**
* Handles messages from the language server.
* @param message Message from the server.
* @throws Error if the message type is unknown.
*/
private handleMessage(message: ResponseMessage | NotificationMessage): void {
if ('id' in message) return this.handleResponseMessage(message)
if ('method' in message) return this.handleNotificationMessage(message)
throw new Error('unknown message type')
}

/**
* Handles response messages from the language server.
* @param message Response message from the server.
* @throws Error if no pending request is found for the message ID.
*/
private handleResponseMessage(message: ResponseMessage): void {
const pending = this.pendingRequests.get(message.id)
if (pending == null) throw new Error(`no pending request found for id: ${message.id}`)
this.pendingRequests.delete(message.id)

if ('error' in message) pending.reject(message.error)
else pending.resolve(message.result)
}

/**
* Handles notification messages from the language server.
* @param message Notification message from the server.
* @throws Error if no handler is found for the notification method.
*/
private handleNotificationMessage(message: NotificationMessage): void {
const handler = this.notificationHandlers.get(message.method)
if (handler == null) throw new Error(`no notification handler found for method: ${message.method}`)

handler(message.params)
}

/**
* Sends a request to the language server and waits for response.
* @param method LSP method name.
* @param params Method parameters.
* @returns Promise that resolves with the response.
*/
request<T>(method: string, params?: any): Promise<T> {
return new Promise((resolve, reject) => {
const id = this.nextRequestId++
this.pendingRequests.set(id, { resolve, reject })

const message: RequestMessage = {
jsonrpc: '2.0',
id,
method,
params
}
this.ls.handleMessage(message)
})
}

/**
* Sends a notification to the language server (no response expected).
* @param method LSP method name.
* @param params Method parameters.
*/
notify(method: string, params?: any): void {
const message: NotificationMessage = {
jsonrpc: '2.0',
method,
params
}
this.ls.handleMessage(message)
}

/**
* Registers a handler for server notifications.
* @param method LSP method name.
* @param handler Function to handle the notification.
*/
onNotification(method: string, handler: (params: any) => void): void {
this.notificationHandlers.set(method, handler)
}

/**
* Cleans up client resources.
*/
dispose(): void {
this.pendingRequests.clear()
this.notificationHandlers.clear()
}
}
16 changes: 16 additions & 0 deletions tools/spxls/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module github.com/goplus/builder/tools/spxls

go 1.21.0

require (
github.com/goplus/gop v1.2.6
github.com/stretchr/testify v1.9.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/goplus/gogen v1.15.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/qiniu/x v1.13.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
16 changes: 16 additions & 0 deletions tools/spxls/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goplus/gogen v1.15.2 h1:Q6XaSx/Zi5tWnjfAziYsQI6Jv6MgODRpFtOYqNkiiqM=
github.com/goplus/gogen v1.15.2/go.mod h1:92qEzVgv7y8JEFICWG9GvYI5IzfEkxYdsA1DbmnTkqk=
github.com/goplus/gop v1.2.6 h1:kog3c5Js+8EopqmI4+CwueXsqibnBwYVt5q5N7juRVY=
github.com/goplus/gop v1.2.6/go.mod h1:uREWbR1MrFaviZ4Mbx4ZCcAYDoqzO0iv1Qo6Np0Xx4E=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qiniu/x v1.13.10 h1:J4Z3XugYzAq85SlyAfqlKVrbf05glMbAOh+QncsDQpE=
github.com/qiniu/x v1.13.10/go.mod h1:INZ2TSWSJVWO/RuELQROERcslBwVgFG7MkTfEdaQz9E=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
122 changes: 122 additions & 0 deletions tools/spxls/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* A lightweight Go+ language server for spx that runs in the browser using WebAssembly.
*/
export interface Spxls {
/**
* Handles incoming LSP messages from the client.
*
* @param message - The message to process. Any required response will be sent via the messageReplier callback.
*/
handleMessage(message: RequestMessage | NotificationMessage): void
}

/**
* Creates a new instance of the spx language server.
*
* @param filesProvider - Function that provides access to the workspace files. This will be called whenever the
* language server needs to access the file system. The returned map should have relative file
* paths as keys and file contents values.
*
* @param messageReplier - Function called when the language server needs to reply to the client. The client should
* handle these messages according to the LSP specification.
*/
export function NewSpxls(filesProvider: () => Files, messageReplier: (message: ResponseMessage | NotificationMessage) => void): Spxls

/**
* A general message as defined by JSON-RPC. The language server protocol always uses “2.0” as the `jsonrpc` version.
*
* See https://microsoft.github.io/language-server-protocol/specifications/base/0.9/specification/#abstractMessage.
*/
export interface Message {
jsonrpc: string
}

/**
* A request message to describe a request between the client and the server. Every processed request must send a
* response back to the sender of the request.
*
* See https://microsoft.github.io/language-server-protocol/specifications/base/0.9/specification/#requestMessage.
*/
export interface RequestMessage extends Message {
/**
* The request id.
*/
id: number

/**
* The method to be invoked.
*/
method: string

/**
* The method's params.
*/
params?: any[] | object
}

/**
* A Response Message sent as a result of a request. If a request doesn’t provide a result value the receiver of a
* request still needs to return a response message to conform to the JSON-RPC specification. The result property of the
* ResponseMessage should be set to `null` in this case to signal a successful request.
*
* See https://microsoft.github.io/language-server-protocol/specifications/base/0.9/specification/#responseMessage.
*/
export interface ResponseMessage extends Message {
/**
* The request id.
*/
id: number

/**
* The result of a request. This member is REQUIRED on success.
* This member MUST NOT exist if there was an error invoking the method.
*/
result?: string | number | boolean | any[] | object | null

/**
* The error object in case a request fails.
*/
error?: ResponseError
}

export interface ResponseError {
/**
* A number indicating the error type that occurred.
*/
code: number

/**
* A string providing a short description of the error.
*/
message: string

/**
* A primitive or structured value that contains additional
* information about the error. Can be omitted.
*/
data?: string | number | boolean | any[] | object | null
}

/**
* A notification message. A processed notification message must not send a response back. They work like events.
*
* See https://microsoft.github.io/language-server-protocol/specifications/base/0.9/specification/#notificationMessage.
*/
export interface NotificationMessage extends Message {
/**
* The method to be invoked.
*/
method: string

/**
* The notification's params.
*/
params?: any[] | object
}

/**
* Map from relative path to file content.
*/
export type Files = {
[path: string]: Uint8Array
}
8 changes: 8 additions & 0 deletions tools/spxls/internal/jsonrpc2/jsonrpc2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package jsonrpc2 is a minimal implementation of the JSON RPC 2 spec.
// https://www.jsonrpc.org/specification
// It is intended to be compatible with other implementations at the wire level.
package jsonrpc2
Loading

0 comments on commit 9a51995

Please sign in to comment.