276 lines
7.0 KiB
TypeScript
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);
|
|
}
|
|
};
|
|
}
|
|
|