From 68d13a4536b69bc2fb80b5c95c4ef6b1e81afa21 Mon Sep 17 00:00:00 2001 From: Hal Henke Date: Fri, 27 Oct 2017 05:09:02 +1100 Subject: [PATCH 1/5] Basic showtype on Hover working - Basic attempt to format hoverType results with Markdown... Add configuration option to enable/disable Show Expression on Hover --- package.json | 5 ++ src/commands/showType.ts | 138 +++++++++++++++++++++++++++++++++++++-- src/extension.ts | 64 ++++++++++++++++++ 3 files changed, 202 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 255716e8..4347296b 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,11 @@ "type": "boolean", "default": true, "description": "Get suggestions from hlint" + }, + "languageServerHaskell.showTypeForSelection": { + "type": "boolean", + "default": false, + "description": "If true, when an expression is selected, vscode will the hover tooltip will try to displaay the type of the entire expression - rather than just the term under the cursor." } } }, diff --git a/src/commands/showType.ts b/src/commands/showType.ts index 023e6fd3..589e56c4 100644 --- a/src/commands/showType.ts +++ b/src/commands/showType.ts @@ -1,18 +1,26 @@ import { + CancellationToken, commands, Disposable, + Hover, + HoverProvider, + MarkedString, OutputChannel, + Position, + ProviderResult, Range, Selection, + TextDocument, window } from 'vscode'; -import { LanguageClient, Range as VLCRange } from 'vscode-languageclient'; +import { LanguageClient, RequestType, Range as VLCRange } from 'vscode-languageclient'; +import * as lng from 'vscode-languageclient'; import { CommandNames } from './constants'; export namespace ShowType { 'use strict'; - let lastRange = new Range(0, 0, 0, 0); + const lastRange = new Range(0, 0, 0, 0); export function registerCommand(client: LanguageClient): [Disposable] { const showTypeChannel = window.createOutputChannel('Haskell Show Type'); @@ -37,10 +45,12 @@ export namespace ShowType { 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; + // lastRange = rng; - editor.selections = [new Selection(rng.end, rng.start)]; - displayType(showTypeChannel, typ); + // editor.selections = [new Selection(rng.end, rng.start)]; + + vscode.window.showInformationMessage(typ); + // displayType(showTypeChannel, typ); }, e => { console.error(e); }); @@ -68,3 +78,121 @@ function displayType(chan: OutputChannel, typ: string) { chan.appendLine(typ); chan.show(true); } + +export interface HaskellShowTypeInformation { + // file: string; + type: string; + // line: number; + // column: number; + // doc: string; + // declarationlines: string[]; + // name: string; + // toolUsed: string; +} + +// export const showTypeHover(client) : HoverProvider = { + // export class ShowTypeHover { +export class ShowTypeHover implements HoverProvider { + client: LanguageClient; + + constructor(client) { + this.client = client; + } + + public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable { + let editor = vscode.window.activeTextEditor; + let lastRange = new Range(0, 0, 0, 0); + + + let cmd = { + command: "ghcmod:type", + arguments: [{ + file: editor.document.uri.toString(), + pos: editor.selections[0].start, + include_constraints: true + }] + }; + + function chooseRange(sel: Selection, rngs: [Range, string][]): [Range, string] { + if (sel.isEqual(lastRange)) { + let curr = rngs.findIndex(([rng, typ]) => sel.isEqual(rng)); + if (curr == -1) { + return rngs[0]; + } else { + return rngs[Math.min(rngs.length-1, curr+1)] + } + } else return rngs[0]; + }; + + const typeFormatter = (typeString: string): MarkedString => { + // const ms = new MarkedString(); + const ms = []; + // definition? + let def = typeString.split("::").map(s => s.trim()); + if (def.length > 1) { + ms.push(`**${def[0]}** :: `); + def.shift() + } + // context? + def = typeString.split("=>").map(s => s.trim()); + if (def.length > 1) { + ms.push(`*${def[0]}* => `); + def.shift() + } + // Process rest... + def = typeString.split("->").map(s => s.trim()); + if (def.length === 1 && def[0] === '') { + return; + } + if (def.length >= 1) { + ms.push(def.map(s => `**${s}**`).join(' -> ')) + } + return ms.join(); + // while def.length >= 1 { + // if (def === '') { + // return; + // } + // ms.push(def.map(s => `*${s}*`).join(' -> ')) + // } + + } + + + return this.client.sendRequest("workspace/executeCommand", cmd).then(hints => { + let arr = hints as [lng.Range, string][]; + if (arr.length == 0) return; + let ranges = arr.map(x => [this.client.protocol2CodeConverter.asRange(x[0]), x[1]]) as [vscode.Range, string][]; + let [rng, typ] = chooseRange(editor.selection, ranges); + // lastRange = rng; + + // editor.selections = [new Selection(rng.end, rng.start)]; + console.log(`SHOWTYPE IS `, typ); + let hover = new vscode.Hover({ + language: 'haskell', + // value: `REAL: ${typeFormatter(typ)}` + // NOTE: Force some better syntax highlighting: + value: `_ :: ${typ}` + }); + // let hover = new vscode.Hover([`\`\`\`haskell\n` + "foo :: ([AWS.Filter] -> Identity [AWS.Filter]) -> AWS.DescribeInstances -> Identity AWS.DescribeInstances" + `\n\`\`\``]); + + // let hover = new vscode.Hover([`\`\`\`haskell\n` + "foo :: this -> that" + `\n\`\`\``]); + // let hover = new vscode.Hover([`\`\`\`haskell\nfoo :: this -> that\n\`\`\``]); + // let hover = new vscode.Hover([`\`\`\`js\nconst elbow = () => pow;\n\`\`\``]); + // let hover = new vscode.Hover([typeFormatter(typ)]); + console.log(`SHOWTYPE HOVER IS `, hover); + // vscode.window.showInformationMessage(typ); + // displayType(showTypeChannel, typ); + return hover; + }, e => { + console.error(e); + }); + } +}; + +const HASKELL_MODE: vscode.DocumentFilter = + { language: 'haskell', scheme: 'file' }; + +export const registerTypeHover = (client) => + vscode + .languages + .registerHoverProvider(HASKELL_MODE, new ShowTypeHover(client)); diff --git a/src/extension.ts b/src/extension.ts index 822cc871..2c42a621 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,6 +21,7 @@ import { import { InsertType } from './commands/insertType'; import { ShowType } from './commands/showType'; import { DocsBrowser } from './docsBrowser'; +import { ShowTypeHover, registerTypeHover } from './commands/showType'; export async function activate(context: ExtensionContext) { try { @@ -45,6 +46,7 @@ export async function activate(context: ExtensionContext) { function activateNoHieCheck(context: ExtensionContext) { +<<<<<<< HEAD const docsDisposable = DocsBrowser.registerDocsBrowser(); context.subscriptions.push(docsDisposable); @@ -95,6 +97,68 @@ function activateNoHieCheck(context: ExtensionContext) { const disposable = langClient.start(); context.subscriptions.push(disposable); +======= + let docsDisposable = DocsBrowser.registerDocsBrowser(); + context.subscriptions.push(docsDisposable); + + // const fixer = languages.registerCodeActionsProvider("haskell", fixProvider); + // context.subscriptions.push(fixer); + // The server is implemented in node + //let serverModule = context.asAbsolutePath(path.join('server', 'server.js')); + let startupScript = ( process.platform == "win32" ) ? "hie-vscode.bat" : "hie-vscode.sh"; + let serverPath = context.asAbsolutePath(path.join('.', startupScript)); + let serverExe = { command: serverPath } + // The debug options for the server + let debugOptions = { execArgv: ["--nolazy", "--debug=6004"] }; + + // If the extension is launched in debug mode then the debug server options are used + // Otherwise the run options are used + let tempDir = ( process.platform == "win32" ) ? "%TEMP%" : "/tmp"; + let serverOptions: ServerOptions = { + //run : { module: serverModule, transport: TransportKind.ipc }, + //debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } + run : { command: serverPath }, + debug: { command: serverPath, args: ["-d", "-l", path.join(tempDir, "hie.log")] } + } + + // Options to control the language client + let clientOptions: LanguageClientOptions = { + // Register the server for plain text documents + documentSelector: ['haskell'], + synchronize: { + // Synchronize the setting section 'languageServerHaskell' to the server + configurationSection: 'languageServerHaskell', + // Notify the server about file changes to '.clientrc files contain in the workspace + fileEvents: workspace.createFileSystemWatcher('**/.clientrc') + }, + middleware: { + provideHover: DocsBrowser.hoverLinksMiddlewareHook + }, + revealOutputChannelOn: RevealOutputChannelOn.never + } + + // Create the language client and start the client. + let langClient = new LanguageClient('Language Server Haskell', serverOptions, clientOptions); + + context.subscriptions.push(InsertType.registerCommand(langClient)); + ShowType.registerCommand(langClient).forEach(x => context.subscriptions.push(x)); + + // context.subscriptions.push(vscode.languages.registerHoverProvider(controller.IDRIS_MODE, new show.IdrisHoverProvider())) + // context.subscriptions.push(vscode.languages.registerHoverProvider(controller.IDRIS_MODE, new showTypeHover())) + if (vscode.workspace.getConfiguration('languageServerHaskell').showTypeForSelection) { + context.subscriptions.push(registerTypeHover(langClient)); + } + + + registerHiePointCommand(langClient,"hie.commands.demoteDef","hare:demote",context); + registerHiePointCommand(langClient,"hie.commands.liftOneLevel","hare:liftonelevel",context); + registerHiePointCommand(langClient,"hie.commands.liftTopLevel","hare:lifttotoplevel",context); + registerHiePointCommand(langClient,"hie.commands.deleteDef","hare:deletedef",context); + registerHiePointCommand(langClient,"hie.commands.genApplicative","hare:genapplicative",context); + let disposable = langClient.start(); + + context.subscriptions.push(disposable); +>>>>>>> I got showtype on Hover working :-D } function isHieInstalled(): Promise { From 23bf709f2cd2b3bda37ac5040fd0b5b2ee33620a Mon Sep 17 00:00:00 2001 From: Hal Henke Date: Mon, 6 Nov 2017 05:39:50 +1100 Subject: [PATCH 2/5] Refactoring out common code between showType implementations - Simplify implementation and remove some dead code - Tried to use configurationChange API but does not seem to function yet --- .vscode/settings.json | 2 +- src/commands/showType.ts | 251 +++++++++++++++--------------------- src/commands/showTypeFix.ts | 247 +++++++++++++++++++++++++++++++++++ src/extension.ts | 116 ++++++++--------- tslint.json | 10 +- 5 files changed, 412 insertions(+), 214 deletions(-) create mode 100644 src/commands/showTypeFix.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index f8ca5d63..09f5ecb7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "files.exclude": { "out": true, // set this to true to hide the "out" folder with the compiled JS files ".vscode-test": true, - "node_modules": true + "node_modules": false }, "search.exclude": { "out": true // set this to false to include "out" folder in search results diff --git a/src/commands/showType.ts b/src/commands/showType.ts index 589e56c4..99a91a06 100644 --- a/src/commands/showType.ts +++ b/src/commands/showType.ts @@ -2,8 +2,10 @@ import { CancellationToken, commands, Disposable, + DocumentFilter, Hover, HoverProvider, + languages, MarkedString, OutputChannel, Position, @@ -13,186 +15,145 @@ import { TextDocument, window } from 'vscode'; -import { LanguageClient, RequestType, Range as VLCRange } from 'vscode-languageclient'; -import * as lng from 'vscode-languageclient'; +import { + LanguageClient, + Range as VLCRange, + RequestType +} from 'vscode-languageclient'; -import { CommandNames } from './constants'; +import {CommandNames} from './constants'; export namespace ShowType { 'use strict'; - const lastRange = new Range(0, 0, 0, 0); export function registerCommand(client: LanguageClient): [Disposable] { const showTypeChannel = window.createOutputChannel('Haskell Show Type'); - 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; - } - 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)]; - - vscode.window.showInformationMessage(typ); + getTypes({client, editor}).then(typ => { + window.showInformationMessage(typ); // displayType(showTypeChannel, typ); - }, e => { - console.error(e); - }); + }).catch(e => console.error(e)); + }); return [cmd, showTypeChannel]; } - - 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)]; - } - } else { - return rngs[0]; - } - } } -function displayType(chan: OutputChannel, typ: string) { +const displayType = (chan: OutputChannel, typ: string) => { chan.clear(); chan.appendLine(typ); chan.show(true); -} +}; -export interface HaskellShowTypeInformation { - // file: string; - type: string; - // line: number; - // column: number; - // doc: string; - // declarationlines: string[]; - // name: string; - // toolUsed: string; -} +// export interface IHaskellShowTypeInformation { +// // file: string; +// type: string; +// // line: number; +// // column: number; +// // doc: string; +// // declarationlines: string[]; +// // name: string; +// // toolUsed: string; +// } + +let lastRange = new Range(0, 0, 0, 0); + +const 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)]; + } + } else { + return rngs[0]; + } +}; + +const getCmd = editor => ({ + command: 'ghcmod:type', + arguments: [{ + file: editor.document.uri.toString(), + pos: editor.selections[0].start, + include_constraints: true, + }], +}); + +const getTypes = ({client, editor}) => client.sendRequest('workspace/executeCommand', getCmd(editor)).then(hints => { + const arr = hints as Array<[VLCRange, string]>; + if (arr.length === 0) { + return; + } + 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; + return typ; + }, e => { + console.error(e); +}); + +const typeFormatter = (typeString: string): MarkedString => { + // const ms = new MarkedString(); + const ms = []; + // definition? + let def = typeString.split('::').map(s => s.trim()); + if (def.length > 1) { + ms.push(`**${def[0]}** :: `); + def.shift(); + } + // context? + def = typeString.split('=>').map(s => s.trim()); + if (def.length > 1) { + ms.push(`*${def[0]}* => `); + def.shift(); + } + // Process rest... + def = typeString.split('->').map(s => s.trim()); + if (def.length === 1 && def[0] === '') { + return; + } + if (def.length >= 1) { + ms.push(def.map(s => `**${s}**`).join(' -> ')); + } + return ms.join(); + // while def.length >= 1 { + // if (def === '') { + // return; + // } + // ms.push(def.map(s => `*${s}*`).join(' -> ')) + // } + +}; -// export const showTypeHover(client) : HoverProvider = { - // export class ShowTypeHover { export class ShowTypeHover implements HoverProvider { - client: LanguageClient; + public client: LanguageClient; constructor(client) { this.client = client; } public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable { - let editor = vscode.window.activeTextEditor; - let lastRange = new Range(0, 0, 0, 0); - - - let cmd = { - command: "ghcmod:type", - arguments: [{ - file: editor.document.uri.toString(), - pos: editor.selections[0].start, - include_constraints: true - }] - }; - - function chooseRange(sel: Selection, rngs: [Range, string][]): [Range, string] { - if (sel.isEqual(lastRange)) { - let curr = rngs.findIndex(([rng, typ]) => sel.isEqual(rng)); - if (curr == -1) { - return rngs[0]; - } else { - return rngs[Math.min(rngs.length-1, curr+1)] - } - } else return rngs[0]; - }; - - const typeFormatter = (typeString: string): MarkedString => { - // const ms = new MarkedString(); - const ms = []; - // definition? - let def = typeString.split("::").map(s => s.trim()); - if (def.length > 1) { - ms.push(`**${def[0]}** :: `); - def.shift() - } - // context? - def = typeString.split("=>").map(s => s.trim()); - if (def.length > 1) { - ms.push(`*${def[0]}* => `); - def.shift() - } - // Process rest... - def = typeString.split("->").map(s => s.trim()); - if (def.length === 1 && def[0] === '') { - return; - } - if (def.length >= 1) { - ms.push(def.map(s => `**${s}**`).join(' -> ')) - } - return ms.join(); - // while def.length >= 1 { - // if (def === '') { - // return; - // } - // ms.push(def.map(s => `*${s}*`).join(' -> ')) - // } + const editor = window.activeTextEditor; - } - - - return this.client.sendRequest("workspace/executeCommand", cmd).then(hints => { - let arr = hints as [lng.Range, string][]; - if (arr.length == 0) return; - let ranges = arr.map(x => [this.client.protocol2CodeConverter.asRange(x[0]), x[1]]) as [vscode.Range, string][]; - let [rng, typ] = chooseRange(editor.selection, ranges); - // lastRange = rng; - - // editor.selections = [new Selection(rng.end, rng.start)]; - console.log(`SHOWTYPE IS `, typ); - let hover = new vscode.Hover({ + return getTypes({client: this.client, editor}).then(typ => { + return new Hover({ language: 'haskell', - // value: `REAL: ${typeFormatter(typ)}` // NOTE: Force some better syntax highlighting: - value: `_ :: ${typ}` + value: `_ :: ${typ}`, }); - // let hover = new vscode.Hover([`\`\`\`haskell\n` + "foo :: ([AWS.Filter] -> Identity [AWS.Filter]) -> AWS.DescribeInstances -> Identity AWS.DescribeInstances" + `\n\`\`\``]); - - // let hover = new vscode.Hover([`\`\`\`haskell\n` + "foo :: this -> that" + `\n\`\`\``]); - // let hover = new vscode.Hover([`\`\`\`haskell\nfoo :: this -> that\n\`\`\``]); - // let hover = new vscode.Hover([`\`\`\`js\nconst elbow = () => pow;\n\`\`\``]); - // let hover = new vscode.Hover([typeFormatter(typ)]); - console.log(`SHOWTYPE HOVER IS `, hover); - // vscode.window.showInformationMessage(typ); - // displayType(showTypeChannel, typ); - return hover; - }, e => { - console.error(e); }); } -}; +} -const HASKELL_MODE: vscode.DocumentFilter = - { language: 'haskell', scheme: 'file' }; +const HASKELL_MODE: DocumentFilter = { + language: 'haskell', + scheme: 'file', +}; -export const registerTypeHover = (client) => - vscode - .languages +export const registerTypeHover = (client) => languages .registerHoverProvider(HASKELL_MODE, new ShowTypeHover(client)); diff --git a/src/commands/showTypeFix.ts b/src/commands/showTypeFix.ts new file mode 100644 index 00000000..de186f69 --- /dev/null +++ b/src/commands/showTypeFix.ts @@ -0,0 +1,247 @@ +import { + CancellationToken, + commands, + Disposable, + DocumentFilter, + Hover, + HoverProvider, + languages, + MarkedString, + OutputChannel, + Position, + // ProviderResult, + Range, + Selection, + TextDocument, + window +} from 'vscode'; +import { + LanguageClient, + Range as VLCRange, + // RequestType +} from 'vscode-languageclient'; + +import {CommandNames} from './constants'; + +export namespace ShowType { + 'use strict'; + + export function registerCommand(client: LanguageClient): [Disposable] { + const showTypeChannel = window.createOutputChannel('Haskell Show Type'); + + const cmd = commands.registerCommand(CommandNames.ShowTypeCommandName, x => { + const editor = window.activeTextEditor; + + getTypes({client, editor}).then(typ => { + window.showInformationMessage(typ); + // displayType(showTypeChannel, typ); + }).catch(e => console.error(e)); + + }); + + return [cmd, showTypeChannel]; + } +} + +// Cache same selections... +const blankRange = new Range(0, 0, 0, 0); +let lastRange = blankRange; +let lastType = ''; +// let lastRange = new Range(0, 0, 0, 0); + +async function getTypes({client, editor}) { + 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; + // return; + } + 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 typ; + } catch (e) { + console.error(e); + } +} + +/* + Yes, it returns a list of types increasing in scope. + Like if you have `add a = a + 1` and you ask type for `a` at the rhs + it gives you [`Int`, `Int -> Int`]. + The first one is what you asked for, the rest ones - + next widened scope then widen it again, etc. + + Comes handy, I want to add it as a feature: + press `Cmd+t` and it shows me the type (in a tooltip), + press again - selection is widened and I see the type of a bigger expression etc,. + */ + +// sel is selection in editor as a Range +// rngs is the type analysis from the server - an array of Range, type pairs +// lastRange is the stored last match +const chooseRange = (sel: Selection, rngs: Array<[Range, string]>): [Range, string] => { + console.log('========='); + console.log(logPos('sel', sel, null)); + // console.log('sel is ', sel); + // console.log('rngs is ', rngs); + console.log(logPosMap('rngs', rngs)); + + const curr = rngs.findIndex(([rng, typ]) => rng.contains(sel)); + + if (curr !== -1) { + console.log('\n', logPos(`container: ${curr}`, rngs[curr][0], rngs[curr][1])); + } else { + console.log('No Match...'); + } + console.log('sel === rng?: ', sel.isEqual(lastRange)); + console.log('========='); + + // This never happens.... + // if (sel.isEqual(lastRange)) { + // if (sel.isEqual(lastRange)) { + // if (true) { + // const curr = rngs.findIndex(([rng, typ]) => sel.isEqual(rng)); + // const curr = rngs.findIndex(([rng, typ]) => rng.contains(sel)); + // const curr = rngs.findIndex(([rng, typ]) => sel.contains(rng)); + + // 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... + console.log('We didnt find the range!!!!'); + return rngs[0]; + } else { + return rngs[curr]; + // return rngs[Math.min(rngs.length - 1, curr + 1)]; + } + // } else { + // return rngs[0]; + // } +}; + +const logPos = (label: string, rng: Range, typ: string): string => { + // tslint:disable-next-line + return `${label}: start Line ${rng.start.line} Character ${rng.start.character} | end Line ${rng.end.line} Character ${rng.end.character}${typ ? `| type ${typ}` : ''}`; +}; + +const logPosMap = (label: string, posList: Array<[Range, string]>) => + posList.map(([r, t], i) => logPos(i.toString(), r, t)).join('\n'); + +const getCmd = editor => ({ + command: 'ghcmod:type', + arguments: [{ + file: editor.document.uri.toString(), + pos: editor.selections[0].start, + include_constraints: true, + }], +}); + +const typeFormatter = (typeString: string): MarkedString => { + // const ms = new MarkedString(); + const ms = []; + // definition? + let def = typeString.split('::').map(s => s.trim()); + if (def.length > 1) { + ms.push(`**${def[0]}** :: `); + def.shift(); + } + // context? + def = typeString.split('=>').map(s => s.trim()); + if (def.length > 1) { + ms.push(`*${def[0]}* => `); + def.shift(); + } + // Process rest... + def = typeString.split('->').map(s => s.trim()); + if (def.length === 1 && def[0] === '') { + return; + } + if (def.length >= 1) { + ms.push(def.map(s => `**${s}**`).join(' -> ')); + } + return ms.join(); + // while def.length >= 1 { + // if (def === '') { + // return; + // } + // ms.push(def.map(s => `*${s}*`).join(' -> ')) + // } + +}; + +export class ShowTypeHover implements HoverProvider { + public client: LanguageClient; + + constructor(client) { + this.client = client; + } + + public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable { + const editor = window.activeTextEditor; + if (editor.selection.isEmpty) { + console.log(`Selection Empty`); + return null; + } + // 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)) { + console.log(`Cursor Outside Selection`); + return null; + } + // NOTE: Not sure if we want this - maybe we can get multiline to work? + if (!editor.selection.isSingleLine) { + console.log(`Muliline Selection`); + return null; + } + // NOTE: No need for server call + // TODO: Selection/highlighted text may be unchanged but cursor may + // be hovering somewhere else and thus need a different type and in fact get + // a different type due to the ordinary ShowType function - need a way to ignore this + // showType when that is the case - not sure if we can without making another server call though...there is no API for getting cursor position outside of selection? + if (lastType && editor.selection.isEqual(lastRange)) { + console.log(`Selection unchanged...`); + return Promise.resolve(this.makeHover(lastType)); + } + + return getTypes({client: this.client, editor}).then(typ => { + + // return new Hover({ + // language: 'haskell', + // // NOTE: Force some better syntax highlighting: + // value: `_ :: ${typ}`, + // }); + console.log('TYP ----- ', typ); + if (typ) { + return this.makeHover(lastType); + // return new Hover({ + // language: 'haskell', + // // NOTE: Force some better syntax highlighting: + // value: `_ :: ${typ}`, + // }); + } else { + return null; + } + }); + } + + private makeHover(typ: string): Hover { + return new Hover({ + language: 'haskell', + // NOTE: Force some better syntax highlighting: + value: `_ :: ${typ}`, + }); + } +} + +const HASKELL_MODE: DocumentFilter = { + language: 'haskell', + scheme: 'file', +}; + +export const registerTypeHover = (client) => languages + .registerHoverProvider(HASKELL_MODE, new ShowTypeHover(client)); diff --git a/src/extension.ts b/src/extension.ts index 2c42a621..777b89ea 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,6 +7,7 @@ import * as child_process from 'child_process'; import * as path from 'path'; import { commands, + DocumentFilter, ExtensionContext, window, workspace @@ -19,9 +20,13 @@ import { } from 'vscode-languageclient'; import { InsertType } from './commands/insertType'; -import { ShowType } from './commands/showType'; +import { + registerTypeHover, + ShowType, + ShowTypeHover, +} from './commands/showTypeFix'; +// } from './commands/showType'; import { DocsBrowser } from './docsBrowser'; -import { ShowTypeHover, registerTypeHover } from './commands/showType'; export async function activate(context: ExtensionContext) { try { @@ -46,7 +51,6 @@ export async function activate(context: ExtensionContext) { function activateNoHieCheck(context: ExtensionContext) { -<<<<<<< HEAD const docsDisposable = DocsBrowser.registerDocsBrowser(); context.subscriptions.push(docsDisposable); @@ -89,6 +93,23 @@ function activateNoHieCheck(context: ExtensionContext) { context.subscriptions.push(InsertType.registerCommand(langClient)); ShowType.registerCommand(langClient).forEach(x => context.subscriptions.push(x)); + if (workspace.getConfiguration('languageServerHaskell').showTypeForSelection) { + context.subscriptions.push(registerTypeHover(langClient)); + } + + // context.subscriptions.push( + // // NOTE: This isnt implemented yet :-\ + // // https://github.com/Microsoft/vscode/pull/36476/files + // workspace.onDidChangeConfiguration((e) => { + // // workspace.onDidChangeConfiguration((e: ConfigurationChangeEvent) => { + // // // tslint:disable-next-line + // // console.log(`CONFIGURATION CHANGED!! `, e); + // // const change = e.affectsConfiguration('languageServerHaskell'); + // // // tslint:disable-next-line + // // console.log(`Changed? `, change); + // }) + // ); + registerHiePointCommand(langClient, 'hie.commands.demoteDef', 'hare:demote', context); registerHiePointCommand(langClient, 'hie.commands.liftOneLevel', 'hare:liftonelevel', context); registerHiePointCommand(langClient, 'hie.commands.liftTopLevel', 'hare:lifttotoplevel', context); @@ -97,68 +118,6 @@ function activateNoHieCheck(context: ExtensionContext) { const disposable = langClient.start(); context.subscriptions.push(disposable); -======= - let docsDisposable = DocsBrowser.registerDocsBrowser(); - context.subscriptions.push(docsDisposable); - - // const fixer = languages.registerCodeActionsProvider("haskell", fixProvider); - // context.subscriptions.push(fixer); - // The server is implemented in node - //let serverModule = context.asAbsolutePath(path.join('server', 'server.js')); - let startupScript = ( process.platform == "win32" ) ? "hie-vscode.bat" : "hie-vscode.sh"; - let serverPath = context.asAbsolutePath(path.join('.', startupScript)); - let serverExe = { command: serverPath } - // The debug options for the server - let debugOptions = { execArgv: ["--nolazy", "--debug=6004"] }; - - // If the extension is launched in debug mode then the debug server options are used - // Otherwise the run options are used - let tempDir = ( process.platform == "win32" ) ? "%TEMP%" : "/tmp"; - let serverOptions: ServerOptions = { - //run : { module: serverModule, transport: TransportKind.ipc }, - //debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } - run : { command: serverPath }, - debug: { command: serverPath, args: ["-d", "-l", path.join(tempDir, "hie.log")] } - } - - // Options to control the language client - let clientOptions: LanguageClientOptions = { - // Register the server for plain text documents - documentSelector: ['haskell'], - synchronize: { - // Synchronize the setting section 'languageServerHaskell' to the server - configurationSection: 'languageServerHaskell', - // Notify the server about file changes to '.clientrc files contain in the workspace - fileEvents: workspace.createFileSystemWatcher('**/.clientrc') - }, - middleware: { - provideHover: DocsBrowser.hoverLinksMiddlewareHook - }, - revealOutputChannelOn: RevealOutputChannelOn.never - } - - // Create the language client and start the client. - let langClient = new LanguageClient('Language Server Haskell', serverOptions, clientOptions); - - context.subscriptions.push(InsertType.registerCommand(langClient)); - ShowType.registerCommand(langClient).forEach(x => context.subscriptions.push(x)); - - // context.subscriptions.push(vscode.languages.registerHoverProvider(controller.IDRIS_MODE, new show.IdrisHoverProvider())) - // context.subscriptions.push(vscode.languages.registerHoverProvider(controller.IDRIS_MODE, new showTypeHover())) - if (vscode.workspace.getConfiguration('languageServerHaskell').showTypeForSelection) { - context.subscriptions.push(registerTypeHover(langClient)); - } - - - registerHiePointCommand(langClient,"hie.commands.demoteDef","hare:demote",context); - registerHiePointCommand(langClient,"hie.commands.liftOneLevel","hare:liftonelevel",context); - registerHiePointCommand(langClient,"hie.commands.liftTopLevel","hare:lifttotoplevel",context); - registerHiePointCommand(langClient,"hie.commands.deleteDef","hare:deletedef",context); - registerHiePointCommand(langClient,"hie.commands.genApplicative","hare:genapplicative",context); - let disposable = langClient.start(); - - context.subscriptions.push(disposable); ->>>>>>> I got showtype on Hover working :-D } function isHieInstalled(): Promise { @@ -188,3 +147,30 @@ function registerHiePointCommand(langClient: LanguageClient, name: string, comma }); context.subscriptions.push(cmd2); } + +// async function registerHiePointCommand(langClient: LanguageClient, +// name: string, command: string, context: ExtensionContext) { +// const cmd2 = commands.registerTextEditorCommand(name, (editor, edit) => { +// const cmd = { +// command, +// arguments: [ +// { +// file: editor.document.uri.toString(), +// pos: editor.selections[0].active, +// }, +// ], +// }; +// try { +// const hints = await langClient.sendRequest('workspace/executeCommand', cmd); +// return true; +// } catch (e) { +// console.error(e); +// } +// // langClient.sendRequest('workspace/executeCommand', cmd).then(hints => { +// // return true; +// // }, e => { +// // console.error(e); +// // }); +// }); +// context.subscriptions.push(cmd2); +// } diff --git a/tslint.json b/tslint.json index a8fe811e..652003aa 100644 --- a/tslint.json +++ b/tslint.json @@ -9,9 +9,13 @@ "object-literal-sort-keys": false, "indent": [true, "spaces", 2], "no-namespace": false, - "no-console": [true, "log"], - "no-unused-variable": true, - "quotemark": [true, "single", "avoid-template"], + // "no-console": [true, "log"], + "no-console": false, + + // "no-unused-variable": true, + // "promise-function-async": true, + // "quotemark": [true, "single", "avoid-template"], + "quotemark": [true, "single"], "trailing-comma": [true, { "multiline": { "objects": "always", From 0d05bf5eae31586c78f3e98f9ea7fe6b89ff0811 Mon Sep 17 00:00:00 2001 From: Hal Henke Date: Thu, 11 Jan 2018 04:32:52 +1100 Subject: [PATCH 3/5] Change configuration settings - Move the trace option into configuration and name appropriately --- package.json | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 4347296b..ecf68692 100644 --- a/package.json +++ b/package.json @@ -82,22 +82,40 @@ "description": "Get suggestions from hlint" }, "languageServerHaskell.showTypeForSelection": { + "type": "object", + "default": { + "on": true, + "location": "hover" + }, + "description": "Settings for showType on editor selections." + }, + "languageServerHaskell.showTypeForSelection.on": { "type": "boolean", - "default": false, - "description": "If true, when an expression is selected, vscode will the hover tooltip will try to displaay the type of the entire expression - rather than just the term under the cursor." + "default": true, + "description": "If true, when an expression is selected, vscode will the hover tooltip will try to display the type of the entire expression - rather than just the term under the cursor." + }, + "languageServerHaskell.showTypeForSelection.location": { + "type": "string", + "enum": [ + "hover", + "dropdown", + "channel" + ], + "default": "hover", + "description": "Determines where the type information for selections will be shown when this feature is enabled.\nHover: in the tooltip\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", From c3f32c52fbfc077966ca397ecb847a8a6856ab9b Mon Sep 17 00:00:00 2001 From: Hal Henke Date: Thu, 11 Jan 2018 04:33:29 +1100 Subject: [PATCH 4/5] Extract cursor/selection checks into separate function - Check if cursor position is within selection before showing selected type information - Get actual Haskell expression from selection to show in type information rather than using placeholder --- src/commands/showTypeFix.ts | 77 ++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/src/commands/showTypeFix.ts b/src/commands/showTypeFix.ts index de186f69..591dc989 100644 --- a/src/commands/showTypeFix.ts +++ b/src/commands/showTypeFix.ts @@ -13,11 +13,13 @@ import { Range, Selection, TextDocument, + TextEditor, window } from 'vscode'; import { LanguageClient, Range as VLCRange, + TextEdit, // RequestType } from 'vscode-languageclient'; @@ -32,7 +34,7 @@ export namespace ShowType { const cmd = commands.registerCommand(CommandNames.ShowTypeCommandName, x => { const editor = window.activeTextEditor; - getTypes({client, editor}).then(typ => { + getTypes({client, editor}).then(([_, typ]) => { window.showInformationMessage(typ); // displayType(showTypeChannel, typ); }).catch(e => console.error(e)); @@ -49,7 +51,7 @@ let lastRange = blankRange; let lastType = ''; // let lastRange = new Range(0, 0, 0, 0); -async function getTypes({client, editor}) { +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]>; @@ -63,7 +65,7 @@ async function getTypes({client, editor}) { const [rng, typ] = chooseRange(editor.selection, ranges); lastRange = rng; lastType = typ; - return typ; + return [rng, typ]; } catch (e) { console.error(e); } @@ -174,6 +176,33 @@ const typeFormatter = (typeString: string): MarkedString => { }; +const showShowType = (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) { + console.log(`Selection Empty`); + 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)) { + console.log(`Cursor Outside Selection`); + return false; + } + // NOTE: Not sure if we want this - maybe we can get multiline to work? + if (!editor.selection.isSingleLine) { + console.log(`Muliline Selection`); + return false; + } + return true; +}; + export class ShowTypeHover implements HoverProvider { public client: LanguageClient; @@ -183,57 +212,33 @@ export class ShowTypeHover implements HoverProvider { public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable { const editor = window.activeTextEditor; - if (editor.selection.isEmpty) { - console.log(`Selection Empty`); - return null; - } - // 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)) { - console.log(`Cursor Outside Selection`); - return null; - } - // NOTE: Not sure if we want this - maybe we can get multiline to work? - if (!editor.selection.isSingleLine) { - console.log(`Muliline Selection`); + + if (!showShowType(editor, position)) { return null; } + // NOTE: No need for server call - // TODO: Selection/highlighted text may be unchanged but cursor may - // be hovering somewhere else and thus need a different type and in fact get - // a different type due to the ordinary ShowType function - need a way to ignore this - // showType when that is the case - not sure if we can without making another server call though...there is no API for getting cursor position outside of selection? if (lastType && editor.selection.isEqual(lastRange)) { console.log(`Selection unchanged...`); - return Promise.resolve(this.makeHover(lastType)); + return Promise.resolve(this.makeHover(document, lastRange, lastType)); } - return getTypes({client: this.client, editor}).then(typ => { - - // return new Hover({ - // language: 'haskell', - // // NOTE: Force some better syntax highlighting: - // value: `_ :: ${typ}`, - // }); + return getTypes({client: this.client, editor}).then(([r, typ]) => { console.log('TYP ----- ', typ); if (typ) { - return this.makeHover(lastType); - // return new Hover({ - // language: 'haskell', - // // NOTE: Force some better syntax highlighting: - // value: `_ :: ${typ}`, - // }); + return this.makeHover(document, r, lastType); } else { return null; } }); } - private makeHover(typ: string): Hover { + private makeHover(document: TextDocument, r: Range, typ: string): Hover { + const expression = document.getText(r); return new Hover({ language: 'haskell', // NOTE: Force some better syntax highlighting: - value: `_ :: ${typ}`, + value: `${expression} :: ${typ}`, }); } } From f435422cd3ad9430ef06e080ede9d54dc5bcd7d7 Mon Sep 17 00:00:00 2001 From: Hal Henke Date: Fri, 12 Jan 2018 02:28:14 +1100 Subject: [PATCH 5/5] Clean up typescript errors etc - tweak configuration details - reorder code --- .vscode/settings.json | 2 +- package.json | 19 +-- src/commands/showType.ts | 238 +++++++++++++++++++--------------- src/commands/showTypeFix.ts | 252 ------------------------------------ src/extension.ts | 64 ++------- tslint.json | 11 +- 6 files changed, 155 insertions(+), 431 deletions(-) delete mode 100644 src/commands/showTypeFix.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 09f5ecb7..f8ca5d63 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "files.exclude": { "out": true, // set this to true to hide the "out" folder with the compiled JS files ".vscode-test": true, - "node_modules": false + "node_modules": true }, "search.exclude": { "out": true // set this to false to include "out" folder in search results diff --git a/package.json b/package.json index ecf68692..1e4dd411 100644 --- a/package.json +++ b/package.json @@ -81,28 +81,19 @@ "default": true, "description": "Get suggestions from hlint" }, - "languageServerHaskell.showTypeForSelection": { - "type": "object", - "default": { - "on": true, - "location": "hover" - }, - "description": "Settings for showType on editor selections." - }, - "languageServerHaskell.showTypeForSelection.on": { + "languageServerHaskell.showTypeForSelection.onHover": { "type": "boolean", "default": true, - "description": "If true, when an expression is selected, vscode will the hover tooltip will try to display the type of the entire expression - rather than just the term under the cursor." + "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.location": { + "languageServerHaskell.showTypeForSelection.command.location": { "type": "string", "enum": [ - "hover", "dropdown", "channel" ], - "default": "hover", - "description": "Determines where the type information for selections will be shown when this feature is enabled.\nHover: in the tooltip\ndropdown: in a dropdown\nchannel: will be revealed in an output 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", diff --git a/src/commands/showType.ts b/src/commands/showType.ts index 99a91a06..7217ebc4 100644 --- a/src/commands/showType.ts +++ b/src/commands/showType.ts @@ -6,73 +6,71 @@ import { Hover, HoverProvider, languages, - MarkedString, OutputChannel, Position, - ProviderResult, Range, Selection, TextDocument, - window + TextEditor, + window, + workspace } from 'vscode'; import { LanguageClient, Range as VLCRange, - RequestType } from 'vscode-languageclient'; import {CommandNames} from './constants'; -export namespace ShowType { - 'use strict'; +const formatExpressionType = (document: TextDocument, r: Range, typ: string): string => + `${document.getText(r)} :: ${typ}`; - export function registerCommand(client: LanguageClient): [Disposable] { - const showTypeChannel = window.createOutputChannel('Haskell Show Type'); - - const cmd = commands.registerCommand(CommandNames.ShowTypeCommandName, x => { - const editor = window.activeTextEditor; - - getTypes({client, editor}).then(typ => { - window.showInformationMessage(typ); - // displayType(showTypeChannel, typ); - }).catch(e => console.error(e)); +const HASKELL_MODE: DocumentFilter = { + language: 'haskell', + scheme: 'file', +}; - }); +// Cache same selections... +const blankRange = new Range(0, 0, 0, 0); +let lastRange = blankRange; +let lastType = ''; - return [cmd, showTypeChannel]; +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); } } -const displayType = (chan: OutputChannel, typ: string) => { - chan.clear(); - chan.appendLine(typ); - chan.show(true); -}; - -// export interface IHaskellShowTypeInformation { -// // file: string; -// type: string; -// // line: number; -// // column: number; -// // doc: string; -// // declarationlines: string[]; -// // name: string; -// // toolUsed: string; -// } - -let lastRange = new Range(0, 0, 0, 0); - +/** + * 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] => { - if (sel.isEqual(lastRange)) { - const curr = rngs.findIndex(([rng, typ]) => sel.isEqual(rng)); + const curr = rngs.findIndex(([rng, typ]) => rng.contains(sel)); + + // 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[Math.min(rngs.length - 1, curr + 1)]; + return rngs[curr]; } - } else { - return rngs[0]; - } }; const getCmd = editor => ({ @@ -84,76 +82,108 @@ const getCmd = editor => ({ }], }); -const getTypes = ({client, editor}) => client.sendRequest('workspace/executeCommand', getCmd(editor)).then(hints => { - const arr = hints as Array<[VLCRange, string]>; - if (arr.length === 0) { - return; - } - 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; - return typ; - }, e => { - console.error(e); -}); +export namespace ShowTypeCommand { + 'use strict'; -const typeFormatter = (typeString: string): MarkedString => { - // const ms = new MarkedString(); - const ms = []; - // definition? - let def = typeString.split('::').map(s => s.trim()); - if (def.length > 1) { - ms.push(`**${def[0]}** :: `); - def.shift(); - } - // context? - def = typeString.split('=>').map(s => s.trim()); - if (def.length > 1) { - ms.push(`*${def[0]}* => `); - def.shift(); - } - // Process rest... - def = typeString.split('->').map(s => s.trim()); - if (def.length === 1 && def[0] === '') { - return; - } - if (def.length >= 1) { - ms.push(def.map(s => `**${s}**`).join(' -> ')); - } - return ms.join(); - // while def.length >= 1 { - // if (def === '') { - // return; - // } - // ms.push(def.map(s => `*${s}*`).join(' -> ')) - // } + 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, x => { + const editor = window.activeTextEditor; + + 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; + } + }).catch(e => console.error(e)); -export class ShowTypeHover implements HoverProvider { - public client: LanguageClient; + }); - constructor(client) { - this.client = client; + return [cmd, showTypeChannel]; } +} - public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable { - const editor = window.activeTextEditor; +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; + }; - return getTypes({client: this.client, editor}).then(typ => { + class TypeHover implements HoverProvider { + public client: LanguageClient; + + constructor(client) { + this.client = client; + } + + public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable { + const editor = window.activeTextEditor; + + if (!showTypeNow(editor, position)) { + return null; + } + + // 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', - // NOTE: Force some better syntax highlighting: - value: `_ :: ${typ}`, + value: formatExpressionType(document, r, typ), }); - }); + } } -} - -const HASKELL_MODE: DocumentFilter = { - language: 'haskell', - scheme: 'file', -}; -export const registerTypeHover = (client) => languages - .registerHoverProvider(HASKELL_MODE, new ShowTypeHover(client)); + export const registerTypeHover = (client) => languages + .registerHoverProvider(HASKELL_MODE, new TypeHover(client)); +} diff --git a/src/commands/showTypeFix.ts b/src/commands/showTypeFix.ts deleted file mode 100644 index 591dc989..00000000 --- a/src/commands/showTypeFix.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { - CancellationToken, - commands, - Disposable, - DocumentFilter, - Hover, - HoverProvider, - languages, - MarkedString, - OutputChannel, - Position, - // ProviderResult, - Range, - Selection, - TextDocument, - TextEditor, - window -} from 'vscode'; -import { - LanguageClient, - Range as VLCRange, - TextEdit, - // RequestType -} from 'vscode-languageclient'; - -import {CommandNames} from './constants'; - -export namespace ShowType { - 'use strict'; - - export function registerCommand(client: LanguageClient): [Disposable] { - const showTypeChannel = window.createOutputChannel('Haskell Show Type'); - - const cmd = commands.registerCommand(CommandNames.ShowTypeCommandName, x => { - const editor = window.activeTextEditor; - - getTypes({client, editor}).then(([_, typ]) => { - window.showInformationMessage(typ); - // displayType(showTypeChannel, typ); - }).catch(e => console.error(e)); - - }); - - return [cmd, showTypeChannel]; - } -} - -// Cache same selections... -const blankRange = new Range(0, 0, 0, 0); -let lastRange = blankRange; -let lastType = ''; -// let lastRange = new Range(0, 0, 0, 0); - -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; - // return; - } - 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); - } -} - -/* - Yes, it returns a list of types increasing in scope. - Like if you have `add a = a + 1` and you ask type for `a` at the rhs - it gives you [`Int`, `Int -> Int`]. - The first one is what you asked for, the rest ones - - next widened scope then widen it again, etc. - - Comes handy, I want to add it as a feature: - press `Cmd+t` and it shows me the type (in a tooltip), - press again - selection is widened and I see the type of a bigger expression etc,. - */ - -// sel is selection in editor as a Range -// rngs is the type analysis from the server - an array of Range, type pairs -// lastRange is the stored last match -const chooseRange = (sel: Selection, rngs: Array<[Range, string]>): [Range, string] => { - console.log('========='); - console.log(logPos('sel', sel, null)); - // console.log('sel is ', sel); - // console.log('rngs is ', rngs); - console.log(logPosMap('rngs', rngs)); - - const curr = rngs.findIndex(([rng, typ]) => rng.contains(sel)); - - if (curr !== -1) { - console.log('\n', logPos(`container: ${curr}`, rngs[curr][0], rngs[curr][1])); - } else { - console.log('No Match...'); - } - console.log('sel === rng?: ', sel.isEqual(lastRange)); - console.log('========='); - - // This never happens.... - // if (sel.isEqual(lastRange)) { - // if (sel.isEqual(lastRange)) { - // if (true) { - // const curr = rngs.findIndex(([rng, typ]) => sel.isEqual(rng)); - // const curr = rngs.findIndex(([rng, typ]) => rng.contains(sel)); - // const curr = rngs.findIndex(([rng, typ]) => sel.contains(rng)); - - // 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... - console.log('We didnt find the range!!!!'); - return rngs[0]; - } else { - return rngs[curr]; - // return rngs[Math.min(rngs.length - 1, curr + 1)]; - } - // } else { - // return rngs[0]; - // } -}; - -const logPos = (label: string, rng: Range, typ: string): string => { - // tslint:disable-next-line - return `${label}: start Line ${rng.start.line} Character ${rng.start.character} | end Line ${rng.end.line} Character ${rng.end.character}${typ ? `| type ${typ}` : ''}`; -}; - -const logPosMap = (label: string, posList: Array<[Range, string]>) => - posList.map(([r, t], i) => logPos(i.toString(), r, t)).join('\n'); - -const getCmd = editor => ({ - command: 'ghcmod:type', - arguments: [{ - file: editor.document.uri.toString(), - pos: editor.selections[0].start, - include_constraints: true, - }], -}); - -const typeFormatter = (typeString: string): MarkedString => { - // const ms = new MarkedString(); - const ms = []; - // definition? - let def = typeString.split('::').map(s => s.trim()); - if (def.length > 1) { - ms.push(`**${def[0]}** :: `); - def.shift(); - } - // context? - def = typeString.split('=>').map(s => s.trim()); - if (def.length > 1) { - ms.push(`*${def[0]}* => `); - def.shift(); - } - // Process rest... - def = typeString.split('->').map(s => s.trim()); - if (def.length === 1 && def[0] === '') { - return; - } - if (def.length >= 1) { - ms.push(def.map(s => `**${s}**`).join(' -> ')); - } - return ms.join(); - // while def.length >= 1 { - // if (def === '') { - // return; - // } - // ms.push(def.map(s => `*${s}*`).join(' -> ')) - // } - -}; - -const showShowType = (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) { - console.log(`Selection Empty`); - 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)) { - console.log(`Cursor Outside Selection`); - return false; - } - // NOTE: Not sure if we want this - maybe we can get multiline to work? - if (!editor.selection.isSingleLine) { - console.log(`Muliline Selection`); - return false; - } - return true; -}; - -export class ShowTypeHover implements HoverProvider { - public client: LanguageClient; - - constructor(client) { - this.client = client; - } - - public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable { - const editor = window.activeTextEditor; - - if (!showShowType(editor, position)) { - return null; - } - - // NOTE: No need for server call - if (lastType && editor.selection.isEqual(lastRange)) { - console.log(`Selection unchanged...`); - return Promise.resolve(this.makeHover(document, lastRange, lastType)); - } - - return getTypes({client: this.client, editor}).then(([r, typ]) => { - console.log('TYP ----- ', typ); - if (typ) { - return this.makeHover(document, r, lastType); - } else { - return null; - } - }); - } - - private makeHover(document: TextDocument, r: Range, typ: string): Hover { - const expression = document.getText(r); - return new Hover({ - language: 'haskell', - // NOTE: Force some better syntax highlighting: - value: `${expression} :: ${typ}`, - }); - } -} - -const HASKELL_MODE: DocumentFilter = { - language: 'haskell', - scheme: 'file', -}; - -export const registerTypeHover = (client) => languages - .registerHoverProvider(HASKELL_MODE, new ShowTypeHover(client)); diff --git a/src/extension.ts b/src/extension.ts index 777b89ea..7a615bb3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,7 +7,6 @@ import * as child_process from 'child_process'; import * as path from 'path'; import { commands, - DocumentFilter, ExtensionContext, window, workspace @@ -18,20 +17,17 @@ import { RevealOutputChannelOn, ServerOptions } from 'vscode-languageclient'; - import { InsertType } from './commands/insertType'; import { - registerTypeHover, - ShowType, + ShowTypeCommand, ShowTypeHover, -} from './commands/showTypeFix'; -// } from './commands/showType'; +} 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.'; @@ -91,24 +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)); - if (workspace.getConfiguration('languageServerHaskell').showTypeForSelection) { - context.subscriptions.push(registerTypeHover(langClient)); - } + ShowTypeCommand.registerCommand(langClient).forEach(x => context.subscriptions.push(x)); - // context.subscriptions.push( - // // NOTE: This isnt implemented yet :-\ - // // https://github.com/Microsoft/vscode/pull/36476/files - // workspace.onDidChangeConfiguration((e) => { - // // workspace.onDidChangeConfiguration((e: ConfigurationChangeEvent) => { - // // // tslint:disable-next-line - // // console.log(`CONFIGURATION CHANGED!! `, e); - // // const change = e.affectsConfiguration('languageServerHaskell'); - // // // tslint:disable-next-line - // // console.log(`Changed? `, change); - // }) - // ); + 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); @@ -120,14 +104,15 @@ function activateNoHieCheck(context: ExtensionContext) { context.subscriptions.push(disposable); } -function isHieInstalled(): Promise { - return new Promise((resolve, reject) => { +async function isHieInstalled(): Promise { + return new Promise((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, @@ -147,30 +132,3 @@ function registerHiePointCommand(langClient: LanguageClient, name: string, comma }); context.subscriptions.push(cmd2); } - -// async function registerHiePointCommand(langClient: LanguageClient, -// name: string, command: string, context: ExtensionContext) { -// const cmd2 = commands.registerTextEditorCommand(name, (editor, edit) => { -// const cmd = { -// command, -// arguments: [ -// { -// file: editor.document.uri.toString(), -// pos: editor.selections[0].active, -// }, -// ], -// }; -// try { -// const hints = await langClient.sendRequest('workspace/executeCommand', cmd); -// return true; -// } catch (e) { -// console.error(e); -// } -// // langClient.sendRequest('workspace/executeCommand', cmd).then(hints => { -// // return true; -// // }, e => { -// // console.error(e); -// // }); -// }); -// context.subscriptions.push(cmd2); -// } diff --git a/tslint.json b/tslint.json index 652003aa..fe83bcb4 100644 --- a/tslint.json +++ b/tslint.json @@ -9,13 +9,10 @@ "object-literal-sort-keys": false, "indent": [true, "spaces", 2], "no-namespace": false, - // "no-console": [true, "log"], - "no-console": false, - - // "no-unused-variable": true, - // "promise-function-async": true, - // "quotemark": [true, "single", "avoid-template"], - "quotemark": [true, "single"], + "no-console": [true, "log"], + "no-unused-variable": true, + "promise-function-async": true, + "quotemark": [true, "single", "avoid-template"], "trailing-comma": [true, { "multiline": { "objects": "always",