Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The autocomplete plugin uses the ModuleInspector program to get decla…
…ration info about modules. However, it works synchronously (blocking the editor), and only after saving a file.
- Loading branch information
Showing
1 changed file
with
64 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,73 +1,77 @@ | ||
import re | ||
from common import * | ||
import json | ||
import os | ||
import sublime | ||
import sublime_plugin | ||
import subprocess | ||
import threading | ||
import time | ||
|
||
# Completion text longer than this is ellipsized: | ||
MAX_COMPLETION_LENGTH = 37 | ||
|
||
# Match a module's name, not followed by an export list: | ||
module_name_regex = re.compile( | ||
r'^module\w+([\w.])+\s+where', | ||
re.DOTALL | re.MULTILINE) | ||
class SublimeHaskellAutocomplete(sublime_plugin.EventListener): | ||
def __init__(self): | ||
# TODO: Recompile the ModuleInspector: | ||
# call_and_wait(['ghc', '--make', 'ModuleInspector.hs', '-outputdir', 'obj']) | ||
self.info_lock = threading.Lock() | ||
self.info = {} | ||
|
||
# Match a module's name and its explicit export list: | ||
export_list_regex = re.compile( | ||
r'^module\w+([\w.])+\s*\((.*)\)\s*where', | ||
re.DOTALL | re.MULTILINE) | ||
def on_query_completions(self, view, prefix, locations): | ||
return self.get_completion_list(view.file_name()) | ||
|
||
# Match a function declaration's name and type: | ||
# TODO: This only matches type declarations that fit on one line. | ||
function_declaration_regex = re.compile( | ||
r'^([\w\']+)\s*::\s*(.+?)\s*$', | ||
re.MULTILINE) | ||
def on_post_save(self, view): | ||
# TODO: Update the module info once per second, at most. | ||
# TODO: Make sure the entire project's module info is up-to-date. | ||
self.refresh_module_info(view.file_name()) | ||
|
||
# Match a type declaration's name and type: | ||
# TODO: This does not capture type parameters or type constructors. | ||
type_declaration_regex = re.compile( | ||
r'^(data|type|newtype)\s+([\w\']+)', | ||
re.MULTILINE) | ||
def refresh_all_module_info(self, base_dir): | ||
"Rebuild module information for all files under the specified directory." | ||
files_in_dir = list_files_in_dir_recursively(base_dir) | ||
haskell_source_files = [x for x in files_in_dir if x.endswith('.hs')] | ||
for filename in haskell_source_files: | ||
self.refresh_module_info(filename) | ||
|
||
class SublimeHaskellAutocomplete(sublime_plugin.EventListener): | ||
def on_query_completions(self, view, prefix, locations): | ||
def refresh_module_info(self, filename): | ||
"Rebuild module information for the specified file." | ||
# TODO: Only do this within Haskell files in Cabal projects. | ||
info = analyze_file(view.file_name()) | ||
completions = [(str(x), x.name) for x in info.declarations] | ||
return completions | ||
|
||
class ModuleCompletionInfo(object): | ||
"""Describe a file's module name, imports, exports, and declarations. | ||
name :: String or None -- name of the module, if present | ||
export_list :: [String] or None -- list of explicit exports, if present | ||
import_list :: [String] -- list of imported modules | ||
declarations :: [(String, String)] -- names and descriptions of | ||
declarations in the file | ||
""" | ||
def __init__(self, name, export_list, import_list, declarations): | ||
self.name = name | ||
self.export_list = export_list | ||
self.import_list = import_list | ||
self.declarations = declarations | ||
# TODO: Skip this file if it hasn't changed since it was last inspected. | ||
print('Inspecting "{0}"...'.format(filename)) | ||
exit_code, stdout, stderr = call_and_wait( | ||
['runhaskell', 'ModuleInspector.hs', filename]) | ||
new_info = json.loads(stdout) | ||
# Remember when this info was collected. | ||
new_info['collectedAt'] = time.time() | ||
# Dump the currently-known module info to disk: | ||
formatted_json = json.dumps(self.info, indent=2) | ||
with open('module_info.cache', 'w') as f: | ||
f.write(formatted_json) | ||
with self.info_lock: | ||
self.info[filename] = new_info | ||
|
||
class ModuleDefinition(object): | ||
"""Describe a single type or function definition. | ||
""" | ||
def __init__(self, name, type_info): | ||
self.name = name | ||
self.type_info = type_info | ||
|
||
def __str__(self): | ||
s = '{0} :: {1}'.format(self.name, self.type_info) | ||
return s[:MAX_COMPLETION_LENGTH] | ||
def get_completion_list(self, current_file_name): | ||
"Get all the completions that apply to the current file." | ||
# TODO: Filter according to what names the current file has in scope. | ||
completions = [] | ||
with self.info_lock: | ||
for file_name, file_info in self.info.items(): | ||
if 'error' in file_info: | ||
# There was an error parsing this file; skip it. | ||
log('skip!') | ||
continue | ||
for d in file_info['declarations']: | ||
identifier = d['identifier'] | ||
declaration_info = d['info'] | ||
# TODO: Show the declaration info somewhere. | ||
completions.append( | ||
(identifier[:MAX_COMPLETION_LENGTH], identifier)) | ||
return completions | ||
|
||
def analyze_file(filename): | ||
"Extract ModuleCompletionInfo from a file." | ||
content = '' | ||
with open(filename) as f: | ||
content = f.read() | ||
# Find the declared functions: | ||
matches = function_declaration_regex.finditer(content) | ||
functions = [] | ||
for m in matches: | ||
name, type_info = m.groups() | ||
functions.append(ModuleDefinition(name, type_info)) | ||
return ModuleCompletionInfo('Unknown', [], [], functions) | ||
def list_files_in_dir_recursively(base_dir): | ||
"""Return a list of a all files in a directory, recursively. | ||
The files will be specified by full paths.""" | ||
files = [] | ||
for dirname, dirnames, filenames in os.walk(base_dir): | ||
for filename in filenames: | ||
files.append(os.path.join(base_dir, dirname, filename)) | ||
return files |