Skip to content

Commit

Permalink
Merge pull request #422 from jneira/fix-ghc-version
Browse files Browse the repository at this point in the history
- Add much more logging in the client side, configured with haskell.trace.client
- Fix error handling of working out project ghc (See #421)
  - And dont use a shell to spawn the subprocess in non windows systems
- Add commands Start Haskell LSP server and Stop Haskell LSP server
  • Loading branch information
jneira authored Aug 4, 2021
2 parents aafd659 + 61d8e3f commit 8f035a4
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 58 deletions.
8 changes: 8 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

- Add tracking of cabal files to work together with the incoming cabal formatter plugin

### 1.5.1

- Add much more logging in the client side, configured with `haskell.trace.client`
- Fix error handling of `working out project ghc` (See #421)
- And dont use a shell to spawn the subprocess in non windows systems
- Show the progress as a cancellable notification
- Add commands `Start Haskell LSP server` and `Stop Haskell LSP server`

### 1.5.0

- Emit warning about limited support for ghc-9.x on hls executable download
Expand Down
16 changes: 3 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "haskell",
"displayName": "Haskell",
"description": "Haskell language support powered by the Haskell Language Server",
"version": "1.5.0",
"version": "1.5.1",
"license": "MIT",
"publisher": "haskell",
"engines": {
Expand Down Expand Up @@ -103,6 +103,17 @@
"default": "off",
"description": "Traces the communication between VS Code and the language server."
},
"haskell.trace.client": {
"scope": "resource",
"type": "string",
"enum": [
"off",
"error",
"debug"
],
"default": "error",
"description": "Traces the communication between VS Code and the language server."
},
"haskell.logFile": {
"scope": "resource",
"type": "string",
Expand Down Expand Up @@ -312,6 +323,16 @@
"command": "haskell.commands.restartServer",
"title": "Haskell: Restart Haskell LSP server",
"description": "Restart the Haskell LSP server"
},
{
"command": "haskell.commands.startServer",
"title": "Haskell: Start Haskell LSP server",
"description": "Start the Haskell LSP server"
},
{
"command": "haskell.commands.stopServer",
"title": "Haskell: Stop Haskell LSP server",
"description": "Stop the Haskell LSP server"
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions src/commands/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export namespace CommandNames {
export const ImportIdentifierCommandName = 'haskell.commands.importIdentifier';
export const RestartServerCommandName = 'haskell.commands.restartServer';
export const StartServerCommandName = 'haskell.commands.startServer';
export const StopServerCommandName = 'haskell.commands.stopServer';
}
75 changes: 58 additions & 17 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ExecutableOptions,
LanguageClient,
LanguageClientOptions,
Logger,
RevealOutputChannelOn,
ServerOptions,
TransportKind,
Expand All @@ -23,7 +24,7 @@ import { CommandNames } from './commands/constants';
import { ImportIdentifier } from './commands/importIdentifier';
import { DocsBrowser } from './docsBrowser';
import { downloadHaskellLanguageServer } from './hlsBinaries';
import { executableExists } from './utils';
import { executableExists, ExtensionLogger } from './utils';

// The current map of documents & folders to language servers.
// It may be null to indicate that we are in the process of launching a server,
Expand All @@ -45,7 +46,10 @@ export async function activate(context: ExtensionContext) {
for (const folder of event.removed) {
const client = clients.get(folder.uri.toString());
if (client) {
clients.delete(folder.uri.toString());
const uri = folder.uri.toString();
client.info(`Deleting folder for clients: ${uri}`);
clients.delete(uri);
client.info('Stopping the server');
client.stop();
}
}
Expand All @@ -54,12 +58,35 @@ export async function activate(context: ExtensionContext) {
// Register editor commands for HIE, but only register the commands once at activation.
const restartCmd = commands.registerCommand(CommandNames.RestartServerCommandName, async () => {
for (const langClient of clients.values()) {
langClient?.info('Stopping the server');
await langClient?.stop();
langClient?.info('Starting the server');
langClient?.start();
}
});

context.subscriptions.push(restartCmd);

const stopCmd = commands.registerCommand(CommandNames.StopServerCommandName, async () => {
for (const langClient of clients.values()) {
langClient?.info('Stopping the server');
await langClient?.stop();
langClient?.info('Server stopped');
}
});

context.subscriptions.push(stopCmd);

const startCmd = commands.registerCommand(CommandNames.StartServerCommandName, async () => {
for (const langClient of clients.values()) {
langClient?.info('Starting the server');
langClient?.start();
langClient?.info('Server started');
}
});

context.subscriptions.push(startCmd);

context.subscriptions.push(ImportIdentifier.registerCommand());

// Set up the documentation browser.
Expand All @@ -70,30 +97,31 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(openOnHackageDisposable);
}

function findManualExecutable(uri: Uri, folder?: WorkspaceFolder): string | null {
function findManualExecutable(logger: Logger, uri: Uri, folder?: WorkspaceFolder): string | null {
let exePath = workspace.getConfiguration('haskell', uri).serverExecutablePath;
if (exePath === '') {
return null;
}

logger.info(`Trying to find the server executable in: ${exePath}`);
// Substitute path variables with their corresponding locations.
exePath = exePath.replace('${HOME}', os.homedir).replace('${home}', os.homedir).replace(/^~/, os.homedir);
if (folder) {
exePath = exePath.replace('${workspaceFolder}', folder.uri.path).replace('${workspaceRoot}', folder.uri.path);
}

logger.info(`Location after path variables subsitution: ${exePath}`);
if (!executableExists(exePath)) {
throw new Error(`serverExecutablePath is set to ${exePath} but it doesn't exist and is not on the PATH`);
throw new Error(`serverExecutablePath is set to ${exePath} but it doesn't exist and it is not on the PATH`);
}
return exePath;
}

/** Searches the PATH for whatever is set in serverVariant */
function findLocalServer(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder): string | null {
function findLocalServer(context: ExtensionContext, logger: Logger, uri: Uri, folder?: WorkspaceFolder): string | null {
const exes: string[] = ['haskell-language-server-wrapper', 'haskell-language-server'];

logger.info(`Searching for server executables ${exes.join(',')} in $PATH`);
for (const exe of exes) {
if (executableExists(exe)) {
logger.info(`Found server executable in $PATH: ${exe}`);
return exe;
}
}
Expand All @@ -120,6 +148,9 @@ async function activeServer(context: ExtensionContext, document: TextDocument) {

async function activateServerForFolder(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder) {
const clientsKey = folder ? folder.uri.toString() : uri.toString();
// Set a unique name per workspace folder (useful for multi-root workspaces).
const langName = 'Haskell' + (folder ? ` (${folder.name})` : '');
const outputChannel: OutputChannel = window.createOutputChannel(langName);

// If the client already has an LSP server for this uri/folder, then don't start a new one.
if (clients.has(clientsKey)) {
Expand All @@ -129,21 +160,25 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
clients.set(clientsKey, null);

const logLevel = workspace.getConfiguration('haskell', uri).trace.server;
const clientLogLevel = workspace.getConfiguration('haskell', uri).trace.client;
const logFile = workspace.getConfiguration('haskell', uri).logFile;

const logger: Logger = new ExtensionLogger('client', clientLogLevel, outputChannel);

let serverExecutable;
try {
// Try and find local installations first
serverExecutable = findManualExecutable(uri, folder) ?? findLocalServer(context, uri, folder);
serverExecutable = findManualExecutable(logger, uri, folder) ?? findLocalServer(context, logger, uri, folder);
if (serverExecutable === null) {
// If not, then try to download haskell-language-server binaries if it's selected
serverExecutable = await downloadHaskellLanguageServer(context, uri, folder);
serverExecutable = await downloadHaskellLanguageServer(context, logger, uri, folder);
if (!serverExecutable) {
return;
}
}
} catch (e) {
if (e instanceof Error) {
logger.error(`Error getting the server executable: ${e.message}`);
window.showErrorMessage(e.message);
}
return;
Expand All @@ -162,6 +197,12 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
// If we're operating on a standalone file (i.e. not in a folder) then we need
// to launch the server in a reasonable current directory. Otherwise the cradle
// guessing logic in hie-bios will be wrong!
if (folder) {
logger.info(`Activating the language server in the workspace folder: ${folder?.uri.fsPath}`);
} else {
logger.info(`Activating the language server in the parent dir of the file: ${uri.fsPath}`);
}

const exeOptions: ExecutableOptions = {
cwd: folder ? undefined : path.dirname(uri.fsPath),
};
Expand All @@ -173,15 +214,14 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
debug: { command: serverExecutable, transport: TransportKind.stdio, args, options: exeOptions },
};

// Set a unique name per workspace folder (useful for multi-root workspaces).
const langName = 'Haskell' + (folder ? ` (${folder.name})` : '');
const outputChannel: OutputChannel = window.createOutputChannel(langName);
outputChannel.appendLine('[client] run command: "' + serverExecutable + ' ' + args.join(' ') + '"');
outputChannel.appendLine('[client] debug command: "' + serverExecutable + ' ' + args.join(' ') + '"');

outputChannel.appendLine(`[client] server cwd: ${exeOptions.cwd}`);
logger.info(`run command: ${serverExecutable} ${args.join(' ')}`);
logger.info(`debug command: ${serverExecutable} ${args.join(' ')}`);
if (exeOptions.cwd) {
logger.info(`server cwd: ${exeOptions.cwd}`);
}

const pat = folder ? `${folder.uri.fsPath}/**/*` : '**/*';
logger.info(`document selector patten: ${pat}`);
const clientOptions: LanguageClientOptions = {
// Use the document selector to only notify the LSP on files inside the folder
// path for the specific workspace.
Expand Down Expand Up @@ -213,6 +253,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
langClient.registerProposedFeatures();

// Finally start the client and add it to the list of clients.
logger.info('Starting language server');
langClient.start();
clients.set(clientsKey, langClient);
}
Expand Down
Loading

0 comments on commit 8f035a4

Please sign in to comment.