123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 |
- import EventEmitter from "node:events";
- import * as path from "node:path";
- import * as vscode from "vscode";
- import {
- LanguageClient,
- MessageSignature,
- type LanguageClientOptions,
- type NotificationMessage,
- type RequestMessage,
- type ResponseMessage,
- type ServerOptions,
- } from "vscode-languageclient/node";
- import { globals } from "../extension";
- import { createLogger, get_configuration, get_project_dir } from "../utils";
- import { MessageIO } from "./MessageIO";
- const log = createLogger("lsp.client", { output: "Godot LSP" });
- export enum ClientStatus {
- PENDING = 0,
- DISCONNECTED = 1,
- CONNECTED = 2,
- REJECTED = 3,
- }
- export enum TargetLSP {
- HEADLESS = 0,
- EDITOR = 1,
- }
- export type Target = {
- host: string;
- port: number;
- type: TargetLSP;
- };
- type HoverResult = {
- contents: {
- kind: string;
- value: string;
- };
- range: {
- end: {
- character: number;
- line: number;
- };
- start: {
- character: number;
- line: number;
- };
- };
- };
- type HoverResponseMesssage = {
- id: number;
- jsonrpc: string;
- result: HoverResult;
- };
- type ChangeWorkspaceNotification = {
- method: string;
- params: {
- path: string;
- };
- };
- type DocumentLinkResult = {
- range: {
- end: {
- character: number;
- line: number;
- };
- start: {
- character: number;
- line: number;
- };
- };
- target: string;
- };
- type DocumentLinkResponseMessage = {
- id: number;
- jsonrpc: string;
- result: DocumentLinkResult[];
- };
- export default class GDScriptLanguageClient extends LanguageClient {
- public io: MessageIO = new MessageIO();
- public target: TargetLSP = TargetLSP.EDITOR;
- public port = -1;
- public lastPortTried = -1;
- public sentMessages = new Map();
- private initMessage: RequestMessage;
- private rejected = false;
- events = new EventEmitter();
- private _status: ClientStatus;
- public set status(v: ClientStatus) {
- this._status = v;
- this.events.emit("status", this._status);
- }
- constructor() {
- const serverOptions: ServerOptions = () => {
- return new Promise((resolve, reject) => {
- resolve({ reader: this.io.reader, writer: this.io.writer });
- });
- };
- const clientOptions: LanguageClientOptions = {
- documentSelector: [
- { scheme: "file", language: "gdscript" },
- { scheme: "untitled", language: "gdscript" },
- ],
- };
- super("GDScriptLanguageClient", serverOptions, clientOptions);
- this.status = ClientStatus.PENDING;
- this.io.on("connected", this.on_connected.bind(this));
- this.io.on("disconnected", this.on_disconnected.bind(this));
- this.io.requestFilter = this.request_filter.bind(this);
- this.io.responseFilter = this.response_filter.bind(this);
- this.io.notificationFilter = this.notification_filter.bind(this);
- }
- connect(target: TargetLSP = TargetLSP.EDITOR) {
- this.rejected = false;
- this.target = target;
- this.status = ClientStatus.PENDING;
- let port = get_configuration("lsp.serverPort");
- if (this.port !== -1) {
- port = this.port;
- }
- if (this.target === TargetLSP.EDITOR) {
- if (port === 6005 || port === 6008) {
- port = 6005;
- }
- }
- this.lastPortTried = port;
- const host = get_configuration("lsp.serverHost");
- log.info(`attempting to connect to LSP at ${host}:${port}`);
- this.io.connect(host, port);
- }
- async send_request(method: string, params) {
- try {
- return this.sendRequest(method, params);
- } catch {
- log.warn("sending request failed!");
- }
- }
- handleFailedRequest<T>(
- type: MessageSignature,
- token: vscode.CancellationToken | undefined,
- error: any,
- defaultValue: T,
- showNotification?: boolean,
- ): T {
- if (type.method === "textDocument/documentSymbol") {
- if (
- error.message.includes("selectionRange must be contained in fullRange")
- ) {
- log.warn(
- `Request failed for method "${type.method}", suppressing notification - see issue #820`
- );
- return super.handleFailedRequest(
- type,
- token,
- error,
- defaultValue,
- false
- );
- }
- }
- return super.handleFailedRequest(
- type,
- token,
- error,
- defaultValue,
- showNotification
- );
- }
- private request_filter(message: RequestMessage) {
- if (this.rejected) {
- if (message.method === "shutdown") {
- return message;
- }
- return false;
- }
- this.sentMessages.set(message.id, message);
- if (!this.initMessage && message.method === "initialize") {
- this.initMessage = message;
- }
- // discard outgoing messages that we know aren't supported
- // if (message.method === "textDocument/didSave") {
- // return false;
- // }
- // if (message.method === "textDocument/willSaveWaitUntil") {
- // return false;
- // }
- if (message.method === "workspace/didChangeWatchedFiles") {
- return false;
- }
- if (message.method === "workspace/symbol") {
- return false;
- }
- return message;
- }
- private response_filter(message: ResponseMessage) {
- const sentMessage = this.sentMessages.get(message.id);
- if (sentMessage?.method === "textDocument/hover") {
- // fix markdown contents
- let value: string = (message as HoverResponseMesssage).result.contents.value;
- if (value) {
- // this is a dirty hack to fix language server sending us prerendered
- // markdown but not correctly stripping leading #'s, leading to
- // docstrings being displayed as titles
- value = value.replace(/\n[#]+/g, "\n");
- // fix bbcode line breaks
- value = value.replaceAll("`br`", "\n\n");
- // fix bbcode code boxes
- value = value.replace("`codeblocks`", "");
- value = value.replace("`/codeblocks`", "");
- value = value.replace("`gdscript`", "\nGDScript:\n```gdscript");
- value = value.replace("`/gdscript`", "```");
- value = value.replace("`csharp`", "\nC#:\n```csharp");
- value = value.replace("`/csharp`", "```");
- (message as HoverResponseMesssage).result.contents.value = value;
- }
- } else if (sentMessage.method === "textDocument/documentLink") {
- const results: DocumentLinkResult[] = (
- message as DocumentLinkResponseMessage
- ).result;
- if (!results) {
- return message;
- }
- const final_result: DocumentLinkResult[] = [];
- // at this point, Godot's LSP server does not
- // return a valid path for resources identified
- // by "uid://""
- //
- // this is a dirty hack to remove any "uid://"
- // document links.
- //
- // to provide links for these, we will be relying on
- // the internal DocumentLinkProvider instead.
- for (const result of results) {
- if (!result.target.startsWith("uid://")) {
- final_result.push(result);
- }
- }
- (message as DocumentLinkResponseMessage).result = final_result;
- }
- return message;
- }
- private async check_workspace(message: ChangeWorkspaceNotification) {
- const server_path = path.normalize(message.params.path);
- const client_path = path.normalize(await get_project_dir());
- if (server_path !== client_path) {
- log.warn("Connected LSP is a different workspace");
- this.io.socket.resetAndDestroy();
- this.rejected = true;
- }
- }
- private notification_filter(message: NotificationMessage) {
- if (message.method === "gdscript_client/changeWorkspace") {
- this.check_workspace(message as ChangeWorkspaceNotification);
- }
- if (message.method === "gdscript/capabilities") {
- globals.docsProvider.register_capabilities(message);
- }
- // if (message.method === "textDocument/publishDiagnostics") {
- // for (const diagnostic of message.params.diagnostics) {
- // if (diagnostic.code === 6) {
- // log.debug("UNUSED_SIGNAL", diagnostic);
- // return;
- // }
- // if (diagnostic.code === 2) {
- // log.debug("UNUSED_VARIABLE", diagnostic);
- // return;
- // }
- // }
- // }
- return message;
- }
- public async get_symbol_at_position(
- uri: vscode.Uri,
- position: vscode.Position
- ) {
- const params = {
- textDocument: { uri: uri.toString() },
- position: { line: position.line, character: position.character },
- };
- const response = await this.send_request("textDocument/hover", params);
- return this.parse_hover_result(response as HoverResult);
- }
- private parse_hover_result(message: HoverResult) {
- const contents = message.contents;
- let decl: string;
- if (Array.isArray(contents)) {
- decl = contents[0];
- } else {
- decl = contents.value;
- }
- if (!decl) {
- return "";
- }
- decl = decl.split("\n")[0].trim();
- let match: RegExpMatchArray;
- let result = undefined;
- match = decl.match(/(?:func|const) (@?\w+)\.(\w+)/);
- if (match) {
- result = `${match[1]}.${match[2]}`;
- }
- match = decl.match(/<Native> class (\w+)/);
- if (match) {
- result = `${match[1]}`;
- }
- return result;
- }
- private on_connected() {
- this.status = ClientStatus.CONNECTED;
- const host = get_configuration("lsp.serverHost");
- log.info(`connected to LSP at ${host}:${this.lastPortTried}`);
- if (this.initMessage) {
- this.send_request(this.initMessage.method, this.initMessage.params);
- }
- }
- private on_disconnected() {
- if (this.rejected) {
- this.status = ClientStatus.REJECTED;
- return;
- }
- if (this.target === TargetLSP.EDITOR) {
- const host = get_configuration("lsp.serverHost");
- let port = get_configuration("lsp.serverPort");
- if (port === 6005 || port === 6008) {
- if (this.lastPortTried === 6005) {
- port = 6008;
- log.info(`attempting to connect to LSP at ${host}:${port}`);
- this.lastPortTried = port;
- this.io.connect(host, port);
- return;
- }
- }
- }
- this.status = ClientStatus.DISCONNECTED;
- }
- }
|