Skip to content

Commit 584228f

Browse files
committed
refactor and simplify; no longer insert the import block;
fix #8: support gutter icons; (!) breaks compatiblity - refer to messages/0.3.0.txt for detail
1 parent 891865e commit 584228f

File tree

6 files changed

+97
-123
lines changed

6 files changed

+97
-123
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.pyc
2+
package-metadata.json
3+

PythonBreakpoints.py

Lines changed: 54 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,60 @@
11
# -*- coding: utf-8 -*-
22
"""
3-
Python Breakpoints plugin for Sublime Text editor
3+
Python Breakpoints plugin for Sublime Text 2/3
44
55
Author: Oscar Ibatullin (github.com/obormot)
66
77
"""
88
from __future__ import print_function
9-
import ast
109
import re
10+
import sys
1111
import uuid
1212

1313
import sublime
1414
import 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
2425
settings = None
25-
pdb_block = ''
2626
tab_size = 4
2727

2828

2929
def 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}) //"
5650
bp_re = re.compile(bp_regex, re.DOTALL)
5751

5852
EXPR_PRE = ['class', 'def', 'if', 'for', 'try', 'while', 'with']
5953
EXPR_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

6660
class 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

102105
def 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

154126
def 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-
245210
def 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

259218
def 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-
286235
def 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)

PythonBreakpoints.sublime-settings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
// name of scope for color highlighting; replace with "mark" if "invalid" is annoying
99
"highlight": "invalid",
1010

11+
// icon name for the gutter, one of: "" (disabled), "dot", "circle", "bookmark" or "cross"
12+
"gutter_icon": "circle",
13+
1114
// auto-save the file on breakpoint toggle
1215
"save_on_toggle": false
1316
}

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ This is a [Sublime Text](http://www.sublimetext.com) plugin allowing to quickly
55

66
## Features
77

8-
* breakpoint color highlighting
8+
* breakpoint color highlighting, gutter icons
99
* auto indentation, auto save on toggle (off by default, configurable)
1010
* your source file stores all breakpoints; plugin detects and recreates them on next load
11+
* support for user comments to help navigate among many breakpoints
1112

1213
## Install
1314

14-
Through [Package Control](https://sublime.wbond.net/packages/Package%20Control):
15+
Through [Package Control](https://sublime.wbond.net/packages/Package%20Control) (recommended):
1516

1617
`Command Palette` > `Package Control: Install Package` > `Python Breakpoints`
1718

@@ -35,4 +36,4 @@ From GitHub: Clone this repository into your version/platform specific Packages
3536
## Caveats
3637

3738
* only space indentation is supported
38-
* your mileage may vary with non-PEP8 one-liners or code without import statements
39+
* in some code fragments the plugin may incorrectly indent the breakpoint; in such cases just use Indent/Unindent keyboard shortcuts to move it into desired position

messages.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"0.3.0": "messages/0.3.0.txt",
3+
}

messages/0.3.0.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Python Breakpoints 0.3.0 Changelog:
2+
3+
Please be sure to restart Sublime Text to start using the new version.
4+
5+
6+
New features:
7+
8+
- The plugin has been simplified to address some cases when it wouldn't work
9+
with Python 2 constructs failing under ST3 syntax tree interpreter.
10+
11+
- Insertion of the import statement has been removed. All breakpoints are now
12+
one-liners in the following format:
13+
14+
import {debugger}; {debugger}.set_trace() # breakpoint {uid} // user-comment
15+
16+
- You can now add your text to the breakpoint (note the "user-comment" above).
17+
It'll be displayed nicely in the "Goto Breakpoint" menu and help navigation.
18+
Make sure to keep all your text in the same line.
19+
20+
- Added a customizable gutter icon (feature request by @j9ac9k), on by default.
21+
22+
23+
Compatibility warnings:
24+
25+
- Breakpoints created and saved by the earlier versions of the plugin will not
26+
be recognized, you'll have to re-insert them and manually delete the import
27+
statement. Sorry..
28+
29+
- One-liner format of the breakpoint isn't PEP8 compliant (compound statement),
30+
so your linter may produce warnings.

0 commit comments

Comments
 (0)