-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathBracketGuard.py
129 lines (79 loc) · 3.73 KB
/
BracketGuard.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
import sublime, sublime_plugin
import re, time
from collections import namedtuple
BracketPosition = namedtuple("BracketPosition", "position opener")
BracketResult = namedtuple("BracketResult", "success start end")
bracketGuardRegions = "BracketGuardRegions"
class EventListener(sublime_plugin.EventListener):
def __init__(self):
self.latest_keypresses = {}
def on_modified(self, view):
if view.settings().get("is_test", False):
self.highlightBracketError(view)
def on_modified_async(self, view):
self.clearRegions(view)
if self.doAutoCheck(view):
self.debounce(view, "modified", self.highlightBracketError)
def on_post_save_async(self, view):
if self.checkOnSave() and not self.doAutoCheck(view):
self.highlightBracketError(view)
def clearRegions(self, view):
view.erase_regions(bracketGuardRegions)
def settings(self):
return sublime.load_settings("BracketGuard.sublime-settings")
def checkOnSave(self):
return self.settings().get("check_on_save")
def doAutoCheck(self, view):
threshold = self.settings().get("file_length_threshold")
return threshold == -1 or threshold >= view.size()
def highlightBracketError(self, view):
bracketResult = self.getFirstBracketError(view)
if not bracketResult.success:
openerRegion = sublime.Region(bracketResult.start, bracketResult.start + 1)
closerRegion = sublime.Region(bracketResult.end, bracketResult.end + 1)
view.add_regions(bracketGuardRegions, [openerRegion, closerRegion], "invalid")
def debounce(self, view, event_type, func):
key = (event_type, view.file_name())
this_keypress = time.time()
self.latest_keypresses[key] = this_keypress
debounceTime = self.settings().get("debounce_time", 500)
def callback():
latest_keypress = self.latest_keypresses.get(key, None)
if this_keypress == latest_keypress:
func(view)
sublime.set_timeout_async(callback, debounceTime)
def getFirstBracketError(self, view):
opener = list("({[")
closer = list(")}]")
matchingStack = []
successResult = BracketResult(True, -1, -1)
codeStr = view.substr(sublime.Region(0, view.size()))
for index, char in enumerate(codeStr):
# this leaks memory ?
# if len(codeStr) != view.size():
# return successResult
if char not in opener and not char in closer:
continue
scopeName = view.scope_name(index)
hasScope = lambda s: s in scopeName
# workaround for the following code in markdown: ![example](img/example.png)
markdownBracketScopeBegin = "punctuation.definition.string.begin.markdown"
markdownBracketScopeEnd = "punctuation.definition.string.end.markdown"
isMarkdownStringBeginOrEnd = lambda s: hasScope(markdownBracketScopeBegin) or hasScope(markdownBracketScopeEnd)
if hasScope("string") and not hasScope("unquoted") and not isMarkdownStringBeginOrEnd(markdownBracketScopeBegin) or hasScope("comment"):
# ignore unmatched brackets in strings and comments
continue
if char in opener:
matchingStack.append(BracketPosition(index, char))
elif char in closer:
matchingOpener = opener[closer.index(char)]
if len(matchingStack) == 0:
return BracketResult(False, -1, index)
poppedOpener = matchingStack.pop()
if matchingOpener != poppedOpener.opener:
return BracketResult(False, poppedOpener.position, index)
if len(matchingStack) == 0:
return successResult
else:
poppedOpener = matchingStack.pop()
return BracketResult(False, poppedOpener.position, -1)