openquest-vscode/client/src/client_handler.ts

276 lines
7.0 KiB
TypeScript

// Local Imports
import constants = require("./constants");
import { clients, outputChannel } from "./extension";
import * as server_binary from "./server_binary";
// External Imports
import { Duplex } from "stream";
import WebSocket, { createWebSocketStream } from 'ws';
import {
ExtensionContext, RelativePattern, WorkspaceFolder,
WorkspaceFoldersChangeEvent, workspace
} from "vscode";
import * as vscode from "vscode";
import { LanguageClient, StreamInfo, integer } from "vscode-languageclient/node";
import { ServerOptions } from 'vscode-languageclient/node';
import { TextDocument, Uri } from 'vscode';
/**
*
*/
async function openDocument(uri: Uri) {
const uriMatch = (d: TextDocument) => d.uri.toString() === uri.toString();
const doc = vscode.workspace.textDocuments.find(uriMatch);
if (doc === undefined) await vscode.workspace.openTextDocument(uri);
return uri;
}
/**
*
*/
async function openPestFilesInFolder(folder: Uri) {
const pattern = filesInFolderPattern(folder);
const uris = await workspace.findFiles(pattern);
return Promise.all(uris.map(openDocument));
}
/**
*
*/
export function filesInFolderPattern(folder: Uri) {
return new RelativePattern(folder, constants.DOCUMENT_EXTENSION_PATTERN);
}
/**
*
*/
export async function startClientsForFolder(folder: WorkspaceFolder, ctx: ExtensionContext) {
// Settings on how to setup and connect the server
let serverOptions = await makeServerOptions();
if (!serverOptions) {
outputChannel.appendLine("[ERROR] Aborting server start.");
/*
await window.showErrorMessage(
"Not starting Pest Language Server as a suitable binary was not found."
);
*/
return;
}
const root = folder.uri;
const deleteWatcher = workspace.createFileSystemWatcher(
filesInFolderPattern(root),
true, // ignoreCreateEvents
true, // ignoreChangeEvents
false // ignoreDeleteEvents
);
const createChangeWatcher = workspace.createFileSystemWatcher(
filesInFolderPattern(root),
false, // ignoreCreateEvents
false, // ignoreChangeEvents
true // ignoreDeleteEvents
);
ctx.subscriptions.push(deleteWatcher);
ctx.subscriptions.push(createChangeWatcher);
const client = new LanguageClient(
constants.EXTENSION_NAME,
serverOptions,
{
documentSelector: [
{
language: constants.LANGUAGE_ID,
pattern: `${root.fsPath}/${constants.DOCUMENT_EXTENSION_PATTERN}`
},
],
synchronize: { fileEvents: deleteWatcher },
diagnosticCollectionName: constants.EXTENSION_NAME,
workspaceFolder: folder,
outputChannel,
}
);
client.start(); // ctx.subscriptions.push(client.start());
ctx.subscriptions.push(createChangeWatcher.onDidCreate(openDocument));
ctx.subscriptions.push(createChangeWatcher.onDidChange(openDocument));
const openedFiles = await openPestFilesInFolder(root);
const pestFiles: Set<string> = new Set();
openedFiles.forEach(f => pestFiles.add(f.toString()));
clients.set(root.toString(), client);
}
/**
* Makes Language Server options based on workspace settings, it might:
* - Try to find where a executable path is in package managers install paths
* - Find a given executable by path in given workspace settings
* - Connect to an existing language server websocket on an address
*/
async function makeServerOptions(): Promise<ServerOptions | undefined> {
const workspaceConfig = vscode.workspace.getConfiguration(constants.SETTINGS_NAMESPACE);
const customArgs = workspaceConfig.get("serverArguments") as string[];
// Make either a configuration to spawn a server locally
// or connect to an existing local or remote one
switch (workspaceConfig.get("handlingMode")) {
// Tries to find a language server executable from known package managers
// and spawns it
case "Automatic (Package Manager)":
const serverPath = await server_binary.findLanguageServerPath();
const automaticOptions: ServerOptions = {
command: serverPath, args: customArgs
};
return automaticOptions;
// Finds specified language server in path and spawns it
case "Local Path":
const command: string = workspaceConfig.get("serverBinaryPath");
let serverOptions: ServerOptions = {
command, args: customArgs
};
return serverOptions;
// Connect to a websocket by specified address in settings
case "Remote/Local Connection":
const addr: string = workspaceConfig.get("serverAddress");
return websocketServerOptions(addr);
default:
vscode.window.showErrorMessage("[ERROR] Something that shouldn't error, errored!")
break;
}
return undefined;
}
/**
*
*/
function ipcSocketServerOptions(pid: number) {
var child_process = require("child_process");
}
/**
*
* @param pid
* @returns
*/
function pipeLanguageServer(pid: number): StreamInfo {
var child_process = require("child_process");
// Obtain the stdin and stdout streams of the preexisting process using its PID
const languageServerProcess = child_process.spawn('sh', ['-c', `exec ${pid}>&0 <&${pid} 2>&1`], {
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
});
// Create a StreamInfo object with the input/output streams of the language server process
const streamInfo: StreamInfo = {
writer: languageServerProcess.stdin,
reader: languageServerProcess.stdout
};
return streamInfo;
}
/**
* Generaters server options with a websocket ready connection
* @param address
*/
function websocketServerOptions(address: string): ServerOptions {
function connectToServer(hostname: string): Duplex {
const socket = new WebSocket(`ws://${hostname}/`);
const stream = createWebSocketStream(socket);
stream.on('error', (e) => {
outputChannel.appendLine(`[WS] Could not connect: ${e.message}`);
});
stream.on('open', (e) => outputChannel.appendLine(`[WS] Connected: ${e.message}`))
stream.on("data", function (chunk) {
console.log(new TextDecoder().decode(chunk));
});
if (socket && (socket.readyState === WebSocket.CONNECTING)) {
// TODO: Delay execution while socket is connecting for a bit
}
if (socket && (socket.readyState === WebSocket.CONNECTING || socket.readyState === WebSocket.CLOSING)) {
outputChannel.append("[WS] Not connected:")
return undefined;
}
return stream;
}
let connection = connectToServer(address);
return () => Promise.resolve({
reader: connection,
writer: connection,
});
}
/**
*
* @param client
* @returns
*/
export function stopClient(client: LanguageClient) {
client.diagnostics?.clear();
return client.stop();
}
/**
*
* @param workspaceFolder
*/
export async function stopClientsForFolder(workspaceFolder: string) {
const client = clients.get(workspaceFolder);
if (client) {
await stopClient(client);
}
clients.delete(workspaceFolder);
}
/**
*
*/
export function updateClients(context: ExtensionContext) {
return async function ({ added, removed }: WorkspaceFoldersChangeEvent) {
for (const folder of removed) {
await stopClientsForFolder(folder.uri.toString());
}
for (const folder of added) {
await startClientsForFolder(folder, context);
}
};
}