Skip to content

Commit 9d5cb30

Browse files
Update language service documentation and sample app with new features
Co-authored-by: Sander-Toonen <5106372+Sander-Toonen@users.noreply.github.com>
1 parent 1137b05 commit 9d5cb30

2 files changed

Lines changed: 354 additions & 21 deletions

File tree

docs/language-service.md

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ The library includes a built-in language service that provides IDE-like features
55
## Features
66

77
- **Code Completions** - Autocomplete for functions, operators, keywords, and user-defined variables
8+
- **Snippet support** - Function completions include tab stops with parameter placeholders (e.g., `sum(${1:a})`)
9+
- **Path-based variable completions** - Completions for nested object properties (e.g., typing `user.` shows `user.name`, `user.profile.email`)
10+
- **Text edits with ranges** - Proper replacement ranges for more accurate completions
811
- **Hover Information** - Documentation tooltips when hovering over functions and variables
12+
- **Variable value previews** - Hovers on variables show a truncated JSON preview of the value
13+
- **Nested path support** - Hovering over `user.name` resolves and shows the value at that path
914
- **Syntax Highlighting** - Token-based highlighting for numbers, strings, keywords, operators, etc.
1015

1116
## Basic Usage
@@ -58,3 +63,301 @@ The sample code is located in `samples/language-service-sample/` and shows how t
5863
2. Connect the language service to Monaco's completion and hover providers
5964
3. Apply syntax highlighting using decorations
6065
4. Create an LSP-compatible text document wrapper for Monaco models
66+
67+
## Advanced Features
68+
69+
### Nested Variable Completions
70+
71+
The language service supports path-based completions for nested object properties. When you type a dot after a variable name, you'll get completions for its properties:
72+
73+
```js
74+
const variables = {
75+
user: {
76+
name: 'Ada',
77+
profile: {
78+
email: 'ada@example.com',
79+
age: 30
80+
}
81+
},
82+
config: {
83+
timeout: 5000,
84+
retries: 3
85+
}
86+
};
87+
88+
// Typing "user." will show completions: user.name, user.profile
89+
// Typing "user.profile." will show: user.profile.email, user.profile.age
90+
```
91+
92+
**Monaco Editor Integration**: Add `triggerCharacters: ['.']` to your completion provider to automatically trigger completions when typing a dot:
93+
94+
```js
95+
monaco.languages.registerCompletionItemProvider(languageId, {
96+
triggerCharacters: ['.'],
97+
provideCompletionItems: function (model, position) {
98+
// ... completion logic
99+
}
100+
});
101+
```
102+
103+
### Snippet Support in Completions
104+
105+
Function completions include snippet support with tab stops for parameters. This provides a better editing experience in editors that support snippets:
106+
107+
```js
108+
// When completing a function like "sum", the insertText is "sum(${1:a})"
109+
// After selecting the completion:
110+
// 1. The text "sum(a)" is inserted
111+
// 2. The parameter "a" is selected, ready for editing
112+
// 3. You can tab to the next parameter (if any)
113+
```
114+
115+
**Monaco Editor Integration**: Use `insertTextRules` with `monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet` when the completion's `insertTextFormat` is 2 (snippet):
116+
117+
```js
118+
const suggestions = items.map(it => ({
119+
label: it.label,
120+
kind: mapKind(it.kind),
121+
detail: it.detail,
122+
documentation: it.documentation,
123+
insertText: it.insertText || it.label,
124+
insertTextRules: it.insertTextFormat === 2
125+
? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
126+
: undefined,
127+
range
128+
}));
129+
```
130+
131+
### Text Edit Ranges
132+
133+
Completion items may include a `textEdit` with a specific `range` for more precise text replacement. This is especially important for path-based completions where only the partial segment after the last dot should be replaced:
134+
135+
```js
136+
// When completing "user.na|" (cursor at |), only "na" should be replaced, not "user.na"
137+
// The textEdit.range will specify the exact range to replace
138+
```
139+
140+
**Monaco Editor Integration**: Check for `textEdit.range` and use it when available:
141+
142+
```js
143+
const suggestions = items.map(it => {
144+
const range = it.textEdit?.range
145+
? new monaco.Range(
146+
it.textEdit.range.start.line + 1,
147+
it.textEdit.range.start.character + 1,
148+
it.textEdit.range.end.line + 1,
149+
it.textEdit.range.end.character + 1
150+
)
151+
: defaultRange;
152+
153+
return {
154+
label: it.label,
155+
insertText: it.textEdit?.newText || it.insertText || it.label,
156+
range
157+
};
158+
});
159+
```
160+
161+
### Variable Value Previews in Hover
162+
163+
When hovering over a variable (including nested paths), the hover will display:
164+
- The variable's type
165+
- A truncated JSON preview of its value
166+
167+
```js
168+
const variables = {
169+
user: { name: 'Ada', score: 95 },
170+
items: [1, 2, 3, 4, 5]
171+
};
172+
173+
// Hovering over "user" shows:
174+
// user: Variable (object)
175+
// Value Preview
176+
// {
177+
// "name": "Ada",
178+
// "score": 95
179+
// }
180+
181+
// Hovering over "user.name" shows:
182+
// user.name: Variable (string)
183+
// Value Preview
184+
// "Ada"
185+
```
186+
187+
The preview is automatically truncated to prevent overwhelming hovers with large data structures.
188+
189+
### HoverV2 Type
190+
191+
The `getHover` method returns a `HoverV2` type, which guarantees that `contents` is a `MarkupContent` object (not a deprecated string or array):
192+
193+
```typescript
194+
interface HoverV2 extends Hover {
195+
contents: MarkupContent; // Always MarkupContent, never string or array
196+
}
197+
```
198+
199+
**Monaco Editor Integration**: The hover contents are always in the `MarkupContent` format:
200+
201+
```js
202+
const hover = ls.getHover({textDocument: doc, position, variables});
203+
if (hover && hover.contents) {
204+
// hover.contents is always a MarkupContent object
205+
const value = hover.contents.value;
206+
const kind = hover.contents.kind; // 'plaintext' or 'markdown'
207+
208+
contents = [{value}];
209+
}
210+
```
211+
212+
## API Reference
213+
214+
### createLanguageService(options?)
215+
216+
Creates a new language service instance.
217+
218+
**Parameters:**
219+
- `options` (optional): `LanguageServiceOptions` - Configuration options for the language service
220+
- `operators`: `Record<string, boolean>` - Map of operator names to booleans indicating whether they are allowed
221+
222+
**Returns:** `LanguageServiceApi` - The language service instance
223+
224+
**Example:**
225+
```js
226+
import { createLanguageService } from '@pro-fa/expr-eval';
227+
228+
const ls = createLanguageService({
229+
operators: {
230+
'+': true,
231+
'-': true,
232+
'*': true,
233+
'/': true
234+
}
235+
});
236+
```
237+
238+
### ls.getCompletions(params)
239+
240+
Returns a list of possible completions for the given position in the document.
241+
242+
**Parameters:**
243+
- `params`: `GetCompletionsParams`
244+
- `textDocument`: `TextDocument` - The text document to analyze
245+
- `position`: `Position` - The cursor position (0-based line and character)
246+
- `variables`: `Values` (optional) - User-defined variables available in the expression
247+
248+
**Returns:** `CompletionItem[]` - Array of completion items
249+
250+
**CompletionItem Properties:**
251+
- `label`: `string` - The display label
252+
- `kind`: `CompletionItemKind` - The kind of completion (Function, Variable, Keyword, etc.)
253+
- `detail`: `string` (optional) - Additional details shown in the completion UI
254+
- `documentation`: `string | MarkupContent` (optional) - Documentation for the item
255+
- `insertText`: `string` (optional) - The text to insert (may be a snippet)
256+
- `insertTextFormat`: `InsertTextFormat` (optional) - 1 = PlainText, 2 = Snippet
257+
- `textEdit`: `TextEdit` (optional) - Text edit with specific range and newText
258+
259+
**Example:**
260+
```js
261+
const completions = ls.getCompletions({
262+
textDocument: doc,
263+
position: { line: 0, character: 5 },
264+
variables: { user: { name: 'Ada' }, x: 42 }
265+
});
266+
```
267+
268+
### ls.getHover(params)
269+
270+
Returns hover information for the given position in the document.
271+
272+
**Parameters:**
273+
- `params`: `GetHoverParams`
274+
- `textDocument`: `TextDocument` - The text document to analyze
275+
- `position`: `Position` - The cursor position (0-based line and character)
276+
- `variables`: `Values` (optional) - User-defined variables available in the expression
277+
278+
**Returns:** `HoverV2` - Hover information with guaranteed MarkupContent
279+
280+
**HoverV2 Properties:**
281+
- `contents`: `MarkupContent` - The hover content
282+
- `kind`: `MarkupKind` - Either 'plaintext' or 'markdown'
283+
- `value`: `string` - The hover text
284+
- `range`: `Range` (optional) - The range of the hovered element
285+
286+
**Example:**
287+
```js
288+
const hover = ls.getHover({
289+
textDocument: doc,
290+
position: { line: 0, character: 3 },
291+
variables: { user: { name: 'Ada' } }
292+
});
293+
294+
console.log(hover.contents.value); // The hover text
295+
console.log(hover.contents.kind); // 'markdown' or 'plaintext'
296+
```
297+
298+
### ls.getHighlighting(textDocument)
299+
300+
Returns a list of syntax highlighting tokens for the given text document.
301+
302+
**Parameters:**
303+
- `textDocument`: `TextDocument` - The text document to analyze
304+
305+
**Returns:** `HighlightToken[]` - Array of highlighting tokens
306+
307+
**HighlightToken Properties:**
308+
- `type`: `'number' | 'string' | 'name' | 'keyword' | 'operator' | 'function' | 'punctuation'`
309+
- `start`: `number` - Start offset in the document
310+
- `end`: `number` - End offset in the document
311+
- `value`: `string | number | boolean | undefined` (optional) - The token value
312+
313+
**Example:**
314+
```js
315+
const tokens = ls.getHighlighting(doc);
316+
tokens.forEach(token => {
317+
console.log(`${token.type} at ${token.start}-${token.end}: ${token.value}`);
318+
});
319+
```
320+
321+
## TypeScript Types
322+
323+
The library exports the following TypeScript types for use in your applications:
324+
325+
### Exported Types
326+
327+
```typescript
328+
import type {
329+
LanguageServiceApi,
330+
HoverV2,
331+
GetCompletionsParams,
332+
GetHoverParams,
333+
HighlightToken,
334+
LanguageServiceOptions
335+
} from '@pro-fa/expr-eval';
336+
```
337+
338+
- **`LanguageServiceApi`** - The main language service interface with `getCompletions`, `getHover`, and `getHighlighting` methods
339+
- **`HoverV2`** - Extended Hover type with guaranteed `MarkupContent` for contents (not deprecated string/array formats)
340+
- **`GetCompletionsParams`** - Parameters for `getCompletions`: `textDocument`, `position`, and optional `variables`
341+
- **`GetHoverParams`** - Parameters for `getHover`: `textDocument`, `position`, and optional `variables`
342+
- **`HighlightToken`** - Syntax highlighting token with `type`, `start`, `end`, and optional `value`
343+
- **`LanguageServiceOptions`** - Configuration options for creating a language service, including optional `operators` map
344+
345+
### LSP Types
346+
347+
The language service uses types from `vscode-languageserver-types` for LSP compatibility:
348+
349+
```typescript
350+
import type {
351+
Position,
352+
Range,
353+
CompletionItem,
354+
CompletionItemKind,
355+
MarkupContent,
356+
MarkupKind,
357+
InsertTextFormat
358+
} from 'vscode-languageserver-types';
359+
360+
import type { TextDocument } from 'vscode-languageserver-textdocument';
361+
```
362+
363+
These types ensure compatibility with Language Server Protocol-based editors and tools.

0 commit comments

Comments
 (0)