1+ import re
2+
3+ import sublime
4+ import sublime_plugin
5+
6+ class PairMatlabStatementsCommand (sublime_plugin .TextCommand ):
7+
8+ """Find opening statement that is paired with the current 'end'
9+ """
10+
11+ general_keywords = 'if|for|while|switch|try|function|classdef'
12+ class_keywords = 'properties|methods|events|enumeration'
13+
14+ def iskeyword (self , point ):
15+ """Does the code contain a Matlab keyword at the point?
16+ """
17+ all_scopes = self .view .scope_name (point )
18+ return any ([scope .startswith ('keyword' )
19+ for scope in all_scopes .split ()])
20+
21+ def run (self , edit , action = 'popup' ):
22+ """Find opening statement that is paired with the current 'end'
23+ """
24+ # determine valid keywords
25+ keywords = self .general_keywords
26+ all_code = self .view .substr (sublime .Region (0 , self .view .size ()))
27+ if all_code .lstrip ().startswith ('classdef' ):
28+ keywords += '|' + self .class_keywords
29+
30+ # get current selection
31+ sel = self .view .sel ()
32+ if not len (sel ):
33+ return
34+ reg_key = self .view .word (sel [0 ])
35+ text_key = self .view .substr (reg_key ).strip ()
36+
37+ # check if the current selection equals a valid keyword
38+ if not self .iskeyword (reg_key .begin ()) \
39+ or not text_key in keywords + '|end' :
40+ if not (action == 'jump' or action == 'select' ):
41+ msg = '[WARNING] AutoMatlab - Cursor not in open/end keyword.'
42+ self .view .window ().status_message (msg )
43+ return
44+
45+ if text_key == 'end' :
46+ reg_paired = self .pair_with_open_statement (reg_key , keywords )
47+ # the below disregards edge-cases with partial one-line statements
48+ # or with , or ; in strings or comments
49+ if self .view .line (reg_key ) == self .view .line (reg_paired ):
50+ sel_lines = [reg_key .begin (), reg_paired .end ()]
51+ else :
52+ sel_lines = [self .view .full_line (reg_key .begin ()).begin (),
53+ self .view .full_line (reg_paired .end ()).end ()]
54+ else :
55+ reg_paired = self .pair_with_end_statement (reg_key , keywords )
56+ if self .view .line (reg_key ) == self .view .line (reg_paired ):
57+ sel_lines = [reg_key .end (), reg_paired .begin ()]
58+ else :
59+ sel_lines = [self .view .full_line (reg_key .end ()).end (),
60+ self .view .full_line (reg_paired .begin ()).begin ()]
61+ if reg_paired == None :
62+ if not (action == 'jump' or action == 'select' ):
63+ msg = '[WARNING] AutoMatlab - Cannot pair statement: ' \
64+ 'invalid syntax.'
65+ self .view .window ().status_message (msg )
66+ return
67+
68+ if action == 'jump' :
69+ self .view .show (reg_paired )
70+ self .view .sel ().clear ()
71+ self .view .sel ().add (sublime .Region (reg_paired .end ()))
72+ elif action == 'select' :
73+ self .view .sel ().clear ()
74+ self .view .sel ().add (sublime .Region (min (sel_lines ), max (sel_lines )))
75+ else :
76+ # read the text surrounding the paired statement
77+ reg_line = self .view .full_line (reg_paired )
78+ reg_surround = self .view .lines (
79+ sublime .Region (reg_line .begin () - 1 , reg_line .end () + 1 ))
80+ text_lines = [self .view .substr (reg ).rstrip ()
81+ for reg in reg_surround ]
82+ nr_lines = [self .view .rowcol (reg .a )[0 ] + 1 for reg in reg_surround ]
83+
84+ # make paired statement bold
85+ nr_paired = self .view .rowcol (reg_line .a )[0 ] + 1
86+ borders_paired = [reg_paired .begin () - reg_line .begin (),
87+ reg_paired .end () - reg_line .begin ()]
88+ text_paired = text_lines [nr_lines .index (nr_paired )]
89+ text_lines [nr_lines .index (nr_paired )] = \
90+ text_paired [:borders_paired [0 ]] + '<i><b>' \
91+ + text_paired [borders_paired [0 ]:borders_paired [1 ]] \
92+ + '</b></i>' + text_paired [borders_paired [1 ]:]
93+
94+ # remove the minimum indentation
95+ lstrip_lines = [len (text ) - len (text .lstrip ())
96+ for text in text_lines ]
97+ text_lines = \
98+ [' ' * (lstrip_lines [ii ]- min (lstrip_lines ))
99+ + text_lines [ii ].lstrip ()
100+ for ii in range (len (text_lines ))]
101+
102+ # add line numbers and concatenate text lines
103+ text = ''
104+ for ii in range (len (nr_lines )):
105+ text += \
106+ ' ' * (len (str (max (nr_lines )))- len (str (nr_lines [ii ]))) \
107+ + '{}: {}<br>' .format (nr_lines [ii ], text_lines [ii ])
108+
109+ # add hyperlink link
110+ text += '<a href="{}">goto</a> <a href="{}">select</a>' .format (
111+ reg_paired , sel_lines )
112+ self .view .show_popup (text ,
113+ max_width = 80 * self .view .em_width (),
114+ max_height = 5 * self .view .line_height (),
115+ on_navigate = self .select )
116+
117+
118+ def pair_with_open_statement (self , reg , keywords ):
119+ """Find the open statement paired with the end statement
120+ in region reg.
121+ """
122+ # read all code until current end statement
123+ code = self .view .substr (sublime .Region (0 , reg .end ()))
124+
125+ # look for end statements
126+ pattern = r'(?:\W|^)(end)(?:\W|$)'
127+ mo_end = re .finditer (pattern , code , re .M )
128+
129+ # look for opening statements to pair
130+ pattern = r'(?:\W|^)(' + keywords + r')(?:\W|$)'
131+ mo_open = re .finditer (pattern , code , re .M )
132+
133+ # strip non-keywords
134+ end_statements = sorted ([mo .start (1 )
135+ for mo in mo_end
136+ if self .iskeyword (mo .start (1 ))], reverse = True )
137+ open_statements = sorted ([mo .start (1 )
138+ for mo in mo_open
139+ if self .iskeyword (mo .start (1 ))], reverse = True )
140+
141+ # check validity of open/end combinations
142+ no = len (open_statements )
143+ ne = len (end_statements )
144+ if ne > no :
145+ return None
146+
147+ for ii in range (no ):
148+ # check how many end statements come after each open statement,
149+ # starting from the last open statement
150+ end_after = sum ([open_statements [ii ] < estat
151+ for estat in end_statements ])
152+ # the paired open statement is found when end_after matches
153+ # the current counter
154+ if end_after == ii + 1 :
155+ reg_paired = self .view .word (open_statements [ii ])
156+ break
157+ return reg_paired
158+
159+ def pair_with_end_statement (self , reg , keywords ):
160+ """Find the end statement paired with the open statement
161+ in region reg.
162+ """
163+ # read all code from current open statement
164+ code = self .view .substr (sublime .Region (reg .begin (), self .view .size ()))
165+
166+ # look for end statements
167+ pattern = r'(?:\W|^)(end)(?:\W|$)'
168+ mo_end = re .finditer (pattern , code , re .M )
169+
170+ # look for opening statements to pair
171+ pattern = r'(?:\W|^)(' + keywords + r')(?:\W|$)'
172+ mo_open = re .finditer (pattern , code , re .M )
173+
174+ # strip non-keywords
175+ end_statements = sorted ([reg .begin () + mo .start (1 )
176+ for mo in mo_end
177+ if self .iskeyword (reg .begin () + mo .start (1 ))])
178+ open_statements = sorted ([reg .begin () + mo .start (1 )
179+ for mo in mo_open
180+ if self .iskeyword (reg .begin () + mo .start (1 ))])
181+
182+ # check validity of open/end combinations
183+ no = len (open_statements )
184+ ne = len (end_statements )
185+ if no > ne :
186+ print (code , no , ne )
187+ return None
188+
189+ for ii in range (ne ):
190+ # check how many open statements come before each end statement,
191+ # starting from the first end statement
192+ open_before = sum ([end_statements [ii ] > ostat
193+ for ostat in open_statements ])
194+ # the paired end statement is found when open_before matches
195+ # the current counter
196+ if open_before == ii + 1 :
197+ reg_paired = self .view .word (end_statements [ii ])
198+ break
199+ return reg_paired
200+
201+ def select (self , reg ):
202+ """Select the lines between the paired open/end statements
203+ """
204+ reg = eval (reg )
205+
206+ ###
207+ # these lines are necessary to force the view to update (Sublime bug?)
208+ self .view .sel ().clear ()
209+ self .view .sel ().add (sublime .Region (reg [0 ]))
210+ self .view .run_command ('insert' , {'characters' :' ' })
211+ self .view .run_command ('left_delete' )
212+ ###
213+
214+ self .view .show (reg [1 ])
215+ self .view .sel ().clear ()
216+ self .view .sel ().add (sublime .Region (reg [0 ], reg [1 ]))
0 commit comments