From 6b7b4574751b8a019274241fcf76358c79774b65 Mon Sep 17 00:00:00 2001 From: Dan Mace Date: Sat, 12 Sep 2015 20:44:34 -0400 Subject: [PATCH] Refactor settings system Use a settings singleton and totally refactor the loading of environment and settings. Should be easier to optimize now. --- gotools_build.py | 82 +++++----- gotools_format.py | 26 ++-- gotools_goto_def.py | 37 +++-- gotools_oracle.py | 28 ++-- gotools_rename.py | 9 +- gotools_settings.py | 332 ++++++++++++++++++++++++++--------------- gotools_suggestions.py | 25 +--- gotools_util.py | 58 ++++--- 8 files changed, 322 insertions(+), 275 deletions(-) diff --git a/gotools_build.py b/gotools_build.py index 7faab7f..6d6a557 100644 --- a/gotools_build.py +++ b/gotools_build.py @@ -18,19 +18,15 @@ def run(self, cmd = None, shell_cmd = None, file_regex = "", line_regex = "", wo clean = False, task = "build", # Catches "path" and "shell" **kwargs): - self.settings = GoToolsSettings() - self.logger = Logger(self.settings) - self.runner = ToolRunner(self.settings, self.logger) - if clean: self.clean() if len(file_regex) == 0: file_regex = "^(.*\\.go):(\\d+):()(.*)$" - env["GOPATH"] = self.settings.gopath - env["GOROOT"] = self.settings.goroot - env["PATH"] = self.settings.ospath + env["GOPATH"] = GoToolsSettings.Instance.gopath + env["GOROOT"] = GoToolsSettings.Instance.goroot + env["PATH"] = GoToolsSettings.Instance.ospath exec_opts = { "cmd": cmd, @@ -52,60 +48,60 @@ def run(self, cmd = None, shell_cmd = None, file_regex = "", line_regex = "", wo self.test_packages(exec_opts, self.find_test_packages()) elif task == "test_tagged_packages": pkgs = [] - for p in self.settings.tagged_test_packages: - pkgs.append(os.path.join(self.settings.project_package, p)) - self.test_packages(exec_opts=exec_opts, packages=pkgs, tags=self.settings.tagged_test_tags) + for p in GoToolsSettings.Instance.tagged_test_packages: + pkgs.append(os.path.join(GoToolsSettings.Instance.project_package, p)) + self.test_packages(exec_opts=exec_opts, packages=pkgs, tags=GoToolsSettings.Instance.tagged_test_tags) elif task == "test_at_cursor": self.test_at_cursor(exec_opts) elif task == "test_current_package": self.test_current_package(exec_opts) elif task == "test_last": - self.logger.log("re-running last test") + Logger.log("re-running last test") self.window.run_command("exec", self.last_test_exec_opts) else: - self.logger.log("invalid task: " + task) + Logger.log("invalid task: " + task) def clean(self): - self.logger.log("cleaning build output directories") - for p in self.settings.gopath.split(":"): - pkgdir = os.path.join(p, "pkg", self.settings.goos + "_" + self.settings.goarch) - self.logger.log("=> " + pkgdir) + Logger.log("cleaning build output directories") + for p in GoToolsSettings.Instance.gopath.split(":"): + pkgdir = os.path.join(p, "pkg", GoToolsSettings.Instance.goos + "_" + GoToolsSettings.Instance.goarch) + Logger.log("=> " + pkgdir) if os.path.exists(pkgdir): try: shutil.rmtree(pkgdir) except Exception as e: - self.logger.log("WARNING: couldn't clean directory: " + str(e)) + Logger.log("WARNING: couldn't clean directory: " + str(e)) def build(self, exec_opts): build_packages = [] - for p in self.settings.build_packages: - build_packages.append(os.path.join(self.settings.project_package, p)) + for p in GoToolsSettings.Instance.build_packages: + build_packages.append(os.path.join(GoToolsSettings.Instance.project_package, p)) - self.logger.log("running build for packages: " + str(build_packages)) + Logger.log("running build for packages: " + str(build_packages)) - go = GoToolsSettings.find_go_binary(self.settings.ospath) + go = GoToolsSettings.Instance.find_go_binary(GoToolsSettings.Instance.ospath) exec_opts["cmd"] = [go, "install"] + build_packages self.window.run_command("exec", exec_opts) def test_packages(self, exec_opts, packages = [], patterns = [], tags = []): - self.logger.log("running tests") + Logger.log("running tests") - self.logger.log("test packages: " + str(packages)) - self.logger.log("test patterns: " + str(patterns)) + Logger.log("test packages: " + str(packages)) + Logger.log("test patterns: " + str(patterns)) - go = GoToolsSettings.find_go_binary(self.settings.ospath) + go = GoToolsSettings.Instance.find_go_binary(GoToolsSettings.Instance.ospath) cmd = [go, "test"] if len(tags) > 0: cmd += ["-tags", ",".join(tags)] - if self.settings.verbose_tests: + if GoToolsSettings.Instance.verbose_tests: cmd.append("-v") - if self.settings.test_timeout: - cmd += ["-timeout", self.settings.test_timeout] + if GoToolsSettings.Instance.test_timeout: + cmd += ["-timeout", GoToolsSettings.Instance.test_timeout] cmd += packages @@ -119,71 +115,71 @@ def test_packages(self, exec_opts, packages = [], patterns = [], tags = []): self.window.run_command("exec", exec_opts) def test_current_package(self, exec_opts): - self.logger.log("running current package tests") + Logger.log("running current package tests") view = self.window.active_view() pkg = self.current_file_pkg(view) if len(pkg) == 0: - self.logger.log("couldn't determine package for current file: " + view.file_name()) + Logger.log("couldn't determine package for current file: " + view.file_name()) return tags = self.tags_for_buffer(view) - self.logger.log("running tests for package: " + pkg) + Logger.log("running tests for package: " + pkg) self.test_packages(exec_opts=exec_opts, packages=[pkg], tags=tags) def test_at_cursor(self, exec_opts): - self.logger.log("running current test under cursor") + Logger.log("running current test under cursor") view = self.window.active_view() func_name = GoBuffers.func_name_at_cursor(view) if len(func_name) == 0: - self.logger.log("no function found near cursor") + Logger.log("no function found near cursor") return pkg = self.current_file_pkg(view) if len(pkg) == 0: - self.logger.log("couldn't determine package for current file: " + view.file_name()) + Logger.log("couldn't determine package for current file: " + view.file_name()) return tags = self.tags_for_buffer(view) - self.logger.log("running test: " + pkg + "#" + func_name) + Logger.log("running test: " + pkg + "#" + func_name) self.test_packages(exec_opts=exec_opts, packages=[pkg], patterns=[func_name], tags=tags) def current_file_pkg(self, view): abs_pkg_dir = os.path.dirname(view.file_name()) try: - return abs_pkg_dir[abs_pkg_dir.index(self.settings.project_package):] + return abs_pkg_dir[abs_pkg_dir.index(GoToolsSettings.Instance.project_package):] except: return "" def find_test_packages(self): proj_package_dir = None - for gopath in self.settings.gopath.split(":"): - d = os.path.join(gopath, "src", self.settings.project_package) + for gopath in GoToolsSettings.Instance.gopath.split(":"): + d = os.path.join(gopath, "src", GoToolsSettings.Instance.project_package) if os.path.exists(d): proj_package_dir = d break if proj_package_dir == None: - self.logger.log("ERROR: couldn't find project package dir '" - + self.settings.project_package + "' in GOPATH: " + self.settings.gopath) + Logger.log("ERROR: couldn't find project package dir '" + + GoToolsSettings.Instance.project_package + "' in GOPATH: " + GoToolsSettings.Instance.gopath) return [] packages = {} - for pkg_dir in self.settings.test_packages: + for pkg_dir in GoToolsSettings.Instance.test_packages: abs_pkg_dir = os.path.join(proj_package_dir, pkg_dir) - self.logger.log("searching for tests in: " + abs_pkg_dir) + Logger.log("searching for tests in: " + abs_pkg_dir) for root, dirnames, filenames in os.walk(abs_pkg_dir): for filename in fnmatch.filter(filenames, '*_test.go'): abs_test_file = os.path.join(root, filename) rel_test_file = os.path.relpath(abs_test_file, proj_package_dir) - test_pkg = os.path.join(self.settings.project_package, os.path.dirname(rel_test_file)) + test_pkg = os.path.join(GoToolsSettings.Instance.project_package, os.path.dirname(rel_test_file)) packages[test_pkg] = None return list(packages.keys()) diff --git a/gotools_format.py b/gotools_format.py index 8cdeebf..a334749 100644 --- a/gotools_format.py +++ b/gotools_format.py @@ -11,11 +11,7 @@ class GotoolsFormatOnSave(sublime_plugin.EventListener): def on_pre_save(self, view): if not GoBuffers.is_go_source(view): return - - settings = GoToolsSettings() - if not settings.format_on_save: - return - + if not GoToolsSettings.Instance.format_on_save: return view.run_command('gotools_format') class GotoolsFormat(sublime_plugin.TextCommand): @@ -23,20 +19,16 @@ def is_enabled(self): return GoBuffers.is_go_source(self.view) def run(self, edit): - self.settings = GoToolsSettings() - self.logger = Logger(self.settings) - self.runner = ToolRunner(self.settings, self.logger) - command = "" args = [] - if self.settings.format_backend == "gofmt": + if GoToolsSettings.Instance.format_backend == "gofmt": command = "gofmt" args = ["-e", "-s"] - elif self.settings.format_backend in ["goimports", "both"] : + elif GoToolsSettings.Instance.format_backend in ["goimports", "both"] : command = "goimports" args = ["-e"] - stdout, stderr, rc = self.runner.run(command, args, stdin=Buffers.buffer_text(self.view)) + stdout, stderr, rc = ToolRunner.run(command, args, stdin=Buffers.buffer_text(self.view)) # Clear previous syntax error marks self.view.erase_regions("mark") @@ -48,10 +40,10 @@ def run(self, edit): if rc != 0: # Ermmm... - self.logger.log("unknown gofmt error (" + str(rc) + ") stderr:\n" + stderr) + Logger.log("unknown gofmt error (" + str(rc) + ") stderr:\n" + stderr) return - if self.settings.format_backend == "both": + if GoToolsSettings.Instance.format_backend == "both": command = "gofmt" args = ["-e", "-s"] stdout, stderr, rc = self.runner.run(command, args, stdin=stdout.encode('utf-8')) @@ -66,7 +58,7 @@ def run(self, edit): if rc != 0: # Ermmm... - self.logger.log("unknown gofmt error (" + str(rc) + ") stderr:\n" + stderr) + Logger.log("unknown gofmt error (" + str(rc) + ") stderr:\n" + stderr) return # Everything's good, hide the syntax error panel @@ -101,12 +93,12 @@ def show_syntax_errors(self, stderr): for error in stderr.splitlines(): match = re.match("(.*):(\d+):(\d+):", error) if not match or not match.group(2): - self.logger.log("skipping unrecognizable error:\n" + error + "\nmatch:" + str(match)) + Logger.log("skipping unrecognizable error:\n" + error + "\nmatch:" + str(match)) continue row = int(match.group(2)) pt = self.view.text_point(row-1, 0) - self.logger.log("adding mark at row " + str(row)) + Logger.log("adding mark at row " + str(row)) marks.append(sublime.Region(pt)) if len(marks) > 0: diff --git a/gotools_goto_def.py b/gotools_goto_def.py index d911f64..a50fd35 100644 --- a/gotools_goto_def.py +++ b/gotools_goto_def.py @@ -18,9 +18,6 @@ def want_event(self): return True def run(self, edit, event=None): - self.settings = GoToolsSettings() - self.logger = Logger(self.settings) - self.runner = ToolRunner(self.settings, self.logger) sublime.set_timeout_async(lambda: self.godef(event), 0) def godef(self, event): @@ -31,26 +28,26 @@ def godef(self, event): else: filename, row, col, offset, offset_end = Buffers.location_at_cursor(self.view) - backend = self.settings.goto_def_backend if self.settings.goto_def_backend else "" + backend = GoToolsSettings.Instance.goto_def_backend if GoToolsSettings.Instance.goto_def_backend else "" try: if backend == "oracle": file, row, col = self.get_oracle_location(filename, offset) elif backend == "godef": file, row, col = self.get_godef_location(filename, offset) else: - self.logger.log("Invalid godef backend '" + backend + "' (supported: godef, oracle)") - self.logger.status("Invalid godef configuration; see console log for details") + Logger.log("Invalid godef backend '" + backend + "' (supported: godef, oracle)") + Logger.status("Invalid godef configuration; see console log for details") return except Exception as e: - self.logger.status(str(e)) + Logger.status(str(e)) return if not os.path.isfile(file): - self.logger.log("WARN: file indicated by godef not found: " + file) - self.logger.status("godef failed: Please enable debugging and check console log") + Logger.log("WARN: file indicated by godef not found: " + file) + Logger.status("godef failed: Please enable debugging and check console log") return - self.logger.log("opening definition at " + file + ":" + str(row) + ":" + str(col)) + Logger.log("opening definition at " + file + ":" + str(row) + ":" + str(col)) w = self.view.window() new_view = w.open_file(file + ':' + str(row) + ':' + str(col), sublime.ENCODED_POSITION) group, index = w.get_view_index(new_view) @@ -64,21 +61,21 @@ def get_oracle_location(self, filename, offset): # configured. # TODO: put into a utility package_scope = [] - for p in self.settings.build_packages: - package_scope.append(os.path.join(self.settings.project_package, p)) - for p in self.settings.test_packages: - package_scope.append(os.path.join(self.settings.project_package, p)) - for p in self.settings.tagged_test_packages: - package_scope.append(os.path.join(self.settings.project_package, p)) + for p in GoToolsSettings.Instance.build_packages: + package_scope.append(os.path.join(GoToolsSettings.Instance.project_package, p)) + for p in GoToolsSettings.Instance.test_packages: + package_scope.append(os.path.join(GoToolsSettings.Instance.project_package, p)) + for p in GoToolsSettings.Instance.tagged_test_packages: + package_scope.append(os.path.join(GoToolsSettings.Instance.project_package, p)) if len(package_scope) > 0: args = args + package_scope - location, err, rc = self.runner.run("oracle", args) + location, err, rc = ToolRunner.run("oracle", args) if rc != 0: raise Exception("no definition found") - self.logger.log("oracle output:\n" + location.rstrip()) + Logger.log("oracle output:\n" + location.rstrip()) # godef is sometimes returning this junk as part of the output, # so just cut anything prior to the first path separator @@ -94,11 +91,11 @@ def get_oracle_location(self, filename, offset): return [file, row, col] def get_godef_location(self, filename, offset): - location, err, rc = self.runner.run("godef", ["-f", filename, "-o", str(offset)]) + location, err, rc = ToolRunner.run("godef", ["-f", filename, "-o", str(offset)]) if rc != 0: raise Exception("no definition found") - self.logger.log("godef output:\n" + location.rstrip()) + Logger.log("godef output:\n" + location.rstrip()) # godef is sometimes returning this junk as part of the output, # so just cut anything prior to the first path separator diff --git a/gotools_oracle.py b/gotools_oracle.py index 5c2b07d..cf24230 100644 --- a/gotools_oracle.py +++ b/gotools_oracle.py @@ -13,12 +13,8 @@ def is_enabled(self): return GoBuffers.is_go_source(self.view) def run(self, edit, command=None): - self.settings = GoToolsSettings() - self.logger = Logger(self.settings) - self.runner = ToolRunner(self.settings, self.logger) - if not command: - self.logger.log("command is required") + Logger.log("command is required") return filename, row, col, offset, offset_end = Buffers.location_at_cursor(self.view) @@ -28,12 +24,12 @@ def run(self, edit, command=None): # configured. # TODO: put into a utility package_scope = [] - for p in self.settings.build_packages: - package_scope.append(os.path.join(self.settings.project_package, p)) - for p in self.settings.test_packages: - package_scope.append(os.path.join(self.settings.project_package, p)) - for p in self.settings.tagged_test_packages: - package_scope.append(os.path.join(self.settings.project_package, p)) + for p in GoToolsSettings.Instance.build_packages: + package_scope.append(os.path.join(GoToolsSettings.Instance.project_package, p)) + for p in GoToolsSettings.Instance.test_packages: + package_scope.append(os.path.join(GoToolsSettings.Instance.project_package, p)) + for p in GoToolsSettings.Instance.tagged_test_packages: + package_scope.append(os.path.join(GoToolsSettings.Instance.project_package, p)) sublime.active_window().run_command("hide_panel", {"panel": "output.gotools_oracle"}) @@ -56,17 +52,17 @@ def run(self, edit, command=None): sublime.set_timeout_async(lambda: self.do_plain_oracle("referrers", pos, package_scope), 0) def do_plain_oracle(self, mode, pos, package_scope=[], regex="^(.*):(\d+):(\d+):(.*)$"): - self.logger.status("running oracle "+mode+"...") + Logger.status("running oracle "+mode+"...") args = ["-pos="+pos, "-format=plain", mode] if len(package_scope) > 0: args = args + package_scope - output, err, rc = self.runner.run("oracle", args, timeout=60) - self.logger.log("oracle "+mode+" output: " + output.rstrip()) + output, err, rc = ToolRunner.run("oracle", args, timeout=60) + Logger.log("oracle "+mode+" output: " + output.rstrip()) if rc != 0: - self.logger.status("oracle call failed (" + str(rc) +")") + Logger.status("oracle call failed (" + str(rc) +")") return - self.logger.status("oracle "+mode+" finished") + Logger.status("oracle "+mode+" finished") panel = self.view.window().create_output_panel('gotools_oracle') panel.set_scratch(True) diff --git a/gotools_rename.py b/gotools_rename.py index 2189585..b558a3a 100644 --- a/gotools_rename.py +++ b/gotools_rename.py @@ -13,9 +13,6 @@ def is_enabled(self): return GoBuffers.is_go_source(self.view) def run(self, edit): - self.settings = GoToolsSettings() - self.logger = Logger(self.settings) - self.runner = ToolRunner(self.settings, self.logger) self.view.window().show_input_panel("Go rename:", "", self.do_rename_async, None, None) def do_rename_async(self, name): @@ -28,12 +25,12 @@ def do_rename(self, name): "-to", name, "-v" ] - output, err, exit = self.runner.run("gorename", args, timeout=15) + output, err, exit = ToolRunner.run("gorename", args, timeout=15) if exit != 0: - self.logger.status("rename failed ({0}): {1}".format(exit, err)) + Logger.status("rename failed ({0}): {1}".format(exit, err)) return - self.logger.status("renamed symbol to {name}".format(name=name)) + Logger.status("renamed symbol to {name}".format(name=name)) panel = self.view.window().create_output_panel('gotools_rename') panel.set_scratch(True) diff --git a/gotools_settings.py b/gotools_settings.py index 3b4d32e..6da5874 100644 --- a/gotools_settings.py +++ b/gotools_settings.py @@ -5,70 +5,229 @@ import subprocess import tempfile -class MergedSettings(): - def __init__(self): - # This is a Sublime settings object. - self.plugin = sublime.load_settings("GoTools.sublime-settings") - # This is just a dict. - self.project = sublime.active_window().active_view().settings().get('GoTools', {}) - - def get(self, key, default = None): - # Treat empty values as undefined. This is more convenient internally. - val = self.project.get(key, '') - if len(str(val)) > 0: return val - val = self.plugin.get(key, '') - if len(str(val)) > 0: return val - return default +def plugin_loaded(): + # This is the plugin initialization, which loads required environment + # variables. If this fails, the plugin is basically broken. + # + # TODO: find a better way to inform the user of problems. + try: + print("GoTools: initializing plugin...") + GoToolsSettings.Instance = GoToolsSettings() + print("GoTools: successfully initialized plugin.") + except Exception as e: + print("GoTools: ERROR: failed to initialize the plugin: {0}".format(str(e))) + raise e class GoToolsSettings(): - def __init__(self): - if not self.GoEnv: - raise Exception("GoTools doesn't appear to be initialized") + Instance = None - # Load the Sublime settings files. - settings = MergedSettings() - - # Project > Plugin > Shell env > OS env > go env - self.gopath = settings.get('gopath', self.GoEnv["GOPATH"]) - self.goroot = settings.get('goroot', self.GoEnv["GOROOT"]) - self.ospath = settings.get('path', self.GoEnv["PATH"]) + def __init__(self): + # Only load the environment once per plugin init. + # TODO: Consider doing this during refresh. Environment shouldn't change + # often and the call can be slow if the login shell has a nontrivial + # amount of init (e.g. bashrc). + self.env = self.create_environment() + # Perform the initial plugin settings load. + self.refresh() + # Only refresh plugin settings when they have changed. + self.plugin_settings.add_on_change("gopath", self.refresh) + + # There's no direct access to project settings, so load them from the active + # view every time. This doesn't seem ideal. + @property + def project_settings(self): + return sublime.active_window().active_view().settings().get('GoTools', {}) + + # Returns setting with key, preferring project settings over plugin settings + # and using default if neither is found. Key values with 0 length when + # converted to a string are treated as None. + def get_setting(self, key, default = None): + val = self.project_settings.get(key, '') + if len(str(val)) > 0: + return val + val = self.plugin_settings.get(key, '') + if len(str(val)) > 0: + return val + return default - self.goarch = self.GoEnv["GOHOSTARCH"] - self.goos = self.GoEnv["GOHOSTOS"] - self.go_tools = self.GoEnv["GOTOOLDIR"] + # Reloads the plugin settings file from disk and validates required setings. + def refresh(self): + # Load settings from disk. + self.plugin_settings = sublime.load_settings("GoTools.sublime-settings") + # Validate properties. + if self.gopath is None or len(self.gopath) == 0: + raise Exception("GoTools requires either the `gopath` setting or the GOPATH environment variable to be s") if not self.goroot or not self.goarch or not self.goos or not self.go_tools: raise Exception("GoTools couldn't find Go runtime information") - # The GOROOT bin directory is namespaced with the GOOS and GOARCH. - self.gorootbin = os.path.join(self.goroot, "bin", self.goos + "_" + self.goarch) + print("GoTools: configuration updated:\n\tgopath={0}\n\tgoroot={1}\n\tpath={2}\n\tdebug_enabled={3}".format(self.gopath, self.goroot, self.ospath, self.debug_enabled)) + # Project > Plugin > Shell env > OS env > go env + @property + def gopath(self): + gopath = self.get_setting('gopath', self.env["GOPATH"]) # Support 'gopath' expansion in project settings. - if 'gopath' in settings.project: - sub = settings.plugin.get('gopath', '') + if 'gopath' in self.project_settings: + sub = self.plugin_settings.get('gopath', '') if len(sub) == 0: - sub = self.GoEnv['GOPATH'] - self.gopath = settings.project['gopath'].replace('${gopath}', sub) - - if self.gopath is None or len(self.gopath) == 0: - raise Exception("GoTools requires either the `gopath` setting or the GOPATH environment variable to be s") + sub = self.env['GOPATH'] + gopath = self.project_settings['gopath'].replace('${gopath}', sub) + return gopath + + @property + def goroot(self): + return self.get_setting('goroot', self.env["GOROOT"]) - # Plugin feature settings. - self.debug_enabled = settings.get("debug_enabled") - self.format_on_save = settings.get("format_on_save") - self.format_backend = settings.get("format_backend") - self.autocomplete = settings.get("autocomplete") - self.goto_def_backend = settings.get("goto_def_backend") - - # Project feature settings. - self.project_package = settings.get("project_package") - self.build_packages = settings.get("build_packages", []) - self.test_packages = settings.get("test_packages", []) - self.tagged_test_tags = settings.get("tagged_test_tags", []) - self.tagged_test_packages = settings.get("tagged_test_packages", []) - self.verbose_tests = settings.get("verbose_tests", False) - self.test_timeout = settings.get("test_timeout", None) + @property + def ospath(self): + return self.get_setting('path', self.env["PATH"]) + @property + def goarch(self): + return self.env["GOHOSTARCH"] + + @property + def goos(self): + return self.env["GOHOSTOS"] + + @property + def go_tools(self): + return self.env["GOTOOLDIR"] + + @property + def gorootbin(self): + # The GOROOT bin directory is namespaced with the GOOS and GOARCH. + return os.path.join(self.goroot, "bin", self.gohostosarch) + + @property + def golibpath(self): + libpath = [] + arch = "{0}_{1}".format(self.goos, self.goarch) + libpath.append(os.path.join(self.goroot, "pkg", self.gohostosarch)) + for p in GoToolsSettings.Instance.gopath.split(":"): + libpath.append(os.path.join(p, "pkg", arch)) + return ":".join(libpath) + + @property + def gohostosarch(self): + return "{0}_{1}".format(self.goos, self.goarch) + + @property + def debug_enabled(self): + return self.get_setting("debug_enabled") + + @property + def format_on_save(self): + return self.get_setting("format_on_save") + + @property + def format_backend(self): + return self.get_setting("format_backend") + + @property + def autocomplete(self): + return self.get_setting("autocomplete") + + @property + def goto_def_backend(self): + return self.get_setting("goto_def_backend") + + @property + def project_package(self): + return self.get_setting("project_package") + + @property + def build_packages(self): + return self.get_setting("build_packages", []) + + @property + def test_packages(self): + return self.get_setting("test_packages", []) + + @property + def tagged_test_tags(self): + return self.get_setting("tagged_test_tags", []) + + @property + def tagged_test_packages(self): + return self.get_setting("tagged_test_packages", []) + + @property + def verbose_tests(self): + return self.get_setting("verbose_tests", False) + + @property + def test_timeout(self): + return self.get_setting("test_timeout", None) + + # Load PATH, GOPATH, GOROOT, and anything `go env` can provide. Use the + # precedence order: Login shell > OS env > go env. The environment is + # returned as a dict. + # + # Raises an exception if PATH can't be resolved or if `go env` fails. + @staticmethod + def create_environment(): + special_keys = ['PATH', 'GOPATH', 'GOROOT'] + env = {} + + # Gather up keys from the OS environment. + for k in special_keys: + env[k] = os.getenv(k, '') + + # Hide popups on Windows + si = None + if platform.system() == "Windows": + si = subprocess.STARTUPINFO() + si.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + # For non-Windows platforms, use a login shell to get environment. Write the + # values to a tempfile; relying on stdout is brittle because of things like + # ANSI color codes which can come over stdout when .profile/.bashrc are + # sourced. + if platform.system() != "Windows": + for k in special_keys: + tempf = tempfile.NamedTemporaryFile() + cmd = [os.getenv("SHELL"), "-l", "-c", "sh -c -l 'echo ${0}>{1}'".format(k, tempf.name)] + try: + subprocess.check_output(cmd) + val = tempf.read().decode("utf-8").rstrip() + if len(val) > 0: + env[k] = val + except subprocess.CalledProcessError as e: + raise Exception("couldn't resolve environment variable '{0}': {1}".format(k, str(e))) + + if len(env['PATH']) == 0: + raise Exception("couldn't resolve PATH via system environment or login shell") + + # Resolve the go binary. + gobinary = GoToolsSettings.find_go_binary(env['PATH']) + + # Gather up the Go environment using `go env`, but only keep keys which + # aren't already set from the shell or OS environment. + cmdenv = os.environ.copy() + for k in env: + cmdenv[k] = env[k] + goenv, stderr = subprocess.Popen([gobinary, 'env'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=si, env=cmdenv).communicate() + if stderr and len(stderr) > 0: + raise Exception("'{0} env' returned an error: {1}".format(gobinary, stderr.decode())) + + for name in goenv.decode().splitlines(): + match = re.match('(.*)=\"(.*)\"', name) + if platform.system() == "Windows": + match = re.match('(?:set\s)(.*)=(.*)', name) + if match and match.group(1) and match.group(2): + k = match.group(1) + v = match.group(2) + if not k in env or len(env[k]) == 0: + env[k] = v + + print("GoTools: using environment: {0}".format(str(env))) + return env + + # Returns the absolute path to the go binary found on path. Raises an + # exception if go can't be found. @staticmethod def find_go_binary(path=""): goname = "go" @@ -79,74 +238,3 @@ def find_go_binary(path=""): if os.path.isfile(candidate): return candidate raise Exception("couldn't find the go binary in path: {0}".format(path)) - -# Load PATH, GOPATH, GOROOT, and anything `go env` can provide. Use the -# precedence order: Login shell > OS env > go env. All the values are stored -# in GoToolsSettings.GoEnv. -def load_goenv(): - special_keys = ['PATH', 'GOPATH', 'GOROOT'] - env = {} - - # Gather up keys from the OS environment. - for k in special_keys: - env[k] = os.getenv(k, '') - - # Hide popups on Windows - si = None - if platform.system() == "Windows": - si = subprocess.STARTUPINFO() - si.dwFlags |= subprocess.STARTF_USESHOWWINDOW - - # For non-Windows platforms, use a login shell to get environment. Write the - # values to a tempfile; relying on stdout is brittle because of things like - # ANSI color codes which can come over stdout when .profile/.bashrc are - # sourced. - if platform.system() != "Windows": - for k in special_keys: - tempf = tempfile.NamedTemporaryFile() - cmd = [os.getenv("SHELL"), "-l", "-c", "sh -c -l 'echo ${0}>{1}'".format(k, tempf.name)] - try: - subprocess.check_output(cmd) - val = tempf.read().decode("utf-8").rstrip() - if len(val) > 0: - env[k] = val - except subprocess.CalledProcessError as e: - raise Exception("couldn't resolve environment variable '{0}': {1}".format(k, str(e))) - - if len(env['PATH']) == 0: - raise Exception("couldn't resolve PATH via system environment or login shell") - - # Resolve the go binary. - gobinary = GoToolsSettings.find_go_binary(env['PATH']) - - # Gather up the Go environment using `go env`, but only keep keys which - # aren't already set from the shell or OS environment. - cmdenv = os.environ.copy() - for k in env: - cmdenv[k] = env[k] - goenv, stderr = subprocess.Popen([gobinary, 'env'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=si, env=cmdenv).communicate() - if stderr and len(stderr) > 0: - raise Exception("'{0} env' returned an error: {1}".format(gobinary, stderr.decode())) - - for name in goenv.decode().splitlines(): - match = re.match('(.*)=\"(.*)\"', name) - if platform.system() == "Windows": - match = re.match('(?:set\s)(.*)=(.*)', name) - if match and match.group(1) and match.group(2): - k = match.group(1) - v = match.group(2) - if not k in env or len(env[k]) == 0: - env[k] = v - return env - -# This is the plugin initialization, which loads required environment -# variables. If this fails, the plugin is basically broken. -# -# TODO: find a better way to inform the user of problems. -try: - GoToolsSettings.GoEnv = load_goenv() - print("GoTools: initialized successfully using Go environment: {0}".format(str(GoToolsSettings.GoEnv))) -except Exception as e: - print("GoTools: ERROR: failed to load environment: {0}".format(str(e))) - raise e diff --git a/gotools_suggestions.py b/gotools_suggestions.py index 0da8cf3..64c27ed 100644 --- a/gotools_suggestions.py +++ b/gotools_suggestions.py @@ -19,27 +19,22 @@ class GotoolsSuggestions(sublime_plugin.EventListener): def on_query_completions(self, view, prefix, locations): if not GoBuffers.is_go_source(view): return - - settings = GoToolsSettings() - logger = Logger(settings) - runner = ToolRunner(settings, logger) - - if not settings.autocomplete: return + if not GoToolsSettings.Instance.autocomplete: return # set the lib-path for gocode's lookups - _, _, rc = runner.run("gocode", ["set", "lib-path", GotoolsSuggestions.gocode_libpath(settings)]) + _, _, rc = ToolRunner.run("gocode", ["set", "lib-path", GoToolsSettings.Instance.golibpath]) - suggestionsJsonStr, stderr, rc = runner.run("gocode", ["-f=json", "autocomplete", + suggestionsJsonStr, stderr, rc = ToolRunner.run("gocode", ["-f=json", "autocomplete", str(Buffers.offset_at_cursor(view)[0])], stdin=Buffers.buffer_text(view)) # TODO: restore gocode's lib-path suggestionsJson = json.loads(suggestionsJsonStr) - logger.log("DEBUG: gocode output: " + suggestionsJsonStr) + Logger.log("DEBUG: gocode output: " + suggestionsJsonStr) if rc != 0: - logger.status("no completions found: " + str(e)) + Logger.status("no completions found: " + str(e)) return [] if len(suggestionsJson) > 0: @@ -47,16 +42,6 @@ def on_query_completions(self, view, prefix, locations): else: return [] - @staticmethod - def gocode_libpath(settings): - libpath = [] - libpath.append(os.path.join(settings.goroot, "pkg", settings.goos + "_" + settings.goarch)) - - for p in settings.gopath.split(":"): - libpath.append(os.path.join(p, "pkg", settings.goos + "_" + settings.goarch)) - - return ":".join(libpath) - @staticmethod def build_suggestion(json): label = '{0: <30.30} {1: <40.40} {2}'.format( diff --git a/gotools_util.py b/gotools_util.py index f63d37d..dc0ed4a 100644 --- a/gotools_util.py +++ b/gotools_util.py @@ -5,6 +5,8 @@ import subprocess import time +from .gotools_settings import GoToolsSettings + class Buffers(): @staticmethod def buffer_text(view): @@ -52,31 +54,28 @@ def is_go_source(view): return view.score_selector(0, 'source.go') != 0 class Logger(): - def __init__(self, settings): - self.settings = settings - - def log(self, msg): - if self.settings.debug_enabled: - print("GoTools: " + msg) + @staticmethod + def log(msg): + if GoToolsSettings.Instance.debug_enabled: + print("GoTools: DEBUG: {0}".format(msg)) - def error(self, msg): - print("GoTools: ERROR: " + msg) + @staticmethod + def error(msg): + print("GoTools: ERROR: {0}".format(msg)) - def status(self, msg): + @staticmethod + def status(msg): sublime.status_message("GoTools: " + msg) class ToolRunner(): - def __init__(self, settings, logger): - self.settings = settings - self.logger = logger - - def run(self, tool, args=[], stdin=None, timeout=5): + @staticmethod + def run(tool, args=[], stdin=None, timeout=5): toolpath = None - searchpaths = list(map(lambda x: os.path.join(x, 'bin'), self.settings.gopath.split(os.pathsep))) - for p in self.settings.ospath.split(os.pathsep): + searchpaths = list(map(lambda x: os.path.join(x, 'bin'), GoToolsSettings.Instance.gopath.split(os.pathsep))) + for p in GoToolsSettings.Instance.ospath.split(os.pathsep): searchpaths.append(p) - searchpaths.append(os.path.join(self.settings.goroot, 'bin')) - searchpaths.append(self.settings.gorootbin) + searchpaths.append(os.path.join(GoToolsSettings.Instance.goroot, 'bin')) + searchpaths.append(GoToolsSettings.Instance.gorootbin) if platform.system() == "Windows": tool = tool + ".exe" @@ -88,23 +87,20 @@ def run(self, tool, args=[], stdin=None, timeout=5): break if not toolpath: - self.logger.log("Couldn't find Go tool '" + tool + "' in:\n" + "\n".join(searchpaths)) - raise Exception("Error running Go tool '" + tool + "'; check the console logs for details") + Logger.log("Couldn't find Go tool '{0}' in:\n{1}".format(tool, "\n".join(searchpaths))) + raise Exception("Error running Go tool '{0}'; check the console logs for details".format(tool)) cmd = [toolpath] + args try: - self.logger.log("spawning process:") + Logger.log("spawning process...") env = os.environ.copy() - env["PATH"] = self.settings.ospath - env["GOPATH"] = self.settings.gopath - env["GOROOT"] = self.settings.goroot + env["PATH"] = GoToolsSettings.Instance.ospath + env["GOPATH"] = GoToolsSettings.Instance.gopath + env["GOROOT"] = GoToolsSettings.Instance.goroot - self.logger.log(" PATH: " + env["PATH"]) - self.logger.log(" GOPATH: " + env["GOPATH"]) - self.logger.log(" GOROOT: " + env["GOROOT"]) - self.logger.log(" environment: " + str(env)) - self.logger.log(" command: " + " ".join(cmd)) + Logger.log("\tcommand: " + " ".join(cmd)) + Logger.log("\tenvironment: " + str(env)) # Hide popups on Windows si = None @@ -117,10 +113,10 @@ def run(self, tool, args=[], stdin=None, timeout=5): stdout, stderr = p.communicate(input=stdin, timeout=timeout) p.wait(timeout=timeout) elapsed = round(time.time() - start) - self.logger.log("process returned ("+ str(p.returncode) +") in " + str(elapsed) + " seconds") + Logger.log("process returned ({0}) in {1} seconds".format(str(p.returncode), str(elapsed))) stderr = stderr.decode("utf-8") if len(stderr) > 0: - self.logger.log("stderr:\n"+stderr) + Logger.log("stderr:\n{0}".format(stderr)) return stdout.decode("utf-8"), stderr, p.returncode except subprocess.CalledProcessError as e: raise