Skip to content

Commit

Permalink
Merge pull request #40 from halhenke/show-type-on-hover
Browse files Browse the repository at this point in the history
Show type on hover
  • Loading branch information
alanz authored Jan 15, 2018
2 parents 49c3326 + f435422 commit a10d6d0
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 60 deletions.
34 changes: 24 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,33 @@
"type": "boolean",
"default": true,
"description": "Get suggestions from hlint"
},
"languageServerHaskell.showTypeForSelection.onHover": {
"type": "boolean",
"default": true,
"description": "If true, when an expression is selected, the hover tooltip will attempt to display the type of the entire expression - rather than just the term under the cursor."
},
"languageServerHaskell.showTypeForSelection.command.location": {
"type": "string",
"enum": [
"dropdown",
"channel"
],
"default": "dropdown",
"description": "Determines where the type information for selected text will be shown when the `showType` command is triggered (distinct from automatically showing this information when hover is triggered).\ndropdown: in a dropdown\nchannel: will be revealed in an output channel"
},
"languageServerHaskell.trace.server": {
"type": "string",
"enum": [
"off",
"messages",
"verbose"
],
"default": "off",
"description": "Traces the communication between VSCode and the languageServerHaskell service."
}
}
},
"languageServerExample.trace.server": {
"type": "string",
"enum": [
"off",
"messages",
"verbose"
],
"default": "off",
"description": "Traces the communication between VSCode and the languageServerExample service."
},
"commands": [
{
"command": "hie.commands.demoteDef",
Expand Down
205 changes: 162 additions & 43 deletions src/commands/showType.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,189 @@
import {
CancellationToken,
commands,
Disposable,
DocumentFilter,
Hover,
HoverProvider,
languages,
OutputChannel,
Position,
Range,
Selection,
window
TextDocument,
TextEditor,
window,
workspace
} from 'vscode';
import { LanguageClient, Range as VLCRange } from 'vscode-languageclient';
import {
LanguageClient,
Range as VLCRange,
} from 'vscode-languageclient';

import {CommandNames} from './constants';

const formatExpressionType = (document: TextDocument, r: Range, typ: string): string =>
`${document.getText(r)} :: ${typ}`;

const HASKELL_MODE: DocumentFilter = {
language: 'haskell',
scheme: 'file',
};

// Cache same selections...
const blankRange = new Range(0, 0, 0, 0);
let lastRange = blankRange;
let lastType = '';

async function getTypes({client, editor}): Promise<[Range, string]> {
try {
const hints = await client.sendRequest('workspace/executeCommand', getCmd(editor));
const arr = hints as Array<[VLCRange, string]>;
if (arr.length === 0) {
// lastRange = blankRange;
return null;
}
const ranges = arr.map(x =>
[client.protocol2CodeConverter.asRange(x[0]), x[1]]) as Array<[Range, string]>;
const [rng, typ] = chooseRange(editor.selection, ranges);
lastRange = rng;
lastType = typ;
return [rng, typ];
} catch (e) {
console.error(e);
}
}

/**
* Choose The range in the editor and coresponding type that best matches the selection
* @param {Selection} sel - selected text in editor
* @param {Array<[Range, string]>} rngs - the type analysis from the server
* @returns {[Range, string]}
*/
const chooseRange = (sel: Selection, rngs: Array<[Range, string]>): [Range, string] => {
const curr = rngs.findIndex(([rng, typ]) => rng.contains(sel));

import { CommandNames } from './constants';
// If we dont find selection start/end in ranges then
// return the type matching the smallest selection range
if (curr === -1) {
// NOTE: not sure this should happen...
return rngs[0];
} else {
return rngs[curr];
}
};

export namespace ShowType {
const getCmd = editor => ({
command: 'ghcmod:type',
arguments: [{
file: editor.document.uri.toString(),
pos: editor.selections[0].start,
include_constraints: true,
}],
});

export namespace ShowTypeCommand {
'use strict';
let lastRange = new Range(0, 0, 0, 0);

const displayType = (chan: OutputChannel, typ: string) => {
chan.clear();
chan.appendLine(typ);
chan.show(true);
};

export function registerCommand(client: LanguageClient): [Disposable] {
const showTypeChannel = window.createOutputChannel('Haskell Show Type');
const document = window.activeTextEditor.document;

const cmd = commands.registerCommand(CommandNames.ShowTypeCommandName, c => {
const cmd = commands.registerCommand(CommandNames.ShowTypeCommandName, x => {
const editor = window.activeTextEditor;

const ghcCmd = {
command: 'ghcmod:type',
arguments: [{
file: editor.document.uri.toString(),
pos: editor.selections[0].start,
include_constraints: true,
}],
};

client.sendRequest('workspace/executeCommand', ghcCmd).then(hints => {
const arr = hints as Array<[VLCRange, string]>;
if (arr.length === 0) {
return;
getTypes({client, editor}).then(([r, typ]) => {
switch (workspace.getConfiguration('languageServerHaskell').showTypeForSelection.command.location) {
case 'dropdown':
window.showInformationMessage(formatExpressionType(document, r, typ));
break;
case 'channel':
displayType(showTypeChannel, formatExpressionType(document, r, typ));
break;
default:
break;
}
const ranges =
arr.map(x => [client.protocol2CodeConverter.asRange(x[0]), x[1]]) as Array<[Range, string]>;
const [rng, typ] = chooseRange(editor.selection, ranges);
lastRange = rng;

editor.selections = [new Selection(rng.end, rng.start)];
displayType(showTypeChannel, typ);
}, e => {
console.error(e);
});
}).catch(e => console.error(e));

});

return [cmd, showTypeChannel];
}
}

export namespace ShowTypeHover {
/**
* Determine if type information should be included in Hover Popup
* @param {TextEditor} editor
* @param {Position} position
* @returns boolean
*/
const showTypeNow = (editor: TextEditor, position: Position): boolean => {
// NOTE: This seems to happen sometimes ¯\_(ツ)_/¯
if (!editor) {
return false;
}
// NOTE: This means cursor is not over selected text
if (!editor.selection.contains(position)) {
return false;
}
if (editor.selection.isEmpty) {
return false;
}
// document.
// NOTE: If cursor is not over highlight then dont show type
if ((editor.selection.active < editor.selection.start) || (editor.selection.active > editor.selection.end)) {
return false;
}
// NOTE: Not sure if we want this - maybe we can get multiline to work?
if (!editor.selection.isSingleLine) {
return false;
}
return true;
};

class TypeHover implements HoverProvider {
public client: LanguageClient;

function chooseRange(sel: Selection, rngs: Array<[Range, string]>): [Range, string] {
if (sel.isEqual(lastRange)) {
const curr = rngs.findIndex(([rng, typ]) => sel.isEqual(rng));
if (curr === -1) {
return rngs[0];
} else {
return rngs[Math.min(rngs.length - 1, curr + 1)];
constructor(client) {
this.client = client;
}

public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable<Hover> {
const editor = window.activeTextEditor;

if (!showTypeNow(editor, position)) {
return null;
}
} else {
return rngs[0];

// NOTE: No need for server call
if (lastType && editor.selection.isEqual(lastRange)) {
return Promise.resolve(this.makeHover(document, lastRange, lastType));
}

return getTypes({client: this.client, editor}).then(([r, typ]) => {
if (typ) {
return this.makeHover(document, r, lastType);
} else {
return null;
}
});
}

private makeHover(document: TextDocument, r: Range, typ: string): Hover {
return new Hover({
language: 'haskell',
value: formatExpressionType(document, r, typ),
});
}
}
}

function displayType(chan: OutputChannel, typ: string) {
chan.clear();
chan.appendLine(typ);
chan.show(true);
export const registerTypeHover = (client) => languages
.registerHoverProvider(HASKELL_MODE, new TypeHover(client));
}
22 changes: 15 additions & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ import {
RevealOutputChannelOn,
ServerOptions
} from 'vscode-languageclient';

import { InsertType } from './commands/insertType';
import { ShowType } from './commands/showType';
import {
ShowTypeCommand,
ShowTypeHover,
} from './commands/showType';
import { DocsBrowser } from './docsBrowser';

export async function activate(context: ExtensionContext) {
try {
// Check if hie is installed.
if (! await isHieInstalled()) {
if (!await isHieInstalled()) {
// TODO: Once haskell-ide-engine is on hackage/stackage, enable an option to install it via cabal/stack.
const notInstalledMsg: string =
'hie executable missing, please make sure it is installed, see github.com/haskell/haskell-ide-engine.';
Expand Down Expand Up @@ -85,7 +87,12 @@ function activateNoHieCheck(context: ExtensionContext) {
const langClient = new LanguageClient('Language Server Haskell', serverOptions, clientOptions);

context.subscriptions.push(InsertType.registerCommand(langClient));
ShowType.registerCommand(langClient).forEach(x => context.subscriptions.push(x));

ShowTypeCommand.registerCommand(langClient).forEach(x => context.subscriptions.push(x));

if (workspace.getConfiguration('languageServerHaskell').showTypeForSelection.onHover) {
context.subscriptions.push(ShowTypeHover.registerTypeHover(langClient));
}

registerHiePointCommand(langClient, 'hie.commands.demoteDef', 'hare:demote', context);
registerHiePointCommand(langClient, 'hie.commands.liftOneLevel', 'hare:liftonelevel', context);
Expand All @@ -97,14 +104,15 @@ function activateNoHieCheck(context: ExtensionContext) {
context.subscriptions.push(disposable);
}

function isHieInstalled(): Promise<boolean> {
return new Promise((resolve, reject) => {
async function isHieInstalled(): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
const cmd: string = ( process.platform === 'win32' ) ? 'where hie' : 'which hie';
child_process.exec(cmd, (error, stdout, stderr) => resolve(!error));
});
}

function registerHiePointCommand(langClient: LanguageClient, name: string, command: string, context: ExtensionContext) {
async function registerHiePointCommand(langClient: LanguageClient, name: string, command: string,
context: ExtensionContext) {
const cmd2 = commands.registerTextEditorCommand(name, (editor, edit) => {
const cmd = {
command,
Expand Down
1 change: 1 addition & 0 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"no-namespace": false,
"no-console": [true, "log"],
"no-unused-variable": true,
"promise-function-async": true,
"quotemark": [true, "single", "avoid-template"],
"trailing-comma": [true, {
"multiline": {
Expand Down

0 comments on commit a10d6d0

Please sign in to comment.