diff --git a/CabalInspector.hs b/CabalInspector.hs index 2516d797..81fbccbe 100644 --- a/CabalInspector.hs +++ b/CabalInspector.hs @@ -12,12 +12,14 @@ import Distribution.PackageDescription.Parse import qualified System.Environment as Environment data CabalInfo = CabalInfo { - cabalExecutables :: [CabalExecutable] } + cabalExecutables :: [CabalExecutable], + cabalSourceDirs :: [FilePath] } deriving (Show) instance Json.ToJSON CabalInfo where toJSON info = Json.object [ - "executables" .= cabalExecutables info] + "executables" .= cabalExecutables info, + "source-dirs" .= cabalSourceDirs info] data CabalExecutable = CabalExecutable { executableName :: String, @@ -31,7 +33,9 @@ instance Json.ToJSON CabalExecutable where analyzeCabal :: String -> Either String CabalInfo analyzeCabal source = case parsePackageDescription source of - ParseOk _ r -> Right $ CabalInfo $ map (uncurry CabalExecutable . second (exeName . condTreeData)) $ condExecutables r + ParseOk _ r -> Right CabalInfo { + cabalExecutables = map (uncurry CabalExecutable . second (exeName . condTreeData)) $ condExecutables r, + cabalSourceDirs = maybe [] (hsSourceDirs . libBuildInfo . condTreeData) $ condLibrary r } ParseFailed e -> Left $ "Parse failed: " ++ show e main :: IO () diff --git a/autocomplete.py b/autocomplete.py index d82a742b..4eda5a15 100644 --- a/autocomplete.py +++ b/autocomplete.py @@ -992,7 +992,7 @@ def _refresh_module_info(self, filename, standalone = True): 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_cwd(filename)) + [MODULE_INSPECTOR_EXE_PATH, filename] + ghc_opts_args, cwd = get_source_dir(filename)) module_inspector_out = MODULE_INSPECTOR_RE.search(stdout) diff --git a/haskell_type.py b/haskell_type.py index c4ead886..618399b7 100644 --- a/haskell_type.py +++ b/haskell_type.py @@ -12,6 +12,7 @@ from SublimeHaskell.autocomplete import autocompletion from SublimeHaskell.hdevtools import hdevtools_type from SublimeHaskell.ghcmod import ghcmod_type + from functools import reduce # Used to find out the module name. MODULE_RE_STR = r'module\s+([^\s\(]*)' # "module" followed by everything that is neither " " nor "(" @@ -24,7 +25,6 @@ # Name of the sublime panel in which type information is shown. TYPE_PANEL_NAME = 'haskell_type_panel' - def parse_ghc_mod_type_line(l): """ Returns the `groupdict()` of GHCMOD_TYPE_LINE_RE matching the given line, @@ -33,17 +33,44 @@ def parse_ghc_mod_type_line(l): match = GHCMOD_TYPE_LINE_RE.match(l) return match and match.groupdict() +def tabs_offset(view, point): + """ + Returns count of '\t' before point in line multiplied by 7 + 8 is size of type as supposed by ghc-mod, to every '\t' will add 7 to column + Subtract this value to get sublime column by ghc-mod column, add to get ghc-mod column by sublime column + """ + cur_line = view.substr(view.line(point)) + return len(filter(lambda ch: ch == '\t', cur_line)) * 7 + +def sublime_column_to_type_column(view, line, column): + cur_line = view.substr(view.line(view.text_point(line, column))) + return column + len(filter(lambda ch: ch == '\t', cur_line)) * 7 + 1 + +def type_column_to_sublime_column(view, line, column): + cur_line = view.substr(view.line(view.text_point(line - 1, 0))) + col = 1 + real_col = 0 + for c in cur_line: + col += (8 if c == '\t' else 1) + real_col += 1 + if col >= column: + return real_col + return real_col + class FilePosition(object): def __init__(self, line, column): self.line = line self.column = column def point(self, view): - return view.text_point(self.line - 1, self.column - 1) + # Note, that sublime suppose that '\t' is 'tab_size' length + # But '\t' is one character + return view.text_point(self.line - 1, type_column_to_sublime_column(view, self.line, self.column)) def position_by_point(view, point): + tabs = tabs_offset(view, point) (r, c) = view.rowcol(point) - return FilePosition(r + 1, c + 1) + return FilePosition(r + 1, c + 1 + tabs) class RegionType(object): def __init__(self, typename, start, end = None): @@ -88,6 +115,7 @@ def parse_type_output(s): def haskell_type(filename, module_name, line, column, cabal = None): result = None + if get_setting_async('enable_hdevtools'): result = hdevtools_type(filename, line, column, cabal = cabal) if not result: @@ -108,6 +136,8 @@ def get_types(self, filename = None, line = None, column = None): line = r + 1 column = c + 1 + column = sublime_column_to_type_column(self.view, r, c) + module_name = None with autocompletion.database.files as files: if filename in files: diff --git a/hdevtools.py b/hdevtools.py index 1d6d39cd..2c3dcd81 100644 --- a/hdevtools.py +++ b/hdevtools.py @@ -27,7 +27,13 @@ def call_hdevtools_and_wait(arg_list, filename = None, cabal = None): if package_db: ghs_opts.append('-package_db {0}'.format(package_db)) - ghc_opts_args = ["-g", ' '.join(ghc_opts)] if ghc_opts else [] + source_dir = get_source_dir(filename) + ghc_opts.append('-i {0}'.format(source_dir)) + + ghc_opts_args = [] + if ghc_opts: + for opt in ghc_opts: + ghc_opts_args.extend(["-g", opt]) if hdevtools_socket: arg_list.append('--socket={0}'.format(hdevtools_socket)) @@ -35,7 +41,7 @@ def call_hdevtools_and_wait(arg_list, filename = None, cabal = None): try: command = ['hdevtools'] + arg_list + ghc_opts_args - exit_code, out, err = call_and_wait(['hdevtools'] + arg_list + ghc_opts_args) + exit_code, out, err = call_and_wait(['hdevtools'] + arg_list + ghc_opts_args, cwd = source_dir) if exit_code != 0: raise Exception("hdevtools exited with status %d and stderr: %s" % (exit_code, err)) diff --git a/sublime_haskell_common.py b/sublime_haskell_common.py index 6d7a021d..e6b5ca33 100644 --- a/sublime_haskell_common.py +++ b/sublime_haskell_common.py @@ -1,6 +1,7 @@ import errno import fnmatch import os +import json import sublime import sublime_plugin import subprocess @@ -17,6 +18,8 @@ # Panel for SublimeHaskell errors SUBLIME_ERROR_PANEL_NAME = 'haskell_sublime_load' +# Used to detect hs-source-dirs for project +CABAL_INSPECTOR_EXE_PATH = None # Setting can't be get from not main threads # So we using a trick: @@ -317,6 +320,37 @@ def set_setting(key, value): def set_setting_async(key, value): sublime.set_timeout(lambda: set_setting(key, value), 0) +def get_source_dir(filename): + """ + Get root of hs-source-dirs for filename in project + """ + if not filename: + return os.getcwd() + + (cabal_dir, project_name) = get_cabal_project_dir_and_name_of_file(filename) + if not cabal_dir: + return os.path.dirname(filename) + + cabal_file = get_cabal_in_dir(cabal_dir) + exit_code, out, err = call_and_wait([CABAL_INSPECTOR_EXE_PATH, cabal_file]) + + if exit_code == 0: + info = json.loads(out) + + dirs = ["."] + + if 'error' not in info: + dirs.extend(info['source-dirs']) + + paths = [os.path.normpath(os.path.join(cabal_dir, d)) for d in dirs] + paths.sort(key = lambda p: -len(p)) + + for p in paths: + if filename.startswith(p): + return p + + return os.path.dirname(filename) + def get_cwd(filename = None): """ Get cwd for filename: cabal project path, file path or os.getcwd() @@ -331,14 +365,17 @@ def call_ghcmod_and_wait(arg_list, filename=None, cabal = None): """ ghc_opts = get_setting_async('ghc_opts') - ghc_opts_args = ["-g", ' '.join(ghc_opts)] if ghc_opts else [] + ghc_opts_args = [] + if ghc_opts: + for opt in ghc_opts: + ghc_opts_args.extend(["-g", opt]) try: command = attach_cabal_sandbox(['ghc-mod'] + arg_list + ghc_opts_args, cabal) # log('running ghc-mod: {0}'.format(command)) - exit_code, out, err = call_and_wait(command, cwd=get_cwd(filename)) + exit_code, out, err = call_and_wait(command, cwd=get_source_dir(filename)) if exit_code != 0: raise Exception("ghc-mod exited with status %d and stderr: %s" % (exit_code, err)) @@ -563,7 +600,9 @@ def sublime_haskell_package_path(): def plugin_loaded(): global PACKAGE_PATH + global CABAL_INSPECTOR_EXE_PATH PACKAGE_PATH = sublime_haskell_package_path() + CABAL_INSPECTOR_EXE_PATH = os.path.join(PACKAGE_PATH, 'CabalInspector') preload_settings() if int(sublime.version()) < 3000: