Building a Basic Language Server for Vue with TypeScript
This challenge involves creating a fundamental language server that can provide basic language features for Vue.js Single File Components (SFCs) using TypeScript. Building language servers is crucial for enhancing developer experience in modern IDEs by enabling features like syntax highlighting, autocompletion, error checking, and more. You will leverage the Language Server Protocol (LSP) to communicate with a hypothetical editor client.
Problem Description
Your task is to implement a simplified language server for Vue SFCs. This server will run in a separate process and communicate with an editor (which we will simulate) using the Language Server Protocol (LSP). The primary goal is to handle basic text synchronization and respond to a simple "hover" request.
What needs to be achieved:
- Initialize Communication: Establish a connection with the client and handle the
initializeandinitializedhandshake. - Text Synchronization: Process
textDocument/didOpen,textDocument/didChange, andtextDocument/didClosenotifications to keep track of the open Vue SFC documents. - Hover Feature: Respond to
textDocument/hoverrequests for specific tags within a Vue SFC template.
Key Requirements:
- LSP Compliance: Implement the necessary LSP methods to communicate with a client.
- Vue SFC Awareness: The server should recognize
.vuefiles and process their content. - Basic Hover: When the cursor is over a common Vue tag (e.g.,
<template>,<script>,<style>,<div>,<button>), the server should return a simple hover description. - TypeScript Implementation: The entire language server should be written in TypeScript.
Expected Behavior:
- When the client sends an
initializerequest, the server should respond with its capabilities, indicating support forhoverandtextDocumentSynckindIncremental. - When a
.vuefile is opened, the server should store its content. - When a
.vuefile is changed, the server should update its stored content. - When a
textDocument/hoverrequest is received for a position within a known Vue tag in a.vuefile, the server should return aHoverobject containing acontentsfield with a markdown string describing the tag. For unknown tags or positions outside tags, it should returnnull.
Edge Cases:
- Requests for files other than
.vueshould be ignored gracefully. - Hover requests for positions that are not part of a recognized tag should return
null. - Handling of malformed SFCs is not a primary concern for this challenge, but the server should not crash.
Examples
Example 1: Initialization and Document Open
// Client sends to Server
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"processId": 12345,
"capabilities": {} // Client capabilities
}
}
// Server responds to Client
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"capabilities": {
"textDocumentSync": 1, // 1 = Incremental
"hoverProvider": true
}
}
}
// Client sends to Server (after initialization)
{
"jsonrpc": "2.0",
"method": "initialized"
}
// Client sends to Server
{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///path/to/MyComponent.vue",
"languageId": "vue",
"version": 1,
"text": "<template>\n <div>Hello</div>\n</template>\n<script>\nexport default {}\n</script>\n<style>\n.my-class {}\n</style>"
}
}
}
Explanation:
The client initiates the connection. The server declares its capabilities (supports incremental text sync and hover). The client confirms initialization. The client then opens a .vue file, sending its content. The server should store this content associated with the URI.
Example 2: Hover Request
// Client sends to Server
{
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": "file:///path/to/MyComponent.vue"
},
"position": {
"line": 0, // Corresponds to the line containing "<template>"
"character": 2 // Within the "<template>" tag
}
}
}
// Server responds to Client
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"contents": {
"kind": "markdown",
"value": "The **`<template>`** block defines the HTML structure of your Vue component."
}
}
}
Explanation:
The client requests hover information for a specific position within the <template> tag. The server analyzes the position, identifies it's within a <template> tag, and returns a markdown description.
Example 3: Hover Request (Different Tag)
// Client sends to Server
{
"jsonrpc": "2.0",
"id": 3,
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": "file:///path/to/MyComponent.vue"
},
"position": {
"line": 3, // Corresponds to the line containing "<script>"
"character": 2 // Within the "<script>" tag
}
}
}
// Server responds to Client
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"contents": {
"kind": "markdown",
"value": "The **`<script>`** block contains the JavaScript or TypeScript logic for your Vue component."
}
}
}
Explanation:
Similar to Example 2, but for the <script> tag.
Example 4: Hover Request (Outside Tag)
// Client sends to Server
{
"jsonrpc": "2.0",
"id": 4,
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": "file:///path/to/MyComponent.vue"
},
"position": {
"line": 1, // Corresponds to the line containing "<div>Hello</div>"
"character": 6 // Within "Hello"
}
}
}
// Server responds to Client
{
"jsonrpc": "2.0",
"id": 4,
"result": null
}
Explanation:
The client requests hover information for a position that is not directly within a recognized SFC block tag (<template>, <script>, <style>) or a common HTML element tag. The server returns null.
Constraints
- The language server must implement at least the following LSP methods:
initialize,textDocument/didOpen,textDocument/didChange,textDocument/didClose, andtextDocument/hover. - Communication between the client and server will be via standard input/output using JSON-RPC.
- The server should be written entirely in TypeScript.
- For hover requests, only consider the top-level SFC blocks (
<template>,<script>,<style>) and common HTML elements like<div>,<button>,<span>,<p>,<h1>to<h6>. - The server does not need to parse the content of the script or style blocks, nor the full AST of the template. A simple regex or string matching for tag names is sufficient for this challenge.
Notes
- You will need to implement a basic JSON-RPC handler for communication. Libraries like
vscode-languageservercan greatly simplify this, but for this challenge, you are encouraged to implement a minimal RPC layer yourself to understand the protocol better. - Consider how to parse incoming JSON-RPC messages from stdin.
- When responding to requests, ensure you correctly format the JSON-RPC response with
id,method(for notifications), andresultorerror. - The
positionin LSP is zero-based for bothlineandcharacter. - The
textDocumentSynccapability should be set to1(meaningIncremental) in yourinitializeresponse. - For the hover feature, you can maintain a simple mapping of tag names to their descriptions.