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 #752)
- Fix declaration order requirements (vars -> funcs -> others). (Fixes
  #591)
- Set up basic project layout.

Updates #1059

Signed-off-by: Aofei Sheng <[email protected]>
  • Loading branch information
aofei committed Nov 19, 2024
1 parent c8b100f commit 98b9a38
Show file tree
Hide file tree
Showing 15 changed files with 1,815 additions and 27 deletions.
138 changes: 111 additions & 27 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,47 +74,118 @@ 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)
| `null` describing the modification to the workspace. `null` should be treated the same as [`WorkspaceEdit`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#workspaceEdit)
| `null` describing the modification to the workspace. `null` should be treated the same as
[`WorkspaceEdit`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#workspaceEdit)
with no changes (no change was required).
- error: code and message set in case when rename could not be performed for any reason.
### Definition lookup
The `spx.getDefinitions` command retrieves definition identifiers at a given position in a document.
*Request:*
- method: [`workspace/executeCommand`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#workspace_executeCommand)
- params: [`ExecuteCommandParams`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#executeCommandParams)
defined as follows:
```typescript
interface ExecuteCommandParams {
/**
* The identifier of the actual command handler.
*/
command: 'spx.getDefinitions'

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

```typescript
/**
* Parameters to get definitions at a specific position in a document.
*/
interface SpxGetDefinitionsParams extends TextDocumentPositionParams {}
```

*Response:*

- result: `SpxDefinitionIdentifier[]` | `null` describing the definitions found at the given position. `null` indicates
no definitions were found.
- error: code and message set in case when definitions could not be retrieved for any reason.

```typescript
interface SpxDefinitionIdentifier {
/**
* Full name of source package.
* If not provided, it's assumed to be kind-statement.
* If `main`, it's the current user package.
* Examples:
* - `fmt`
* - `github.com/goplus/spx`
* - `main`
*/
package?: string;

/**
* Exported name of the definition.
* If not provided, it's assumed to be kind-package.
* Examples:
* - `Println`
* - `Sprite`
* - `Sprite.turn`
* - `for_statement_with_single_condition`
*/
name?: string;

/** Index in overloads. */
overloadIndex?: number;
}
```
116 changes: 116 additions & 0 deletions tools/spxls/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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) {
const ls = NewSpxls(filesProvider, this.handleMessage.bind(this))
if (ls instanceof Error) throw ls
this.ls = ls
}

/**
* 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++

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

this.pendingRequests.set(id, { resolve, reject })
})
}

/**
* 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
}
const err = this.ls.handleMessage(message)
if (err != null) throw err
}

/**
* 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=
Loading

0 comments on commit 98b9a38

Please sign in to comment.