commit 6ffb2aed80ba4a43d23511d88d2ca4b7f002d4eb Author: Krzosa Karol Date: Sun Jan 22 10:31:55 2023 +0100 Init repository diff --git a/Default.sublime-commands b/Default.sublime-commands new file mode 100644 index 0000000..13472fb --- /dev/null +++ b/Default.sublime-commands @@ -0,0 +1,30 @@ +[ + { + "caption": "RemedyBG: Launch", + "command": "remedy_launch", + }, + { + "caption": "RemedyBG: Goto the sublime cursor in remedy", + "command": "remedy_go_to_cursor", + }, + { + "caption": "RemedyBG: Start debugging", + "command": "remedy_start_debugging", + }, + { + "caption": "RemedyBG: Stop debugging", + "command": "remedy_stop_debugging", + }, + { + "caption": "RemedyBG: Run to cursor, launch and stop at cursor", + "command": "remedy_run_to_cursor", + }, + { + "caption": "RemedyBG: Restart debugging", + "command": "remedy_restart_debugging", + }, + { + "caption": "RemedyBG: Add selection or word under cursor to watch window", + "command": "remedy_restart_debugging", + }, +] \ No newline at end of file diff --git a/Default.sublime-keymap b/Default.sublime-keymap new file mode 100644 index 0000000..056e031 --- /dev/null +++ b/Default.sublime-keymap @@ -0,0 +1,7 @@ +[ + { "keys": ["ctrl+f10"], "command": "remedy_run_to_cursor" }, + { "keys": ["f5"], "command": "remedy_start_debugging" }, + { "keys": ["shift+f5"], "command": "remedy_stop_debugging" }, + { "keys": ["ctrl+shift+f5"], "command": "remedy_restart_debugging" }, + { "keys": ["ctrl+t"], "command": "remedy_all_in_one" }, +] \ No newline at end of file diff --git a/Default.sublime-mousemap b/Default.sublime-mousemap new file mode 100644 index 0000000..f226528 --- /dev/null +++ b/Default.sublime-mousemap @@ -0,0 +1,9 @@ +[ + { + "button": "button3", + "count": 1, + "modifiers": [], + "command": "remedy_all_in_one", + "press_command": "drag_select" + } +] diff --git a/dependencies.json b/dependencies.json new file mode 100644 index 0000000..35e38d5 --- /dev/null +++ b/dependencies.json @@ -0,0 +1,7 @@ +{ + "windows": { + "*": [ + "python-pywin32" + ] + } +} \ No newline at end of file diff --git a/remedy.py b/remedy.py new file mode 100644 index 0000000..ab7e6a6 --- /dev/null +++ b/remedy.py @@ -0,0 +1,438 @@ +import subprocess +import os, io, ctypes + +import sublime +import sublime_plugin +from Default.exec import ExecCommand + +import win32pipe, win32file, pywintypes +from .remedy_api import * + +class RemedyInstance: + def __init__(self): + self.cmd_pipe = None + self.event_pipe = None + self.process = None + self.servername = "" + + def send_command(self, cmd, **cmd_args): + if self.cmd_pipe is None: return 0 + + cmd_buffer = io.BytesIO() + cmd_buffer.write(ctypes.c_uint16(cmd)) + + if cmd == COMMAND_ADD_BREAKPOINT_AT_FILENAME_LINE: + filepath = cmd_args['filename'] + cmd_buffer.write(ctypes.c_uint16(len(filepath))) + cmd_buffer.write(bytes(filepath, 'utf-8')) + cmd_buffer.write(ctypes.c_uint32(cmd_args['line'])) + cmd_buffer.write(ctypes.c_uint16(0)) + elif cmd == COMMAND_DELETE_BREAKPOINT: + if cmd_args['id'] in self.breakpoints: + rdbg_id = self.breakpoints[cmd_args['id']] + cmd_buffer.write(ctypes.c_uint32(rdbg_id)) + self.breakpoints.pop(cmd_args['id']) + if rdbg_id in self.breakpoints_rdbg: + self.breakpoints_rdbg.pop(rdbg_id) + else: + return 0 + elif cmd == COMMAND_GOTO_FILE_AT_LINE: + filepath = cmd_args['filename'] + cmd_buffer.write(ctypes.c_uint16(len(filepath))) + cmd_buffer.write(bytes(filepath, 'utf-8')) + cmd_buffer.write(ctypes.c_uint32(cmd_args['line'])) + elif cmd == COMMAND_START_DEBUGGING: + cmd_buffer.write(ctypes.c_uint8(0)) + elif cmd == COMMAND_STEP_INTO_BY_LINE: + pass + elif cmd == COMMAND_STEP_OVER_BY_LINE: + pass + elif cmd == COMMAND_STEP_OVER_BY_LINE: + pass + elif cmd == COMMAND_STOP_DEBUGGING: + pass + elif cmd == COMMAND_RESTART_DEBUGGING: + pass + elif cmd == COMMAND_CONTINUE_EXECUTION: + pass + elif cmd == COMMAND_RUN_TO_FILE_AT_LINE: + filepath = cmd_args['filename'] + cmd_buffer.write(ctypes.c_uint16(len(filepath))) + cmd_buffer.write(bytes(filepath, 'utf-8')) + cmd_buffer.write(ctypes.c_uint32(cmd_args['line'])) + elif cmd == COMMAND_GET_TARGET_STATE: + pass + elif cmd == COMMAND_ADD_WATCH: + print(cmd_args) + expr = cmd_args['expr'] + cmd_buffer.write(ctypes.c_uint8(1)) # watch window 1 + cmd_buffer.write(ctypes.c_uint16(len(expr))) + cmd_buffer.write(bytes(expr, 'utf-8')) + cmd_buffer.write(ctypes.c_uint16(0)) + elif cmd == COMMAND_UPDATE_BREAKPOINT_LINE: + if cmd_args['id'] in self.breakpoints: + rdbg_id = self.breakpoints[cmd_args['id']] + cmd_buffer.write(ctypes.c_uint32(rdbg_id)) + cmd_buffer.write(ctypes.c_uint32(cmd_args['line'])) + elif cmd == COMMAND_SET_WINDOW_POS: + cmd_buffer.write(ctypes.c_int32(cmd_args['x'])) + cmd_buffer.write(ctypes.c_int32(cmd_args['y'])) + cmd_buffer.write(ctypes.c_int32(cmd_args['w'])) + cmd_buffer.write(ctypes.c_int32(cmd_args['h'])) + elif cmd == COMMAND_GET_WINDOW_POS: + pass + else: + assert 0 + return 0 # not implemented + + + try: + out_data = win32pipe.TransactNamedPipe(self.cmd_pipe, cmd_buffer.getvalue(), 8192, None) + except pywintypes.error as pipe_error: + print('RDBG', pipe_error) + self.close(stop=False) + return 0 + + out_buffer = io.BytesIO(out_data[1]) + result_code = int.from_bytes(out_buffer.read(2), 'little') + if result_code == 1: + if cmd == COMMAND_ADD_BREAKPOINT_AT_FILENAME_LINE: + return 0 + # bp_id = int.from_bytes(out_buffer.read(4), 'little') + # if bp_id not in self.breakpoints_rdbg: + # self.breakpoints[cmd_args['id']] = bp_id + # self.breakpoints_rdbg[bp_id] = (cmd_args['id'], cmd_args['filename'], cmd_args['line']) + # else: + # print('RDBG: Breakpoint (%i) %s@%i skipped, because it will not get triggered' % (cmd_args['id'], cmd_args['filename'], cmd_args['line'])) + # self.ignore_next_remove_breakpoint = True + # Editor.RemoveBreakpointById(cmd_args['id']) + # return bp_id + elif cmd == COMMAND_GET_TARGET_STATE: + return int.from_bytes(out_buffer.read(2), 'little') + elif cmd == COMMAND_ADD_WATCH: + return int.from_bytes(out_buffer.read(4), 'little') + elif cmd == COMMAND_GET_WINDOW_POS: + x = int.from_bytes(out_buffer.read(4), 'little') + y = int.from_bytes(out_buffer.read(4), 'little') + w = int.from_bytes(out_buffer.read(4), 'little') + h = int.from_bytes(out_buffer.read(4), 'little') + return (x, y, w, h) + else: + sublime.message_dialog('RDBG: ' + str(cmd) + ' failed') + return 0 + + return 1 + + def close(self, stop=True): + if stop: + self.stop() + + if self.cmd_pipe: + win32file.CloseHandle(self.cmd_pipe) + self.cmd_pipe = None + + if self.event_pipe is not None: + win32file.CloseHandle(self.event_pipe) + self.event_pipe = None + + if self.process is not None: + self.process.kill() + self.process = None + + print("RDBG: Connection closed") + + def try_launching(self): + if self.process == None: + self.figure_out_target_and_launch() + return True + return False + + def cmd_pipe_name(self): + return "\\\\.\\pipe\\" + self.servername + + def event_pipe_name(self): + return "\\\\.\\pipe\\" + self.servername + "-events" + + def figure_out_target_and_launch(self): + self.window = sublime.active_window() + + remedy_target = None + if remedy_target == None: + project = self.window.project_data() + if project and project.get("remedy_target"): + remedy_target = project.get("remedy_target") + + if remedy_target: + self.launch(remedy_target) + + if remedy_target == None: + vars = self.window.extract_variables() + self.current_dir = vars.get("project_path") + if self.current_dir == None: + self.current_dir = vars.get("folder") + if self.current_dir == None: + self.current_dir = vars.get("file_path") + if self.current_dir == None: + sublime.message_dialog("RemedyBG: Trying to launch but cant figure out starting directory, open a file or project") + return + + self.filelist = os.listdir(self.current_dir) + def walk_the_user_to_executable(item_index): + if item_index == -1: + return + item = self.filelist[item_index] + item_path = self.current_dir + "/" + item + if os.path.isdir(item_path): + self.current_dir += "/" + item + self.filelist = os.listdir(self.current_dir) + self.window.show_quick_panel(self.filelist, walk_the_user_to_executable) + elif os.path.isfile(item_path): + self.launch(item_path) + self.window.show_quick_panel(self.filelist, walk_the_user_to_executable) + + def launch(self, target): + try: + self.servername = "default" + window = sublime.active_window() + vars = window.extract_variables() + project = vars.get("project_base_name") + if project: + self.servername = project + hex(hash(vars["project"])) + else: + folder = vars.get("folder") + if folder: + self.servername = hex(hash(folder)) + print("RemedyBG: Server name = ", self.servername) + + + cmd = [get_remedy_executable(), "--servername", self.servername, target] + print("Launching Remedy with command: " + str(cmd)) + self.process = subprocess.Popen(cmd) + + import time + wait_time = 0.1 + time.sleep(wait_time) + pipe_success = False + for retry in range(0, 5): + try: + self.cmd_pipe = win32file.CreateFile(self.cmd_pipe_name(), win32file.GENERIC_READ|win32file.GENERIC_WRITE, \ + 0, None, win32file.OPEN_EXISTING, 0, None) + except pywintypes.error: + time.sleep(wait_time) + wait_time = wait_time*2.0 + continue + except Exception as e: + sublime.error_message('RemedyBG: Pipe error:' + str(e)) + return False + pipe_success = True + break + + if not pipe_success: + sublime.error_message('RemedyBG: Named pipe could not be opened to remedybg. Make sure remedybg version is above 0.3.8') + return False + + win32pipe.SetNamedPipeHandleState(self.cmd_pipe, win32pipe.PIPE_READMODE_MESSAGE, None, None) + + assert self.event_pipe == None + self.event_pipe = win32file.CreateFile(self.event_pipe_name(), win32file.GENERIC_READ|256, \ + 0, None, win32file.OPEN_EXISTING, 0, None) + win32pipe.SetNamedPipeHandleState(self.event_pipe, win32pipe.PIPE_READMODE_MESSAGE, None, None) + + print("RemedyBG: Connected") + + def update(): + global remedy_instance + if self.process is None: + return + if self.process is not None and self.process.poll() is not None: + print('RemedyBG: quit with code: %i' % (self.process.poll())) + self.process = None + self.close(stop=False) + return + + sublime.set_timeout(update, 1000) + sublime.set_timeout(update, 1000) + except FileNotFoundError as not_found: + sublime.error_message("RemedyBG: " + str(not_found) + ': ' + target) + except pywintypes.error as connection_error: + sublime.error_message("RemedyBG: " + str(connection_error)) + except OSError as os_error: + sublime.error_message("RemedyBG: " + str(os_error)) + + def run_to_cursor(self): + window = sublime.active_window() + view = window.active_view() + line = view.rowcol(view.sel()[0].b)[0] + 1 + file = view.file_name() + self.send_command(COMMAND_RUN_TO_FILE_AT_LINE, filename=file, line=line) + + def goto_cursor(self): + window = sublime.active_window() + view = window.active_view() + line = view.rowcol(view.sel()[0].b)[0] + 1 + file = view.file_name() + self.send_command(COMMAND_GOTO_FILE_AT_LINE, filename=file, line=line) + + +remedy_instance = RemedyInstance() + +def get_remedy_executable(): + window = sublime.active_window() + settings = window.settings() + result = settings.get("remedy_executable") + if result == None: + result = "remedybg" + return result + +def execute_process(view, cmd, offset = 1): + line = view.rowcol(view.sel()[0].b)[0] + offset + line = str(line) + file = view.file_name() + window = sublime.active_window() + cmd = sublime.expand_variables(cmd, {"file": file, "line": line, "remedybg": get_remedy_executable()}) + print(cmd) + subprocess.Popen(cmd) + +class RemedyBuildCommand(ExecCommand): + def run(self, **kwargs): + self.command = kwargs.get("command") + if self.command == None: + sublime.message_dialog("RemedyBG: remedy_build expects a command, one of [run_to_cursor, start_debugging, goto_cursor]\n\nexample :: \"args\":{\"command\": \"run_to_cursor\"}") + + project = self.window.project_data() + build = None + if project: + bs = project.get("build_systems") + rbs = project.get("remedy_build_system") + if bs: + if len(bs) == 1: + build = bs[0] + elif rbs: + for i in bs: + if rbs == i["name"]: + build = i + break + + + if project == None or build == None: + sublime.error_message(""" + RemedyBG: You need a project and a build system inside that project to call this function, + Sublime API doesnt allow for querying the selected build system. + Look here to figure out the project format: https://www.sublimetext.com/docs/projects.html + Additionally you need a field called "remedy_build_system" to signal which + build system was chosen + """) + return + + if remedy_instance.try_launching(): + return + + kwargs = { + "cmd": build.get("cmd", None), + "shell_cmd": build.get("shell_cmd", None), + "file_regex": build.get("file_regex", ""), + "line_regex": build.get("line_regex", ""), + "working_dir": build.get("working_dir", ""), + "encoding": build.get("encoding", "utf-8"), + "env": build.get("env", {}), + "quiet": build.get("quiet", False), + "kill": build.get("kill", False), + "kill_previous": build.get("kill_previous", False), + "update_annotations_only": build.get("update_annotations_only", False), + "word_wrap": build.get("word_wrap", True), + "syntax": build.get("syntax", "Packages/Text/Plain text.tmLanguage"), + } + super().run(**kwargs) + def on_finished(self, proc): + super().on_finished(proc) + if self.command == "run_to_cursor": + remedy_instance.run_to_cursor() + elif self.command == "start_debugging": + remedy_instance.send_command(COMMAND_START_DEBUGGING) + elif self.command == "goto_cursor": + remedy_instance.goto_cursor() + else: # @warning: While adding here also need to change error message !!!! + sublime.message_dialog("RemedyBG: Unrecognized command =", self.command) + +class RemedyLaunchCommand(sublime_plugin.WindowCommand): + def run(self): + remedy_instance.figure_out_target_and_launch() + +class RemedyStartDebuggingCommand(sublime_plugin.WindowCommand): + def run(self): + if remedy_instance.try_launching(): return + remedy_instance.send_command(COMMAND_START_DEBUGGING) + +class RemedyStopDebuggingCommand(sublime_plugin.WindowCommand): + def run(self): + if remedy_instance.try_launching(): return + remedy_instance.send_command(COMMAND_STOP_DEBUGGING) + +class RemedyRestartDebuggingCommand(sublime_plugin.WindowCommand): + def run(self): + if remedy_instance.try_launching(): return + remedy_instance.send_command(COMMAND_RESTART_DEBUGGING) + +class RemedyRunToCursorCommand(sublime_plugin.TextCommand): + def run(self, edit): + if remedy_instance.try_launching(): return + remedy_instance.run_to_cursor() + +class RemedyGotoCursorCommand(sublime_plugin.TextCommand): + def run(self, edit): + if remedy_instance.try_launching(): return + remedy_instance.goto_cursor() + +class RemedyAddToWatchCommand(sublime_plugin.TextCommand): + def run(self, edit): + if remedy_instance.try_launching(): return + + sel = self.view.sel() + if len(sel) > 1: + return + + region_cursor = sel[0] + if region_cursor.a - region_cursor.b == 0: + settings = self.view.settings() + old_boundaries = settings.get("word_separators") + settings.set("word_separators"," ;,") + region_cursor = self.view.word(region_cursor) + settings.set("word_separators", old_boundaries) + + remedy_instance.send_command(COMMAND_ADD_WATCH, expr=self.view.substr(region_cursor)) + + +class RemedyAllInOneCommand(sublime_plugin.TextCommand): + def run(self, edit): + if remedy_instance.try_launching(): + return + + sel = self.view.sel() + if len(sel) > 1: + return + + region_cursor = sel[0] + settings = self.view.settings() + old_boundaries = settings.get("word_separators") + settings.set("word_separators"," ;,") + region_word_on_cursor = self.view.word(region_cursor) + settings.set("word_separators", old_boundaries) + + remedy_instance.goto_cursor() + + content = self.view.substr(region_word_on_cursor) + if content == "r": + remedy_instance.send_command(COMMAND_START_DEBUGGING) + self.view.replace(edit, region_word_on_cursor, "") + elif content == "rr": + remedy_instance.send_command(COMMAND_STOP_DEBUGGING) + self.view.replace(edit, region_word_on_cursor, "") + elif content == "rrr": + remedy_instance.send_command(COMMAND_RESTART_DEBUGGING) + self.view.replace(edit, region_word_on_cursor, "") + elif content == "rt": + remedy_instance.run_to_cursor() + self.view.replace(edit, region_word_on_cursor, "") + else: + remedy_instance.send_command(COMMAND_ADD_WATCH, expr=content) + diff --git a/remedy_api.py b/remedy_api.py new file mode 100644 index 0000000..9407ceb --- /dev/null +++ b/remedy_api.py @@ -0,0 +1,78 @@ +TARGETSTATE_NONE = 1 +TARGETSTATE_SUSPENDED = 2 +TARGETSTATE_EXECUTING = 3 + +COMMAND_BRING_DEBUGGER_TO_FOREGROUND = 50 +COMMAND_SET_WINDOW_POS = 51 +COMMAND_GET_WINDOW_POS = 52 +COMMAND_COMMAND_EXIT_DEBUGGER = 75 +COMMAND_GET_IS_SESSION_MODIFIED = 100 +COMMAND_GET_SESSION_FILENAME = 101 +COMMAND_NEW_SESSION = 102 +COMMAND_OPEN_SESSION = 103 +COMMAND_SAVE_SESSION = 104 +COMMAND_SAVE_AS_SESSION = 105 +COMMAND_GOTO_FILE_AT_LINE = 200 +COMMAND_CLOSE_FILE = 201 +COMMAND_CLOSE_ALL_FILES = 202 +COMMAND_GET_CURRENT_FILE = 203 +COMMAND_GET_TARGET_STATE = 300 +COMMAND_START_DEBUGGING = 301 +COMMAND_STOP_DEBUGGING = 302 +COMMAND_RESTART_DEBUGGING = 303 +COMMAND_STEP_INTO_BY_LINE = 307 +COMMAND_STEP_OVER_BY_LINE = 309 +COMMAND_STEP_OUT = 311, +COMMAND_CONTINUE_EXECUTION = 312 +COMMAND_RUN_TO_FILE_AT_LINE = 313 +COMMAND_GET_BREAKPOINT_LOCATIONS = 601 +COMMAND_ADD_BREAKPOINT_AT_FILENAME_LINE = 604 +COMMAND_UPDATE_BREAKPOINT_LINE = 608 +COMMAND_DELETE_BREAKPOINT = 610 +COMMAND_DELETE_ALL_BREAKPOINTS = 611 +COMMAND_ADD_WATCH = 701 + +BREAKPOINTKIND_FUNCTION_NAME = 1 +BREAKPOINTKIND_FILENAME_LINE = 2 +BREAKPOINTKIND_ADDRESS = 3 +BREAKPOINTKIND_PROCESSOR = 4 + +COMMANDRESULT_UNKNOWN = 0 +COMMANDRESULT_OK = 1 +COMMANDRESULT_FAIL = 2 +COMMANDRESULT_ABORTED = 3 +COMMANDRESULT_INVALID_COMMAND = 4 +COMMANDRESULT_BUFFER_TOO_SMALL = 5 +COMMANDRESULT_FAILED_OPENING_FILE = 6 +COMMANDRESULT_FAILED_SAVING_SESSION = 7 +COMMANDRESULT_INVALID_ID = 8 +COMMANDRESULT_INVALID_TARGET_STATE = 9 +COMMANDRESULT_FAILED_NO_ACTIVE_CONFIG = 10 +COMMANDRESULT_INVALID_BREAKPOINT_KIND = 11 + +SOURCELOCCHANGEDREASON_UNSPECIFIED = 0 +SOURCELOCCHANGEDREASON_COMMAND_LINE = 1 +SOURCELOCCHANGEDREASON_DRIVER = 2 +SOURCELOCCHANGEDREASON_BREAKPOINT_SELECTED = 3 +SOURCELOCCHANGEDREASON_CURRENT_FRAME_CHANGED = 4 +SOURCELOCCHANGEDREASON_THREAD_CHANGED = 5 +SOURCELOCCHANGEDREASON_BREAKPOINT_HIT = 6 +SOURCELOCCHANGEDREASON_EXCEPTION_HIT = 7 +SOURCELOCCHANGEDREASON_STEP_OVER = 8 +SOURCELOCCHANGEDREASON_STEP_IN = 9 +SOURCELOCCHANGEDREASON_STEP_OUT = 10 +SOURCELOCCHANGEDREASON_NON_USER_BREAKPOINT = 11 +SOURCELOCCHANGEDREASON_DEBUG_BREAK = 12 + +EVENTTYPE_EXIT_PROCESS = 100 +EVENTTYPE_TARGET_STARTED = 101 +EVENTTYPE_TARGET_ATTACHED = 102 +EVENTTYPE_TARGET_DETACHED = 103 +EVENTTYPE_TARGET_CONTINUED = 104 +EVENTTYPE_KIND_BREAKPOINT_HIT = 600 +EVENTTYPE_KIND_BREAKPOINT_RESOLVED = 601 +EVENTTYPE_OUTPUT_DEBUG_STRING = 800 +EVENTTYPE_BREAKPOINT_ADDED = 602 +EVENTTYPE_BREAKPOINT_MODIFIED = 603 +EVENTTYPE_BREAKPOINT_REMOVED = 604 +EVENTTYPE_SOURCE_LOCATION_CHANGED = 200 \ No newline at end of file diff --git a/setup_vsvars.py b/setup_vsvars.py new file mode 100644 index 0000000..5cbf534 --- /dev/null +++ b/setup_vsvars.py @@ -0,0 +1,112 @@ +import sublime +import sublime_plugin + +from threading import Thread +from subprocess import Popen, PIPE +from os import environ + +# Related reading; +# https://stackoverflow.com/questions/39881091/how-to-run-sublimetext-with-visual-studio-environment-enabled/ + +# For the unfamiliar, Visual Studio ships with a batch file which sets up the +# environment variables you need to be able to run visual studio tools from a +# command prompt. +# +# This pluguin was written in response to someone that wanted to know how you +# could run Sublime and have it have the visual studio environment already set +# up. +# +# This plugin will use a subprocess to execute the batch file in the background +# and then issue the 'set' command to get the command interpreter to output the +# state of the environment before it exits. +# +# This output is gathered and parsed to come up with a dictionary similar to +# the environ table that python uses. From here we can easily detect what new +# environment variables were added and the values of those that changed, and +# set them as appropriate. +# +# As written Sublime needs to be restarted in order to execute the batch file +# again. A more elegant solution would be to save the environment prior to +# modifying it so that it could be restored and a new environment applied. + +# To use this, you need to specify a setting in your user preferences named +# 'vc_vars_cmd' which should contain a complete path to the batch file you want +# to execute. Optionally you can also specify 'vc_vars_arch', which will be +# passed as a command line argument to the batch file executed. Remember that +# the preferences are JSON, so you need to quote all path separators. + +SENTINEL="SUBL_VC_VARS" + +def _get_vc_env(): + """ + Run the batch file specified in the vc_vars_cmd setting (with an + optional architecture type) and return back a dictionary of the + environment that the batch file sets up. + + Returns None if the preference is missing or the batch file fails. + """ + settings = sublime.load_settings("Preferences.sublime-settings") + vars_cmd = settings.get("vc_vars_cmd") + vars_arch = settings.get("vc_vars_arch", "amd64") + + if vars_cmd is None: + print("set_vc_vars: Cannot set Visual Studio Environment") + print("set_vc_vars: Add 'vc_vars_cmd' setting to settings and restart") + return None + + try: + # Run the batch, outputting a sentinel value so we can separate out + # any error messages the batch might generate. + shell_cmd = "\"{0}\" {1} && echo {2} && set".format( + vars_cmd, vars_arch, SENTINEL) + + output = Popen(shell_cmd, stdout=PIPE, shell=True).stdout.read() + + lines = [line.strip() for line in output.decode("utf-8").splitlines()] + env_lines = lines[lines.index(SENTINEL) + 1:] + except: + return None + + # Convert from var=value to dictionary key/value pairs. We upper case the + # keys, since Python does that to the mapping it stores in environ. + env = {} + for env_var in env_lines: + parts = env_var.split("=", maxsplit=1) + env[parts[0].upper()] = parts[1] + + return env + +def install_vc_env(): + """ + Try to collect the appropriate Visual Studio environment variables and + set them into the current environment. + """ + vc_env = _get_vc_env() + if vc_env is None: + print("set_vc_vars: Unable to fetch the Visual Studio Environment") + return sublime.status_message("Error fetching VS Environment") + + # Add newly set environment variables + for key in vc_env.keys(): + if key not in environ: + environ[key] = vc_env[key] + + # Update existing variables whose values changed. + for key in environ: + if key in vc_env and environ[key] != vc_env[key]: + environ[key] = vc_env[key] + + # Set a sentinel variable so we know not to try setting up the path again. + environ[SENTINEL] = "BOOTSTRAPPED" + sublime.status_message("VS Environment enabled") + +def plugin_loaded(): + if sublime.platform() != "windows": + return sublime.status_message("VS is not supported on this platform") + + # To reload the environment if it changes, restart Sublime. + if SENTINEL in environ: + return sublime.status_message("VS Environment already enabled") + + # Update in the background so we don't block the UI + Thread(target=install_vc_env).start()