Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

1339 lines (1038 sloc) 52.967 kB
import json
import os
import re
import sublime
import sublime_plugin
import threading
import time
if int(sublime.version()) < 3000:
from sublime_haskell_common import *
import symbols
import cache
import util
import hdocs
from ghci import ghci_info
from haskell_docs import haskell_docs
from hdevtools import start_hdevtools, stop_hdevtools
else:
from SublimeHaskell.sublime_haskell_common import *
import SublimeHaskell.symbols as symbols
import SublimeHaskell.cache as cache
import SublimeHaskell.util as util
import SublimeHaskell.hdocs as hdocs
from SublimeHaskell.ghci import ghci_info
from SublimeHaskell.haskell_docs import haskell_docs
from SublimeHaskell.hdevtools import start_hdevtools, stop_hdevtools
# If true, files that have not changed will not be re-inspected.
CHECK_MTIME = True
MODULE_INSPECTOR_SOURCE_PATH = None
MODULE_INSPECTOR_EXE_PATH = None
MODULE_INSPECTOR_OBJ_DIR = None
CABAL_INSPECTOR_SOURCE_PATH = None
CABAL_INSPECTOR_EXE_PATH = None
CABAL_INSPECTOR_OBJ_DIR = None
INSPECTOR_ENABLED = False
INSPECTOR_RUNNING = False
# ModuleInspector output
MODULE_INSPECTOR_RE = re.compile(r'ModuleInfo:(?P<result>.+)')
# The agent sleeps this long between inspections.
AGENT_SLEEP_TIMEOUT = 60.0
# Checks if we are in a LANGUAGE pragma.
LANGUAGE_RE = re.compile(r'.*{-#\s+LANGUAGE.*')
# Checks if we are in an import statement.
IMPORT_RE = re.compile(r'.*import(\s+qualified)?\s+')
IMPORT_RE_PREFIX = re.compile(r'^\s*import(\s+qualified)?\s+(.*)$')
IMPORT_QUALIFIED_POSSIBLE_RE = re.compile(r'.*import\s+(?P<qualifiedprefix>\S*)$')
# Checks if a word contains only alhanums, -, and _, and dot
NO_SPECIAL_CHARS_RE = re.compile(r'^(\w|[\-\.])*$')
# Get symbol qualified prefix and its name
SYMBOL_RE = re.compile(r'((?P<module>\w+(\.\w+)*)\.)?(?P<identifier>\w*)$')
# Get symbol module scope and its name within import statement
IMPORT_SYMBOL_RE = re.compile(r'import(\s+qualified)?\s+(?P<module>\w+(\.\w+)*)(\s+as\s+(?P<as>\w+))?\s*\(.*?(?P<identifier>\w*)$')
def get_line_contents(view, location):
"""
Returns contents of line at the given location.
"""
return view.substr(sublime.Region(view.line(location).a, location))
def get_line_contents_before_region(view, region):
"""
Returns contents of line before the given region (including it).
"""
return view.substr(sublime.Region(view.line(region).a, region.b))
def get_qualified_name(s):
"""
'bla bla bla Data.List.fo' -> ('Data.List', 'Data.List.fo')
"""
if len(s) == 0:
return ('', '')
quals = s.split()[-1].split('.')
filtered = map(lambda s: list(filter(lambda c: c.isalpha() or c.isdigit() or c == '_', s)), quals)
return ('.'.join(filtered[0:len(filtered) - 1]), '.'.join(filtered))
def get_qualified_symbol(line):
"""
Get module context of symbol and symbol itself
Returns (module, name, is_import_list), where module (or one of) can be None
"""
res = IMPORT_SYMBOL_RE.search(line)
if res:
return (res.group('module'), res.group('identifier'), True)
res = SYMBOL_RE.search(line)
# res always match
return (res.group('module'), res.group('identifier'), False)
def get_qualified_symbol_at_region(view, region):
"""
Get module context of symbol and symbol itself for line before (and with) word on region
Returns (module, name), where module (or one of) can be None
"""
word_region = view.word(region)
preline = get_line_contents_before_region(view, word_region)
return get_qualified_symbol(preline)
# Gets available LANGUAGE options and import modules from ghc-mod
def get_ghcmod_language_pragmas():
if get_setting_async('enable_ghc_mod'):
return call_ghcmod_and_wait(['lang']).splitlines()
return []
# Autocompletion data
class AutoCompletion(object):
"""Information for completion"""
def __init__(self):
self.language_pragmas = get_ghcmod_language_pragmas()
# cabal name => set of modules, where cabal name is 'cabal' for cabal or sandbox path for cabal-devs
self.module_completions = LockedObject({})
# Currently used projects
# name => project where project is:
# dir - project dir
# cabal - cabal file
# executables - list of executables where executable is
# name - name of executable
self.projects = LockedObject({})
# Storage of information
self.database = symbols.Database()
# keywords
# TODO: keywords can't appear anywhere, we can suggest in right places
self.keyword_completions = map(
lambda k: (k + '\t(keyword)', k),
['case', 'data', 'instance', 'type', 'where', 'deriving', 'import', 'module'])
self.current_filename = None
def clear_inspected(self):
# self.info = {}
# self.std_info = {}
self.projects.object = {}
self.database = symbols.Database()
def unalias_module_name(self, view, alias):
"Get module names by alias"
current_file_name = view.file_name()
with self.database.files as files:
if current_file_name in files:
return files[current_file_name].unalias(alias)
return []
def get_completions(self, view, prefix, locations):
"Get all the completions that apply to the current file."
current_file_name = view.file_name()
if not current_file_name:
return []
self.current_filename = current_file_name
# Contents of the line under the first cursor
line_contents = get_line_contents(view, locations[0])
# If the current line is an import line, gives us (My.Module, ident)
(qualified_module, symbol_name, is_import_list) = get_qualified_symbol(line_contents)
qualified_prefix = '{0}.{1}'.format(qualified_module, symbol_name) if qualified_module else symbol_name
# The list of completions we're going to assemble
completions = []
# Complete with modules too
if qualified_module and not is_import_list:
completions.extend(self.get_module_completions_for(qualified_prefix))
moduleImports = []
cur_info = None
with self.database.files as files:
if current_file_name in files:
cur_info = files[current_file_name]
if cur_info:
if qualified_module:
# If symbol is qualified, use completions from module specified
moduleImports.append(qualified_module)
moduleImports.extend([i.module for i in cur_info.imports.values() if i.import_as == qualified_module])
else:
# Otherwise, use completions from all importred unqualified modules and from this module
moduleImports.append('Prelude')
moduleImports.extend([i.module for i in cur_info.imports.values() if not i.is_qualified])
# Add this module as well
completions.extend(self.completions_for_module(cur_info, current_file_name))
# Add keyword completions and module completions
completions.extend(self.keyword_completions)
completions.extend(self.get_module_completions_for(qualified_prefix, [i.module for i in cur_info.imports.values()]))
for mi in set(moduleImports):
completions.extend(self.completions_for(mi, current_file_name))
return list(set(completions))
def completions_for_module(self, module, filename = None):
"""
Returns completions for module
"""
if not module:
return []
return map(lambda d: d.suggest(), module.declarations.values())
def completions_for(self, module_name, filename = None):
"""
Returns completions for module
"""
with self.database.modules as modules:
if module_name not in modules:
return []
# TODO: Show all possible completions?
return self.completions_for_module(symbols.get_visible_module(modules[module_name], filename), filename)
def get_import_completions(self, view, prefix, locations):
self.current_filename = view.file_name()
# Contents of the current line up to the cursor
line_contents = get_line_contents(view, locations[0])
# Autocompletion for import statements
if get_setting('auto_complete_imports'):
match_import_list = IMPORT_SYMBOL_RE.search(line_contents)
if match_import_list:
module_name = match_import_list.group('module')
import_list_completions = []
import_list_completions.extend(self.completions_for(module_name, self.current_filename))
return import_list_completions
match_import = IMPORT_RE_PREFIX.match(line_contents)
if match_import:
(qualified, pref) = match_import.groups()
import_completions = self.get_module_completions_for(pref)
# Right after "import "? Propose "qualified" as well!
qualified_match = IMPORT_QUALIFIED_POSSIBLE_RE.match(line_contents)
if qualified_match:
qualified_prefix = qualified_match.group('qualifiedprefix')
if qualified_prefix == "" or "qualified".startswith(qualified_prefix):
import_completions.insert(0, (u"qualified", "qualified "))
return list(set(import_completions))
return []
def get_special_completions(self, view, prefix, locations):
# Contents of the current line up to the cursor
line_contents = get_line_contents(view, locations[0])
# Autocompletion for LANGUAGE pragmas
if get_setting('auto_complete_language_pragmas'):
# TODO handle multiple selections
match_language = LANGUAGE_RE.match(line_contents)
if match_language:
return [(to_unicode(c),) * 2 for c in self.language_pragmas]
return []
def get_module_completions_for(self, qualified_prefix, modules = None):
def module_next_name(mname):
"""
Returns next name for prefix
pref = Control.Con, mname = Control.Concurrent.MVar, result = Concurrent.MVar
"""
suffix = mname.split('.')[(len(qualified_prefix.split('.')) - 1):]
# Sublime replaces full module name with suffix, if it contains no dots?
return suffix[0]
module_list = modules if modules else self.get_current_module_completions()
return list(set((module_next_name(m) + '\t(module)', module_next_name(m)) for m in module_list if m.startswith(qualified_prefix)))
def get_current_module_completions(self):
completions = []
cabal = current_cabal()
with self.database.get_cabal_modules() as cabal_modules:
completions.extend(list(cabal_modules.keys()))
if self.current_filename:
(project_path, _) = get_cabal_project_dir_and_name_of_file(self.current_filename)
if project_path:
completions.extend([m.name for m in self.database.get_project_modules(project_path).values()])
with self.module_completions as module_completions:
if cabal in module_completions:
completions.extend(module_completions[cabal])
return set(completions)
autocompletion = AutoCompletion()
def can_complete_qualified_symbol(info):
"""
Helper function, returns whether sublime_haskell_complete can run for (module, symbol, is_import_list)
"""
(module_name, symbol_name, is_import_list) = info
if not module_name:
return False
if is_import_list:
return module_name in autocompletion.get_current_module_completions()
else:
return list(filter(lambda m: m.startswith(module_name), autocompletion.get_current_module_completions())) != []
class SublimeHaskellComplete(sublime_plugin.TextCommand):
""" Shows autocompletion popup """
def run(self, edit, characters):
for region in self.view.sel():
self.view.insert(edit, region.end(), characters)
if can_complete_qualified_symbol(get_qualified_symbol_at_region(self.view, self.view.sel()[0])):
self.view.run_command("hide_auto_complete")
sublime.set_timeout(self.do_complete, 1)
def do_complete(self):
self.view.run_command("auto_complete")
def is_enabled(self):
return is_enabled_haskell_command(self.view, False)
class SublimeHaskellBrowseDeclarations(sublime_plugin.WindowCommand):
"""
Show all available declarations from current cabal and opened projects
"""
def run(self):
self.decls = []
self.declarations = []
# (module, ident) => symbols.Declaration
decls = {}
with autocompletion.database.files as files:
for m in files.values():
for decl in m.declarations.values():
decls[(m.name, decl.name)] = decl
for decl in decls.values():
self.decls.append(decl)
self.declarations.append(decl.module.name + ': ' + decl.brief())
self.window.show_quick_panel(self.declarations, self.on_done)
def on_done(self, idx):
if idx == -1:
return
view = self.window.active_view()
if not view:
return
decl = self.decls[idx]
view.run_command('sublime_haskell_symbol_info', {
'filename': decl.location.filename,
'decl': decl.name })
def is_enabled(self):
return is_enabled_haskell_command(None, False)
class SublimeHaskellGoToAnyDeclaration(sublime_plugin.WindowCommand):
def run(self):
self.files = []
self.declarations = []
with autocompletion.database.files as files:
for f, m in files.items():
for decl in m.declarations.values():
self.files.append([f, str(decl.location.line), str(decl.location.column)])
self.declarations.append([decl.brief(), '{0}:{1}:{2}'.format(decl.module.name, decl.location.line, decl.location.column)])
self.window.show_quick_panel(self.declarations, self.on_done)
def on_done(self, idx):
if idx == -1:
return
self.window.open_file(':'.join(self.files[idx]), sublime.ENCODED_POSITION)
def is_enabled(self):
return is_enabled_haskell_command(None, False)
class SublimeHaskellReinspectAll(sublime_plugin.WindowCommand):
def run(self):
global INSPECTOR_ENABLED
if INSPECTOR_ENABLED:
autocompletion.clear_inspected()
inspector.mark_all_files(self.window)
else:
show_status_message("inspector_enabled setting is false", isok=False)
class SublimeHaskellSymbolInfoCommand(sublime_plugin.TextCommand):
"""
Show information about selected symbol
"""
def run(self, edit, filename = None, module_name = None, decl = None):
if decl and filename:
with autocompletion.database.files as files:
if filename in files:
m = files[filename]
if decl in m.declarations:
self.show_symbol_info(m.declarations[decl])
else:
show_status_message('Symbol "{0}" not found in {1}'.format(decl, filename))
else:
show_status_message('No info about module in {0}'.format(filename))
return
if decl and module_name:
with autocompletion.database.get_cabal_modules() as cabal_modules:
if module_name in cabal_modules:
m = cabal_modules[module_name]
if decl in m.declarations:
self.show_symbol_info(m.declarations[decl])
else:
show_status_message('Symbol "{0}" not found in {1}'.format(decl, filename))
else:
show_status_message('No info about module {0}'.format(module_name))
return
(module_word, ident, _) = get_qualified_symbol_at_region(self.view, self.view.sel()[0])
full_name = '{0}.{1}'.format(module_word, ident) if module_word else ident
current_file_name = self.view.file_name()
candidates = []
imported_symbol_not_found = False
with autocompletion.database.symbols as decl_symbols:
if ident in decl_symbols:
decls = decl_symbols[ident] if not module_word else [d for d in decl_symbols[ident] if d.full_name() == full_name]
modules_dict = symbols.declarations_modules(decls, lambda ms: symbols.get_visible_module(ms, current_file_name)).values()
with autocompletion.database.files as files:
if current_file_name in files:
cur_info = files[current_file_name]
if not module_word or module_word == cur_info.name:
# this module declaration
candidates.extend([m.declarations[ident] for m in modules_dict if symbols.is_this_module(cur_info, m) and ident in m.declarations])
if not candidates:
# declarations from imported modules
candidates.extend([m.declarations[ident] for m in modules_dict if symbols.is_imported_module(cur_info, m, module_word) and ident in m.declarations])
if not candidates:
imported_symbol_not_found = True
# show all possible candidates
candidates.extend([m.declarations[ident] for m in modules_dict if ident in m.declarations])
# No info about imports for this file, just add all declarations
else:
candidates.extend([m.declarations[ident] for m in modules_dict if ident in m.declarations])
else:
imported_symbol_not_found = True
if imported_symbol_not_found or not candidates:
browse_for_module = False
browse_module_candidate = None
with autocompletion.database.modules as modules:
if full_name in modules:
# Browse symbols in module
browse_for_module = True
browse_module_candidate = symbols.get_preferred_module(modules[full_name], current_file_name)
if browse_for_module:
if browse_module_candidate:
self.view.window().run_command('sublime_haskell_browse_module', {
'module_name': browse_module_candidate.name,
'filename': current_file_name })
return
else:
show_status_message("No info about module {0}".format(full_name))
return
elif not candidates:
# Sometimes ghc-mod returns no info about module, but module exists
# So there are no info about valid symbol
# But if user sure, that symbol exists, he can force to call for ghci to get info
import_list = []
with autocompletion.database.files as files:
if current_file_name in files:
import_list.extend(files[current_file_name].imports.keys())
if module_word:
# Full qualified name, just call to ghci_info
info = ghci_info(module_word, ident)
if info:
self.show_symbol_info(info)
return
elif import_list:
# Allow user to select module
self.candidates = [(m, ident) for m in import_list]
self.view.window().show_quick_panel([
['Select imported module', 'from where {0} may be imported'.format(ident)],
['No, thanks', '']], self.on_import_selected)
return
show_status_message('Symbol "{0}" not found'.format(ident), False)
return
if not candidates:
show_status_message('Symbol "{0}" not found'.format(ident), False)
return
if len(candidates) == 1:
self.show_symbol_info(candidates[0])
return
self.candidates = candidates
self.view.window().show_quick_panel([[c.qualified_name()] for c in candidates], self.on_done)
def on_done(self, idx):
if idx == -1:
return
self.show_symbol_info(self.candidates[idx])
def on_import_selected(self, idx):
if idx == 0: # Yes, select imported module
sublime.set_timeout(
lambda: self.view.window().show_quick_panel(['{0}.{1}'.format(i[0], i[1]) for i in self.candidates],
self.on_candidate_selected), 10
)
def on_candidate_selected(self, idx):
if idx == -1:
return
(module_name, ident_name) = self.candidates[idx]
info = util.symbol_info(self.view.file_name(), module_name, ident_name)
if info:
self.show_symbol_info(info)
else:
show_status_message("Can't get info for {0}.{1}".format(module_name, ident_name), False)
def show_symbol_info(self, decl):
output_view = self.view.window().get_output_panel('sublime_haskell_symbol_info')
output_view.set_read_only(False)
util.refine_decl(decl)
# TODO: Move to separate command for Sublime Text 3
output_view.run_command('sublime_haskell_output_text', {
'text': decl.detailed() })
output_view.sel().clear()
output_view.set_read_only(True)
self.view.window().run_command('show_panel', {
'panel': 'output.' + 'sublime_haskell_symbol_info' })
def browse_module(self, module):
with autocompletion.database.modules as modules:
decls = list(module.declarations.values())
self.candidates = decls
self.view.window().show_quick_panel([[decl.brief(), decl.docs.splitlines()[0]] if decl.docs else [decl.brief()] for decl in decls], self.on_done)
def is_enabled(self):
return is_enabled_haskell_command(self.view, False)
class SublimeHaskellBrowseModule(sublime_plugin.WindowCommand):
"""
Browse module symbols
"""
def run(self, module_name = None, filename = None):
if module_name:
with autocompletion.database.modules as modules:
if module_name not in modules:
show_status_message('Module "{0}" not found'.format(module_name), False)
return
current_file_name = filename if filename else self.window.active_view().file_name()
module_candidate = symbols.get_preferred_module(modules[module_name], current_file_name)
if hdocs.load_module_docs(module_candidate):
# FIXME: Not here!
cache.dump_cabal_cache(autocompletion.database, module_candidate.cabal)
decls = list(module_candidate.declarations.values())
self.candidates = sorted(decls, key = lambda d: d.brief())
self.window.show_quick_panel([[decl.brief(), decl.docs.splitlines()[0]] if decl.docs else [decl.brief()] for decl in self.candidates], self.on_symbol_selected)
return
self.candidates = []
with autocompletion.database.files as files:
for fname, m in files.items():
self.candidates.append([m.name, fname])
with autocompletion.database.get_cabal_modules() as cabal_modules:
for m in cabal_modules.values():
self.candidates.append([m.name])
self.candidates.sort(key = lambda c: c[0])
self.window.show_quick_panel(self.candidates, self.on_done)
def on_done(self, idx):
if idx == -1:
return
module_name = self.candidates[idx][0]
self.window.run_command('sublime_haskell_browse_module', {
'module_name': module_name })
def on_symbol_selected(self, idx):
if idx == -1:
return
candidate = self.candidates[idx]
if candidate.module.location:
self.window.active_view().run_command('sublime_haskell_symbol_info', {
'filename': candidate.module.location.filename,
'decl': candidate.name })
else:
self.window.active_view().run_command('sublime_haskell_symbol_info', {
'module_name': candidate.module.name,
'decl': candidate.name })
class SublimeHaskellGoToDeclaration(sublime_plugin.TextCommand):
def run(self, edit):
(module_word, ident, _) = get_qualified_symbol_at_region(self.view, self.view.sel()[0])
full_name = '.'.join([module_word, ident]) if module_word else ident
current_file_name = self.view.file_name()
current_project = get_cabal_project_dir_of_file(current_file_name)
module_candidates = []
candidates = []
with autocompletion.database.symbols as decl_symbols:
if ident not in decl_symbols:
show_status_message('Declaration for "{0}" not found'.format(ident), False)
return
decls = decl_symbols[ident]
modules_dict = symbols.flatten(symbols.declarations_modules(decls, lambda ms: list(filter(symbols.is_by_sources, ms))).values())
with autocompletion.database.files as files:
if current_file_name in files:
cur_info = files[current_file_name]
if not module_word or module_word == cur_info.name:
# this module declarations
candidates.extend([m.declarations[ident] for m in modules_dict if symbols.is_this_module(cur_info, m) and ident in m.declarations])
if not candidates:
# declarations from imported modules within this project
candidates.extend([m.declarations[ident] for m in modules_dict if symbols.is_imported_module(cur_info, m, module_word) and symbols.is_within_project(m, cur_info.location.project) and ident in m.declarations])
if not candidates:
# declarations from imported modules within other projects
candidates.extend([m.declarations[ident] for m in modules_dict if symbols.is_imported_module(cur_info, m, module_word) and ident in m.declarations])
if not candidates:
# show all possible candidates
candidates.extend([m.declarations[ident] for m in modules_dict if ident in m.declarations])
# No info about imports for this file, just add all declarations
else:
candidates.extend([m.declarations[ident] for m in modules_dict if ident in declarations])
with autocompletion.database.modules as modules:
if full_name in modules:
modules_list = list(filter(symbols.is_by_sources, modules[full_name]))
# Find module in this project
module_candidates.extend([m for m in modules_list if symbols.is_within_project(m, current_project)])
if not module_candidates:
# Modules from other projects
module_candidates.extend(modules_list)
if not candidates and not module_candidates:
show_status_message('Declaration for "{0}" not found'.format(ident), False)
return
if len(candidates) + len(module_candidates) == 1:
if len(module_candidates) == 1:
self.view.window().open_file(module_candidates[0].location.filename)
return
if len(candidates) == 1:
self.view.window().open_file(candidates[0].location.position(), sublime.ENCODED_POSITION)
return
# many candidates
self.select_candidates = [([c.name, c.location.position()], True) for c in candidates] + [([m.name, m.location.filename], False) for m in module_candidates]
self.view.window().show_quick_panel([c[0] for c in self.select_candidates], self.on_done)
def on_done(self, idx):
if idx == -1:
return
selected = self.select_candidates[idx]
if selected[1]:
self.view.window().open_file(selected[0][1], sublime.ENCODED_POSITION)
else:
self.view.window().open_file(selected[0][1])
def is_enabled(self):
return is_enabled_haskell_command(self.view, False)
class StandardInspectorAgent(threading.Thread):
def __init__(self):
super(StandardInspectorAgent, self).__init__()
self.daemon = True
self.modules_lock = threading.Lock()
self.modules_to_load = []
self.cabal_lock = threading.Lock()
self.cabal_to_load = []
self.module_docs = LockedObject([])
self.update_event = threading.Event()
def run(self):
self.load_module_completions()
while True:
load_modules = []
with self.modules_lock:
load_modules = self.modules_to_load
self.modules_to_load = []
cabal = current_cabal()
if len(load_modules) > 0:
try:
for m in load_modules:
self._load_standard_module(m, cabal)
# self._load_standard_module_docs(m, cabal)
except:
continue
load_module_docs = []
with self.module_docs as module_docs:
load_module_docs = module_docs[:]
module_docs[:] = []
if len(load_module_docs) > 0:
for m in load_module_docs:
self._load_standard_module_docs(m, cabal)
with self.cabal_lock:
load_cabal = self.cabal_to_load
self.cabal_to_load = []
if len(load_cabal) > 0:
try:
for c in load_cabal:
self.load_module_completions(c)
except:
continue
if len(load_modules) > 0:
cache.dump_cabal_cache(autocompletion.database)
self.update_event.wait(AGENT_SLEEP_TIMEOUT)
self.update_event.clear()
def load_module_info(self, module_name):
with self.modules_lock:
self.modules_to_load.append(module_name)
self.update_event.set()
def load_module_docs(self, module_name):
with self.module_docs as module_docs:
module_docs.append(module_name)
self.update_event.set()
def load_cabal_info(self, cabal_name = None):
if not cabal_name:
cabal_name = current_cabal()
with self.cabal_lock:
self.cabal_to_load.append(cabal_name)
self.update_event.set()
# Load modules info for cabal/cabal-dev specified
def load_module_completions(self, cabal = None):
if not get_setting_async('enable_ghc_mod'):
return
if not cabal:
cabal = current_cabal()
try:
with status_message_process('Loading standard modules info for {0}'.format(cabal)) as s:
cache_begin_time = time.clock()
cache.load_cabal_cache(autocompletion.database, cabal)
cache_end_time = time.clock()
log('loading standard modules cache for {0} within {1} seconds'.format(cabal, cache_end_time - cache_begin_time))
modules = None
with autocompletion.module_completions as module_completions:
if cabal in module_completions:
return
module_completions[cabal] = set(call_ghcmod_and_wait(['list'], cabal = cabal).splitlines())
modules = module_completions[cabal].copy()
begin_time = time.clock()
log('loading standard modules info for {0}'.format(cabal))
loaded_modules = 0
for m in modules:
self._load_standard_module(m, cabal)
# self._load_standard_module_docs(m, cabal)
loaded_modules += 1
s.percentage_message(loaded_modules, len(modules))
end_time = time.clock()
log('loading standard modules info for {0} within {1} seconds'.format(cabal, end_time - begin_time))
cache.dump_cabal_cache(autocompletion.database, cabal)
except Exception as e:
log('loading standard modules info for {0} failed with {1}'.format(cabal, e))
def _load_standard_module(self, module_name, cabal = None):
if not cabal:
cabal = current_cabal()
with autocompletion.database.get_cabal_modules(cabal) as cabal_modules:
if module_name in cabal_modules:
return
if get_setting_async('enable_ghc_mod'):
try:
m = util.browse_module(module_name, cabal = cabal)
if m:
autocompletion.database.add_module(m)
except Exception as e:
log('Inspecting in-cabal module {0} failed: {1}'.format(module_name, e))
def _load_standard_module_docs(self, module_name, cabal = None):
if not cabal:
cabal = current_cabal()
with autocompletion.database.get_cabal_modules(cabal) as cabal_modules:
if module_name in cabal_modules:
try:
hdocs.load_module_docs(cabal_modules[module_name])
except Exception as e:
log('Loading docs for in-cabal module {0} failed: {1}'.format(module_name, e))
std_inspector = None
class InspectorAgent(threading.Thread):
def __init__(self):
# Call the superclass constructor:
super(InspectorAgent, self).__init__()
# Make this thread daemonic so that it won't prevent the program
# from exiting.
self.daemon = True
# Files that need to be re-inspected:
self.dirty_files_lock = threading.Lock()
self.dirty_files = []
self.active_files = LockedObject([])
# Event that is set (notified) when files have changed
self.reinspect_event = threading.Event()
CABALMSG = 'Compiling Haskell CabalInspector'
MODULEMSG = 'Compiling Haskell ModuleInspector'
def run(self):
# Compile the CabalInspector:
with status_message(InspectorAgent.CABALMSG) as s:
exit_code, out, err = call_and_wait(['ghc',
'--make', CABAL_INSPECTOR_SOURCE_PATH,
'-o', CABAL_INSPECTOR_EXE_PATH,
'-outputdir', CABAL_INSPECTOR_OBJ_DIR])
if exit_code != 0:
s.fail()
error_msg = u"SublimeHaskell: Failed to compile CabalInspector\n{0}".format(err)
wait_for_window(lambda w: self.show_errors(w, error_msg))
# Continue anyway
# Compile the ModuleInspector:
with status_message(InspectorAgent.MODULEMSG) as s:
exit_code, out, err = call_and_wait(['ghc',
'--make', MODULE_INSPECTOR_SOURCE_PATH,
'-package', 'ghc',
'-o', MODULE_INSPECTOR_EXE_PATH,
'-outputdir', MODULE_INSPECTOR_OBJ_DIR])
if exit_code != 0:
s.fail()
error_msg = u"SublimeHaskell: Failed to compile ModuleInspector\n{0}".format(err)
wait_for_window(lambda w: self.show_errors(w, error_msg))
return
# For first time, inspect all open folders and files
wait_for_window(lambda w: self.mark_all_files(w))
self.mark_active_files()
# TODO: If compilation failed, we can't proceed; handle this.
# Periodically wake up and see if there is anything to inspect.
while True:
files_to_reinspect = []
files_to_doc = []
with self.dirty_files_lock:
files_to_reinspect = self.dirty_files
self.dirty_files = []
with self.active_files as active_files:
files_to_doc = active_files[:]
active_files[:] = []
# Find the cabal project corresponding to each "dirty" file:
cabal_dirs = []
standalone_files = []
for filename in files_to_reinspect:
d = get_cabal_project_dir_of_file(filename)
if d is not None:
cabal_dirs.append(d)
else:
standalone_files.append(filename)
# Eliminate duplicate project directories:
cabal_dirs = list(set(cabal_dirs))
standalone_files = list(set(standalone_files))
for i, d in enumerate(cabal_dirs):
self._refresh_all_module_info(d, i + 1, len(cabal_dirs))
for f in standalone_files:
self._refresh_module_info(f)
for f in files_to_doc:
with autocompletion.database.files as files:
if f in files:
for i in files[f].imports.values():
std_inspector.load_module_docs(i.module)
self.reinspect_event.wait(AGENT_SLEEP_TIMEOUT)
self.reinspect_event.clear()
def mark_active_files(self):
def mark_active_files_():
for w in sublime.windows():
for v in w.views():
with self.active_files as active_files:
active_files.append(v.file_name())
sublime.set_timeout(lambda: mark_active_files_, 0)
self.reinspect_event.set()
def mark_all_files(self, window):
folder_files = []
for folder in window.folders():
folder_files.extend(list_files_in_dir_recursively(folder))
with self.dirty_files_lock:
self.dirty_files.extend([f for f in folder_files if f.endswith('.hs')])
self.reinspect_event.set()
def show_errors(self, window, error_text):
sublime.set_timeout(lambda: output_error(window, error_text), 0)
def mark_file_active(self, filename):
with self.active_files as active_files:
active_files.append(filename)
self.reinspect_event.set()
def mark_file_dirty(self, filename):
"Report that a file should be reinspected."
if filename is None:
return
with self.dirty_files_lock:
self.dirty_files.append(filename)
self.reinspect_event.set()
def _refresh_all_module_info(self, cabal_dir, index, count):
"Rebuild module information for all files under the specified directory."
begin_time = time.clock()
log('reinspecting project ({0})'.format(cabal_dir))
# Process all files within the Cabal project:
# TODO: Only process files within the .cabal file's "src" directory.
(project_name, cabal_file) = get_cabal_in_dir(cabal_dir)
with status_message_process('Reinspecting ({0}/{1}) {2}'.format(index, count, project_name), priority = 1) as s:
cache.load_project_cache(autocompletion.database, cabal_dir)
# set project and read cabal
if cabal_file and project_name:
self._refresh_project_info(cabal_dir, project_name, cabal_file)
files_in_dir = list_files_in_dir_recursively(cabal_dir)
haskell_source_files = [x for x in files_in_dir if x.endswith('.hs') and ('dist/build/autogen' not in x)]
filenames_loaded = 0
for filename in haskell_source_files:
self._refresh_module_info(filename, False)
filenames_loaded += 1
s.percentage_message(filenames_loaded, len(haskell_source_files))
end_time = time.clock()
log('total inspection time: {0} seconds'.format(end_time - begin_time))
cache.dump_project_cache(autocompletion.database, cabal_dir)
def _refresh_project_info(self, cabal_dir, project_name, cabal_file):
exit_code, out, err = call_and_wait(
[CABAL_INSPECTOR_EXE_PATH, cabal_file])
if exit_code == 0:
new_info = json.loads(out)
if 'error' not in new_info:
if 'executables' in new_info and 'library' in new_info:
with autocompletion.projects as projects:
projects[project_name] = {
'dir': cabal_dir,
'cabal': os.path.basename(cabal_file),
'library': new_info['library'],
'executables': new_info['executables'],
'tests': new_info['tests'] }
def _refresh_module_info(self, filename, standalone = True):
"Rebuild module information for the specified file."
# TODO: Only do this within Haskell files in Cabal projects.
# TODO: Currently the ModuleInspector only delivers top-level functions
# with hand-written type signatures. This code should make that clear.
# If the file hasn't changed since it was last inspected, do nothing:
if not filename.endswith('.hs'):
return
with autocompletion.database.files as files:
if filename in files:
last_inspection_time = files[filename].last_inspection_time
modification_time = os.stat(filename).st_mtime
# Skip if we already inspected after last modification
if modification_time <= last_inspection_time:
# log('skipping inspecting %s' % filename)
return
else:
files[filename].last_inspection_time = time.time()
ghc_opts = get_ghc_opts()
ghc_opts_args = [' '.join(ghc_opts)] if ghc_opts else []
exit_code, stdout, stderr = call_and_wait(
[MODULE_INSPECTOR_EXE_PATH, filename] + ghc_opts_args, cwd = get_source_dir(filename))
module_inspector_out = MODULE_INSPECTOR_RE.search(stdout)
# Update only when module is ok
if exit_code == 0 and module_inspector_out:
new_info = json.loads(module_inspector_out.group('result'))
if 'error' not in new_info:
# # Load standard modules
if 'imports' in new_info:
for mi in new_info['imports']:
if 'importName' in mi:
std_inspector.load_module_info(mi['importName'])
try:
def make_import(import_info):
import_name = import_info['importName']
ret = symbols.Import(import_name, import_info['qualified'], import_info['as'])
return (import_name, ret)
module_imports = dict(map(make_import, new_info['imports']))
import_list = new_info['exportList'] if ('exportList' in new_info and new_info['exportList'] is not None) else []
new_module = symbols.Module(new_info['moduleName'], import_list, module_imports, {}, symbols.module_location(filename), last_inspection_time=time.time())
for d in new_info['declarations']:
location = symbols.Location(filename, d['line'], d['column'])
if d['what'] == 'function':
new_function = symbols.Function(d['name'], d['type'], d['docs'], location, new_module)
util.refine_type(new_function)
new_module.add_declaration(new_function)
elif d['what'] == 'type':
new_module.add_declaration(symbols.Type(d['name'], d['context'], d['args'], None, d['docs'], location))
elif d['what'] == 'newtype':
new_module.add_declaration(symbols.Newtype(d['name'], d['context'], d['args'], None, d['docs'], location))
elif d['what'] == 'data':
new_module.add_declaration(symbols.Data(d['name'], d['context'], d['args'], None, d['docs'], location))
elif d['what'] == 'class':
new_module.add_declaration(symbols.Class(d['name'], d['context'], d['args'], d['docs'], location))
else:
new_module.add_declaration(symbols.Declaration(d['name'], 'declaration', d['docs'], location))
autocompletion.database.add_file(filename, new_module)
if standalone:
# Do we need save cache for standalone files?
pass
for i in new_module.imports.values():
std_inspector.load_module_info(i.module)
except Exception as e:
log('Inspecting file {0} failed: {1}'.format(filename, e))
else:
log('ModuleInspector returns error: {0}'.format(new_info['error']))
else:
log('ModuleInspector exited with code {0}. Stderr: {1}'.format(exit_code, stderr))
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
inspector = None
class SublimeHaskellAutocomplete(sublime_plugin.EventListener):
def __init__(self):
self.local_settings = {
'enable_ghc_mod': None,
'use_cabal_dev': None,
'cabal_dev_sandbox': None,
}
for s in self.local_settings.keys():
self.local_settings[s] = get_setting(s)
# Subscribe to settings changes to update data
get_settings().add_on_change('enable_ghc_mod', lambda: self.on_setting_changed())
def on_setting_changed(self):
global INSPECTOR_ENABLED
INSPECTOR_ENABLED = get_setting('inspect_modules')
# Start the inspector if needed
# TODO Also stop it if needed!
if INSPECTOR_ENABLED and not INSPECTOR_RUNNING:
start_inspector()
elif (not INSPECTOR_ENABLED) and INSPECTOR_RUNNING:
# TODO Implement stopping it
log('The ModuleInspector cannot be stopped as of now. You have to restart Sublime for that.')
same = True
for k, v in self.local_settings.items():
r = get_setting(k)
same = same and v == r
self.local_settings[k] = r
# Update cabal status of active view
window = sublime.active_window()
if window:
view = window.active_view()
if view:
self.set_cabal_status(view)
if INSPECTOR_ENABLED and not same:
# TODO: Changed completion settings! Update autocompletion data properly
# For now at least try to load cabal modules info
std_inspector.load_cabal_info()
pass
def on_query_completions(self, view, prefix, locations):
if not is_haskell_source(view):
return []
begin_time = time.clock()
# Only suggest symbols if the current file is part of a Cabal project.
completions = (autocompletion.get_import_completions(view, prefix, locations) +
autocompletion.get_special_completions(view, prefix, locations))
if not completions:
completions = autocompletion.get_completions(view, prefix, locations)
end_time = time.clock()
log('time to get completions: {0} seconds'.format(end_time - begin_time))
# Don't put completions with special characters (?, !, ==, etc.)
# into completion because that wipes all default Sublime completions:
# See http://www.sublimetext.com/forum/viewtopic.php?t=8659
# TODO: work around this
comp = [c for c in completions if NO_SPECIAL_CHARS_RE.match(c[0].split('\t')[0])]
if get_setting('inhibit_completions') and len(comp) != 0:
return (comp, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)
return comp
def set_cabal_status(self, view):
filename = view.file_name()
if filename:
(cabal_dir, project_name) = get_cabal_project_dir_and_name_of_file(filename)
cabal = 'cabal-dev' if get_setting_async('use_cabal_dev') else 'cabal'
if project_name:
view.set_status('sublime_haskell_cabal', '{0}: {1}'.format(cabal, project_name))
def on_new(self, view):
global INSPECTOR_ENABLED
self.set_cabal_status(view)
if is_haskell_source(view):
filename = view.file_name()
if INSPECTOR_ENABLED:
inspector.mark_file_dirty(filename)
inspector.mark_file_active(filename)
def on_load(self, view):
global INSPECTOR_ENABLED
self.set_cabal_status(view)
if is_haskell_source(view):
filename = view.file_name()
if INSPECTOR_ENABLED:
inspector.mark_file_dirty(filename)
inspector.mark_file_active(filename)
def on_activated(self, view):
self.set_cabal_status(view)
def on_post_save(self, view):
global INSPECTOR_ENABLED
if is_haskell_source(view):
if INSPECTOR_ENABLED:
inspector.mark_file_dirty(view.file_name())
def on_query_context(self, view, key, operator, operand, match_all):
if key == 'auto_completion_popup':
return get_setting('auto_completion_popup')
elif key == 'is_haskell_source':
return is_haskell_source(view)
elif key == "is_module_completion" or key == "is_import_completion":
chars = {
"is_module_completion": '.',
"is_import_completion": '(' }
region = view.sel()[0]
if region.a != region.b:
return False
word_region = view.word(region)
preline = get_line_contents_before_region(view, word_region)
preline += chars[key]
return can_complete_qualified_symbol(get_qualified_symbol(preline))
else:
return False
def start_inspector():
global INSPECTOR_RUNNING
if INSPECTOR_RUNNING:
raise Exception('SublimeHaskell: ModuleInspector is already running!')
log('starting ModuleInspector')
global std_inspector
std_inspector = StandardInspectorAgent()
std_inspector.start()
global inspector
inspector = InspectorAgent()
inspector.start()
INSPECTOR_RUNNING = True
def plugin_loaded():
global MODULE_INSPECTOR_SOURCE_PATH
global MODULE_INSPECTOR_EXE_PATH
global MODULE_INSPECTOR_OBJ_DIR
global CABAL_INSPECTOR_SOURCE_PATH
global CABAL_INSPECTOR_EXE_PATH
global CABAL_INSPECTOR_OBJ_DIR
global INSPECTOR_ENABLED
global INSPECTOR_RUNNING
package_path = sublime_haskell_package_path()
cache_path = sublime_haskell_cache_path()
MODULE_INSPECTOR_SOURCE_PATH = os.path.join(package_path, 'ModuleInspector.hs')
MODULE_INSPECTOR_EXE_PATH = os.path.join(cache_path, 'ModuleInspector')
MODULE_INSPECTOR_OBJ_DIR = os.path.join(cache_path, 'obj/ModuleInspector')
CABAL_INSPECTOR_SOURCE_PATH = os.path.join(package_path, 'CabalInspector.hs')
CABAL_INSPECTOR_EXE_PATH = os.path.join(cache_path, 'CabalInspector')
CABAL_INSPECTOR_OBJ_DIR = os.path.join(cache_path, 'obj/CabalInspector')
INSPECTOR_ENABLED = get_setting('inspect_modules')
if INSPECTOR_ENABLED:
start_inspector()
# TODO: How to stop_hdevtools() in Sublime Text 2?
start_hdevtools()
def plugin_unloaded():
# Does this work properly on exit?
stop_hdevtools()
if int(sublime.version()) < 3000:
plugin_loaded()
Jump to Line
Something went wrong with that request. Please try again.