Skip to content
This repository has been archived by the owner on Mar 14, 2021. It is now read-only.

Haddock experiment (WIP) #166

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions lib/ghc-mod/ghc-modi-process.coffee
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{Range, Point, Emitter, CompositeDisposable} = require 'atom'
Haddock = require '../haddock'
Util = require '../util'
{extname} = require('path')
Queue = require 'promise-queue'
Expand Down Expand Up @@ -187,6 +188,45 @@ class GhcModiProcess
onQueueIdle: (callback) =>
@emitter.on 'queue-idle', callback

docsCmd: (symbol_to_lookup, runArgs) =>

unless runArgs.buffer? or runArgs.dir?
throw new Error ("Neither dir nor buffer is set in docsCmd invocation")
runArgs.dir ?= @getRootDir(runArgs.buffer) if runArgs.buffer?

rd = runArgs.dir or Util.getRootDir(runArgs.options.cwd)
package_name = rd.getBaseName()
console.log "Package name: " + package_name

# TODO Use the "stack path --local-doc-root" command to obtain this string
filepath = '../.stack-work/install/x86_64-linux/lts-5.9/7.10.3/doc/' + package_name + '-0.1.0.0/' + package_name + '.txt'

console.log "Haddock file path: " + filepath

my_promise = new Promise (resolve, reject) ->
file = rd.getFile(filepath)
file.exists()
.then (ex) ->
if ex
file.read().then (contents) ->
try
docs_by_module = Haddock.parseHoogleTextDoc(contents)
found_doc = Haddock.findDocsForSymbol(docs_by_module, symbol_to_lookup)
# TODO Get current module name!
resolve found_doc
catch err
atom.notifications.addError 'Failed to parse haddock-hoogle text file',
detail: err
dismissable: true
reject err
else
reject new Error('haddock-hoogle text file does not exist')
.catch (error) ->
Util.warn error
return {}

return my_promise

queueCmd: (queueName, runArgs, backend) =>
unless runArgs.buffer? or runArgs.dir?
throw new Error ("Neither dir nor buffer is set in queueCmd invocation")
Expand Down Expand Up @@ -380,6 +420,21 @@ class GhcModiProcess
else
return {range, info}


getDocInBuffer: (editor, crange) =>
buffer = editor.getBuffer()
return Promise.resolve null unless buffer.getUri()?
{symbol, range} = Util.getSymbolInRange(editor, crange)

@docsCmd symbol,
buffer: buffer
.then (found_doc) ->
if found_doc == null
throw new Error "No docs"
else
console.log "Symbol documentation: " + found_doc
return {range, found_doc}

findSymbolProvidersInBuffer: (editor, crange) =>
buffer = editor.getBuffer()
{symbol} = Util.getSymbolInRange(editor, crange)
Expand Down
75 changes: 75 additions & 0 deletions lib/haddock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
COMMENT_PREFIX = "-- ";
DOCSTRING_PREFIX = COMMENT_PREFIX + "| ";


function setdefault(dict, key, value) {
if (!(key in dict))
dict[key] = value;
return dict[key];
}


function findDocsForSymbol(docs_by_module, symbol) {

var module_qualification_chunks = symbol.split(".");
var last_chunk = module_qualification_chunks[module_qualification_chunks.length - 1]

for (var mkey in docs_by_module) {
var module_dict = docs_by_module[mkey];

for (var skey in module_dict) {
if (skey == last_chunk) {
return module_dict[skey];
}
}
}
return "-- <no documentation>";
}

function parseHoogleTextDoc(file_contents) {
lines = file_contents.trim().split('\n');

var current_module = null;
var docstring_lines = [];

// Nested dict; outer key is module name.
// inner key is function/variable name.
var entity_docs_map = {};

for (var i=0; i<lines.length; i++) {
var line = lines[i];

if (line.startsWith(COMMENT_PREFIX)) {
docstring_lines.push(line);
} else {

var tokens = line.split(/\s+/);

if (tokens.length > 0) {
if (tokens[0] == "module") {
current_module = tokens[1];
} else {

if (current_module != null) {
if (tokens.length > 1) {
if (tokens[1] == "::") {
var entity_name = tokens[0];
if (docstring_lines.length > 0) {
var sub_dict = setdefault(entity_docs_map, current_module, {});
sub_dict[entity_name] = docstring_lines.join("\n");
}
}
}
}
}
}

docstring_lines = [];
}
}

return entity_docs_map;
}

exports.parseHoogleTextDoc = parseHoogleTextDoc;
exports.findDocsForSymbol = findDocsForSymbol;
22 changes: 19 additions & 3 deletions lib/upi-consumer.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ class UPIConsumer
@upi.withEventRange {editor, detail}, ({crange}) =>
@process.getInfoInBuffer(editor, crange)
.then ({range, info}) =>

console.log "KARL - GHC-MOD: range: " + range

res = /.*-- Defined at (.+):(\d+):(\d+)/.exec info
return unless res?
[_, fn, line, col] = res
Expand Down Expand Up @@ -233,6 +236,17 @@ class UPIConsumer
if atom.config.get('haskell-ghc-mod.highlightTooltips')
'source.haskell'

docTooltip: (e, p) =>
@process.getDocInBuffer(e, p)
.then ({range, dox}) ->
range: range
text:
text: dox
highlighter:
if atom.config.get('haskell-ghc-mod.highlightTooltips')
'source.haskell'


infoTypeTooltip: (e, p) =>
args = arguments
@infoTooltip(e, p)
Expand All @@ -251,8 +265,10 @@ class UPIConsumer
@typeTooltip(e, p).catch -> return null
infoP =
@infoTooltip(e, p).catch -> return null
Promise.all [typeP, infoP]
.then ([type, info]) ->
docP =
@docTooltip(e, p).catch -> return null
Promise.all [typeP, infoP, docP]
.then ([type, info, doc]) ->
range:
if type? and info?
type.range.union(info.range)
Expand All @@ -263,7 +279,7 @@ class UPIConsumer
else
throw new Error('Got neither type nor info')
text:
text: "#{if type?.text?.text then ':: '+type.text.text+'\n' else ''}#{info?.text?.text ? ''}"
text: (Object.keys(doc).join(", ") + "\nBLERG 0: " + doc[Object.keys(doc)[0]] + "\nBLERG 1: " + doc[Object.keys(doc)[1]] + "\nBLERG 1 keys: " + Object.keys(doc[Object.keys(doc)[1]]) + "\nBLERG 1[0]: " + doc[Object.keys(doc)[1]]["text"] + "--(Karl was here)\n") + "#{if type?.text?.text then ':: '+type.text.text+'\n' else ''}#{info?.text?.text ? ''}"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm piggybacking this experimental functionality on the Info+Type display, which is the default tooltip behavior in the package.

I've created a promise as docP which returns a value in doc. For some reason the value of doc.text.text displays as undefined in the tooltip.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're trying to debug it, you might consider using JSON.stringify(doc) instead of this long-winded construction.

That said, I have two thoughts on this whole thing. First, I would think that since this has virtually nothing to do with ghc-mod, this would be better-suited to be provided as a separate package, unless it eventually does (see below).

Second, there's some work wrt haddock happening on ghc-mod, which you might want to take a look at: DanielG/ghc-mod#810

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, that ghc-imported-from merge is exciting. I feel like I should put my effort on hold until that is finished.

Pardon the digression from Haddock, but it seems like that ghc-imported-from project could also improve the reliability of the "Go to Declaration" functionality. Would you agree? I saw your comment in atom-haskell/ide-haskell#137 that there are two ways to "Go to Declaration", provided by either the ide-haskell-hasktags package or haskell-ghc-mod package. However, for me neither seem to properly handle jumping to function definitions that are qualified imports. To me it's unfortunately ironic, as jumping to a declaration that is "far away" (i.e. in a differnt file) is the more valuable automation.

I saw your other comment in atom-haskell/ide-haskell#110 that indicated some limitations:

  • haskell-ghc-mod:

    more accurate, but doesn't work for symbols that are not exported

  • ide-haskell-hasktags:

    less accurate, but works for all top-level symbols

With regard to haskell-ghc-mod, were you referring to symbols that are not explicitly given in a module's export list? For me, symbols from a different module are rarely found regardless. Sometimes, when I mouseover a symbol, I see the following in the console:

EXCEPTION: info:     Not in scope: 'MyOtherModule.someFunction'

In that case, I of course cannot "Go to Declaration" with haskell-ghc-mod eitiher.

In other cases, I am able to mouseover a qualified-imported symbol and see the "info" tooltip, and it even says -- Defined in Foo.Bar.Blah, but "Go to Declaration" will not take me there. I believe that this happens when the symbol is in a different local package (which happens to be one of two listed in my stack.yaml file.

Regarding ide-haskell-hasktags, it is much faster than haskell-ghc-mod for jumping to declarations in the same module, but it also suffers from the inability to jump to symbols that are external to the current module when those symbols are qualified imports.

Would it be valuable for me to submit some test cases to demonstrate these exact problems? I'd also try my hand at fixing the problems.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not about qualified imports (if actual qualified imports fail, file a bug with a testcase -- those should work), but rather cross-package imports I believe? Horrible truth is, ghc is not aware of any cross-package source files -- packages are compiled. So jumping source files between packages is problematic.

Hasktags just parses all open Atom projects for Haskell sources and collects top-level declarations. So it stands to reason that it's faster. Besides, if you need to jump sources between two packages, you can just open both as Atom projects and that should work. There's a caveat with symbols sharing the same name: hasktags has no way to distinguish those, since it's not context-aware, unlike ghc-mod.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I have created minimal test cases that demonstrate various failure modes of "Jump to Declaration" for both haskell-ghc-mod and ide-haskell-hasktags in this demo project.

In summary:

  • Indeed, haskell-ghc-mod never works "across" packages
  • ide-haskell-hasktags works both within the same package and "across" packages when:
    • the symbol is not highlighted; one must place the cursor within the symbol without highlighting it
    • the symbol is not qualified by a namespace; it must be "bare"
  • haskell-ghc-mod does not work when a symbol is qualified by a renamed import, unless one manually highlights the symbol alone while excluding the leading namespace and period.

Are the latter two items known limitations?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kostmo, thank you for your work, but please file new issues against relevant packages or at least ide-haskell in the future. It's hard enough to maintain this mess as it is.

So second point is an oversight on my part, should be fixed by ide-haskell-hasktags-0.0.6.

Third point seems to be a problem with ghc-mod -- it seems it doesn't resolve import aliases to files. Can't tell from the top of my head if it's easy to fix, or even fixable in principle -- it's very probable that it's a limitation of GHC API.

highlighter:
if atom.config.get('haskell-ghc-mod.highlightTooltips')
'source.haskell'
Expand Down
Loading