11# -*- coding: utf-8 -*-
22"""
3- Python Breakpoints plugin for Sublime Text editor
3+ Python Breakpoints plugin for Sublime Text 2/3
44
55Author: Oscar Ibatullin (github.com/obormot)
66
77"""
88from __future__ import print_function
9- import ast
109import re
10+ import sys
1111import uuid
1212
1313import sublime
1414import sublime_plugin
1515
1616
17- debug = lambda * a : None # replace with debug = print if needed
18-
19-
2017############
2118# Settings #
2219############
2320
21+ # replace with "debug = print" to print debug messages to the ST console
22+ debug = lambda * a : None
23+
24+ # defaults
2425settings = None
25- pdb_block = ''
2626tab_size = 4
2727
2828
2929def plugin_loaded ():
3030 global settings
31- settings = sublime .load_settings (" PythonBreakpoints.sublime-settings" )
31+ settings = sublime .load_settings (' PythonBreakpoints.sublime-settings' )
3232
3333 global tab_size
3434 tab_size = settings .get ('tab_size' )
3535 if tab_size == 'auto' or tab_size is None :
3636 g_settings = sublime .load_settings ('Preferences.sublime-settings' )
3737 tab_size = g_settings .get ('tab_size' , 4 )
3838
39- global pdb_block
40- pdb_block = """\
41- # do not edit! added by PythonBreakpoints
42- from %s import set_trace as _breakpoint
43-
44-
45- """ % settings .get ('debugger' , 'pdb' )
4639
4740# for ST2
48- plugin_loaded ()
41+ if sys .version_info < (3 ,):
42+ plugin_loaded ()
4943
5044
5145#############
5246# Constants #
5347#############
5448
55- bp_regex = r"^[\t ]*_breakpoint\ (\) # ([a-f0-9]{8})"
49+ bp_regex = r"^[\t ]*import [\w.; ]+set_trace\ (\) # breakpoint ([a-f0-9]{8}) // "
5650bp_re = re .compile (bp_regex , re .DOTALL )
5751
5852EXPR_PRE = ['class' , 'def' , 'if' , 'for' , 'try' , 'while' , 'with' ]
5953EXPR_PST = ['elif' , 'else' , 'except' , 'finally' ]
6054
61- expr_re0 = re .compile (r"^[\t ]*(%s )[: ]" % '|' .join (EXPR_PRE ))
62- expr_re1 = re .compile (r"^[\t ]*(%s )[: ]" % '|' .join (EXPR_PRE + EXPR_PST ))
63- expr_re2 = re .compile (r"^[\t ]*(%s )[: ]" % '|' .join (EXPR_PST ))
55+ expr_re0 = re .compile (r"^[\t ]*({tokens} )[: ]" . format ( tokens = '|' .join (EXPR_PRE ) ))
56+ expr_re1 = re .compile (r"^[\t ]*({tokens} )[: ]" . format ( tokens = '|' .join (EXPR_PRE + EXPR_PST ) ))
57+ expr_re2 = re .compile (r"^[\t ]*({tokens} )[: ]" . format ( tokens = '|' .join (EXPR_PST ) ))
6458
6559
6660class Breakpoint (object ):
@@ -77,18 +71,27 @@ def __init__(self, from_text=None):
7771 self .uid = str (uuid .uuid4 ())[- 8 :]
7872
7973 @property
80- def rg_key (self ):
81- """breakpoint's region ID"""
82- return 'bp-%s' % self .uid
74+ def region_id (self ):
75+ """
76+ breakpoint's region ID
77+ """
78+ return "bp-{uid}" .format (uid = self .uid )
8379
84- def format (self , indent ):
85- """format breakpoint string"""
86- return "%s_breakpoint() # %s\n " % (' ' * indent , self .uid )
80+ def as_string (self , indent ):
81+ """
82+ format breakpoint string
83+ """
84+ debugger = settings .get ('debugger' , 'pdb' )
85+ return "{indent}import {debugger}; {debugger}.set_trace() # breakpoint {uid} //\n " .format (
86+ indent = ' ' * indent , debugger = debugger , uid = self .uid )
8787
8888 def highlight (self , view , rg ):
89- """colorize the breakpoint's region"""
89+ """
90+ colorize the breakpoint's region
91+ """
9092 scope = settings .get ('highlight' , 'invalid' )
91- view .add_regions (self .rg_key , [rg ], scope )
93+ gutter_icon = settings .get ('gutter_icon' , 'circle' )
94+ view .add_regions (self .region_id , [rg ], scope , gutter_icon , sublime .PERSISTENT )
9295
9396
9497###################
@@ -100,55 +103,24 @@ def is_python(view):
100103
101104
102105def save_file (view ):
103- save_on_toggle = settings .get ('save_on_toggle' , True )
106+ save_on_toggle = settings .get ('save_on_toggle' , False )
104107 if save_on_toggle and view .is_dirty () and view .file_name ():
105108 view .run_command ('save' )
106109
107110
108- def ln_from_region (view , rg ): # line number from region
111+ def get_line_number (view , rg ):
112+ """
113+ line number from region
114+ """
109115 return view .rowcol (rg .end ())[0 ]
110116
111117
112- def goto_position (view , pos ): # move cursor to position
113- view .sel ().clear ()
114- view .sel ().add (pos )
115-
116-
117- def calc_pdb_position (view ):
118+ def goto_position (view , pos ):
118119 """
119- find and return injection spot for the pdb_block; None on failure
120+ move cursor to position
120121 """
121- size = view .size ()
122- text = view .substr (sublime .Region (0 , size ))
123- lines = view .lines (sublime .Region (0 , size ))
124-
125- # make a few tries to compile the AST
126- # if code contains errors strip at line before the error and retry
127- for i in range (5 ):
128- try :
129- # parse through import statements to find a sweet spot for the
130- # pdb_block injection, outside of any complex/multiline import
131- # constructs, preferrably after the last import statement
132- fst = imp = nxt = None
133- for node in ast .iter_child_nodes (ast .parse (text )):
134- tx = view .substr (lines [node .lineno - 1 ])
135- if type (node ) in (ast .Import , ast .ImportFrom ):
136- if not fst :
137- fst = node .lineno
138- imp = node .lineno
139- elif not fst :
140- fst = node .lineno
141- elif imp and not (tx .endswith ('"""' ) or tx .endswith ("'''" )):
142- nxt = node .lineno
143- break
144- ln = nxt if nxt else imp if imp else fst
145- return view .text_point (ln - 1 if ln > 1 else ln , 0 )
146- except (IndentationError , SyntaxError ) as e :
147- debug ('err in line %d %r' % (
148- e .lineno , view .substr (lines [e .lineno - 1 ])))
149- size = lines [e .lineno - 2 ].begin ()
150- text = view .substr (sublime .Region (0 , size ))
151- lines = view .lines (sublime .Region (0 , size ))
122+ view .sel ().clear ()
123+ view .sel ().add (pos )
152124
153125
154126def calc_indent (view , rg ):
@@ -235,25 +207,12 @@ def _result(msg, indent):
235207 return _result ('he1-2' , next_indent )
236208
237209
238- def find_pdb_block (view ):
239- """return position of the pdb_block, or None"""
240- rg = view .find (pdb_block .strip (), 0 , sublime .LITERAL )
241- if rg :
242- return rg .begin ()
243-
244-
245210def find_breakpoint (view ):
246- """return position of the 1st breakpoint, or None"""
211+ """
212+ return position of the 1st breakpoint, or None
213+ """
247214 rg = view .find (bp_regex , 0 )
248- if rg :
249- return rg .end ()
250-
251-
252- def remove_pdb_block (edit , view ):
253- pos = find_pdb_block (view )
254- rg = sublime .Region (pos , pos + len (pdb_block ))
255- assert pdb_block in view .substr (rg ), rg
256- view .erase (edit , rg )
215+ if rg : return rg .end ()
257216
258217
259218def remove_breakpoint (edit , view , rg ):
@@ -262,33 +221,23 @@ def remove_breakpoint(edit, view, rg):
262221 """
263222 rg = view .full_line (rg )
264223 lines = view .lines (sublime .Region (0 , rg .end ()))
265- ln = min (ln_from_region (view , rg ), len (lines ) - 1 )
224+ ln = min (get_line_number (view , rg ), len (lines ) - 1 )
266225
267226 for line in (lines [ln ], lines [ln - 1 ]): # search current and prev lines
268227 bp = Breakpoint (view .substr (line ))
269228 if bp .uid :
270229 view .erase (edit , view .full_line (line ))
271- view .erase_regions (bp .rg_key )
230+ view .erase_regions (bp .region_id )
272231 return True
273232 return False
274233
275234
276- def insert_pdb_block (edit , view ):
277- """
278- inject the pdb_block construct, return its position
279- """
280- pos = calc_pdb_position (view )
281- if pos is not None :
282- view .insert (edit , pos , pdb_block )
283- return pos
284-
285-
286235def insert_breakpoint (edit , view , rg ):
287236 bp = Breakpoint ()
288237 rg_a = rg .begin ()
289238 indent = calc_indent (view , rg )
290239 if indent is not None :
291- bp_rg_sz = view .insert (edit , rg_a , bp .format (indent ))
240+ bp_rg_sz = view .insert (edit , rg_a , bp .as_string (indent ))
292241 color_rg = sublime .Region (rg_a , rg_a + bp_rg_sz )
293242 bp .highlight (view , color_rg )
294243 goto_position (view , rg_a + indent )
@@ -307,24 +256,10 @@ def run(self, edit):
307256 if not (is_python (view ) and view .sel ()[0 ].empty ()):
308257 return
309258
310- # check/insert the pdb_block
311- pdb_pos = find_pdb_block (view )
312- if pdb_pos is None :
313- pdb_pos = insert_pdb_block (edit , view )
314-
315259 # remove/insert the breakpoint
316260 rg = view .line (view .sel ()[0 ])
317- if remove_breakpoint (edit , view , rg ):
318- # if no more breakpoints remove pdb_block
319- if not find_breakpoint (view ):
320- remove_pdb_block (edit , view )
321- else :
322- # inserting a new breakpoint below pdb_block
323- if pdb_pos and rg .begin () >= pdb_pos + len (pdb_block ):
324- insert_breakpoint (edit , view , rg )
325- # if insertion didn't happen undo the pdb_block
326- elif not find_breakpoint (view ) and find_pdb_block (view ):
327- remove_pdb_block (edit , view )
261+ if not remove_breakpoint (edit , view , rg ):
262+ insert_breakpoint (edit , view , rg )
328263 save_file (view )
329264
330265
@@ -341,7 +276,7 @@ def run(self, edit):
341276
342277 for i , rg in enumerate (bp_regions ):
343278 rg = view .full_line (rg )
344- ln = ln_from_region (view , rg )
279+ ln = get_line_number (view , rg )
345280
346281 # grab 2 next non-empty code lines
347282 for j , l in enumerate (lines [ln - 1 :]):
@@ -350,7 +285,10 @@ def run(self, edit):
350285 continue
351286 if not j : # strip the 1st line
352287 s = s .strip ()
353- lnn = ln_from_region (view , l ) + 1
288+ lnn = get_line_number (view , l ) + 1
289+
290+ if bp_re .match (s ):
291+ s = s [s .find ('# breakpoint' ) + 2 :]
354292
355293 items [i ].append ('%d: %s' % (lnn , s ))
356294 if len (items [i ]) > 2 :
@@ -374,10 +312,6 @@ def run(self, edit):
374312 rg = find_breakpoint (view )
375313 if not (rg and remove_breakpoint (edit , view , rg )):
376314 break
377-
378- if find_pdb_block (view ):
379- remove_pdb_block (edit , view )
380-
381315 save_file (view )
382316
383317
@@ -391,7 +325,7 @@ def on_load(self, view):
391325 """
392326 on file load, scan it for breakpoints and highlight them
393327 """
394- if is_python (view ) and find_pdb_block ( view ) :
328+ if is_python (view ):
395329 for rg in view .find_all (bp_regex , 0 ):
396330 bp = Breakpoint (view .substr (rg ))
397331 bp .highlight (view , rg )
0 commit comments