-
Notifications
You must be signed in to change notification settings - Fork 3
/
subdebug.py
263 lines (217 loc) · 8.09 KB
/
subdebug.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# Main entry point for the plugin.
# Author: Yuri van Geffen
import sublime, sublime_plugin
import os
import threading
import queue
import asyncore
import socket
from itertools import chain
import re
settings = sublime.load_settings("subdebug")
TCP_IP = '127.0.0.1'
TCP_PORT = 8172
BUFFER_SIZE = 1024
BASEDIR = settings.get("basedir", "")
STEP_ON_CONNECT = settings.get("step_on_connect", False)
# Handles incoming and outgoing messages for the MobDebug client
class SubDebugHandler(asyncore.dispatcher):
def __init__(self, socket, handler_id):
asyncore.dispatcher.__init__(self, socket)
self.handler_id = handler_id
msg_queue.put(b"STEP\n" if STEP_ON_CONNECT else b"RUN\n")
for view_name,row in state_handler.breakpoints():
msg_queue.put("SETB {0} {1}\n".format(view_name, row).encode('latin-1'))
# Reads the message-code of incomming messages and passes
# them to the right function
def handle_read(self):
data = self.recv(BUFFER_SIZE)
if data:
print(self.handler_id, "Received: ", data)
split = data.split()
if split[0] in message_parsers:
message_parsers[split[0]](split)
def handle_write(self):
if not msg_queue.empty():
msg = msg_queue.get()
print("Sending: ", msg)
self.send(msg)
def handle_error(self):
raise
# Starts listening on TCP_PORT and accepts incoming connections
# before passing them to an instance of SubDebugHandler
class SubDebugServer(asyncore.dispatcher):
def __init__(self, host, port):
asyncore.dispatcher.__init__(self)
self.handler_id = 0
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind((host, port))
self.listen(1)
print("Started listening on: ", host, ":", port)
def handle_accept(self):
pair = self.accept()
if pair is not None:
(conn_sock, client_address) = pair
print("Incoming connection: ", client_address)
SubDebugHandler(conn_sock, ++self.handler_id)
def handle_close(self):
print("Closing server.")
self.close()
def handle_error(self):
self.close()
# Lets the user run the script (until breakpoint)
class RunCommand(sublime_plugin.WindowCommand):
def run(self):
print("Running until breakpoint...")
msg_queue.put(b"RUN\n")
state_handler.remove_line_marker()
# Lets the user step to the next line
class StepCommand(sublime_plugin.WindowCommand):
def run(self):
print("Stepping to next line...")
msg_queue.put(b"STEP\n")
# Lets the user step to the next line
class ToggleBreakpointCommand(sublime_plugin.TextCommand):
def run(self, edit):
view_name = simplify_path(self.view.file_name())
row,_ = self.view.rowcol(self.view.sel()[0].begin())
print("Toggling breakpoint:", view_name, row)
state_handler.toggle_breakpoint(view_name, row + 1)
# Lets the user pick a base directory from where the lua is executed
class SetBasedirCommand(sublime_plugin.WindowCommand):
def run(self):
# Ran if the user want to choose their own base directory
def choose_other(path):
global BASEDIR
BASEDIR = path.replace('\\','/')
if(BASEDIR[-1] != "/"):
BASEDIR += "/"
print("BASEDIR:", BASEDIR)
# Ran if the user has chosen a base directory option
def selected_folder(index):
global BASEDIR
if index != -1: # The last option lets the user choose a base dir themself
if(index == len(folders)-1):
sublime.active_window().show_input_panel("Give the base directory path.", BASEDIR, choose_other, None, None)
else:
BASEDIR = folders[index] + "/"
state_handler.clear_state()
print("BASEDIR:", BASEDIR)
folders = list(chain.from_iterable([w.folders() for w in sublime.windows()]))
folders = [f.replace("\\", "/") for f in folders]
folders.insert(len(folders), "Choose other directory...")
sublime.active_window().show_quick_panel(folders, selected_folder)
# Lets the user step to the next line
class ToggleStepOnConnectCommand(sublime_plugin.WindowCommand):
def run(self):
global STEP_ON_CONNECT
STEP_ON_CONNECT = not STEP_ON_CONNECT
print("Step on connect:", STEP_ON_CONNECT)
def is_checked(self):
return STEP_ON_CONNECT or False
#=========Incomming message parsers=========#
# Called when the "202 Paused" message is received
def paused_command(args):
state_handler.set_line_marker(args[2].decode("utf-8"), int(args[3]))
# Mapping from incomming messages to the functions that parse them
message_parsers = {
b"202": paused_command,
}
#===========================================#
class StateHandler():
# Initiates object by checking which views are available and
# clearing the state
def __init__(self):
self.clear_state()
self.update_regions()
def clear_state(self):
self.state = {}
self.update_regions()
# Gets all available views in sublime and adds the missing ones to the state
def add_missing_views(self):
views = [v for v in sum([w.views() for w in sublime.windows()], [])]
self.views = {simplify_path(v.file_name()):v for v in views if v.file_name() != None}
print(self.views)
for view_name, view in self.views.items():
if view_name not in self.state:
self.state[view_name] = []
# Updates all views with the available state-objects using the
# assigned functions
def update_regions(self):
self.add_missing_views()
# Iterate over all files in the state
for view_name,regions in self.state.items():
# Remove all old regions
for reg_type_name in self.region_types:
self.views[view_name].erase_regions(reg_type_name)
region_sets = {}
# Iterate over all regions in that file
for (reg_type,line) in regions:
if reg_type == "line_marker" or ("line_marker", line) not in regions:
if reg_type not in region_sets:
region_sets[reg_type] = []
region_sets[reg_type].append(sublime.Region(self.views[view_name].text_point(line-1, 0)))
# Register all new regions except the line-marker with sublime
for reg_name,v in region_sets.items():
print("Adding region:", view_name, reg_name, v)
self.views[view_name].add_regions(reg_name, v, *self.region_types[reg_name])
def set_line_marker(self, view_name, line_number):
view_name = simplify_path(view_name)
print("Setting line marker:", view_name, line_number)
self.add_missing_views()
if view_name in self.views:
self.state.setdefault(view_name, [])
self.state[view_name] = [(k,v) for k, v in self.state[view_name] if k != "line_marker"]
self.state[view_name].append(("line_marker", line_number))
self.update_regions()
def remove_line_marker(self):
for name,view in self.state.items():
self.state[name] = [(t,n) for t,n in view if t != "line_marker"]
self.update_regions()
def toggle_breakpoint(self, view_name, line_number):
self.add_missing_views()
if view_name in self.views and ("breakpoint", line_number) in self.state[view_name]:
self.remove_breakpoint(view_name, line_number)
else:
self.set_breakpoint(view_name, line_number)
self.update_regions()
def set_breakpoint(self, view_name, line_number):
self.state.setdefault(view_name, [])
self.state[view_name].append(("breakpoint", line_number))
msg_queue.put("SETB {0} {1}\n".format(view_name, line_number).encode('latin-1'))
def remove_breakpoint(self, view_name, line_number):
self.state[view_name].remove(("breakpoint", line_number))
msg_queue.put("DELB {0} {1}\n".format(view_name, line_number).encode('latin-1'))
def breakpoints(self):
ret = []
for k,v in self.state.items():
for t in v:
if t[0] == "breakpoint":
ret.append((k,t[1]))
return ret
views = {}
state = {}
region_types = {
"breakpoint": ("keyword", "circle"),
"line_marker": ("keyword", "bookmark"),
}
def plugin_unloaded():
settings.set("basedir", BASEDIR)
settings.set("step_on_connect", STEP_ON_CONNECT)
print("Closing down the server...")
server.close()
def simplify_path(path):
path = path.replace("\\","/").replace(BASEDIR,"")
path = re.sub('\.lua$', '', path) # Strip ".lua" from the path
return path
# Open a threadsafe message queue
msg_queue = queue.Queue()
state_handler = StateHandler()
# Start listening and open the asyncore loop
server = SubDebugServer(TCP_IP, TCP_PORT)
if os.name == "posix":
thread = threading.Thread(target=asyncore.loop, kwargs={"use_poll": True})
else:
thread = threading.Thread(target=asyncore.loop)
thread.start()