First iteration of monaco editor lsp client (#5044)

This commit is contained in:
Henning Dieterichs 2025-10-10 12:18:12 +02:00 committed by GitHub
parent a59f6c8a72
commit 0fd6f29a23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 14026 additions and 4 deletions

View file

@ -0,0 +1,5 @@
# Monaco LSP Client
Provides a Language Server Protocol (LSP) client for the Monaco Editor.
This package is in alpha stage and might contain many bugs.

View file

@ -0,0 +1,687 @@
import * as fs from 'fs';
import * as path from 'path';
/**
* Utility class for writing formatted code with proper indentation
*/
class LineWriter {
private lines: string[] = [];
private indentLevel: number = 0;
private indentStr: string = ' '; // 4 spaces
/**
* Write a line with current indentation
*/
writeLine(line: string = ''): void {
if (line.trim() === '') {
this.lines.push('');
} else {
this.lines.push(this.indentStr.repeat(this.indentLevel) + line);
}
}
/**
* Write text without adding a new line
*/
write(text: string): void {
if (this.lines.length === 0) {
this.lines.push('');
}
const lastIndex = this.lines.length - 1;
if (this.lines[lastIndex] === '') {
this.lines[lastIndex] = this.indentStr.repeat(this.indentLevel) + text;
} else {
this.lines[lastIndex] += text;
}
}
/**
* Increase indentation level
*/
indent(): void {
this.indentLevel++;
}
/**
* Decrease indentation level
*/
outdent(): void {
if (this.indentLevel > 0) {
this.indentLevel--;
}
}
/**
* Get the generated content as a string
*/
toString(): string {
return this.lines.join('\n');
}
/**
* Clear all content and reset indentation
*/
clear(): void {
this.lines = [];
this.indentLevel = 0;
}
}
/**
* Interface definitions based on the metaModel schema
*/
interface MetaModel {
metaData: MetaData;
requests: Request[];
notifications: Notification[];
structures: Structure[];
enumerations: Enumeration[];
typeAliases: TypeAlias[];
}
interface MetaData {
version: string;
}
interface Request {
method: string;
result: Type;
messageDirection: MessageDirection;
params?: Type | Type[];
partialResult?: Type;
errorData?: Type;
registrationOptions?: Type;
registrationMethod?: string;
documentation?: string;
since?: string;
proposed?: boolean;
deprecated?: string;
}
interface Notification {
method: string;
messageDirection: MessageDirection;
params?: Type | Type[];
registrationOptions?: Type;
registrationMethod?: string;
documentation?: string;
since?: string;
proposed?: boolean;
deprecated?: string;
}
interface Structure {
name: string;
properties: Property[];
extends?: Type[];
mixins?: Type[];
documentation?: string;
since?: string;
proposed?: boolean;
deprecated?: string;
}
interface Property {
name: string;
type: Type;
optional?: boolean;
documentation?: string;
since?: string;
proposed?: boolean;
deprecated?: string;
}
interface Enumeration {
name: string;
type: EnumerationType;
values: EnumerationEntry[];
supportsCustomValues?: boolean;
documentation?: string;
since?: string;
proposed?: boolean;
deprecated?: string;
}
interface EnumerationEntry {
name: string;
value: string | number;
documentation?: string;
since?: string;
proposed?: boolean;
deprecated?: string;
}
interface EnumerationType {
kind: 'base';
name: 'string' | 'integer' | 'uinteger';
}
interface TypeAlias {
name: string;
type: Type;
documentation?: string;
since?: string;
proposed?: boolean;
deprecated?: string;
}
type MessageDirection = 'clientToServer' | 'serverToClient' | 'both';
type Type =
| BaseType
| ReferenceType
| ArrayType
| MapType
| AndType
| OrType
| TupleType
| StructureLiteralType
| StringLiteralType
| IntegerLiteralType
| BooleanLiteralType;
interface BaseType {
kind: 'base';
name:
| 'URI'
| 'DocumentUri'
| 'integer'
| 'uinteger'
| 'decimal'
| 'RegExp'
| 'string'
| 'boolean'
| 'null';
}
interface ReferenceType {
kind: 'reference';
name: string;
}
interface ArrayType {
kind: 'array';
element: Type;
}
interface MapType {
kind: 'map';
key: Type;
value: Type;
}
interface AndType {
kind: 'and';
items: Type[];
}
interface OrType {
kind: 'or';
items: Type[];
}
interface TupleType {
kind: 'tuple';
items: Type[];
}
interface StructureLiteralType {
kind: 'literal';
value: StructureLiteral;
}
interface StructureLiteral {
properties: Property[];
documentation?: string;
since?: string;
proposed?: boolean;
deprecated?: string;
}
interface StringLiteralType {
kind: 'stringLiteral';
value: string;
}
interface IntegerLiteralType {
kind: 'integerLiteral';
value: number;
}
interface BooleanLiteralType {
kind: 'booleanLiteral';
value: boolean;
}
/**
* TypeScript types generator for LSP client
*/
class LSPTypesGenerator {
private writer = new LineWriter();
/**
* Load and parse the metaModel.json file
*/
private loadMetaModel(): MetaModel {
const metaModelPath = path.join(__dirname, '..', 'metaModel.json');
const content = fs.readFileSync(metaModelPath, 'utf-8');
return JSON.parse(content) as MetaModel;
}
/**
* Convert Type to TypeScript type string
*/
private typeToTypeScript(type: Type): string {
switch (type.kind) {
case 'base':
switch (type.name) {
case 'string':
case 'DocumentUri':
case 'URI':
return 'string';
case 'integer':
case 'uinteger':
case 'decimal':
return 'number';
case 'boolean':
return 'boolean';
case 'null':
return 'null';
case 'RegExp':
return 'RegExp';
default:
return 'any';
}
case 'reference':
return type.name;
case 'array':
return `(${this.typeToTypeScript(type.element)})[]`;
case 'map':
return `{ [key: ${this.typeToTypeScript(type.key)}]: ${this.typeToTypeScript(
type.value
)} }`;
case 'and':
return type.items.map((item) => this.typeToTypeScript(item)).join(' & ');
case 'or':
return type.items.map((item) => this.typeToTypeScript(item)).join(' | ');
case 'tuple':
return `[${type.items.map((item) => this.typeToTypeScript(item)).join(', ')}]`;
case 'literal':
return this.structureLiteralToTypeScript(type.value);
case 'stringLiteral':
return `'${type.value}'`;
case 'integerLiteral':
return type.value.toString();
case 'booleanLiteral':
return type.value.toString();
default:
return 'any';
}
}
/**
* Convert structure literal to TypeScript interface
*/
private structureLiteralToTypeScript(literal: StructureLiteral): string {
const properties = literal.properties.map((prop) => {
const optional = prop.optional ? '?' : '';
return `${prop.name}${optional}: ${this.typeToTypeScript(prop.type)}`;
});
return `{\n ${properties.join(';\n ')}\n}`;
}
/**
* Generate TypeScript interface for a structure
*/
private generateStructure(structure: Structure): void {
if (structure.documentation) {
this.writer.writeLine('/**');
this.writer.writeLine(` * ${structure.documentation.replace(/\n/g, '\n * ')}`);
this.writer.writeLine(' */');
}
// Build extends clause combining extends and mixins
const allParents: string[] = [];
if (structure.extends && structure.extends.length > 0) {
allParents.push(...structure.extends.map((type) => this.typeToTypeScript(type)));
}
if (structure.mixins && structure.mixins.length > 0) {
allParents.push(...structure.mixins.map((type) => this.typeToTypeScript(type)));
}
const extendsClause = allParents.length > 0 ? ` extends ${allParents.join(', ')}` : '';
this.writer.writeLine(`export interface ${structure.name}${extendsClause} {`);
this.writer.indent();
// Add properties
for (const property of structure.properties) {
if (property.documentation) {
this.writer.writeLine('/**');
this.writer.writeLine(` * ${property.documentation.replace(/\n/g, '\n * ')}`);
this.writer.writeLine(' */');
}
const optional = property.optional ? '?' : '';
this.writer.writeLine(
`${property.name}${optional}: ${this.typeToTypeScript(property.type)};`
);
}
this.writer.outdent();
this.writer.writeLine('}');
this.writer.writeLine('');
}
/**
* Generate TypeScript enum for an enumeration
*/
private generateEnumeration(enumeration: Enumeration): void {
if (enumeration.documentation) {
this.writer.writeLine('/**');
this.writer.writeLine(` * ${enumeration.documentation.replace(/\n/g, '\n * ')}`);
this.writer.writeLine(' */');
}
this.writer.writeLine(`export enum ${enumeration.name} {`);
this.writer.indent();
for (let i = 0; i < enumeration.values.length; i++) {
const entry = enumeration.values[i];
if (entry.documentation) {
this.writer.writeLine('/**');
this.writer.writeLine(` * ${entry.documentation.replace(/\n/g, '\n * ')}`);
this.writer.writeLine(' */');
}
const isLast = i === enumeration.values.length - 1;
const comma = isLast ? '' : ',';
if (typeof entry.value === 'string') {
this.writer.writeLine(`${entry.name} = '${entry.value}'${comma}`);
} else {
this.writer.writeLine(`${entry.name} = ${entry.value}${comma}`);
}
}
this.writer.outdent();
this.writer.writeLine('}');
this.writer.writeLine('');
}
/**
* Generate TypeScript type alias
*/
private generateTypeAlias(typeAlias: TypeAlias): void {
if (typeAlias.documentation) {
this.writer.writeLine('/**');
this.writer.writeLine(` * ${typeAlias.documentation.replace(/\n/g, '\n * ')}`);
this.writer.writeLine(' */');
}
this.writer.writeLine(
`export type ${typeAlias.name} = ${this.typeToTypeScript(typeAlias.type)};`
);
this.writer.writeLine('');
}
/**
* Generate the Capability class
*/
private generateCapabilityClass(): void {
this.writer.writeLine('/**');
this.writer.writeLine(
' * Represents a capability with its associated method and registration options type'
);
this.writer.writeLine(' */');
this.writer.writeLine('export class Capability<T> {');
this.writer.indent();
this.writer.writeLine('constructor(public readonly method: string) {}');
this.writer.outdent();
this.writer.writeLine('}');
this.writer.writeLine('');
}
/**
* Generate the capabilities map
*/
private generateCapabilitiesMap(metaModel: MetaModel): void {
this.writer.writeLine('/**');
this.writer.writeLine(' * Map of all LSP capabilities with their registration options');
this.writer.writeLine(' */');
this.writer.writeLine('export const capabilities = {');
this.writer.indent();
// Collect all requests and notifications with registration options
const itemsWithRegistration: Array<{ method: string; registrationOptions?: Type }> = [];
for (const request of metaModel.requests) {
if (request.registrationOptions) {
itemsWithRegistration.push({
method: request.method,
registrationOptions: request.registrationOptions
});
}
}
for (const notification of metaModel.notifications) {
if (notification.registrationOptions) {
itemsWithRegistration.push({
method: notification.method,
registrationOptions: notification.registrationOptions
});
}
}
// Generate capability entries
for (const item of itemsWithRegistration) {
const methodIdentifier = this.methodToIdentifier(item.method);
const registrationType = item.registrationOptions
? this.typeToTypeScript(item.registrationOptions)
: 'unknown';
this.writer.writeLine(
`${methodIdentifier}: new Capability<${registrationType}>('${item.method}'),`
);
}
this.writer.outdent();
this.writer.writeLine('};');
this.writer.writeLine('');
}
/**
* Convert LSP method name to valid JavaScript identifier
*/
private methodToIdentifier(method: string): string {
const parts = method
.replace(/\$/g, '') // Remove $ characters
.split('/') // Split on forward slashes
.filter((part) => part.length > 0); // Remove empty parts
return parts
.map((part, index) => {
// Convert kebab-case to camelCase for each part
const camelCase = part.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
// Capitalize first letter of all parts except the first non-empty part
return index === 0 ? camelCase : camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
})
.join('');
}
/**
* Generate the API contract object
*/
private generateApiContract(metaModel: MetaModel): void {
this.writer.writeLine('/**');
this.writer.writeLine(' * LSP API Contract');
this.writer.writeLine(' */');
this.writer.writeLine('export const api = contract({');
this.writer.indent();
this.writer.writeLine('name: "LSP",');
// Helper function to generate request entries
const generateRequest = (request: Request, isOptional: boolean = false) => {
const methodIdentifier = this.methodToIdentifier(request.method);
const paramsType = this.getParamsType(request.params);
const resultType = this.typeToTypeScript(request.result);
if (request.documentation) {
this.writer.writeLine('/**');
this.writer.writeLine(` * ${request.documentation.replace(/\n/g, '\n * ')}`);
this.writer.writeLine(' */');
}
const optional = isOptional ? '.optional()' : '';
this.writer.writeLine(
`${methodIdentifier}: unverifiedRequest<${paramsType}, ${resultType}>({ method: "${request.method}" })${optional},`
);
};
// Helper function to generate notification entries
const generateNotification = (notification: Notification) => {
const methodIdentifier = this.methodToIdentifier(notification.method);
const paramsType = this.getParamsType(notification.params);
if (notification.documentation) {
this.writer.writeLine('/**');
this.writer.writeLine(` * ${notification.documentation.replace(/\n/g, '\n * ')}`);
this.writer.writeLine(' */');
}
this.writer.writeLine(
`${methodIdentifier}: unverifiedNotification<${paramsType}>({ method: "${notification.method}" }),`
);
};
// Server section
this.writer.writeLine('server: {');
this.writer.indent();
// Server requests (sent from client to server)
for (const request of metaModel.requests) {
if (request.messageDirection === 'clientToServer' || request.messageDirection === 'both') {
generateRequest(request);
}
}
// Server notifications (sent from client to server)
for (const notification of metaModel.notifications) {
if (
notification.messageDirection === 'clientToServer' ||
notification.messageDirection === 'both'
) {
generateNotification(notification);
}
}
this.writer.outdent();
this.writer.writeLine('},');
// Client section
this.writer.writeLine('client: {');
this.writer.indent();
// Client requests (handled by server)
for (const request of metaModel.requests) {
if (request.messageDirection === 'serverToClient' || request.messageDirection === 'both') {
generateRequest(request, true); // serverToClient requests are optional
}
}
// Client notifications (sent from server to client)
for (const notification of metaModel.notifications) {
if (
notification.messageDirection === 'serverToClient' ||
notification.messageDirection === 'both'
) {
generateNotification(notification);
}
}
this.writer.outdent();
this.writer.writeLine('}');
this.writer.outdent();
this.writer.writeLine('});');
this.writer.writeLine('');
}
/**
* Helper method to get parameter type
*/
private getParamsType(params?: Type | Type[]): string {
if (!params) {
return 'void';
}
if (Array.isArray(params)) {
const paramTypes = params.map((p) => this.typeToTypeScript(p));
return `[${paramTypes.join(', ')}]`;
} else {
return this.typeToTypeScript(params);
}
}
/**
* Generate the complete TypeScript types
*/
generate(): void {
const metaModel = this.loadMetaModel();
this.writer.clear();
this.writer.writeLine('// Generated TypeScript definitions for LSP');
this.writer.writeLine(`// Protocol version: ${metaModel.metaData.version}`);
this.writer.writeLine('// This file is auto-generated. Do not edit manually.');
this.writer.writeLine('');
// Import contract types from @hediet/json-rpc
this.writer.writeLine('import {');
this.writer.indent();
this.writer.writeLine('contract,');
this.writer.writeLine('Contract,');
this.writer.writeLine('unverifiedRequest,');
this.writer.writeLine('unverifiedNotification,');
this.writer.outdent();
this.writer.writeLine('} from "@hediet/json-rpc";');
this.writer.writeLine('');
// Generate enumerations
for (const enumeration of metaModel.enumerations) {
this.generateEnumeration(enumeration);
}
// Generate type aliases
for (const typeAlias of metaModel.typeAliases) {
this.generateTypeAlias(typeAlias);
}
// Generate structures
for (const structure of metaModel.structures) {
this.generateStructure(structure);
}
// Generate Capability class
this.generateCapabilityClass();
// Generate capabilities map
this.generateCapabilitiesMap(metaModel);
// Generate API contract
this.generateApiContract(metaModel);
// Write types file
const srcDir = path.join(__dirname, '..', 'src');
if (!fs.existsSync(srcDir)) {
fs.mkdirSync(srcDir, { recursive: true });
}
fs.writeFileSync(path.join(srcDir, 'types.ts'), this.writer.toString());
console.log('Generated LSP types file: src/types.ts');
}
}
// Run the generator
if (require.main === module) {
const generator = new LSPTypesGenerator();
generator.generate();
}

1564
monaco-lsp-client/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
{
"name": "@vscode/monaco-lsp-client",
"description": "description",
"authors": "vscode",
"version": "0.1.0",
"main": "out/index.js",
"types": "out/index.d.ts",
"dependencies": {
"@hediet/json-rpc": "^0.5.0",
"@hediet/json-rpc-browser": "^0.5.1",
"@hediet/json-rpc-websocket": "^0.5.1"
},
"peerDependencies": {
"monaco-editor-core": "^0.54.0-dev-20250929"
},
"devDependencies": {
"rolldown": "^1.0.0-beta.41",
"rolldown-plugin-dts": "^0.16.11",
"rollup-plugin-delete": "^3.0.1"
},
"scripts": {
"build": "npx rolldown -c rolldown.config.mjs",
"dev": "npx rolldown -c rolldown.config.mjs --watch",
"generate": "tsx generator/index.ts"
}
}

View file

@ -0,0 +1,33 @@
// @ts-check
import { join } from 'path';
import { defineConfig } from 'rolldown';
import { dts } from 'rolldown-plugin-dts';
import del from 'rollup-plugin-delete';
import alias from '@rollup/plugin-alias';
export default defineConfig({
input: {
index: join(import.meta.dirname, './src/index.ts')
},
output: {
dir: join(import.meta.dirname, './out'),
format: 'es'
},
external: ['monaco-editor-core'],
plugins: [
del({ targets: 'out/*' }),
alias({
entries: {
ws: 'undefined'
}
}),
dts({
tsconfig: false,
compilerOptions: {
stripInternal: true
},
resolve: true
})
]
});

View file

@ -0,0 +1,40 @@
import * as monaco from 'monaco-editor-core';
import { Position, Range, TextDocumentIdentifier } from '../../src/types';
export interface ITextModelBridge {
translate(
textModel: monaco.editor.ITextModel,
monacoPos: monaco.Position
): {
textDocument: TextDocumentIdentifier;
position: Position;
};
translateRange(textModel: monaco.editor.ITextModel, monacoRange: monaco.Range): Range;
translateBack(
textDocument: TextDocumentIdentifier,
position: Position
): {
textModel: monaco.editor.ITextModel;
position: monaco.Position;
};
translateBackRange(
textDocument: TextDocumentIdentifier,
range: Range
): {
textModel: monaco.editor.ITextModel;
range: monaco.Range;
};
}
export function assertTargetTextModel<T extends { textModel: monaco.editor.ITextModel }>(
input: T,
expectedTextModel: monaco.editor.ITextModel
): T {
if (input.textModel !== expectedTextModel) {
throw new Error(`Expected text model to be ${expectedTextModel}, but got ${input.textModel}`);
}
return input;
}

View file

@ -0,0 +1,254 @@
import { TypedChannel } from '@hediet/json-rpc';
import { ClientCapabilities, Capability, ServerCapabilities, api, capabilities, TextDocumentChangeRegistrationOptions, TextDocumentSyncKind } from '../../src/types';
import { IDisposable, Disposable } from '../utils';
export interface ILspCapabilitiesRegistry {
addStaticClientCapabilities(capability: ClientCapabilities): IDisposable;
registerCapabilityHandler<T>(capability: Capability<T>, handleStaticCapability: boolean, handler: (capability: T) => IDisposable): IDisposable;
}
export class LspCapabilitiesRegistry extends Disposable implements ILspCapabilitiesRegistry {
private readonly _staticCapabilities = new Set<{ cap: ClientCapabilities; }>();
private readonly _dynamicFromStatic = DynamicFromStaticOptions.create();
private readonly _registrations = new Map<Capability<any>, CapabilityInfo<any>>();
private _serverCapabilities: ServerCapabilities | undefined = undefined;
constructor(
private readonly _connection: TypedChannel
) {
super();
this._register(this._connection.registerRequestHandler(api.client.clientRegisterCapability, async (params) => {
for (const registration of params.registrations) {
const capability = getCapabilityByMethod(registration.method);
const r = new CapabilityRegistration(registration.id, capability, registration.registerOptions, false);
this._registerCapabilityOptions(r);
}
return { ok: null };
}));
this._register(this._connection.registerRequestHandler(api.client.clientUnregisterCapability, async (params) => {
for (const unregistration of params.unregisterations) {
const capability = getCapabilityByMethod(unregistration.method);
const info = this._registrations.get(capability);
const handlerInfo = info?.registrations.get(unregistration.id);
if (!handlerInfo) {
throw new Error(`No registration for method ${unregistration.method} with id ${unregistration.id}`);
}
handlerInfo?.handlerDisposables.forEach(d => d.dispose());
info?.registrations.delete(unregistration.id);
}
return { ok: null };
}));
}
private _registerCapabilityOptions<T>(registration: CapabilityRegistration<T>) {
let registrationForMethod = this._registrations.get(registration.capability);
if (!registrationForMethod) {
registrationForMethod = new CapabilityInfo();
this._registrations.set(registration.capability, registrationForMethod);
}
if (registrationForMethod.registrations.has(registration.id)) {
throw new Error(`Handler for method ${registration.capability.method} with id ${registration.id} already registered`);
}
registrationForMethod.registrations.set(registration.id, registration);
for (const h of registrationForMethod.handlers) {
if (!h.handleStaticCapability && registration.isFromStatic) {
continue;
}
registration.handlerDisposables.set(h, h.handler(registration.options));
}
}
setServerCapabilities(serverCapabilities: ServerCapabilities) {
if (this._serverCapabilities) {
throw new Error('Server capabilities already set');
}
this._serverCapabilities = serverCapabilities;
for (const cap of Object.values(capabilities)) {
const options = this._dynamicFromStatic.getOptions(cap, serverCapabilities);
if (options) {
this._registerCapabilityOptions(new CapabilityRegistration(cap.method, cap, options, true));
}
}
}
getClientCapabilities(): ClientCapabilities {
const result: ClientCapabilities = {};
for (const c of this._staticCapabilities) {
deepAssign(result, c.cap);
}
return result;
}
addStaticClientCapabilities(capability: ClientCapabilities): IDisposable {
const obj = { cap: capability };
this._staticCapabilities.add(obj);
return {
dispose: () => {
this._staticCapabilities.delete(obj);
}
};
}
registerCapabilityHandler<T>(capability: Capability<T>, handleStaticCapability: boolean, handler: (capability: T) => IDisposable): IDisposable {
let info = this._registrations.get(capability);
if (!info) {
info = new CapabilityInfo();
this._registrations.set(capability, info);
}
const handlerInfo = new CapabilityHandler(capability, handleStaticCapability, handler);
info.handlers.add(handlerInfo);
for (const registration of info.registrations.values()) {
if (!handlerInfo.handleStaticCapability && registration.isFromStatic) {
continue;
}
registration.handlerDisposables.set(handlerInfo, handler(registration.options));
}
return {
dispose: () => {
info.handlers.delete(handlerInfo);
for (const registration of info.registrations.values()) {
const disposable = registration.handlerDisposables.get(handlerInfo);
if (disposable) {
disposable.dispose();
registration.handlerDisposables.delete(handlerInfo);
}
}
}
};
}
}
class CapabilityHandler<T> {
constructor(
public readonly capability: Capability<T>,
public readonly handleStaticCapability: boolean,
public readonly handler: (capabilityOptions: T) => IDisposable
) { }
}
class CapabilityRegistration<T> {
public readonly handlerDisposables = new Map<CapabilityHandler<any>, IDisposable>();
constructor(
public readonly id: string,
public readonly capability: Capability<T>,
public readonly options: T,
public readonly isFromStatic: boolean
) { }
}
const capabilitiesByMethod = new Map([...Object.values(capabilities)].map(c => [c.method, c]));
function getCapabilityByMethod(method: string): Capability<any> {
const c = capabilitiesByMethod.get(method);
if (!c) {
throw new Error(`No capability found for method ${method}`);
}
return c;
}
class CapabilityInfo<T> {
public readonly handlers = new Set<CapabilityHandler<T>>();
public readonly registrations = new Map</* id */ string, CapabilityRegistration<T>>();
}
class DynamicFromStaticOptions {
private readonly _mappings = new Map</* method */ string, (serverCapabilities: ServerCapabilities) => any>();
public static create(): DynamicFromStaticOptions {
const o = new DynamicFromStaticOptions();
o.set(capabilities.textDocumentDidChange, s => {
if (s.textDocumentSync === undefined) {
return undefined;
}
if (typeof s.textDocumentSync === 'object') {
return {
syncKind: s.textDocumentSync.change ?? TextDocumentSyncKind.None,
documentSelector: null,
} satisfies TextDocumentChangeRegistrationOptions;
} else {
return {
syncKind: s.textDocumentSync,
documentSelector: null,
} satisfies TextDocumentChangeRegistrationOptions;
}
return null!;
});
o.set(capabilities.textDocumentCompletion, s => s.completionProvider);
o.set(capabilities.textDocumentHover, s => s.hoverProvider);
o.set(capabilities.textDocumentSignatureHelp, s => s.signatureHelpProvider);
o.set(capabilities.textDocumentDefinition, s => s.definitionProvider);
o.set(capabilities.textDocumentReferences, s => s.referencesProvider);
o.set(capabilities.textDocumentDocumentHighlight, s => s.documentHighlightProvider);
o.set(capabilities.textDocumentDocumentSymbol, s => s.documentSymbolProvider);
o.set(capabilities.textDocumentCodeAction, s => s.codeActionProvider);
o.set(capabilities.textDocumentCodeLens, s => s.codeLensProvider);
o.set(capabilities.textDocumentDocumentLink, s => s.documentLinkProvider);
o.set(capabilities.textDocumentFormatting, s => s.documentFormattingProvider);
o.set(capabilities.textDocumentRangeFormatting, s => s.documentRangeFormattingProvider);
o.set(capabilities.textDocumentOnTypeFormatting, s => s.documentOnTypeFormattingProvider);
o.set(capabilities.textDocumentRename, s => s.renameProvider);
o.set(capabilities.textDocumentFoldingRange, s => s.foldingRangeProvider);
o.set(capabilities.textDocumentDeclaration, s => s.declarationProvider);
o.set(capabilities.textDocumentTypeDefinition, s => s.typeDefinitionProvider);
o.set(capabilities.textDocumentImplementation, s => s.implementationProvider);
o.set(capabilities.textDocumentDocumentColor, s => s.colorProvider);
o.set(capabilities.textDocumentSelectionRange, s => s.selectionRangeProvider);
o.set(capabilities.textDocumentLinkedEditingRange, s => s.linkedEditingRangeProvider);
o.set(capabilities.textDocumentPrepareCallHierarchy, s => s.callHierarchyProvider);
o.set(capabilities.textDocumentSemanticTokensFull, s => s.semanticTokensProvider);
o.set(capabilities.textDocumentInlayHint, s => s.inlayHintProvider);
o.set(capabilities.textDocumentInlineValue, s => s.inlineValueProvider);
o.set(capabilities.textDocumentDiagnostic, s => s.diagnosticProvider);
o.set(capabilities.textDocumentMoniker, s => s.monikerProvider);
o.set(capabilities.textDocumentPrepareTypeHierarchy, s => s.typeHierarchyProvider);
o.set(capabilities.workspaceSymbol, s => s.workspaceSymbolProvider);
o.set(capabilities.workspaceExecuteCommand, s => s.executeCommandProvider);
return o;
}
set<T>(capability: Capability<T>, getOptionsFromStatic: (serverCapabilities: ServerCapabilities) => T | boolean | undefined): void {
if (this._mappings.has(capability.method)) {
throw new Error(`Capability for method ${capability.method} already registered`);
}
this._mappings.set(capability.method, getOptionsFromStatic);
}
getOptions<T>(capability: Capability<T>, serverCapabilities: ServerCapabilities): T | undefined {
const getter = this._mappings.get(capability.method);
if (!getter) {
return undefined;
}
const result = getter(serverCapabilities);
return result;
}
}
function deepAssign(target: any, source: any) {
for (const key of Object.keys(source)) {
const srcValue = source[key];
if (srcValue === undefined) {
continue;
}
const tgtValue = target[key];
if (tgtValue === undefined) {
target[key] = srcValue;
continue;
}
if (typeof srcValue !== 'object' || srcValue === null) {
target[key] = srcValue;
continue;
}
if (typeof tgtValue !== 'object' || tgtValue === null) {
target[key] = srcValue;
continue;
}
deepAssign(tgtValue, srcValue);
}
}

View file

@ -0,0 +1,90 @@
import { IMessageTransport, TypedChannel } from "@hediet/json-rpc";
import { LspCompletionFeature } from "./languageFeatures/LspCompletionFeature";
import { LspHoverFeature } from "./languageFeatures/LspHoverFeature";
import { LspSignatureHelpFeature } from "./languageFeatures/LspSignatureHelpFeature";
import { LspDefinitionFeature } from "./languageFeatures/LspDefinitionFeature";
import { LspDeclarationFeature } from "./languageFeatures/LspDeclarationFeature";
import { LspTypeDefinitionFeature } from "./languageFeatures/LspTypeDefinitionFeature";
import { LspImplementationFeature } from "./languageFeatures/LspImplementationFeature";
import { LspReferencesFeature } from "./languageFeatures/LspReferencesFeature";
import { LspDocumentHighlightFeature } from "./languageFeatures/LspDocumentHighlightFeature";
import { LspDocumentSymbolFeature } from "./languageFeatures/LspDocumentSymbolFeature";
import { LspRenameFeature } from "./languageFeatures/LspRenameFeature";
import { LspCodeActionFeature } from "./languageFeatures/LspCodeActionFeature";
import { LspCodeLensFeature } from "./languageFeatures/LspCodeLensFeature";
import { LspDocumentLinkFeature } from "./languageFeatures/LspDocumentLinkFeature";
import { LspFormattingFeature } from "./languageFeatures/LspFormattingFeature";
import { LspRangeFormattingFeature } from "./languageFeatures/LspRangeFormattingFeature";
import { LspOnTypeFormattingFeature } from "./languageFeatures/LspOnTypeFormattingFeature";
import { LspFoldingRangeFeature } from "./languageFeatures/LspFoldingRangeFeature";
import { LspSelectionRangeFeature } from "./languageFeatures/LspSelectionRangeFeature";
import { LspInlayHintsFeature } from "./languageFeatures/LspInlayHintsFeature";
import { LspSemanticTokensFeature } from "./languageFeatures/LspSemanticTokensFeature";
import { LspDiagnosticsFeature } from "./languageFeatures/LspDiagnosticsFeature";
import { api } from "../../src/types";
import { LspConnection } from "./LspConnection";
import { LspCapabilitiesRegistry } from './LspCapabilitiesRegistry';
import { TextDocumentSynchronizer } from "./TextDocumentSynchronizer";
import { DisposableStore, IDisposable } from "../utils";
export class MonacoLspClient {
private _connection: LspConnection;
private readonly _capabilitiesRegistry: LspCapabilitiesRegistry;
private readonly _bridge: TextDocumentSynchronizer;
private _initPromise: Promise<void>;
constructor(transport: IMessageTransport) {
const c = TypedChannel.fromTransport(transport);
const s = api.getServer(c, {});
c.startListen();
this._capabilitiesRegistry = new LspCapabilitiesRegistry(c);
this._bridge = new TextDocumentSynchronizer(s.server, this._capabilitiesRegistry);
this._connection = new LspConnection(s.server, this._bridge, this._capabilitiesRegistry, c);
this.createFeatures();
this._initPromise = this._init();
}
private async _init() {
const result = await this._connection.server.initialize({
processId: null,
capabilities: this._capabilitiesRegistry.getClientCapabilities(),
rootUri: null,
});
this._connection.server.initialized({});
this._capabilitiesRegistry.setServerCapabilities(result.capabilities);
}
protected createFeatures(): IDisposable {
const store = new DisposableStore();
store.add(new LspCompletionFeature(this._connection));
store.add(new LspHoverFeature(this._connection));
store.add(new LspSignatureHelpFeature(this._connection));
store.add(new LspDefinitionFeature(this._connection));
store.add(new LspDeclarationFeature(this._connection));
store.add(new LspTypeDefinitionFeature(this._connection));
store.add(new LspImplementationFeature(this._connection));
store.add(new LspReferencesFeature(this._connection));
store.add(new LspDocumentHighlightFeature(this._connection));
store.add(new LspDocumentSymbolFeature(this._connection));
store.add(new LspRenameFeature(this._connection));
store.add(new LspCodeActionFeature(this._connection));
store.add(new LspCodeLensFeature(this._connection));
store.add(new LspDocumentLinkFeature(this._connection));
store.add(new LspFormattingFeature(this._connection));
store.add(new LspRangeFormattingFeature(this._connection));
store.add(new LspOnTypeFormattingFeature(this._connection));
store.add(new LspFoldingRangeFeature(this._connection));
store.add(new LspSelectionRangeFeature(this._connection));
store.add(new LspInlayHintsFeature(this._connection));
store.add(new LspSemanticTokensFeature(this._connection));
store.add(new LspDiagnosticsFeature(this._connection));
return store;
}
}

View file

@ -0,0 +1,13 @@
import { TypedChannel } from '@hediet/json-rpc';
import { api } from '../../src/types';
import { ITextModelBridge } from './ITextModelBridge';
import { LspCapabilitiesRegistry } from './LspCapabilitiesRegistry';
export class LspConnection {
constructor(
public readonly server: typeof api.TServerInterface,
public readonly bridge: ITextModelBridge,
public readonly capabilities: LspCapabilitiesRegistry,
public readonly connection: TypedChannel,
) { }
}

View file

@ -0,0 +1,183 @@
import * as monaco from 'monaco-editor-core';
import { api, capabilities, Position, Range, TextDocumentContentChangeEvent, TextDocumentIdentifier } from '../../src/types';
import { Disposable } from '../utils';
import { ITextModelBridge } from './ITextModelBridge';
import { ILspCapabilitiesRegistry } from './LspCapabilitiesRegistry';
export class TextDocumentSynchronizer extends Disposable implements ITextModelBridge {
private readonly _managedModels = new Map<monaco.editor.ITextModel, ManagedModel>();
private readonly _managedModelsReverse = new Map</* uri */ string, monaco.editor.ITextModel>();
private _started = false;
constructor(
private readonly _server: typeof api.TServerInterface,
private readonly _capabilities: ILspCapabilitiesRegistry,
) {
super();
this._register(this._capabilities.addStaticClientCapabilities({
textDocument: {
synchronization: {
dynamicRegistration: true,
willSave: false,
willSaveWaitUntil: false,
didSave: false,
}
}
}));
this._register(_capabilities.registerCapabilityHandler(capabilities.textDocumentDidChange, true, e => {
if (this._started) {
return {
dispose: () => {
}
}
}
this._started = true;
this._register(monaco.editor.onDidCreateModel(m => {
this._getOrCreateManagedModel(m);
}));
for (const m of monaco.editor.getModels()) {
this._getOrCreateManagedModel(m);
}
return {
dispose: () => {
}
}
}));
}
private _getOrCreateManagedModel(m: monaco.editor.ITextModel) {
if (!this._started) {
throw new Error('Not started');
}
const uriStr = m.uri.toString(true).toLowerCase();
let mm = this._managedModels.get(m);
if (!mm) {
mm = new ManagedModel(m, this._server);
this._managedModels.set(m, mm);
this._managedModelsReverse.set(uriStr, m);
}
m.onWillDispose(() => {
mm!.dispose();
this._managedModels.delete(m);
this._managedModelsReverse.delete(uriStr);
});
return mm;
}
translateBack(textDocument: TextDocumentIdentifier, position: Position): { textModel: monaco.editor.ITextModel; position: monaco.Position; } {
const uri = textDocument.uri.toLowerCase();
const textModel = this._managedModelsReverse.get(uri);
if (!textModel) {
throw new Error(`No text model for uri ${uri}`);
}
const monacoPosition = new monaco.Position(position.line + 1, position.character + 1);
return { textModel, position: monacoPosition };
}
translateBackRange(textDocument: TextDocumentIdentifier, range: Range): { textModel: monaco.editor.ITextModel; range: monaco.Range; } {
const uri = textDocument.uri.toLowerCase();
const textModel = this._managedModelsReverse.get(uri);
if (!textModel) {
throw new Error(`No text model for uri ${uri}`);
}
const monacoRange = new monaco.Range(
range.start.line + 1,
range.start.character + 1,
range.end.line + 1,
range.end.character + 1
);
return { textModel, range: monacoRange };
}
translate(textModel: monaco.editor.ITextModel, monacoPos: monaco.Position): { textDocument: TextDocumentIdentifier; position: Position; } {
return {
textDocument: {
uri: textModel.uri.toString(true),
},
position: {
line: monacoPos.lineNumber - 1,
character: monacoPos.column - 1,
}
};
}
translateRange(textModel: monaco.editor.ITextModel, monacoRange: monaco.Range): Range {
return {
start: {
line: monacoRange.startLineNumber - 1,
character: monacoRange.startColumn - 1,
},
end: {
line: monacoRange.endLineNumber - 1,
character: monacoRange.endColumn - 1,
}
};
}
}
class ManagedModel extends Disposable {
constructor(
private readonly _textModel: monaco.editor.ITextModel,
private readonly _api: typeof api.TServerInterface
) {
super();
const uri = _textModel.uri.toString(true).toLowerCase();
this._api.textDocumentDidOpen({
textDocument: {
languageId: _textModel.getLanguageId(),
uri: uri,
version: _textModel.getVersionId(),
text: _textModel.getValue(),
}
});
this._register(_textModel.onDidChangeContent(e => {
const contentChanges = e.changes.map(c => toLspTextDocumentContentChangeEvent(c));
this._api.textDocumentDidChange({
textDocument: {
uri: uri,
version: _textModel.getVersionId(),
},
contentChanges: contentChanges
});
}));
this._register({
dispose: () => {
this._api.textDocumentDidClose({
textDocument: {
uri: uri,
}
});
}
});
}
}
function toLspTextDocumentContentChangeEvent(change: monaco.editor.IModelContentChange): TextDocumentContentChangeEvent {
return {
range: toLspRange(change.range),
rangeLength: change.rangeLength,
text: change.text,
};
}
function toLspRange(range: monaco.IRange): Range {
return {
start: {
line: range.startLineNumber - 1,
character: range.startColumn - 1,
},
end: {
line: range.endLineNumber - 1,
character: range.endColumn - 1,
}
};
}

View file

@ -0,0 +1,169 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, CodeActionRegistrationOptions, Command, WorkspaceEdit, CodeAction } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import { lspCodeActionKindToMonacoCodeActionKind, toMonacoCodeActionKind, toLspDiagnosticSeverity, toLspCodeActionTriggerKind, toMonacoCommand } from './common';
export class LspCodeActionFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
codeAction: {
dynamicRegistration: true,
codeActionLiteralSupport: {
codeActionKind: {
valueSet: Array.from(lspCodeActionKindToMonacoCodeActionKind.keys()),
}
},
isPreferredSupport: true,
disabledSupport: true,
dataSupport: true,
resolveSupport: {
properties: ['edit'],
},
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentCodeAction, true, capability => {
return monaco.languages.registerCodeActionProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspCodeActionProvider(this._connection, capability),
);
}));
}
}
interface ExtendedCodeAction extends monaco.languages.CodeAction {
_lspAction?: CodeAction;
}
class LspCodeActionProvider implements monaco.languages.CodeActionProvider {
public readonly resolveCodeAction;
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: CodeActionRegistrationOptions,
) {
if (_capabilities.resolveProvider) {
this.resolveCodeAction = async (codeAction: ExtendedCodeAction, token: monaco.CancellationToken): Promise<ExtendedCodeAction> => {
if (codeAction._lspAction) {
const resolved = await this._client.server.codeActionResolve(codeAction._lspAction);
if (resolved.edit) {
codeAction.edit = toMonacoWorkspaceEdit(resolved.edit, this._client);
}
if (resolved.command) {
codeAction.command = toMonacoCommand(resolved.command);
}
}
return codeAction;
};
}
}
async provideCodeActions(
model: monaco.editor.ITextModel,
range: monaco.Range,
context: monaco.languages.CodeActionContext,
token: monaco.CancellationToken
): Promise<monaco.languages.CodeActionList | null> {
const translated = this._client.bridge.translate(model, range.getStartPosition());
const result = await this._client.server.textDocumentCodeAction({
textDocument: translated.textDocument,
range: this._client.bridge.translateRange(model, range),
context: {
diagnostics: context.markers.map(marker => ({
range: this._client.bridge.translateRange(model, monaco.Range.lift(marker)),
message: marker.message,
severity: toLspDiagnosticSeverity(marker.severity),
})),
triggerKind: toLspCodeActionTriggerKind(context.trigger),
},
});
if (!result) {
return null;
}
const actions = Array.isArray(result) ? result : [result];
return {
actions: actions.map(action => {
if ('title' in action && !('kind' in action)) {
// Command
const cmd = action as Command;
const monacoAction: ExtendedCodeAction = {
title: cmd.title,
command: toMonacoCommand(cmd),
};
return monacoAction;
} else {
// CodeAction
const codeAction = action as CodeAction;
const monacoAction: ExtendedCodeAction = {
title: codeAction.title,
kind: toMonacoCodeActionKind(codeAction.kind),
isPreferred: codeAction.isPreferred,
disabled: codeAction.disabled?.reason,
edit: codeAction.edit ? toMonacoWorkspaceEdit(codeAction.edit, this._client) : undefined,
command: toMonacoCommand(codeAction.command),
_lspAction: codeAction,
};
return monacoAction;
}
}),
dispose: () => { },
};
}
}
function toMonacoWorkspaceEdit(
edit: WorkspaceEdit,
client: LspConnection
): monaco.languages.WorkspaceEdit {
const edits: monaco.languages.IWorkspaceTextEdit[] = [];
if (edit.changes) {
for (const uri in edit.changes) {
const textEdits = edit.changes[uri];
for (const textEdit of textEdits) {
const translated = client.bridge.translateBackRange({ uri }, textEdit.range);
edits.push({
resource: translated.textModel.uri,
versionId: undefined,
textEdit: {
range: translated.range,
text: textEdit.newText,
},
});
}
}
}
if (edit.documentChanges) {
for (const change of edit.documentChanges) {
if ('textDocument' in change) {
const uri = change.textDocument.uri;
for (const textEdit of change.edits) {
const translated = client.bridge.translateBackRange({ uri }, textEdit.range);
edits.push({
resource: translated.textModel.uri,
versionId: change.textDocument.version ?? undefined,
textEdit: {
range: translated.range,
text: textEdit.newText,
},
});
}
}
}
}
return { edits };
}

View file

@ -0,0 +1,90 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, CodeLensRegistrationOptions, CodeLens } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import { assertTargetTextModel } from '../ITextModelBridge';
import { toMonacoCommand } from './common';
export class LspCodeLensFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
codeLens: {
dynamicRegistration: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentCodeLens, true, capability => {
return monaco.languages.registerCodeLensProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspCodeLensProvider(this._connection, capability),
);
}));
}
}
interface ExtendedCodeLens extends monaco.languages.CodeLens {
_lspCodeLens?: CodeLens;
}
class LspCodeLensProvider implements monaco.languages.CodeLensProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: CodeLensRegistrationOptions,
) { }
async provideCodeLenses(
model: monaco.editor.ITextModel,
token: monaco.CancellationToken
): Promise<monaco.languages.CodeLensList | null> {
const translated = this._client.bridge.translate(model, new monaco.Position(1, 1));
const result = await this._client.server.textDocumentCodeLens({
textDocument: translated.textDocument,
});
if (!result) {
return null;
}
return {
lenses: result.map(lens => {
const monacoLens: ExtendedCodeLens = {
range: assertTargetTextModel(this._client.bridge.translateBackRange(translated.textDocument, lens.range), model).range,
command: toMonacoCommand(lens.command),
_lspCodeLens: lens,
};
return monacoLens;
}),
dispose: () => { },
};
}
async resolveCodeLens(
model: monaco.editor.ITextModel,
codeLens: ExtendedCodeLens,
token: monaco.CancellationToken
): Promise<monaco.languages.CodeLens> {
if (!this._capabilities.resolveProvider || !codeLens._lspCodeLens) {
return codeLens;
}
const resolved = await this._client.server.codeLensResolve(codeLens._lspCodeLens);
if (resolved.command) {
codeLens.command = {
id: resolved.command.command,
title: resolved.command.title,
arguments: resolved.command.arguments,
};
}
return codeLens;
}
}

View file

@ -0,0 +1,202 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, CompletionRegistrationOptions, MarkupContent, CompletionItem, TextDocumentPositionParams } from '../../../src/types';
import { assertTargetTextModel, ITextModelBridge } from '../ITextModelBridge';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import {
lspCompletionItemKindToMonacoCompletionItemKind,
lspCompletionItemTagToMonacoCompletionItemTag,
toMonacoCompletionItemKind,
toMonacoCompletionItemTag,
toLspCompletionTriggerKind,
toMonacoInsertTextRules,
toMonacoCommand,
} from './common';
export class LspCompletionFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
completion: {
dynamicRegistration: true,
contextSupport: true,
completionItemKind: {
valueSet: Array.from(lspCompletionItemKindToMonacoCompletionItemKind.keys()),
},
completionItem: {
tagSupport: {
valueSet: Array.from(lspCompletionItemTagToMonacoCompletionItemTag.keys()),
},
commitCharactersSupport: true,
deprecatedSupport: true,
preselectSupport: true,
}
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentCompletion, true, capability => {
return monaco.languages.registerCompletionItemProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspCompletionProvider(this._connection, capability),
);
}));
}
}
interface ExtendedCompletionItem extends monaco.languages.CompletionItem {
_lspItem: CompletionItem;
_translated: TextDocumentPositionParams;
_model: monaco.editor.ITextModel;
}
class LspCompletionProvider implements monaco.languages.CompletionItemProvider {
public readonly resolveCompletionItem;
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: CompletionRegistrationOptions,
) {
if (_capabilities.resolveProvider) {
this.resolveCompletionItem = async (item: ExtendedCompletionItem, token: monaco.CancellationToken): Promise<ExtendedCompletionItem> => {
const resolved = await this._client.server.completionItemResolve(item._lspItem);
applyLspCompletionItemProperties(item, resolved, this._client.bridge, item._translated, item._model);
return item;
}
}
}
get triggerCharacters(): string[] | undefined {
return this._capabilities.triggerCharacters;
}
async provideCompletionItems(
model: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.CompletionContext,
token: monaco.CancellationToken
): Promise<monaco.languages.CompletionList & { suggestions: ExtendedCompletionItem[] }> {
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentCompletion({
textDocument: translated.textDocument,
position: translated.position,
context: context.triggerCharacter ? {
triggerKind: toLspCompletionTriggerKind(context.triggerKind),
triggerCharacter: context.triggerCharacter,
} : undefined,
});
if (!result) {
return { suggestions: [] };
}
const items = Array.isArray(result) ? result : result.items;
return {
suggestions: items.map<ExtendedCompletionItem>(i => {
const item: ExtendedCompletionItem = {
...convertLspToMonacoCompletionItem(
i,
this._client.bridge,
translated,
model,
position
),
_lspItem: i,
_translated: translated,
_model: model,
};
return item;
})
};
}
}
function convertLspToMonacoCompletionItem(
lspItem: CompletionItem,
bridge: ITextModelBridge,
translated: TextDocumentPositionParams,
model: monaco.editor.ITextModel,
position: monaco.Position
): monaco.languages.CompletionItem {
let insertText = lspItem.insertText || lspItem.label;
let range: monaco.IRange | monaco.languages.CompletionItemRanges | undefined = undefined;
if (lspItem.textEdit) {
if ('range' in lspItem.textEdit) {
insertText = lspItem.textEdit.newText;
range = assertTargetTextModel(bridge.translateBackRange(translated.textDocument, lspItem.textEdit.range), model).range;
} else {
insertText = lspItem.textEdit.newText;
range = {
insert: assertTargetTextModel(bridge.translateBackRange(translated.textDocument, lspItem.textEdit.insert), model).range,
replace: assertTargetTextModel(bridge.translateBackRange(translated.textDocument, lspItem.textEdit.replace), model).range,
};
}
}
if (!range) {
range = monaco.Range.fromPositions(position, position);
}
const item: monaco.languages.CompletionItem = {
label: lspItem.label,
kind: toMonacoCompletionItemKind(lspItem.kind),
insertText,
sortText: lspItem.sortText,
filterText: lspItem.filterText,
preselect: lspItem.preselect,
commitCharacters: lspItem.commitCharacters,
range: range,
};
applyLspCompletionItemProperties(item, lspItem, bridge, translated, model);
return item;
}
function applyLspCompletionItemProperties(
monacoItem: monaco.languages.CompletionItem,
lspItem: CompletionItem,
bridge: ITextModelBridge,
translated: TextDocumentPositionParams,
targetModel: monaco.editor.ITextModel
): void {
if (lspItem.detail !== undefined) {
monacoItem.detail = lspItem.detail;
}
if (lspItem.documentation !== undefined) {
monacoItem.documentation = toMonacoDocumentation(lspItem.documentation);
}
if (lspItem.insertTextFormat !== undefined) {
const insertTextRules = toMonacoInsertTextRules(lspItem.insertTextFormat);
monacoItem.insertTextRules = insertTextRules;
}
if (lspItem.tags && lspItem.tags.length > 0) {
monacoItem.tags = lspItem.tags.map(toMonacoCompletionItemTag).filter((tag): tag is monaco.languages.CompletionItemTag => tag !== undefined);
}
if (lspItem.additionalTextEdits && lspItem.additionalTextEdits.length > 0) {
monacoItem.additionalTextEdits = lspItem.additionalTextEdits.map(edit => ({
range: assertTargetTextModel(bridge.translateBackRange(translated.textDocument, edit.range), targetModel).range,
text: edit.newText,
}));
}
if (lspItem.command) {
monacoItem.command = toMonacoCommand(lspItem.command);
}
}
function toMonacoDocumentation(doc: string | MarkupContent | undefined): string | monaco.IMarkdownString | undefined {
if (!doc) return undefined;
if (typeof doc === 'string') return doc;
return {
value: doc.value,
isTrusted: true,
};
}

View file

@ -0,0 +1,60 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, DeclarationRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import { toMonacoLocation } from "./common";
export class LspDeclarationFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
declaration: {
dynamicRegistration: true,
linkSupport: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentDeclaration, true, capability => {
return monaco.languages.registerDeclarationProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspDeclarationProvider(this._connection, capability),
);
}));
}
}
class LspDeclarationProvider implements monaco.languages.DeclarationProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: DeclarationRegistrationOptions,
) { }
async provideDeclaration(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken
): Promise<monaco.languages.Definition | monaco.languages.LocationLink[] | null> {
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentDeclaration({
textDocument: translated.textDocument,
position: translated.position,
});
if (!result) {
return null;
}
if (Array.isArray(result)) {
return result.map(loc => toMonacoLocation(loc, this._client));
}
return toMonacoLocation(result, this._client);
}
}

View file

@ -0,0 +1,60 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, DefinitionRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import { toMonacoLocation } from "./common";
export class LspDefinitionFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
definition: {
dynamicRegistration: true,
linkSupport: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentDefinition, true, capability => {
return monaco.languages.registerDefinitionProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspDefinitionProvider(this._connection, capability),
);
}));
}
}
class LspDefinitionProvider implements monaco.languages.DefinitionProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: DefinitionRegistrationOptions,
) { }
async provideDefinition(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken
): Promise<monaco.languages.Definition | monaco.languages.LocationLink[] | null> {
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentDefinition({
textDocument: translated.textDocument,
position: translated.position,
});
if (!result) {
return null;
}
if (Array.isArray(result)) {
return result.map(loc => toMonacoLocation(loc, this._client));
}
return toMonacoLocation(result, this._client);
}
}

View file

@ -0,0 +1,208 @@
import * as monaco from 'monaco-editor-core';
import { api, capabilities, Diagnostic, DiagnosticRegistrationOptions, DocumentDiagnosticReport, PublishDiagnosticsParams } from '../../../src/types';
import { Disposable, DisposableStore } from '../../utils';
import { LspConnection } from '../LspConnection';
import { lspDiagnosticTagToMonacoMarkerTag, matchesDocumentSelector, toDiagnosticMarker } from './common';
export class LspDiagnosticsFeature extends Disposable {
private readonly _diagnosticsMarkerOwner = 'lsp';
private readonly _pullDiagnosticProviders = new Map<monaco.editor.ITextModel, ModelDiagnosticProvider>();
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
publishDiagnostics: {
relatedInformation: true,
tagSupport: {
valueSet: [...lspDiagnosticTagToMonacoMarkerTag.keys()],
},
versionSupport: true,
codeDescriptionSupport: true,
dataSupport: true,
},
diagnostic: {
dynamicRegistration: true,
relatedDocumentSupport: true,
}
}
}));
debugger;
this._register(this._connection.connection.registerNotificationHandler(
api.client.textDocumentPublishDiagnostics,
(params) => this._handlePublishDiagnostics(params)
));
this._register(this._connection.capabilities.registerCapabilityHandler(
capabilities.textDocumentDiagnostic,
true,
(capability) => {
const disposables = new DisposableStore();
for (const model of monaco.editor.getModels()) {
this._addPullDiagnosticProvider(model, capability, disposables);
}
disposables.add(monaco.editor.onDidCreateModel(model => {
this._addPullDiagnosticProvider(model, capability, disposables);
}));
return disposables;
}
));
}
private _addPullDiagnosticProvider(
model: monaco.editor.ITextModel,
capability: DiagnosticRegistrationOptions,
disposables: DisposableStore
): void {
// Check if model matches the document selector
const languageId = model.getLanguageId();
if (!matchesDocumentSelector(model, capability.documentSelector)) {
return;
}
const provider = new ModelDiagnosticProvider(
model,
this._connection,
this._diagnosticsMarkerOwner,
capability
);
this._pullDiagnosticProviders.set(model, provider);
disposables.add(provider);
disposables.add(model.onWillDispose(() => {
this._pullDiagnosticProviders.delete(model);
}));
}
private _handlePublishDiagnostics(params: PublishDiagnosticsParams): void {
const uri = params.uri;
try {
const translated = this._connection.bridge.translateBack({ uri }, { line: 0, character: 0 });
const model = translated.textModel;
if (!model || model.isDisposed()) {
return;
}
const markers = params.diagnostics.map(diagnostic =>
toDiagnosticMarker(diagnostic)
);
monaco.editor.setModelMarkers(model, this._diagnosticsMarkerOwner, markers);
} catch (error) {
// Model not found or already disposed - this is normal when files are closed
console.debug(`Could not set diagnostics for ${uri}:`, error);
}
}
}
/**
* Manages pull diagnostics for a single text model
*/
class ModelDiagnosticProvider extends Disposable {
private _updateHandle: number | undefined;
private _previousResultId: string | undefined;
constructor(
private readonly _model: monaco.editor.ITextModel,
private readonly _connection: LspConnection,
private readonly _markerOwner: string,
private readonly _capability: DiagnosticRegistrationOptions,
) {
super();
this._register(this._model.onDidChangeContent(() => {
this._scheduleDiagnosticUpdate();
}));
this._scheduleDiagnosticUpdate();
}
private _scheduleDiagnosticUpdate(): void {
if (this._updateHandle !== undefined) {
clearTimeout(this._updateHandle);
}
this._updateHandle = window.setTimeout(() => {
this._updateHandle = undefined;
this._requestDiagnostics();
}, 500);
}
private async _requestDiagnostics(): Promise<void> {
if (this._model.isDisposed()) {
return;
}
try {
const translated = this._connection.bridge.translate(this._model, new monaco.Position(1, 1));
const result = await this._connection.server.textDocumentDiagnostic({
textDocument: translated.textDocument,
identifier: this._capability.identifier,
previousResultId: this._previousResultId,
});
if (this._model.isDisposed()) {
return;
}
this._handleDiagnosticReport(result);
} catch (error) {
console.error('Error requesting diagnostics:', error);
}
}
private _handleDiagnosticReport(report: DocumentDiagnosticReport): void {
if (report.kind === 'full') {
// Full diagnostic report
this._previousResultId = report.resultId;
const markers = report.items.map(diagnostic => toDiagnosticMarker(diagnostic));
monaco.editor.setModelMarkers(this._model, this._markerOwner, markers);
// Handle related documents if present
if ('relatedDocuments' in report && report.relatedDocuments) {
this._handleRelatedDocuments(report.relatedDocuments);
}
} else if (report.kind === 'unchanged') {
// Unchanged report - diagnostics are still valid
this._previousResultId = report.resultId;
// No need to update markers
}
}
private _handleRelatedDocuments(relatedDocuments: { [key: string]: any }): void {
for (const [uri, report] of Object.entries(relatedDocuments)) {
try {
const translated = this._connection.bridge.translateBack({ uri }, { line: 0, character: 0 });
const model = translated.textModel;
if (!model || model.isDisposed()) {
continue;
}
if (report.kind === 'full') {
const markers = report.items.map((diagnostic: Diagnostic) => toDiagnosticMarker(diagnostic));
monaco.editor.setModelMarkers(model, this._markerOwner, markers);
}
} catch (error) {
// Model not found - this is normal
console.debug(`Could not set related diagnostics for ${uri}:`, error);
}
}
}
override dispose(): void {
if (this._updateHandle !== undefined) {
clearTimeout(this._updateHandle);
this._updateHandle = undefined;
}
super.dispose();
}
}

View file

@ -0,0 +1,58 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, DocumentHighlightRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import { toMonacoDocumentHighlightKind } from './common';
export class LspDocumentHighlightFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
documentHighlight: {
dynamicRegistration: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentDocumentHighlight, true, capability => {
return monaco.languages.registerDocumentHighlightProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspDocumentHighlightProvider(this._connection, capability),
);
}));
}
}
class LspDocumentHighlightProvider implements monaco.languages.DocumentHighlightProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: DocumentHighlightRegistrationOptions,
) { }
async provideDocumentHighlights(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken
): Promise<monaco.languages.DocumentHighlight[] | null> {
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentDocumentHighlight({
textDocument: translated.textDocument,
position: translated.position,
});
if (!result) {
return null;
}
return result.map(highlight => ({
range: this._client.bridge.translateBackRange(translated.textDocument, highlight.range).range,
kind: toMonacoDocumentHighlightKind(highlight.kind),
}));
}
}

View file

@ -0,0 +1,71 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, DocumentLinkRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
export class LspDocumentLinkFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
documentLink: {
dynamicRegistration: true,
tooltipSupport: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentDocumentLink, true, capability => {
return monaco.languages.registerLinkProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspDocumentLinkProvider(this._connection, capability),
);
}));
}
}
class LspDocumentLinkProvider implements monaco.languages.LinkProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: DocumentLinkRegistrationOptions,
) { }
async provideLinks(
model: monaco.editor.ITextModel,
token: monaco.CancellationToken
): Promise<monaco.languages.ILinksList | null> {
const translated = this._client.bridge.translate(model, new monaco.Position(1, 1));
const result = await this._client.server.textDocumentDocumentLink({
textDocument: translated.textDocument,
});
if (!result) {
return null;
}
return {
links: result.map(link => ({
range: this._client.bridge.translateBackRange(translated.textDocument, link.range).range,
url: link.target,
tooltip: link.tooltip,
})),
};
}
async resolveLink(
link: monaco.languages.ILink,
token: monaco.CancellationToken
): Promise<monaco.languages.ILink> {
if (!this._capabilities.resolveProvider) {
return link;
}
// TODO: Implement resolve
return link;
}
}

View file

@ -0,0 +1,101 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, DocumentSymbolRegistrationOptions, DocumentSymbol, SymbolInformation, SymbolTag } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import { lspSymbolKindToMonacoSymbolKind, toMonacoSymbolKind, toMonacoSymbolTag } from './common';
export class LspDocumentSymbolFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
documentSymbol: {
dynamicRegistration: true,
hierarchicalDocumentSymbolSupport: true,
symbolKind: {
valueSet: Array.from(lspSymbolKindToMonacoSymbolKind.keys()),
},
tagSupport: {
valueSet: [SymbolTag.Deprecated],
},
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentDocumentSymbol, true, capability => {
return monaco.languages.registerDocumentSymbolProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspDocumentSymbolProvider(this._connection, capability),
);
}));
}
}
class LspDocumentSymbolProvider implements monaco.languages.DocumentSymbolProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: DocumentSymbolRegistrationOptions,
) { }
async provideDocumentSymbols(
model: monaco.editor.ITextModel,
token: monaco.CancellationToken
): Promise<monaco.languages.DocumentSymbol[] | null> {
const translated = this._client.bridge.translate(model, new monaco.Position(1, 1));
const result = await this._client.server.textDocumentDocumentSymbol({
textDocument: translated.textDocument,
});
if (!result) {
return null;
}
if (Array.isArray(result) && result.length > 0) {
if ('location' in result[0]) {
// SymbolInformation[]
return (result as SymbolInformation[]).map(symbol => toMonacoSymbolInformation(symbol, this._client));
} else {
// DocumentSymbol[]
return (result as DocumentSymbol[]).map(symbol => toMonacoDocumentSymbol(symbol, this._client, translated.textDocument));
}
}
return [];
}
}
function toMonacoDocumentSymbol(
symbol: DocumentSymbol,
client: LspConnection,
textDocument: { uri: string }
): monaco.languages.DocumentSymbol {
return {
name: symbol.name,
detail: symbol.detail || '',
kind: toMonacoSymbolKind(symbol.kind),
tags: symbol.tags?.map(tag => toMonacoSymbolTag(tag)).filter((t): t is monaco.languages.SymbolTag => t !== undefined) || [],
range: client.bridge.translateBackRange(textDocument, symbol.range).range,
selectionRange: client.bridge.translateBackRange(textDocument, symbol.selectionRange).range,
children: symbol.children?.map(child => toMonacoDocumentSymbol(child, client, textDocument)) || [],
};
}
function toMonacoSymbolInformation(
symbol: SymbolInformation,
client: LspConnection
): monaco.languages.DocumentSymbol {
return {
name: symbol.name,
detail: '',
kind: toMonacoSymbolKind(symbol.kind),
tags: symbol.tags?.map(tag => toMonacoSymbolTag(tag)).filter((t): t is monaco.languages.SymbolTag => t !== undefined) || [],
range: client.bridge.translateBackRange({ uri: symbol.location.uri }, symbol.location.range).range,
selectionRange: client.bridge.translateBackRange({ uri: symbol.location.uri }, symbol.location.range).range,
children: [],
};
}

View file

@ -0,0 +1,63 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, FoldingRangeRegistrationOptions, FoldingRangeKind } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import { toMonacoFoldingRangeKind } from './common';
export class LspFoldingRangeFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
foldingRange: {
dynamicRegistration: true,
rangeLimit: 5000,
lineFoldingOnly: false,
foldingRangeKind: {
valueSet: [FoldingRangeKind.Comment, FoldingRangeKind.Imports, FoldingRangeKind.Region],
},
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentFoldingRange, true, capability => {
return monaco.languages.registerFoldingRangeProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspFoldingRangeProvider(this._connection, capability),
);
}));
}
}
class LspFoldingRangeProvider implements monaco.languages.FoldingRangeProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: FoldingRangeRegistrationOptions,
) { }
async provideFoldingRanges(
model: monaco.editor.ITextModel,
context: monaco.languages.FoldingContext,
token: monaco.CancellationToken
): Promise<monaco.languages.FoldingRange[] | null> {
const translated = this._client.bridge.translate(model, new monaco.Position(1, 1));
const result = await this._client.server.textDocumentFoldingRange({
textDocument: translated.textDocument,
});
if (!result) {
return null;
}
return result.map(range => ({
start: range.startLine + 1,
end: range.endLine + 1,
kind: toMonacoFoldingRangeKind(range.kind),
}));
}
}

View file

@ -0,0 +1,60 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, DocumentFormattingRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
export class LspFormattingFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
formatting: {
dynamicRegistration: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentFormatting, true, capability => {
return monaco.languages.registerDocumentFormattingEditProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspDocumentFormattingProvider(this._connection, capability),
);
}));
}
}
class LspDocumentFormattingProvider implements monaco.languages.DocumentFormattingEditProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: DocumentFormattingRegistrationOptions,
) { }
async provideDocumentFormattingEdits(
model: monaco.editor.ITextModel,
options: monaco.languages.FormattingOptions,
token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[] | null> {
const translated = this._client.bridge.translate(model, new monaco.Position(1, 1));
const result = await this._client.server.textDocumentFormatting({
textDocument: translated.textDocument,
options: {
tabSize: options.tabSize,
insertSpaces: options.insertSpaces,
},
});
if (!result) {
return null;
}
return result.map(edit => ({
range: this._client.bridge.translateBackRange(translated.textDocument, edit.range).range,
text: edit.newText,
}));
}
}

View file

@ -0,0 +1,79 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, HoverRegistrationOptions, MarkupContent, MarkedString, MarkupKind } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
export class LspHoverFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
hover: {
dynamicRegistration: true,
contentFormat: [MarkupKind.Markdown, MarkupKind.PlainText],
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentHover, true, capability => {
return monaco.languages.registerHoverProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspHoverProvider(this._connection, capability),
);
}));
}
}
class LspHoverProvider implements monaco.languages.HoverProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: HoverRegistrationOptions,
) { }
async provideHover(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken
): Promise<monaco.languages.Hover | null> {
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentHover({
textDocument: translated.textDocument,
position: translated.position,
});
if (!result || !result.contents) {
return null;
}
return {
contents: toMonacoMarkdownString(result.contents),
range: result.range ? this._client.bridge.translateBackRange(translated.textDocument, result.range).range : undefined,
};
}
}
function toMonacoMarkdownString(
contents: MarkupContent | MarkedString | MarkedString[]
): monaco.IMarkdownString[] {
if (Array.isArray(contents)) {
return contents.map(c => toSingleMarkdownString(c));
}
return [toSingleMarkdownString(contents)];
}
function toSingleMarkdownString(content: MarkupContent | MarkedString): monaco.IMarkdownString {
if (typeof content === 'string') {
return { value: content, isTrusted: true };
}
if ('kind' in content) {
// MarkupContent
return { value: content.value, isTrusted: true };
}
// MarkedString with language
return { value: `\`\`\`${content.language}\n${content.value}\n\`\`\``, isTrusted: true };
}

View file

@ -0,0 +1,60 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, ImplementationRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import { toMonacoLocation } from "./common";
export class LspImplementationFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
implementation: {
dynamicRegistration: true,
linkSupport: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentImplementation, true, capability => {
return monaco.languages.registerImplementationProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspImplementationProvider(this._connection, capability),
);
}));
}
}
class LspImplementationProvider implements monaco.languages.ImplementationProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: ImplementationRegistrationOptions,
) { }
async provideImplementation(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken
): Promise<monaco.languages.Definition | monaco.languages.LocationLink[] | null> {
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentImplementation({
textDocument: translated.textDocument,
position: translated.position,
});
if (!result) {
return null;
}
if (Array.isArray(result)) {
return result.map(loc => toMonacoLocation(loc, this._client));
}
return toMonacoLocation(result, this._client);
}
}

View file

@ -0,0 +1,212 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, InlayHintRegistrationOptions, InlayHint, MarkupContent, api } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import { assertTargetTextModel } from '../ITextModelBridge';
import { toMonacoCommand, toMonacoInlayHintKind } from './common';
export class LspInlayHintsFeature extends Disposable {
private readonly _providers = new Set<LspInlayHintsProvider>();
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
inlayHint: {
dynamicRegistration: true,
resolveSupport: {
properties: ['tooltip', 'textEdits', 'label.tooltip', 'label.location', 'label.command'],
},
}
},
workspace: {
inlayHint: {
refreshSupport: true,
}
}
}));
this._register(this._connection.connection.registerRequestHandler(api.client.workspaceInlayHintRefresh, async () => {
// Fire onDidChangeInlayHints for all providers
for (const provider of this._providers) {
provider.refresh();
}
return { ok: null };
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentInlayHint, true, capability => {
const provider = new LspInlayHintsProvider(this._connection, capability);
this._providers.add(provider);
const disposable = monaco.languages.registerInlayHintsProvider(
toMonacoLanguageSelector(capability.documentSelector),
provider,
);
return {
dispose: () => {
this._providers.delete(provider);
disposable.dispose();
}
};
}));
}
}
interface ExtendedInlayHint extends monaco.languages.InlayHint {
_lspInlayHint: InlayHint;
_targetUri: string;
}
class LspInlayHintsProvider implements monaco.languages.InlayHintsProvider {
private readonly _onDidChangeInlayHints = new monaco.Emitter<void>();
public readonly onDidChangeInlayHints = this._onDidChangeInlayHints.event;
public readonly resolveInlayHint;
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: InlayHintRegistrationOptions,
) {
if (_capabilities.resolveProvider) {
this.resolveInlayHint = async (hint: ExtendedInlayHint, token: monaco.CancellationToken): Promise<monaco.languages.InlayHint> => {
const resolved = await this._client.server.inlayHintResolve(hint._lspInlayHint);
if (resolved.tooltip) {
hint.tooltip = toMonacoTooltip(resolved.tooltip);
}
if (resolved.label !== hint._lspInlayHint.label) {
hint.label = toLspInlayHintLabel(resolved.label);
}
if (resolved.textEdits) {
hint.textEdits = resolved.textEdits.map(edit => {
const translated = this._client.bridge.translateBackRange(
{ uri: hint._targetUri },
edit.range
);
return {
range: translated.range,
text: edit.newText,
};
});
}
return hint;
};
}
}
public refresh(): void {
this._onDidChangeInlayHints.fire();
}
async provideInlayHints(
model: monaco.editor.ITextModel,
range: monaco.Range,
token: monaco.CancellationToken
): Promise<monaco.languages.InlayHintList | null> {
const translated = this._client.bridge.translate(model, range.getStartPosition());
const result = await retryOnContentModified(async () =>
await this._client.server.textDocumentInlayHint({
textDocument: translated.textDocument,
range: this._client.bridge.translateRange(model, range),
})
);
if (!result) {
return null;
}
return {
hints: result.map(hint => {
const monacoHint: ExtendedInlayHint = {
label: toLspInlayHintLabel(hint.label),
position: assertTargetTextModel(
this._client.bridge.translateBack(translated.textDocument, hint.position),
model
).position,
kind: toMonacoInlayHintKind(hint.kind),
tooltip: toMonacoTooltip(hint.tooltip),
paddingLeft: hint.paddingLeft,
paddingRight: hint.paddingRight,
textEdits: hint.textEdits?.map(edit => ({
range: assertTargetTextModel(
this._client.bridge.translateBackRange(translated.textDocument, edit.range),
model
).range,
text: edit.newText,
})),
_lspInlayHint: hint,
_targetUri: translated.textDocument.uri,
};
return monacoHint;
}),
dispose: () => { },
};
}
}
async function retryOnContentModified<T>(cb: () => Promise<T>): Promise<T> {
const nRetries = 3;
for (let triesLeft = nRetries; ; triesLeft--) {
try {
return await cb();
} catch (e: any) {
if (e.message === 'content modified' && triesLeft > 0) {
continue;
}
throw e;
}
}
}
function toLspInlayHintLabel(label: string | any[]): string | monaco.languages.InlayHintLabelPart[] {
if (typeof label === 'string') {
return label;
}
return label.map(part => {
const monacoLabelPart: monaco.languages.InlayHintLabelPart = {
label: part.value,
tooltip: toMonacoTooltip(part.tooltip),
command: toMonacoCommand(part.command),
};
if (part.location) {
monacoLabelPart.location = {
uri: monaco.Uri.parse(part.location.uri),
range: new monaco.Range(
part.location.range.start.line + 1,
part.location.range.start.character + 1,
part.location.range.end.line + 1,
part.location.range.end.character + 1
),
};
}
return monacoLabelPart;
});
}
function toMonacoTooltip(tooltip: string | MarkupContent | undefined): string | monaco.IMarkdownString | undefined {
if (!tooltip) {
return undefined;
}
if (typeof tooltip === 'string') {
return tooltip;
}
return {
value: tooltip.value,
isTrusted: true,
};
}

View file

@ -0,0 +1,71 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, DocumentOnTypeFormattingRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
export class LspOnTypeFormattingFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
onTypeFormatting: {
dynamicRegistration: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentOnTypeFormatting, true, capability => {
return monaco.languages.registerOnTypeFormattingEditProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspOnTypeFormattingProvider(this._connection, capability),
);
}));
}
}
class LspOnTypeFormattingProvider implements monaco.languages.OnTypeFormattingEditProvider {
public readonly autoFormatTriggerCharacters: string[];
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: DocumentOnTypeFormattingRegistrationOptions,
) {
this.autoFormatTriggerCharacters = [
_capabilities.firstTriggerCharacter,
...(_capabilities.moreTriggerCharacter || [])
];
}
async provideOnTypeFormattingEdits(
model: monaco.editor.ITextModel,
position: monaco.Position,
ch: string,
options: monaco.languages.FormattingOptions,
token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[] | null> {
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentOnTypeFormatting({
textDocument: translated.textDocument,
position: translated.position,
ch,
options: {
tabSize: options.tabSize,
insertSpaces: options.insertSpaces,
},
});
if (!result) {
return null;
}
return result.map(edit => ({
range: this._client.bridge.translateBackRange(translated.textDocument, edit.range).range,
text: edit.newText,
}));
}
}

View file

@ -0,0 +1,62 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, DocumentRangeFormattingRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
export class LspRangeFormattingFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
rangeFormatting: {
dynamicRegistration: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentRangeFormatting, true, capability => {
return monaco.languages.registerDocumentRangeFormattingEditProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspDocumentRangeFormattingProvider(this._connection, capability),
);
}));
}
}
class LspDocumentRangeFormattingProvider implements monaco.languages.DocumentRangeFormattingEditProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: DocumentRangeFormattingRegistrationOptions,
) { }
async provideDocumentRangeFormattingEdits(
model: monaco.editor.ITextModel,
range: monaco.Range,
options: monaco.languages.FormattingOptions,
token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[] | null> {
const translated = this._client.bridge.translate(model, range.getStartPosition());
const result = await this._client.server.textDocumentRangeFormatting({
textDocument: translated.textDocument,
range: this._client.bridge.translateRange(model, range),
options: {
tabSize: options.tabSize,
insertSpaces: options.insertSpaces,
},
});
if (!result) {
return null;
}
return result.map(edit => ({
range: this._client.bridge.translateBackRange(translated.textDocument, edit.range).range,
text: edit.newText,
}));
}
}

View file

@ -0,0 +1,64 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, ReferenceRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
export class LspReferencesFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
references: {
dynamicRegistration: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentReferences, true, capability => {
return monaco.languages.registerReferenceProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspReferenceProvider(this._connection, capability),
);
}));
}
}
class LspReferenceProvider implements monaco.languages.ReferenceProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: ReferenceRegistrationOptions,
) { }
async provideReferences(
model: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.ReferenceContext,
token: monaco.CancellationToken
): Promise<monaco.languages.Location[] | null> {
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentReferences({
textDocument: translated.textDocument,
position: translated.position,
context: {
includeDeclaration: context.includeDeclaration,
},
});
if (!result) {
return null;
}
return result.map(loc => {
const translated = this._client.bridge.translateBackRange({ uri: loc.uri }, loc.range);
return {
uri: translated.textModel.uri,
range: translated.range,
};
});
}
}

View file

@ -0,0 +1,142 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, RenameRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
export class LspRenameFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
rename: {
dynamicRegistration: true,
prepareSupport: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentRename, true, capability => {
return monaco.languages.registerRenameProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspRenameProvider(this._connection, capability),
);
}));
}
}
class LspRenameProvider implements monaco.languages.RenameProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: RenameRegistrationOptions,
) { }
async provideRenameEdits(
model: monaco.editor.ITextModel,
position: monaco.Position,
newName: string,
token: monaco.CancellationToken
): Promise<monaco.languages.WorkspaceEdit | null> {
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentRename({
textDocument: translated.textDocument,
position: translated.position,
newName,
});
if (!result) {
return null;
}
return toMonacoWorkspaceEdit(result, this._client);
}
async resolveRenameLocation(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken
): Promise<monaco.languages.RenameLocation | null> {
if (!this._capabilities.prepareProvider) {
return null;
}
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentPrepareRename({
textDocument: translated.textDocument,
position: translated.position,
});
if (!result) {
return null;
}
if ('range' in result && 'placeholder' in result) {
return {
range: this._client.bridge.translateBackRange(translated.textDocument, result.range).range,
text: result.placeholder,
};
} else if ('defaultBehavior' in result) {
return null;
} else if ('start' in result && 'end' in result) {
const range = this._client.bridge.translateBackRange(translated.textDocument, result).range;
return {
range,
text: model.getValueInRange(range),
};
}
return null;
}
}
function toMonacoWorkspaceEdit(
edit: any,
client: LspConnection
): monaco.languages.WorkspaceEdit {
const edits: monaco.languages.IWorkspaceTextEdit[] = [];
if (edit.changes) {
for (const uri in edit.changes) {
const textEdits = edit.changes[uri];
for (const textEdit of textEdits) {
const translated = client.bridge.translateBackRange({ uri }, textEdit.range);
edits.push({
resource: translated.textModel.uri,
versionId: undefined,
textEdit: {
range: translated.range,
text: textEdit.newText,
},
});
}
}
}
if (edit.documentChanges) {
for (const change of edit.documentChanges) {
if ('textDocument' in change) {
// TextDocumentEdit
const uri = change.textDocument.uri;
for (const textEdit of change.edits) {
const translated = client.bridge.translateBackRange({ uri }, textEdit.range);
edits.push({
resource: translated.textModel.uri,
versionId: change.textDocument.version,
textEdit: {
range: translated.range,
text: textEdit.newText,
},
});
}
}
// TODO: Handle CreateFile, RenameFile, DeleteFile
}
}
return { edits };
}

View file

@ -0,0 +1,71 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, SelectionRangeRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
export class LspSelectionRangeFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
selectionRange: {
dynamicRegistration: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentSelectionRange, true, capability => {
return monaco.languages.registerSelectionRangeProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspSelectionRangeProvider(this._connection, capability),
);
}));
}
}
class LspSelectionRangeProvider implements monaco.languages.SelectionRangeProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: SelectionRangeRegistrationOptions,
) { }
async provideSelectionRanges(
model: monaco.editor.ITextModel,
positions: monaco.Position[],
token: monaco.CancellationToken
): Promise<monaco.languages.SelectionRange[][] | null> {
const translated = this._client.bridge.translate(model, positions[0]);
const result = await this._client.server.textDocumentSelectionRange({
textDocument: translated.textDocument,
positions: positions.map(pos => this._client.bridge.translate(model, pos).position),
});
if (!result) {
return null;
}
return result.map(selRange => this.convertSelectionRange(selRange, translated.textDocument));
}
private convertSelectionRange(
range: any,
textDocument: { uri: string }
): monaco.languages.SelectionRange[] {
const result: monaco.languages.SelectionRange[] = [];
let current = range;
while (current) {
result.push({
range: this._client.bridge.translateBackRange(textDocument, current.range).range,
});
current = current.parent;
}
return result;
}
}

View file

@ -0,0 +1,130 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, SemanticTokensRegistrationOptions, TokenFormat } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
export class LspSemanticTokensFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
semanticTokens: {
dynamicRegistration: true,
requests: {
range: true,
full: {
delta: true,
},
},
tokenTypes: [
'namespace', 'type', 'class', 'enum', 'interface', 'struct',
'typeParameter', 'parameter', 'variable', 'property', 'enumMember',
'event', 'function', 'method', 'macro', 'keyword', 'modifier',
'comment', 'string', 'number', 'regexp', 'operator', 'decorator'
],
tokenModifiers: [
'declaration', 'definition', 'readonly', 'static', 'deprecated',
'abstract', 'async', 'modification', 'documentation', 'defaultLibrary'
],
formats: [TokenFormat.Relative],
overlappingTokenSupport: false,
multilineTokenSupport: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentSemanticTokensFull, true, capability => {
return monaco.languages.registerDocumentSemanticTokensProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspSemanticTokensProvider(this._connection, capability),
);
}));
}
}
class LspSemanticTokensProvider implements monaco.languages.DocumentSemanticTokensProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: SemanticTokensRegistrationOptions,
) { }
getLegend(): monaco.languages.SemanticTokensLegend {
return {
tokenTypes: this._capabilities.legend.tokenTypes,
tokenModifiers: this._capabilities.legend.tokenModifiers,
};
}
releaseDocumentSemanticTokens(resultId: string | undefined): void {
// Monaco will call this when it's done with a result
// We can potentially notify the server if needed
}
async provideDocumentSemanticTokens(
model: monaco.editor.ITextModel,
lastResultId: string | null,
token: monaco.CancellationToken
): Promise<monaco.languages.SemanticTokens | monaco.languages.SemanticTokensEdits | null> {
const translated = this._client.bridge.translate(model, model.getPositionAt(0));
// Try delta request if we have a previous result and server supports it
const full = this._capabilities.full;
if (lastResultId && full && typeof full === 'object' && full.delta) {
const deltaResult = await this._client.server.textDocumentSemanticTokensFullDelta({
textDocument: translated.textDocument,
previousResultId: lastResultId,
});
if (!deltaResult) {
return null;
}
// Check if it's a delta or full result
if ('edits' in deltaResult) {
// It's a delta
return {
resultId: deltaResult.resultId,
edits: deltaResult.edits.map(edit => ({
start: edit.start,
deleteCount: edit.deleteCount,
data: edit.data ? new Uint32Array(edit.data) : undefined,
})),
};
} else {
// It's a full result
return {
resultId: deltaResult.resultId,
data: new Uint32Array(deltaResult.data),
};
}
}
// Full request
const result = await this._client.server.textDocumentSemanticTokensFull({
textDocument: translated.textDocument,
});
if (!result) {
return null;
}
return {
resultId: result.resultId,
data: new Uint32Array(result.data),
};
}
async provideDocumentSemanticTokensEdits?(
model: monaco.editor.ITextModel,
previousResultId: string,
token: monaco.CancellationToken
): Promise<monaco.languages.SemanticTokens | monaco.languages.SemanticTokensEdits | null> {
// This method is called when Monaco wants to use delta updates
// We can delegate to provideDocumentSemanticTokens which handles both
return this.provideDocumentSemanticTokens(model, previousResultId, token);
}
}

View file

@ -0,0 +1,101 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, SignatureHelpRegistrationOptions, MarkupContent, MarkupKind } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import { toLspSignatureHelpTriggerKind } from './common';
export class LspSignatureHelpFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
signatureHelp: {
dynamicRegistration: true,
contextSupport: true,
signatureInformation: {
documentationFormat: [MarkupKind.Markdown, MarkupKind.PlainText],
parameterInformation: {
labelOffsetSupport: true,
},
activeParameterSupport: true,
}
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentSignatureHelp, true, capability => {
return monaco.languages.registerSignatureHelpProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspSignatureHelpProvider(this._connection, capability),
);
}));
}
}
class LspSignatureHelpProvider implements monaco.languages.SignatureHelpProvider {
public readonly signatureHelpTriggerCharacters?: readonly string[];
public readonly signatureHelpRetriggerCharacters?: readonly string[];
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: SignatureHelpRegistrationOptions,
) {
this.signatureHelpTriggerCharacters = _capabilities.triggerCharacters;
this.signatureHelpRetriggerCharacters = _capabilities.retriggerCharacters;
}
async provideSignatureHelp(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken,
context: monaco.languages.SignatureHelpContext
): Promise<monaco.languages.SignatureHelpResult | null> {
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentSignatureHelp({
textDocument: translated.textDocument,
position: translated.position,
context: {
triggerKind: toLspSignatureHelpTriggerKind(context.triggerKind),
triggerCharacter: context.triggerCharacter,
isRetrigger: context.isRetrigger,
},
});
if (!result) {
return null;
}
return {
value: {
signatures: result.signatures.map(sig => ({
label: sig.label,
documentation: toMonacoDocumentation(sig.documentation),
parameters: sig.parameters?.map(param => ({
label: param.label,
documentation: toMonacoDocumentation(param.documentation),
})) || [],
activeParameter: sig.activeParameter,
})),
activeSignature: result.activeSignature || 0,
activeParameter: result.activeParameter || 0,
},
dispose: () => { },
};
}
}
function toMonacoDocumentation(
doc: string | MarkupContent | undefined
): string | monaco.IMarkdownString | undefined {
if (!doc) return undefined;
if (typeof doc === 'string') return doc;
return {
value: doc.value,
isTrusted: true,
};
}

View file

@ -0,0 +1,60 @@
import * as monaco from 'monaco-editor-core';
import { capabilities, TypeDefinitionRegistrationOptions } from '../../../src/types';
import { Disposable } from '../../utils';
import { LspConnection } from '../LspConnection';
import { toMonacoLanguageSelector } from './common';
import { toMonacoLocation } from "./common";
export class LspTypeDefinitionFeature extends Disposable {
constructor(
private readonly _connection: LspConnection,
) {
super();
this._register(this._connection.capabilities.addStaticClientCapabilities({
textDocument: {
typeDefinition: {
dynamicRegistration: true,
linkSupport: true,
}
}
}));
this._register(this._connection.capabilities.registerCapabilityHandler(capabilities.textDocumentTypeDefinition, true, capability => {
return monaco.languages.registerTypeDefinitionProvider(
toMonacoLanguageSelector(capability.documentSelector),
new LspTypeDefinitionProvider(this._connection, capability),
);
}));
}
}
class LspTypeDefinitionProvider implements monaco.languages.TypeDefinitionProvider {
constructor(
private readonly _client: LspConnection,
private readonly _capabilities: TypeDefinitionRegistrationOptions,
) { }
async provideTypeDefinition(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken
): Promise<monaco.languages.Definition | monaco.languages.LocationLink[] | null> {
const translated = this._client.bridge.translate(model, position);
const result = await this._client.server.textDocumentTypeDefinition({
textDocument: translated.textDocument,
position: translated.position,
});
if (!result) {
return null;
}
if (Array.isArray(result)) {
return result.map(loc => toMonacoLocation(loc, this._client));
}
return toMonacoLocation(result, this._client);
}
}

View file

@ -0,0 +1,401 @@
import * as monaco from 'monaco-editor-core';
import {
CodeActionKind,
CodeActionTriggerKind,
Command,
CompletionItemKind,
CompletionItemTag,
CompletionTriggerKind,
Diagnostic,
DiagnosticSeverity,
DiagnosticTag,
DocumentHighlightKind,
DocumentSelector,
FoldingRangeKind,
InlayHintKind,
InsertTextFormat,
Location,
LocationLink,
SignatureHelpTriggerKind,
SymbolKind,
SymbolTag,
} from '../../../src/types';
import { LspConnection } from '../LspConnection';
// ============================================================================
// Code Action Kind
// ============================================================================
export const lspCodeActionKindToMonacoCodeActionKind = new Map<CodeActionKind, string>([
[CodeActionKind.Empty, ''],
[CodeActionKind.QuickFix, 'quickfix'],
[CodeActionKind.Refactor, 'refactor'],
[CodeActionKind.RefactorExtract, 'refactor.extract'],
[CodeActionKind.RefactorInline, 'refactor.inline'],
[CodeActionKind.RefactorRewrite, 'refactor.rewrite'],
[CodeActionKind.Source, 'source'],
[CodeActionKind.SourceOrganizeImports, 'source.organizeImports'],
[CodeActionKind.SourceFixAll, 'source.fixAll'],
]);
export function toMonacoCodeActionKind(kind: CodeActionKind | undefined): string | undefined {
if (!kind) {
return undefined;
}
return lspCodeActionKindToMonacoCodeActionKind.get(kind) ?? kind;
}
// ============================================================================
// Code Action Trigger Kind
// ============================================================================
export const monacoCodeActionTriggerTypeToLspCodeActionTriggerKind = new Map<monaco.languages.CodeActionTriggerType, CodeActionTriggerKind>([
[monaco.languages.CodeActionTriggerType.Invoke, CodeActionTriggerKind.Invoked],
[monaco.languages.CodeActionTriggerType.Auto, CodeActionTriggerKind.Automatic],
]);
export function toLspCodeActionTriggerKind(monacoTrigger: monaco.languages.CodeActionTriggerType): CodeActionTriggerKind {
return monacoCodeActionTriggerTypeToLspCodeActionTriggerKind.get(monacoTrigger) ?? CodeActionTriggerKind.Invoked;
}
// ============================================================================
// Completion Item Kind
// ============================================================================
export const lspCompletionItemKindToMonacoCompletionItemKind = new Map<CompletionItemKind, monaco.languages.CompletionItemKind>([
[CompletionItemKind.Text, monaco.languages.CompletionItemKind.Text],
[CompletionItemKind.Method, monaco.languages.CompletionItemKind.Method],
[CompletionItemKind.Function, monaco.languages.CompletionItemKind.Function],
[CompletionItemKind.Constructor, monaco.languages.CompletionItemKind.Constructor],
[CompletionItemKind.Field, monaco.languages.CompletionItemKind.Field],
[CompletionItemKind.Variable, monaco.languages.CompletionItemKind.Variable],
[CompletionItemKind.Class, monaco.languages.CompletionItemKind.Class],
[CompletionItemKind.Interface, monaco.languages.CompletionItemKind.Interface],
[CompletionItemKind.Module, monaco.languages.CompletionItemKind.Module],
[CompletionItemKind.Property, monaco.languages.CompletionItemKind.Property],
[CompletionItemKind.Unit, monaco.languages.CompletionItemKind.Unit],
[CompletionItemKind.Value, monaco.languages.CompletionItemKind.Value],
[CompletionItemKind.Enum, monaco.languages.CompletionItemKind.Enum],
[CompletionItemKind.Keyword, monaco.languages.CompletionItemKind.Keyword],
[CompletionItemKind.Snippet, monaco.languages.CompletionItemKind.Snippet],
[CompletionItemKind.Color, monaco.languages.CompletionItemKind.Color],
[CompletionItemKind.File, monaco.languages.CompletionItemKind.File],
[CompletionItemKind.Reference, monaco.languages.CompletionItemKind.Reference],
[CompletionItemKind.Folder, monaco.languages.CompletionItemKind.Folder],
[CompletionItemKind.EnumMember, monaco.languages.CompletionItemKind.EnumMember],
[CompletionItemKind.Constant, monaco.languages.CompletionItemKind.Constant],
[CompletionItemKind.Struct, monaco.languages.CompletionItemKind.Struct],
[CompletionItemKind.Event, monaco.languages.CompletionItemKind.Event],
[CompletionItemKind.Operator, monaco.languages.CompletionItemKind.Operator],
[CompletionItemKind.TypeParameter, monaco.languages.CompletionItemKind.TypeParameter],
]);
export function toMonacoCompletionItemKind(kind: CompletionItemKind | undefined): monaco.languages.CompletionItemKind {
if (!kind) {
return monaco.languages.CompletionItemKind.Text;
}
return lspCompletionItemKindToMonacoCompletionItemKind.get(kind) ?? monaco.languages.CompletionItemKind.Text;
}
// ============================================================================
// Completion Item Tag
// ============================================================================
export const lspCompletionItemTagToMonacoCompletionItemTag = new Map<CompletionItemTag, monaco.languages.CompletionItemTag>([
[CompletionItemTag.Deprecated, monaco.languages.CompletionItemTag.Deprecated],
]);
export function toMonacoCompletionItemTag(tag: CompletionItemTag): monaco.languages.CompletionItemTag | undefined {
return lspCompletionItemTagToMonacoCompletionItemTag.get(tag);
}
// ============================================================================
// Completion Trigger Kind
// ============================================================================
export const monacoCompletionTriggerKindToLspCompletionTriggerKind = new Map<monaco.languages.CompletionTriggerKind, CompletionTriggerKind>([
[monaco.languages.CompletionTriggerKind.Invoke, CompletionTriggerKind.Invoked],
[monaco.languages.CompletionTriggerKind.TriggerCharacter, CompletionTriggerKind.TriggerCharacter],
[monaco.languages.CompletionTriggerKind.TriggerForIncompleteCompletions, CompletionTriggerKind.TriggerForIncompleteCompletions],
]);
export function toLspCompletionTriggerKind(monacoKind: monaco.languages.CompletionTriggerKind): CompletionTriggerKind {
return monacoCompletionTriggerKindToLspCompletionTriggerKind.get(monacoKind) ?? CompletionTriggerKind.Invoked;
}
// ============================================================================
// Insert Text Format
// ============================================================================
export const lspInsertTextFormatToMonacoInsertTextRules = new Map<InsertTextFormat, monaco.languages.CompletionItemInsertTextRule>([
[InsertTextFormat.Snippet, monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet],
]);
export function toMonacoInsertTextRules(format: InsertTextFormat | undefined): monaco.languages.CompletionItemInsertTextRule | undefined {
if (!format) {
return undefined;
}
return lspInsertTextFormatToMonacoInsertTextRules.get(format);
}
// ============================================================================
// Symbol Kind
// ============================================================================
export const lspSymbolKindToMonacoSymbolKind = new Map<SymbolKind, monaco.languages.SymbolKind>([
[SymbolKind.File, monaco.languages.SymbolKind.File],
[SymbolKind.Module, monaco.languages.SymbolKind.Module],
[SymbolKind.Namespace, monaco.languages.SymbolKind.Namespace],
[SymbolKind.Package, monaco.languages.SymbolKind.Package],
[SymbolKind.Class, monaco.languages.SymbolKind.Class],
[SymbolKind.Method, monaco.languages.SymbolKind.Method],
[SymbolKind.Property, monaco.languages.SymbolKind.Property],
[SymbolKind.Field, monaco.languages.SymbolKind.Field],
[SymbolKind.Constructor, monaco.languages.SymbolKind.Constructor],
[SymbolKind.Enum, monaco.languages.SymbolKind.Enum],
[SymbolKind.Interface, monaco.languages.SymbolKind.Interface],
[SymbolKind.Function, monaco.languages.SymbolKind.Function],
[SymbolKind.Variable, monaco.languages.SymbolKind.Variable],
[SymbolKind.Constant, monaco.languages.SymbolKind.Constant],
[SymbolKind.String, monaco.languages.SymbolKind.String],
[SymbolKind.Number, monaco.languages.SymbolKind.Number],
[SymbolKind.Boolean, monaco.languages.SymbolKind.Boolean],
[SymbolKind.Array, monaco.languages.SymbolKind.Array],
[SymbolKind.Object, monaco.languages.SymbolKind.Object],
[SymbolKind.Key, monaco.languages.SymbolKind.Key],
[SymbolKind.Null, monaco.languages.SymbolKind.Null],
[SymbolKind.EnumMember, monaco.languages.SymbolKind.EnumMember],
[SymbolKind.Struct, monaco.languages.SymbolKind.Struct],
[SymbolKind.Event, monaco.languages.SymbolKind.Event],
[SymbolKind.Operator, monaco.languages.SymbolKind.Operator],
[SymbolKind.TypeParameter, monaco.languages.SymbolKind.TypeParameter],
]);
export function toMonacoSymbolKind(kind: SymbolKind): monaco.languages.SymbolKind {
return lspSymbolKindToMonacoSymbolKind.get(kind) ?? monaco.languages.SymbolKind.File;
}
// ============================================================================
// Symbol Tag
// ============================================================================
export const lspSymbolTagToMonacoSymbolTag = new Map<SymbolTag, monaco.languages.SymbolTag>([
[SymbolTag.Deprecated, monaco.languages.SymbolTag.Deprecated],
]);
export function toMonacoSymbolTag(tag: SymbolTag): monaco.languages.SymbolTag | undefined {
return lspSymbolTagToMonacoSymbolTag.get(tag);
}
// ============================================================================
// Document Highlight Kind
// ============================================================================
export const lspDocumentHighlightKindToMonacoDocumentHighlightKind = new Map<DocumentHighlightKind, monaco.languages.DocumentHighlightKind>([
[DocumentHighlightKind.Text, monaco.languages.DocumentHighlightKind.Text],
[DocumentHighlightKind.Read, monaco.languages.DocumentHighlightKind.Read],
[DocumentHighlightKind.Write, monaco.languages.DocumentHighlightKind.Write],
]);
export function toMonacoDocumentHighlightKind(kind: DocumentHighlightKind | undefined): monaco.languages.DocumentHighlightKind {
if (!kind) {
return monaco.languages.DocumentHighlightKind.Text;
}
return lspDocumentHighlightKindToMonacoDocumentHighlightKind.get(kind) ?? monaco.languages.DocumentHighlightKind.Text;
}
// ============================================================================
// Folding Range Kind
// ============================================================================
export const lspFoldingRangeKindToMonacoFoldingRangeKind = new Map<FoldingRangeKind, monaco.languages.FoldingRangeKind>([
[FoldingRangeKind.Comment, monaco.languages.FoldingRangeKind.Comment],
[FoldingRangeKind.Imports, monaco.languages.FoldingRangeKind.Imports],
[FoldingRangeKind.Region, monaco.languages.FoldingRangeKind.Region],
]);
export function toMonacoFoldingRangeKind(kind: FoldingRangeKind | undefined): monaco.languages.FoldingRangeKind | undefined {
if (!kind) {
return undefined;
}
return lspFoldingRangeKindToMonacoFoldingRangeKind.get(kind);
}
// ============================================================================
// Diagnostic Severity
// ============================================================================
export const monacoMarkerSeverityToLspDiagnosticSeverity = new Map<monaco.MarkerSeverity, DiagnosticSeverity>([
[monaco.MarkerSeverity.Error, DiagnosticSeverity.Error],
[monaco.MarkerSeverity.Warning, DiagnosticSeverity.Warning],
[monaco.MarkerSeverity.Info, DiagnosticSeverity.Information],
[monaco.MarkerSeverity.Hint, DiagnosticSeverity.Hint],
]);
export function toLspDiagnosticSeverity(severity: monaco.MarkerSeverity): DiagnosticSeverity {
return monacoMarkerSeverityToLspDiagnosticSeverity.get(severity) ?? DiagnosticSeverity.Error;
}
export const lspDiagnosticSeverityToMonacoMarkerSeverity = new Map<DiagnosticSeverity, monaco.MarkerSeverity>([
[DiagnosticSeverity.Error, monaco.MarkerSeverity.Error],
[DiagnosticSeverity.Warning, monaco.MarkerSeverity.Warning],
[DiagnosticSeverity.Information, monaco.MarkerSeverity.Info],
[DiagnosticSeverity.Hint, monaco.MarkerSeverity.Hint],
]);
export function toMonacoDiagnosticSeverity(severity: DiagnosticSeverity | undefined): monaco.MarkerSeverity {
if (!severity) {
return monaco.MarkerSeverity.Error;
}
return lspDiagnosticSeverityToMonacoMarkerSeverity.get(severity) ?? monaco.MarkerSeverity.Error;
}
// ============================================================================
// Diagnostic Tag
// ============================================================================
export const lspDiagnosticTagToMonacoMarkerTag = new Map<DiagnosticTag, monaco.MarkerTag>([
[DiagnosticTag.Unnecessary, monaco.MarkerTag.Unnecessary],
[DiagnosticTag.Deprecated, monaco.MarkerTag.Deprecated],
]);
export function toMonacoDiagnosticTag(tag: DiagnosticTag): monaco.MarkerTag | undefined {
return lspDiagnosticTagToMonacoMarkerTag.get(tag);
}
// ============================================================================
// Signature Help Trigger Kind
// ============================================================================
export const monacoSignatureHelpTriggerKindToLspSignatureHelpTriggerKind = new Map<monaco.languages.SignatureHelpTriggerKind, SignatureHelpTriggerKind>([
[monaco.languages.SignatureHelpTriggerKind.Invoke, SignatureHelpTriggerKind.Invoked],
[monaco.languages.SignatureHelpTriggerKind.TriggerCharacter, SignatureHelpTriggerKind.TriggerCharacter],
[monaco.languages.SignatureHelpTriggerKind.ContentChange, SignatureHelpTriggerKind.ContentChange],
]);
export function toLspSignatureHelpTriggerKind(monacoKind: monaco.languages.SignatureHelpTriggerKind): SignatureHelpTriggerKind {
return monacoSignatureHelpTriggerKindToLspSignatureHelpTriggerKind.get(monacoKind) ?? SignatureHelpTriggerKind.Invoked;
}
// ============================================================================
// Command
// ============================================================================
export function toMonacoCommand(command: Command | undefined): monaco.languages.Command | undefined {
if (!command) {
return undefined;
}
return {
id: command.command,
title: command.title,
arguments: command.arguments,
};
}
// ============================================================================
// Inlay Hint Kind
// ============================================================================
export const lspInlayHintKindToMonacoInlayHintKind = new Map<InlayHintKind, monaco.languages.InlayHintKind>([
[InlayHintKind.Type, monaco.languages.InlayHintKind.Type],
[InlayHintKind.Parameter, monaco.languages.InlayHintKind.Parameter],
]);
export function toMonacoInlayHintKind(kind: InlayHintKind | undefined): monaco.languages.InlayHintKind {
if (!kind) {
return monaco.languages.InlayHintKind.Type;
}
return lspInlayHintKindToMonacoInlayHintKind.get(kind) ?? monaco.languages.InlayHintKind.Type;
} export function toMonacoLocation(
location: Location | LocationLink,
client: LspConnection
): monaco.languages.Location | monaco.languages.LocationLink {
if ('targetUri' in location) {
// LocationLink
const translatedRange = client.bridge.translateBackRange({ uri: location.targetUri }, location.targetRange);
return {
uri: translatedRange.textModel.uri,
range: translatedRange.range,
originSelectionRange: location.originSelectionRange
? client.bridge.translateBackRange({ uri: location.targetUri }, location.originSelectionRange).range
: undefined,
targetSelectionRange: location.targetSelectionRange
? client.bridge.translateBackRange({ uri: location.targetUri }, location.targetSelectionRange).range
: undefined,
};
} else {
// Location
const translatedRange = client.bridge.translateBackRange({ uri: location.uri }, location.range);
return {
uri: translatedRange.textModel.uri,
range: translatedRange.range,
};
}
}
export function toMonacoLanguageSelector(s: DocumentSelector | null): monaco.languages.LanguageSelector {
if (!s || s.length === 0) {
return { language: '*' };
}
return s.map<monaco.languages.LanguageFilter>(s => {
if ('notebook' in s) {
if (typeof s.notebook === 'string') {
return { notebookType: s.notebook, language: s.language };
} else {
return { notebookType: s.notebook.notebookType, language: s.language, pattern: s.notebook.pattern, scheme: s.notebook.scheme };
}
} else {
return { language: s.language, pattern: s.pattern, scheme: s.scheme };
}
});
}
export function matchesDocumentSelector(model: monaco.editor.ITextModel, selector: DocumentSelector | null): boolean {
if (!selector) {
return true;
}
const languageId = model.getLanguageId();
const uri = model.uri.toString(true);
if (!selector || selector.length === 0) {
return true;
}
for (const filter of selector) {
if (filter.language && filter.language !== '*' && filter.language !== languageId) {
continue;
}
return true;
}
return false;
}
export function toDiagnosticMarker(diagnostic: Diagnostic): monaco.editor.IMarkerData {
const marker: monaco.editor.IMarkerData = {
severity: toMonacoDiagnosticSeverity(diagnostic.severity),
startLineNumber: diagnostic.range.start.line + 1,
startColumn: diagnostic.range.start.character + 1,
endLineNumber: diagnostic.range.end.line + 1,
endColumn: diagnostic.range.end.character + 1,
message: diagnostic.message,
source: diagnostic.source,
code: typeof diagnostic.code === 'string' ? diagnostic.code : diagnostic.code?.toString(),
};
if (diagnostic.tags) {
marker.tags = diagnostic.tags.map(tag => toMonacoDiagnosticTag(tag)).filter((tag): tag is monaco.MarkerTag => tag !== undefined);
}
if (diagnostic.relatedInformation) {
marker.relatedInformation = diagnostic.relatedInformation.map(info => ({
resource: monaco.Uri.parse(info.location.uri),
startLineNumber: info.location.range.start.line + 1,
startColumn: info.location.range.start.character + 1,
endLineNumber: info.location.range.end.line + 1,
endColumn: info.location.range.end.character + 1,
message: info.message,
}));
}
return marker;
}

View file

@ -0,0 +1,5 @@
import { MonacoLspClient } from './adapters/LspClient';
import { WebSocketTransport } from '@hediet/json-rpc-websocket';
import { createTransportToWorker, createTransportToIFrame } from '@hediet/json-rpc-browser';
export { MonacoLspClient, WebSocketTransport, createTransportToWorker, createTransportToIFrame };

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,75 @@
export interface IDisposable {
dispose(): void;
}
export class Disposable implements IDisposable {
static None = Object.freeze<IDisposable>({ dispose() { } });
private _store = new DisposableStore();
constructor() { }
public dispose(): void {
this._store.dispose();
}
protected _register<T extends IDisposable>(t: T): T {
if ((t as any) === this) {
throw new Error('Cannot register a disposable on itself!');
}
return this._store.add(t);
}
}
export class DisposableStore implements IDisposable {
static DISABLE_DISPOSED_WARNING = false;
private _toDispose = new Set<IDisposable>();
private _isDisposed = false;
public dispose(): void {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
this.clear();
}
public clear(): void {
if (this._toDispose.size === 0) {
return;
}
try {
for (const item of this._toDispose) {
item.dispose();
}
} finally {
this._toDispose.clear();
}
}
public add<T extends IDisposable>(t: T): T {
if (!t) {
return t;
}
if ((t as any) === this) {
throw new Error('Cannot register a disposable on itself!');
}
if (this._isDisposed) {
if (!DisposableStore.DISABLE_DISPOSED_WARNING) {
console.warn(
new Error(
'Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!'
).stack
);
}
} else {
this._toDispose.add(t);
}
return t;
}
}