-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #40 from halhenke/show-type-on-hover
Show type on hover
- Loading branch information
Showing
4 changed files
with
202 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters