| 1 | # peppy Copyright (c) 2006-2008 Rob McMullen |
|---|
| 2 | # Licenced under the GPLv2; see http://peppy.flipturn.org for more info |
|---|
| 3 | """Some simple text transformation actions. |
|---|
| 4 | |
|---|
| 5 | This plugin is a collection of some simple text transformation actions that |
|---|
| 6 | should be applicable to more than one major mode. |
|---|
| 7 | """ |
|---|
| 8 | |
|---|
| 9 | import os, glob, re |
|---|
| 10 | |
|---|
| 11 | import wx |
|---|
| 12 | import wx.lib.stattext |
|---|
| 13 | from wx.lib.pubsub import Publisher |
|---|
| 14 | |
|---|
| 15 | from peppy.yapsy.plugins import * |
|---|
| 16 | from peppy.actions.minibuffer import * |
|---|
| 17 | |
|---|
| 18 | from peppy.about import AddCredit |
|---|
| 19 | from peppy.fundamental import FundamentalMode |
|---|
| 20 | from peppy.actions import * |
|---|
| 21 | from peppy.actions.base import * |
|---|
| 22 | from peppy.debug import * |
|---|
| 23 | |
|---|
| 24 | AddCredit("Jan Hudec", "for the shell-style wildcard to regex converter from bzrlib") |
|---|
| 25 | |
|---|
| 26 | |
|---|
| 27 | class ReplacementError(Exception): |
|---|
| 28 | pass |
|---|
| 29 | |
|---|
| 30 | |
|---|
| 31 | class FindSettings(debugmixin): |
|---|
| 32 | def __init__(self, match_case=False, smart_case=True): |
|---|
| 33 | self.match_case = match_case |
|---|
| 34 | self.smart_case = smart_case |
|---|
| 35 | self.find = '' |
|---|
| 36 | self.find_user = '' |
|---|
| 37 | self.replace = '' |
|---|
| 38 | self.replace_user = '' |
|---|
| 39 | |
|---|
| 40 | def serialize(self, storage): |
|---|
| 41 | storage['last_search'] = self.find_user |
|---|
| 42 | storage['last_replace'] = self.replace_user |
|---|
| 43 | |
|---|
| 44 | def unserialize(self, storage): |
|---|
| 45 | if 'last_search' in storage: |
|---|
| 46 | self.find_user = storage['last_search'] |
|---|
| 47 | else: |
|---|
| 48 | self.find_user = '' |
|---|
| 49 | if 'last_replace' in storage: |
|---|
| 50 | self.replace_user = storage['last_replace'] |
|---|
| 51 | else: |
|---|
| 52 | self.replace_user = '' |
|---|
| 53 | |
|---|
| 54 | |
|---|
| 55 | class FindService(debugmixin): |
|---|
| 56 | forward = "Find" |
|---|
| 57 | backward = "Find backward" |
|---|
| 58 | |
|---|
| 59 | help = """The default find and replace uses literal strings. No wildcard |
|---|
| 60 | or regular expressions are used to match text.""" |
|---|
| 61 | |
|---|
| 62 | def __init__(self, stc, settings=None): |
|---|
| 63 | self.flags = 0 |
|---|
| 64 | self.stc = stc |
|---|
| 65 | if settings is None: |
|---|
| 66 | self.settings = FindSettings() |
|---|
| 67 | else: |
|---|
| 68 | self.settings = settings |
|---|
| 69 | |
|---|
| 70 | if self.settings.find_user: |
|---|
| 71 | self.setFindString(self.settings.find_user) |
|---|
| 72 | if self.settings.replace: |
|---|
| 73 | self.setReplaceString(self.settings.replace_user) |
|---|
| 74 | |
|---|
| 75 | def serialize(self, storage): |
|---|
| 76 | self.settings.serialize(storage) |
|---|
| 77 | |
|---|
| 78 | def unserialize(self, storage): |
|---|
| 79 | self.settings.unserialize(storage) |
|---|
| 80 | self.setFindString(self.settings.find_user) |
|---|
| 81 | self.setReplaceString(self.settings.replace_user) |
|---|
| 82 | |
|---|
| 83 | def allowBackward(self): |
|---|
| 84 | return True |
|---|
| 85 | |
|---|
| 86 | def setFlags(self): |
|---|
| 87 | text = self.settings.find |
|---|
| 88 | |
|---|
| 89 | if text != text.lower(): |
|---|
| 90 | match_case = True |
|---|
| 91 | else: |
|---|
| 92 | match_case = self.settings.match_case |
|---|
| 93 | |
|---|
| 94 | if match_case: |
|---|
| 95 | self.flags = wx.stc.STC_FIND_MATCHCASE |
|---|
| 96 | else: |
|---|
| 97 | self.flags = 0 |
|---|
| 98 | |
|---|
| 99 | def getFlags(self, user_flags=0): |
|---|
| 100 | if self.settings.match_case != self.stc.locals.case_sensitive_search: |
|---|
| 101 | self.settings.match_case = self.stc.locals.case_sensitive_search |
|---|
| 102 | self.setFlags() |
|---|
| 103 | |
|---|
| 104 | return self.flags | user_flags |
|---|
| 105 | |
|---|
| 106 | def expandSearchText(self, findTxt): |
|---|
| 107 | """Convert the search string that the user enters into the string |
|---|
| 108 | actually searched for by the service. |
|---|
| 109 | |
|---|
| 110 | This is designed to be overridden in subclasses, and has most use for |
|---|
| 111 | regular expression searches where the string the user types has to be |
|---|
| 112 | compiled into a form that the regular expression engine uses to search. |
|---|
| 113 | """ |
|---|
| 114 | if not findTxt: |
|---|
| 115 | findTxt = "" |
|---|
| 116 | |
|---|
| 117 | return findTxt |
|---|
| 118 | |
|---|
| 119 | def expandReplaceText(self, text): |
|---|
| 120 | """Convert the replacement string that the user enters into the string |
|---|
| 121 | actually searched for by the service. |
|---|
| 122 | |
|---|
| 123 | Like L{expandSearchText}, this is primarily designed to be overridden |
|---|
| 124 | in subclasses. |
|---|
| 125 | """ |
|---|
| 126 | return self.expandSearchText(text) |
|---|
| 127 | |
|---|
| 128 | def setFindString(self, text): |
|---|
| 129 | self.settings.find_user = text |
|---|
| 130 | self.settings.find = self.expandSearchText(text) |
|---|
| 131 | self.setFlags() |
|---|
| 132 | |
|---|
| 133 | def setReplaceString(self, text): |
|---|
| 134 | self.settings.replace_user = text |
|---|
| 135 | self.settings.replace = self.expandReplaceText(text) |
|---|
| 136 | |
|---|
| 137 | def getReplacement(self, replacing): |
|---|
| 138 | """Return the string that will be substituted in the text |
|---|
| 139 | |
|---|
| 140 | @param replacing: the original string from the document |
|---|
| 141 | @return: the string after the substitutions have been made |
|---|
| 142 | """ |
|---|
| 143 | findTxt = self.settings.find |
|---|
| 144 | replaceTxt = self.settings.replace |
|---|
| 145 | |
|---|
| 146 | # in order to use smart casing, both the find and replace strings |
|---|
| 147 | # must be lower case and the target string needs to have at least one |
|---|
| 148 | # alphabetic character. If the target converted to upper case is the |
|---|
| 149 | # same as the target converted to lower case, we know that it doesn't |
|---|
| 150 | # have any alphabetic chars in it so we punt and use the replace |
|---|
| 151 | # string as is. |
|---|
| 152 | if self.settings.smart_case and findTxt.lower() == findTxt and replaceTxt.lower() == replaceTxt and replacing.upper() != replacing.lower(): |
|---|
| 153 | if replacing.upper() == replacing: |
|---|
| 154 | ## print "all upper", replacing |
|---|
| 155 | replaceTxt = replaceTxt.upper() |
|---|
| 156 | elif replacing.lower() == replacing: |
|---|
| 157 | ## print "all lower", replacing |
|---|
| 158 | replaceTxt = replaceTxt.lower() |
|---|
| 159 | elif len(replacing) == len(replaceTxt): |
|---|
| 160 | ## print "smartcasing", replacing |
|---|
| 161 | r = [] |
|---|
| 162 | for i,j in zip(replacing, replaceTxt): |
|---|
| 163 | if i.isupper(): |
|---|
| 164 | r.append(j.upper()) |
|---|
| 165 | elif i.islower(): |
|---|
| 166 | r.append(j.lower()) |
|---|
| 167 | else: |
|---|
| 168 | r.append(j) |
|---|
| 169 | replaceTxt = ''.join(r) |
|---|
| 170 | elif replacing and replaceTxt and replacing[:1].upper() == replacing[:1]: |
|---|
| 171 | ## print "first upper", replacing |
|---|
| 172 | replaceTxt = replaceTxt[:1].upper() + replaceTxt[1:] |
|---|
| 173 | elif replacing and replaceTxt and replacing[:1].lower() == replacing[:1]: |
|---|
| 174 | ## print "first lower", replacing |
|---|
| 175 | replaceTxt = replaceTxt[:1].lower() + replaceTxt[1:] |
|---|
| 176 | |
|---|
| 177 | return replaceTxt |
|---|
| 178 | |
|---|
| 179 | def getRange(self, start, chars, dire=1): |
|---|
| 180 | end = start |
|---|
| 181 | if dire==1: |
|---|
| 182 | fcn = self.stc.PositionAfter |
|---|
| 183 | else: |
|---|
| 184 | fcn = self.stc.PositionBefore |
|---|
| 185 | for i in xrange(chars): |
|---|
| 186 | z = fcn(end) |
|---|
| 187 | y = self.stc.GetCharAt(end) |
|---|
| 188 | x = abs(z-end)==2 and (((dire==1) and (y == 13)) or ((dire==0) and (y == 10))) |
|---|
| 189 | ## print y, z-end |
|---|
| 190 | end = z - x |
|---|
| 191 | return start, end |
|---|
| 192 | |
|---|
| 193 | def highlightSelection(self, pos, count): |
|---|
| 194 | sel_start, sel_end = self.getRange(pos, count) |
|---|
| 195 | #dprint("selection = %d - %d" % (sel_start, sel_end)) |
|---|
| 196 | self.stc.SetSelection(sel_start, sel_end) |
|---|
| 197 | |
|---|
| 198 | def findMatchLength(self, pos): |
|---|
| 199 | """Scintilla doesn't return the end of the match, so we have to compute |
|---|
| 200 | it ourselves. |
|---|
| 201 | |
|---|
| 202 | This is designed to be overridden in subclasses, particularly by |
|---|
| 203 | subclasses that use regular expressions to match. |
|---|
| 204 | |
|---|
| 205 | This can also be used as a verification method to reject a match. |
|---|
| 206 | Should a subclass need this capability, return a -1 here and the |
|---|
| 207 | L{doFindNext} method will move to the next possible match and try |
|---|
| 208 | again. This will repeat until a match is found or the end of the |
|---|
| 209 | document is reached. |
|---|
| 210 | |
|---|
| 211 | @return: number of characters matched by the search string, or -1 to |
|---|
| 212 | reject the match. |
|---|
| 213 | """ |
|---|
| 214 | count = len(self.settings.find) |
|---|
| 215 | return count |
|---|
| 216 | |
|---|
| 217 | def doFindNext(self, start=-1, incremental=False): |
|---|
| 218 | """Find and highlight the next match in the document. |
|---|
| 219 | |
|---|
| 220 | @param start: starting position in text, or -1 to use current position |
|---|
| 221 | |
|---|
| 222 | @param incremental: True if an incremental search that will be added to |
|---|
| 223 | the current search result |
|---|
| 224 | |
|---|
| 225 | @return: tuple containing the position of the match or -1 if no match |
|---|
| 226 | is found, and the position of the original start of the search. If |
|---|
| 227 | the search string is invalid, a tuple of (None, None) is returned. |
|---|
| 228 | """ |
|---|
| 229 | if not self.settings.find: |
|---|
| 230 | return None, None |
|---|
| 231 | flags = self.getFlags(wx.FR_DOWN) |
|---|
| 232 | |
|---|
| 233 | #handle finding next item, handling wrap-arounds as necessary |
|---|
| 234 | if start < 0: |
|---|
| 235 | sel = self.stc.GetSelection() |
|---|
| 236 | if incremental: |
|---|
| 237 | start = min(sel) |
|---|
| 238 | else: |
|---|
| 239 | start = max(sel) |
|---|
| 240 | |
|---|
| 241 | while True: |
|---|
| 242 | pos = self.stc.FindText(start, self.stc.GetTextLength(), self.settings.find, flags) |
|---|
| 243 | #dprint("start=%d find=%s flags=%d pos=%d" % (start, self.settings.find, flags, pos)) |
|---|
| 244 | |
|---|
| 245 | if pos < 0: |
|---|
| 246 | # no match; done. |
|---|
| 247 | break |
|---|
| 248 | |
|---|
| 249 | count = self.findMatchLength(pos) |
|---|
| 250 | if count >= 0: |
|---|
| 251 | self.highlightSelection(pos, count) |
|---|
| 252 | break |
|---|
| 253 | |
|---|
| 254 | start = pos + 1 |
|---|
| 255 | |
|---|
| 256 | return pos, start |
|---|
| 257 | |
|---|
| 258 | def doFindPrev(self, start=-1, incremental=False): |
|---|
| 259 | """Find and highlight the previous match in the document. |
|---|
| 260 | |
|---|
| 261 | @param start: starting position in text, or -1 to use current position |
|---|
| 262 | |
|---|
| 263 | @param incremental: True if an incremental search that will be added to |
|---|
| 264 | the current search result |
|---|
| 265 | |
|---|
| 266 | @return: tuple containing the position of the match or -1 if no match |
|---|
| 267 | is found, and the position of the original start of the search. If |
|---|
| 268 | the search string is invalid, a tuple of (None, None) is returned. |
|---|
| 269 | """ |
|---|
| 270 | if not self.settings.find: |
|---|
| 271 | return None, None |
|---|
| 272 | flags = self.getFlags() |
|---|
| 273 | |
|---|
| 274 | if start < 0: |
|---|
| 275 | sel = self.stc.GetSelection() |
|---|
| 276 | if incremental: |
|---|
| 277 | start = min(sel) + len(self.settings.find) |
|---|
| 278 | #dprint("sel=%s min=%d len=%s" % (str(sel), min(sel), len(self.settings.find))) |
|---|
| 279 | else: |
|---|
| 280 | start = min(sel) |
|---|
| 281 | pos = self.stc.FindText(start, 0, self.settings.find, flags) |
|---|
| 282 | |
|---|
| 283 | if pos >= 0: |
|---|
| 284 | count = self.findMatchLength(pos) |
|---|
| 285 | self.highlightSelection(pos, count) |
|---|
| 286 | |
|---|
| 287 | return pos, start |
|---|
| 288 | |
|---|
| 289 | def doReplace(self): |
|---|
| 290 | """Replace the selection |
|---|
| 291 | |
|---|
| 292 | Replace the current selection in the stc with the replacement text |
|---|
| 293 | """ |
|---|
| 294 | sel = self.stc.GetSelection() |
|---|
| 295 | replacing = self.stc.GetTextRange(sel[0], sel[1]) |
|---|
| 296 | |
|---|
| 297 | replacement = self.getReplacement(replacing) |
|---|
| 298 | |
|---|
| 299 | self.stc.ReplaceSelection(replacement) |
|---|
| 300 | self.stc.SetSelection(min(sel), min(sel) + len(replacement.encode('utf-8'))) |
|---|
| 301 | |
|---|
| 302 | |
|---|
| 303 | class FindBasicRegexService(FindService): |
|---|
| 304 | forward = "Find Scintilla regex" |
|---|
| 305 | |
|---|
| 306 | help = r""" |
|---|
| 307 | Basic regular expressions are a limited form supported by the scintilla |
|---|
| 308 | editor, not the full python regular expressions. Scintilla regular |
|---|
| 309 | expressions are limited to: |
|---|
| 310 | |
|---|
| 311 | . Matches any character |
|---|
| 312 | \( This marks the start of a region for tagging a match. |
|---|
| 313 | \) This marks the end of a tagged region. |
|---|
| 314 | \n Where n is 1 through 9 refers to the first through ninth tagged region when replacing. For example if the search string was Fred\([1-9]\)XXX and the replace string was Sam\1YYY applied to Fred2XXX this would generate Sam2YYY. |
|---|
| 315 | \< This matches the start of a word using Scintilla's definitions of words. |
|---|
| 316 | \> This matches the end of a word using Scintilla's definition of words. |
|---|
| 317 | \x This allows you to use a character x that would otherwise have a special meaning. For example, \[ would be interpreted as [ and not as the start of a character set. |
|---|
| 318 | [...] This indicates a set of characters, for example [abc] means any of the characters a, b or c. You can also use ranges, for example [a-z] for any lower case character. |
|---|
| 319 | [^...] The complement of the characters in the set. For example, [^A-Za-z] means any character except an alphabetic character. |
|---|
| 320 | ^ This matches the start of a line (unless used inside a set, see above). |
|---|
| 321 | $ This matches the end of a line. |
|---|
| 322 | * This matches 0 or more times. For example Sa*m matches Sm, Sam, Saam, Saaam and so on. |
|---|
| 323 | + This matches 1 or more times. For example Sa+m matches Sam, Saam, Saaam and so on.""" |
|---|
| 324 | |
|---|
| 325 | def __init__(self, stc, settings=None): |
|---|
| 326 | FindService.__init__(self, stc, settings) |
|---|
| 327 | |
|---|
| 328 | def allowBackward(self): |
|---|
| 329 | return False |
|---|
| 330 | |
|---|
| 331 | def findMatchLength(self, pos): |
|---|
| 332 | """Determine the length of the regular expression match |
|---|
| 333 | """ |
|---|
| 334 | line = self.stc.LineFromPosition(pos) |
|---|
| 335 | last = self.stc.GetLineEndPosition(line) |
|---|
| 336 | self.stc.SetTargetStart(pos) |
|---|
| 337 | self.stc.SetTargetEnd(last) |
|---|
| 338 | self.stc.SetSearchFlags(self.flags) |
|---|
| 339 | found = self.stc.SearchInTarget(self.settings.find) |
|---|
| 340 | last = self.stc.GetTargetEnd() |
|---|
| 341 | #dprint("Target: found=%d pos=%d last=%d" % (found, pos, last)) |
|---|
| 342 | if found >= 0: |
|---|
| 343 | return last - pos |
|---|
| 344 | return -1 |
|---|
| 345 | |
|---|
| 346 | def setFlags(self): |
|---|
| 347 | self.flags = self.getFlags(wx.stc.STC_FIND_REGEXP) |
|---|
| 348 | |
|---|
| 349 | def doReplace(self): |
|---|
| 350 | """Replace the selection |
|---|
| 351 | |
|---|
| 352 | Replace the current selection in the stc with the replacement text |
|---|
| 353 | """ |
|---|
| 354 | sel = self.stc.GetSelection() |
|---|
| 355 | self.stc.SetTargetStart(sel[0]) |
|---|
| 356 | self.stc.SetTargetEnd(sel[1]) |
|---|
| 357 | self.stc.SetSearchFlags(self.flags) |
|---|
| 358 | #dprint("target = %d - %d" % (sel[0], sel[1])) |
|---|
| 359 | count = self.stc.ReplaceTargetRE(self.settings.replace) |
|---|
| 360 | |
|---|
| 361 | self.stc.SetSelection(min(sel), min(sel) + count) |
|---|
| 362 | |
|---|
| 363 | |
|---|
| 364 | class FindWildcardService(FindService): |
|---|
| 365 | """Find and replace using shell style wildcards. |
|---|
| 366 | |
|---|
| 367 | Simpler than regular expressions, shell style wildcards allow simple |
|---|
| 368 | search and replace on patterns. Internally, they are converted to regular |
|---|
| 369 | expressions, but to the user's perspective they are simple, less powerful |
|---|
| 370 | pattern matching. |
|---|
| 371 | |
|---|
| 372 | Replacements can be made using the wildcard characters -- the same set of |
|---|
| 373 | wildcard characters must be given in the replacement string, in the same |
|---|
| 374 | order. For instance, if the find string is: |
|---|
| 375 | |
|---|
| 376 | blah*blah???blah* |
|---|
| 377 | |
|---|
| 378 | the replacement string should have the wildcards in the same order: '*', |
|---|
| 379 | '?', '?', '?', and '*'. E.g.: |
|---|
| 380 | |
|---|
| 381 | stuff*stuff?stuff?stuff?stuff*stuff |
|---|
| 382 | """ |
|---|
| 383 | forward = "Find wildcard" |
|---|
| 384 | |
|---|
| 385 | help = """ |
|---|
| 386 | Shell-style wildcards are simpler than full regular expressions, but you |
|---|
| 387 | give up some power for the simplicity. '*' and '?' are the wildcards, |
|---|
| 388 | where '*' matches zero or more non-whitespace characters and '?' matches |
|---|
| 389 | exactly one non-whitespace character. |
|---|
| 390 | |
|---|
| 391 | Wildcards are also allowed in replacement strings, where each wildcard in |
|---|
| 392 | the replacement string will be replaced by the value found in the search |
|---|
| 393 | result matched by the corresponding wildcard character. For instance, if |
|---|
| 394 | the find string is: |
|---|
| 395 | |
|---|
| 396 | a*b???c* |
|---|
| 397 | |
|---|
| 398 | and finds the match a222b345c678, given the replacement string: |
|---|
| 399 | |
|---|
| 400 | d*e?f?g?h*i |
|---|
| 401 | |
|---|
| 402 | the resulting replacement will be: |
|---|
| 403 | |
|---|
| 404 | d222e3f4g5h678i""" |
|---|
| 405 | |
|---|
| 406 | def findMatchLength(self, pos): |
|---|
| 407 | """Have to convert a scintilla regex to a python one so we can find out |
|---|
| 408 | how many characters the regex matched. |
|---|
| 409 | """ |
|---|
| 410 | pyre = self.settings.find.replace(r"\(.*\)", r"([^ \t\n\r]*)").replace(r"\(.\)", r"([^ \t\n\r])") |
|---|
| 411 | |
|---|
| 412 | line = self.stc.LineFromPosition(pos) |
|---|
| 413 | last = self.stc.GetLineEndPosition(line) |
|---|
| 414 | text = self.stc.GetTextRange(pos, last) |
|---|
| 415 | match = re.match(pyre, text, flags=re.IGNORECASE) |
|---|
| 416 | if match: |
|---|
| 417 | #dprint(match.group(0)) |
|---|
| 418 | return len(match.group(0)) |
|---|
| 419 | #dprint("Not matched!!! pat=%s text=%s pos=%d last=%d" % (pyre, text, pos, last)) |
|---|
| 420 | return -1 |
|---|
| 421 | |
|---|
| 422 | def setFlags(self): |
|---|
| 423 | self.flags = self.getFlags(wx.stc.STC_FIND_REGEXP) |
|---|
| 424 | |
|---|
| 425 | def expandSearchText(self, text): |
|---|
| 426 | """Convert from shell style wildcards to regular expressions""" |
|---|
| 427 | import peppy.lib.glob |
|---|
| 428 | |
|---|
| 429 | if not text: |
|---|
| 430 | text = "" |
|---|
| 431 | regex = peppy.lib.glob.translate(text) |
|---|
| 432 | #dprint(regex) |
|---|
| 433 | |
|---|
| 434 | return regex |
|---|
| 435 | |
|---|
| 436 | def expandReplaceText(self, text): |
|---|
| 437 | if not text: |
|---|
| 438 | text = "" |
|---|
| 439 | return text |
|---|
| 440 | |
|---|
| 441 | # def repgroup(self, m): |
|---|
| 442 | # dprint("match=%s span=%s" % (m.group(), str(m.span()))) |
|---|
| 443 | # self._group_count += 1 |
|---|
| 444 | # return "\\%d" % self._group_count |
|---|
| 445 | # |
|---|
| 446 | # def expandReplaceText(self, text): |
|---|
| 447 | # """Convert from shell style wildcards to regular expressions""" |
|---|
| 448 | # import peppy.lib.glob |
|---|
| 449 | # |
|---|
| 450 | # if not text: |
|---|
| 451 | # text = "" |
|---|
| 452 | # regex = peppy.lib.glob.translate(text) |
|---|
| 453 | # dprint(regex) |
|---|
| 454 | # |
|---|
| 455 | # groups = r"([\\][(].+[\\][)])" |
|---|
| 456 | # self._group_count = 0 |
|---|
| 457 | # regex = re.sub(groups, self.repgroup, regex) |
|---|
| 458 | # dprint(regex) |
|---|
| 459 | # |
|---|
| 460 | # return regex |
|---|
| 461 | |
|---|
| 462 | def getReplacement(self, replacing): |
|---|
| 463 | """Get the replacement text. |
|---|
| 464 | |
|---|
| 465 | Can't use the built-in scintilla regex replacement method, because |
|---|
| 466 | it seems that scintilla regexs are greedy and ignore the target end |
|---|
| 467 | specified by SetTargetEnd. So, we have to convert to a python regex |
|---|
| 468 | and replace that way. |
|---|
| 469 | """ |
|---|
| 470 | pyre = self.settings.find.replace(r"\(.*\)", r"([^ \t\n\r]*)").replace(r"\(.\)", r"([^ \t\n\r])") |
|---|
| 471 | #dprint("replacing %s: %s, %s" % (replacing, pyre, self.settings.replace)) |
|---|
| 472 | matches = re.match(pyre, replacing, flags=re.IGNORECASE) |
|---|
| 473 | if matches and matches.groups(): |
|---|
| 474 | groups = matches.groups() |
|---|
| 475 | else: |
|---|
| 476 | groups = [] |
|---|
| 477 | #dprint("matches: %s" % str(groups)) |
|---|
| 478 | |
|---|
| 479 | output = [] |
|---|
| 480 | index = 0 |
|---|
| 481 | # replace each wildcard with the corresponding match from the search |
|---|
| 482 | # string |
|---|
| 483 | for c in self.settings.replace: |
|---|
| 484 | if c in '*?': |
|---|
| 485 | if index < len(groups): |
|---|
| 486 | output.append(groups[index]) |
|---|
| 487 | else: |
|---|
| 488 | output.append("") |
|---|
| 489 | index += 1 |
|---|
| 490 | else: |
|---|
| 491 | output.append(c) |
|---|
| 492 | text = "".join(output) |
|---|
| 493 | return text |
|---|
| 494 | |
|---|
| 495 | |
|---|
| 496 | class FindRegexService(FindService): |
|---|
| 497 | """Find and replace using python regular expressions. |
|---|
| 498 | |
|---|
| 499 | """ |
|---|
| 500 | forward = "Find regex" |
|---|
| 501 | |
|---|
| 502 | def __init__(self, *args, **kwargs): |
|---|
| 503 | FindService.__init__(self, *args, **kwargs) |
|---|
| 504 | self.regex = None |
|---|
| 505 | self.shadow = None |
|---|
| 506 | |
|---|
| 507 | def setFlags(self): |
|---|
| 508 | text = self.settings.find |
|---|
| 509 | |
|---|
| 510 | if text != text.lower(): |
|---|
| 511 | match_case = True |
|---|
| 512 | else: |
|---|
| 513 | match_case = self.settings.match_case |
|---|
| 514 | |
|---|
| 515 | self.flags = re.MULTILINE |
|---|
| 516 | if not match_case: |
|---|
| 517 | self.flags |= re.IGNORECASE |
|---|
| 518 | |
|---|
| 519 | # Force the next search to start from a new shadow copy of the text |
|---|
| 520 | self.shadow = None |
|---|
| 521 | |
|---|
| 522 | def getFlags(self, user_flags=0): |
|---|
| 523 | if hasattr(self.stc, 'locals') and self.settings.match_case != self.stc.locals.case_sensitive_search: |
|---|
| 524 | self.settings.match_case = self.stc.locals.case_sensitive_search |
|---|
| 525 | self.setFlags() |
|---|
| 526 | |
|---|
| 527 | flags = self.flags | user_flags |
|---|
| 528 | |
|---|
| 529 | try: |
|---|
| 530 | self.regex = re.compile(self.settings.find, flags) |
|---|
| 531 | except re.error: |
|---|
| 532 | self.regex = None |
|---|
| 533 | return flags |
|---|
| 534 | |
|---|
| 535 | def verifyShadow(self, start=-1, incremental=False): |
|---|
| 536 | if self.shadow is None or start >= 0: |
|---|
| 537 | #handle finding next item, handling wrap-arounds as necessary |
|---|
| 538 | if start < 0: |
|---|
| 539 | sel = self.stc.GetSelection() |
|---|
| 540 | if incremental: |
|---|
| 541 | start = min(sel) |
|---|
| 542 | else: |
|---|
| 543 | start = max(sel) |
|---|
| 544 | self.shadow = self.stc.GetTextRange(start, self.stc.GetTextLength()) |
|---|
| 545 | self.shadow_equiv_pos = 0 |
|---|
| 546 | self.stc_equiv_start = start |
|---|
| 547 | self.stc_equiv_pos = start |
|---|
| 548 | |
|---|
| 549 | def doFindNext(self, start=-1, incremental=False): |
|---|
| 550 | """Find and highlight the next match in the document. |
|---|
| 551 | |
|---|
| 552 | @param start: starting position in text, or -1 to use current position |
|---|
| 553 | |
|---|
| 554 | @param incremental: True if an incremental search that will be added to |
|---|
| 555 | the current search result |
|---|
| 556 | |
|---|
| 557 | @return: tuple of two elements. The first element is the position of |
|---|
| 558 | the match, -1 if no match, a string indicating that an error occurred. |
|---|
| 559 | The second element is the position of the original start of the |
|---|
| 560 | search. If the search string is invalid, a tuple of (None, None) |
|---|
| 561 | is returned. |
|---|
| 562 | """ |
|---|
| 563 | if not self.settings.find: |
|---|
| 564 | return None, None |
|---|
| 565 | self.getFlags() |
|---|
| 566 | if self.regex is None: |
|---|
| 567 | return _("Incomplete regex"), None |
|---|
| 568 | |
|---|
| 569 | self.verifyShadow(start, incremental) |
|---|
| 570 | |
|---|
| 571 | match = self.regex.search(self.shadow, self.shadow_equiv_pos) |
|---|
| 572 | if match: |
|---|
| 573 | # Because unicode characters are stored as utf-8 in the stc and the |
|---|
| 574 | # positions in the stc correspond to the raw bytes, not the number |
|---|
| 575 | # of unicode characters, we have to find out the offset to the |
|---|
| 576 | # unicode chars in terms of raw bytes. |
|---|
| 577 | pos = self.stc_equiv_pos + len(self.shadow[self.shadow_equiv_pos:match.start(0)].encode('utf-8')) |
|---|
| 578 | count = len(self.shadow[match.start(0):match.end(0)].encode('utf-8')) |
|---|
| 579 | self.stc_equiv_start = pos |
|---|
| 580 | self.stc_equiv_pos = pos + count |
|---|
| 581 | |
|---|
| 582 | self.shadow_equiv_pos = match.end(0) |
|---|
| 583 | |
|---|
| 584 | #dprint("match=%s shadow: (%d-%d) equiv=%d, stc: (%d-%d) equiv=%d" % (match.group(0), match.start(0), match.end(0), self.shadow_equiv_pos, pos, pos+count, self.stc_equiv_pos)) |
|---|
| 585 | self.stc.SetSelection(self.stc_equiv_start, self.stc_equiv_pos) |
|---|
| 586 | |
|---|
| 587 | else: |
|---|
| 588 | pos = -1 |
|---|
| 589 | |
|---|
| 590 | return pos, start |
|---|
| 591 | |
|---|
| 592 | def getReplacement(self, replacing): |
|---|
| 593 | """Extended regex replacement |
|---|
| 594 | |
|---|
| 595 | This handles extra regex replacement targets, like upper and lower |
|---|
| 596 | casing of targets, that the standard python regular expression matcher |
|---|
| 597 | doesn't include. |
|---|
| 598 | """ |
|---|
| 599 | def firstLower(s): |
|---|
| 600 | if len(s) > 1: |
|---|
| 601 | return s[0].lower() + s[1:] |
|---|
| 602 | return s.lower() |
|---|
| 603 | |
|---|
| 604 | def firstUpper(s): |
|---|
| 605 | if len(s) > 1: |
|---|
| 606 | return s[0].upper() + s[1:] |
|---|
| 607 | return s.upper() |
|---|
| 608 | |
|---|
| 609 | def lower(s): |
|---|
| 610 | return s.lower() |
|---|
| 611 | |
|---|
| 612 | def upper(s): |
|---|
| 613 | return s.upper() |
|---|
| 614 | |
|---|
| 615 | match = self.regex.match(replacing) |
|---|
| 616 | if not match: |
|---|
| 617 | # Hmmm. This should have worked because theoretically we should |
|---|
| 618 | # have been matching a value returned by the same regex. |
|---|
| 619 | return replacing |
|---|
| 620 | self.dprint("matches: %s" % str(match.groups())) |
|---|
| 621 | |
|---|
| 622 | output = [] |
|---|
| 623 | next_once = None |
|---|
| 624 | next_until = None |
|---|
| 625 | parts = re.split("(\\\\(?:[0-9]{1,2}|g<[0-9]+>|l|L|u|U|E))", self.settings.replace) |
|---|
| 626 | self.dprint(parts) |
|---|
| 627 | for part in parts: |
|---|
| 628 | if part.startswith("\\"): |
|---|
| 629 | escape = part[1:] |
|---|
| 630 | if escape == "l": |
|---|
| 631 | next_once = firstLower |
|---|
| 632 | elif escape == "u": |
|---|
| 633 | next_once = firstUpper |
|---|
| 634 | elif escape == "L": |
|---|
| 635 | next_until = lower |
|---|
| 636 | elif escape == "U": |
|---|
| 637 | next_until = upper |
|---|
| 638 | elif escape == "E": |
|---|
| 639 | next_until = None |
|---|
| 640 | else: |
|---|
| 641 | try: |
|---|
| 642 | if escape.startswith("g"): |
|---|
| 643 | index = int(part[1:]) |
|---|
| 644 | self.dprint("found %s: %d" % (escape, index)) |
|---|
| 645 | else: |
|---|
| 646 | index = int(part[1:]) |
|---|
| 647 | self.dprint("found index %d" % index) |
|---|
| 648 | value = match.group(index) |
|---|
| 649 | if value: |
|---|
| 650 | if next_once: |
|---|
| 651 | value = next_once(value) |
|---|
| 652 | self.dprint("next_once converted to %s" % value) |
|---|
| 653 | next_once = None |
|---|
| 654 | elif next_until: |
|---|
| 655 | value = next_until(value) |
|---|
| 656 | self.dprint("next_until converted to %s" % value) |
|---|
| 657 | output.append(value) |
|---|
| 658 | except ValueError: |
|---|
| 659 | # not an integer means we just insert the value |
|---|
| 660 | output.append(part) |
|---|
| 661 | except IndexError: |
|---|
| 662 | # no matching group with that index, so put no value in the |
|---|
| 663 | # output for this match |
|---|
| 664 | pass |
|---|
| 665 | self.dprint("part=%s: once=%s until=%s" % (part, str(next_once), str(next_until))) |
|---|
| 666 | elif part: |
|---|
| 667 | if next_once: |
|---|
| 668 | part = next_once(part) |
|---|
| 669 | self.dprint("converted to %s" % part) |
|---|
| 670 | next_once = None |
|---|
| 671 | elif next_until: |
|---|
| 672 | part = next_until(part) |
|---|
| 673 | self.dprint("converted to %s" % part) |
|---|
| 674 | output.append(part) |
|---|
| 675 | self.dprint("output = %s" % str(output)) |
|---|
| 676 | text = "".join(output) |
|---|
| 677 | return text |
|---|
| 678 | |
|---|
| 679 | def doReplace(self): |
|---|
| 680 | """Replace the selection |
|---|
| 681 | |
|---|
| 682 | Replace the current selection with the regex replacement |
|---|
| 683 | """ |
|---|
| 684 | self.verifyShadow() |
|---|
| 685 | |
|---|
| 686 | # We assume that doFindNext has been called, setting up the equivalent |
|---|
| 687 | # start and end positions |
|---|
| 688 | replacing = self.stc.GetTextRange(self.stc_equiv_start, self.stc_equiv_pos) |
|---|
| 689 | replacement = self.getReplacement(replacing) |
|---|
| 690 | |
|---|
| 691 | self.stc.SetTargetStart(self.stc_equiv_start) |
|---|
| 692 | self.stc.SetTargetEnd(self.stc_equiv_pos) |
|---|
| 693 | |
|---|
| 694 | # The stc equivalent position must be adjusted for the difference in |
|---|
| 695 | # numbers of bytes, not numbers of characters. |
|---|
| 696 | self.stc_equiv_pos += len(replacement.encode('utf-8')) - len(replacing.encode('utf-8')) |
|---|
| 697 | self.stc.ReplaceTarget(replacement) |
|---|
| 698 | self.stc.SetSelection(self.stc_equiv_start, self.stc_equiv_pos) |
|---|
| 699 | |
|---|
| 700 | class FindBar(wx.Panel, debugmixin): |
|---|
| 701 | """Find panel customized from PyPE's findbar.py |
|---|
| 702 | |
|---|
| 703 | """ |
|---|
| 704 | debuglevel = 0 |
|---|
| 705 | |
|---|
| 706 | def __init__(self, parent, frame, stc, storage=None, service=None, direction=1, **kwargs): |
|---|
| 707 | wx.Panel.__init__(self, parent, style=wx.NO_BORDER|wx.TAB_TRAVERSAL) |
|---|
| 708 | self.frame = frame |
|---|
| 709 | self.stc = stc |
|---|
| 710 | if isinstance(storage, dict): |
|---|
| 711 | self.storage = storage |
|---|
| 712 | else: |
|---|
| 713 | self.storage = {} |
|---|
| 714 | self.settings = FindSettings() |
|---|
| 715 | if service: |
|---|
| 716 | self.service = service(self.stc, self.settings) |
|---|
| 717 | else: |
|---|
| 718 | self.service = FindService(self.stc, self.settings) |
|---|
| 719 | |
|---|
| 720 | self.createCtrls() |
|---|
| 721 | |
|---|
| 722 | # PyPE compat |
|---|
| 723 | self._lastcall = None |
|---|
| 724 | self.setDirection(direction) |
|---|
| 725 | |
|---|
| 726 | def createCtrls(self): |
|---|
| 727 | sizer = wx.BoxSizer(wx.HORIZONTAL) |
|---|
| 728 | self.label = wx.StaticText(self, -1, _(self.service.forward) + u":") |
|---|
| 729 | sizer.Add(self.label, 0, wx.CENTER) |
|---|
| 730 | self.find = wx.TextCtrl(self, -1, size=(-1,-1), style=wx.TE_PROCESS_ENTER) |
|---|
| 731 | sizer.Add(self.find, 1, wx.EXPAND) |
|---|
| 732 | self.SetSizer(sizer) |
|---|
| 733 | |
|---|
| 734 | self.find.Bind(wx.EVT_TEXT_ENTER, self.OnEnter) |
|---|
| 735 | self.find.Bind(wx.EVT_TEXT, self.OnChar) |
|---|
| 736 | |
|---|
| 737 | def setDirection(self, dir=1): |
|---|
| 738 | if dir > 0: |
|---|
| 739 | self._lastcall = self.OnFindN |
|---|
| 740 | text = self.service.forward |
|---|
| 741 | else: |
|---|
| 742 | self._lastcall = self.OnFindP |
|---|
| 743 | text = self.service.backward |
|---|
| 744 | self.label.SetLabel(_(text) + u":") |
|---|
| 745 | self.Layout() |
|---|
| 746 | |
|---|
| 747 | def OnChar(self, evt): |
|---|
| 748 | #handle updating the background color |
|---|
| 749 | self.resetColor() |
|---|
| 750 | |
|---|
| 751 | self.service.setFindString(self.find.GetValue()) |
|---|
| 752 | |
|---|
| 753 | #search in whatever direction we were before |
|---|
| 754 | self._lastcall(evt, incremental=True) |
|---|
| 755 | |
|---|
| 756 | evt.Skip() |
|---|
| 757 | |
|---|
| 758 | def OnEnter(self, evt): |
|---|
| 759 | return self._lastcall(evt) |
|---|
| 760 | |
|---|
| 761 | def OnNotFound(self, msg=None): |
|---|
| 762 | self.find.SetForegroundColour(wx.RED) |
|---|
| 763 | if msg is None: |
|---|
| 764 | msg = _("Search string was not found.") |
|---|
| 765 | self.frame.SetStatusText(msg) |
|---|
| 766 | self.Refresh() |
|---|
| 767 | |
|---|
| 768 | def resetColor(self): |
|---|
| 769 | if self.find.GetForegroundColour() != wx.BLACK: |
|---|
| 770 | self.find.SetForegroundColour(wx.BLACK) |
|---|
| 771 | self.Refresh() |
|---|
| 772 | |
|---|
| 773 | def showLine(self, pos, msg): |
|---|
| 774 | self.dprint() |
|---|
| 775 | self.resetColor() |
|---|
| 776 | self.dprint() |
|---|
| 777 | self.frame.SetStatusText(msg) |
|---|
| 778 | self.dprint() |
|---|
| 779 | line = self.stc.LineFromPosition(pos) |
|---|
| 780 | self.dprint() |
|---|
| 781 | if not self.stc.GetLineVisible(line): |
|---|
| 782 | self.stc.EnsureVisible(line) |
|---|
| 783 | self.dprint() |
|---|
| 784 | self.stc.EnsureCaretVisible() |
|---|
| 785 | self.dprint() |
|---|
| 786 | |
|---|
| 787 | def cancel(self, pos_at_end=False): |
|---|
| 788 | self.resetColor() |
|---|
| 789 | self.frame.SetStatusText('') |
|---|
| 790 | if pos_at_end: |
|---|
| 791 | pos = self.stc.GetSelectionEnd() |
|---|
| 792 | else: |
|---|
| 793 | pos = self.stc.GetSelectionStart() |
|---|
| 794 | self.stc.GotoPos(pos) |
|---|
| 795 | self.stc.EnsureCaretVisible() |
|---|
| 796 | |
|---|
| 797 | def OnFindN(self, evt, allow_wrap=True, help='', interactive=True, incremental=False): |
|---|
| 798 | self._lastcall = self.OnFindN |
|---|
| 799 | |
|---|
| 800 | posn, st = self.service.doFindNext(incremental=incremental) |
|---|
| 801 | self.dprint("start=%s pos=%s" % (st, posn)) |
|---|
| 802 | if posn is None: |
|---|
| 803 | self.cancel() |
|---|
| 804 | return |
|---|
| 805 | elif not isinstance(posn, int): |
|---|
| 806 | return self.OnNotFound(posn) |
|---|
| 807 | elif posn != -1: |
|---|
| 808 | self.dprint("interactive=%s" % interactive) |
|---|
| 809 | if interactive: |
|---|
| 810 | self.showLine(posn, help) |
|---|
| 811 | self.loop = 0 |
|---|
| 812 | return |
|---|
| 813 | |
|---|
| 814 | if allow_wrap and st != 0: |
|---|
| 815 | posn, st = self.service.doFindNext(0) |
|---|
| 816 | self.dprint("wrapped: start=%d pos=%d" % (st, posn)) |
|---|
| 817 | self.loop = 1 |
|---|
| 818 | |
|---|
| 819 | if posn != -1: |
|---|
| 820 | if interactive: |
|---|
| 821 | self.showLine(posn, "Reached end of document, continued from start.") |
|---|
| 822 | return |
|---|
| 823 | |
|---|
| 824 | self.dprint("not found: start=%d pos=%d" % (st, posn)) |
|---|
| 825 | self.OnNotFound() |
|---|
| 826 | |
|---|
| 827 | def OnFindP(self, evt, allow_wrap=True, help='', incremental=False): |
|---|
| 828 | self._lastcall = self.OnFindP |
|---|
| 829 | |
|---|
| 830 | posn, st = self.service.doFindPrev(incremental=incremental) |
|---|
| 831 | if posn != -1: |
|---|
| 832 | self.showLine(posn, help) |
|---|
| 833 | self.loop = 0 |
|---|
| 834 | return |
|---|
| 835 | |
|---|
| 836 | if allow_wrap and st != self.stc.GetTextLength(): |
|---|
| 837 | posn, st = self.service.doFindPrev(self.stc.GetTextLength()) |
|---|
| 838 | self.loop = 1 |
|---|
| 839 | |
|---|
| 840 | if posn != -1: |
|---|
| 841 | self.showLine(posn, "Reached start of document, continued from end.") |
|---|
| 842 | return |
|---|
| 843 | |
|---|
| 844 | self.OnNotFound() |
|---|
| 845 | |
|---|
| 846 | def repeatLastUserInput(self): |
|---|
| 847 | """Load the user interface with the last saved user input |
|---|
| 848 | |
|---|
| 849 | @return: True if user input was loaded from the storage space |
|---|
| 850 | """ |
|---|
| 851 | if not self.settings.find_user: |
|---|
| 852 | self.service.unserialize(self.storage) |
|---|
| 853 | self.find.ChangeValue(self.settings.find_user) |
|---|
| 854 | self.find.SetInsertionPointEnd() |
|---|
| 855 | return True |
|---|
| 856 | return False |
|---|
| 857 | |
|---|
| 858 | def repeat(self, direction, service=None): |
|---|
| 859 | self.resetColor() |
|---|
| 860 | if service is not None: |
|---|
| 861 | self.service = service(self.stc, self.settings) |
|---|
| 862 | |
|---|
| 863 | if direction < 0: |
|---|
| 864 | self.setDirection(-1) |
|---|
| 865 | else: |
|---|
| 866 | self.setDirection(1) |
|---|
| 867 | |
|---|
| 868 | if self.repeatLastUserInput(): |
|---|
| 869 | return |
|---|
| 870 | |
|---|
| 871 | # replace doesn't use an automatic search, so make sure that a method |
|---|
| 872 | # has been specified before calling it |
|---|
| 873 | if self._lastcall: |
|---|
| 874 | self._lastcall(None) |
|---|
| 875 | |
|---|
| 876 | def saveState(self): |
|---|
| 877 | self.service.serialize(self.storage) |
|---|
| 878 | |
|---|
| 879 | |
|---|
| 880 | class FindMinibuffer(Minibuffer): |
|---|
| 881 | """ |
|---|
| 882 | Adapter for PyPE findbar. Maps findbar callbacks to our stuff. |
|---|
| 883 | """ |
|---|
| 884 | search_storage = {} |
|---|
| 885 | |
|---|
| 886 | def createWindow(self, parent, **kwargs): |
|---|
| 887 | self.win = FindBar(parent, self.mode.frame, self.mode, self.search_storage, direction=self.action.find_direction, service=self.action.find_service) |
|---|
| 888 | |
|---|
| 889 | def getHelp(self): |
|---|
| 890 | return self.win.service.help |
|---|
| 891 | |
|---|
| 892 | def repeat(self, action): |
|---|
| 893 | self.win.SetFocus() |
|---|
| 894 | self.win.repeat(action.find_direction, action.find_service) |
|---|
| 895 | |
|---|
| 896 | def focus(self): |
|---|
| 897 | # When the focus is asked for by the minibuffer driver, set it |
|---|
| 898 | # to the text ctrl or combo box of the pype findbar. |
|---|
| 899 | self.win.find.SetFocus() |
|---|
| 900 | |
|---|
| 901 | def closePreHook(self): |
|---|
| 902 | self.win.saveState() |
|---|
| 903 | self.dprint(self.search_storage) |
|---|
| 904 | |
|---|
| 905 | |
|---|
| 906 | class FindText(MinibufferRepeatAction): |
|---|
| 907 | name = "Find..." |
|---|
| 908 | tooltip = "Search for a string in the text." |
|---|
| 909 | default_menu = ("Edit", -400) |
|---|
| 910 | icon = "icons/find.png" |
|---|
| 911 | key_bindings = {'default': "C-F", 'emacs': 'C-S', } |
|---|
| 912 | minibuffer = FindMinibuffer |
|---|
| 913 | find_service = FindService |
|---|
| 914 | find_direction = 1 |
|---|
| 915 | |
|---|
| 916 | class FindRegex(MinibufferRepeatAction): |
|---|
| 917 | name = "Find Regex..." |
|---|
| 918 | tooltip = "Search for a python regular expression." |
|---|
| 919 | default_menu = ("Edit", 401) |
|---|
| 920 | key_bindings = {'emacs': 'C-M-S', } |
|---|
| 921 | minibuffer = FindMinibuffer |
|---|
| 922 | find_service = FindRegexService |
|---|
| 923 | find_direction = 1 |
|---|
| 924 | |
|---|
| 925 | class FindWildcard(MinibufferRepeatAction): |
|---|
| 926 | name = "Find Wildcard..." |
|---|
| 927 | tooltip = "Search using shell-style wildcards." |
|---|
| 928 | default_menu = ("Edit", 402) |
|---|
| 929 | key_bindings = {'emacs': 'C-S-M-S', } |
|---|
| 930 | minibuffer = FindMinibuffer |
|---|
| 931 | find_service = FindWildcardService |
|---|
| 932 | find_direction = 1 |
|---|
| 933 | |
|---|
| 934 | class FindPrevText(MinibufferRepeatAction): |
|---|
| 935 | name = "Find Previous..." |
|---|
| 936 | tooltip = "Search backwards for a string in the text." |
|---|
| 937 | default_menu = ("Edit", 402) |
|---|
| 938 | key_bindings = {'default': "C-S-F", 'emacs': 'C-R', } |
|---|
| 939 | minibuffer = FindMinibuffer |
|---|
| 940 | find_service = FindService |
|---|
| 941 | find_direction = -1 |
|---|
| 942 | |
|---|
| 943 | |
|---|
| 944 | |
|---|
| 945 | |
|---|
| 946 | class FakeButton(wx.lib.stattext.GenStaticText): |
|---|
| 947 | """Simple text label that accepts focus. |
|---|
| 948 | |
|---|
| 949 | Used as the key processor for replace. This label accepts focus but |
|---|
| 950 | doesn't display any indicator that focus has been taken, so it looks like |
|---|
| 951 | an ordinary label. However, events can be processed through this label |
|---|
| 952 | and it is used to handle the keyboard commands for replacing. |
|---|
| 953 | """ |
|---|
| 954 | def AcceptsFocus(self): |
|---|
| 955 | return True |
|---|
| 956 | |
|---|
| 957 | class ReplaceBar(FindBar): |
|---|
| 958 | """Replace panel in the style of the Emacs replace function |
|---|
| 959 | |
|---|
| 960 | The control of the minibuffer is similar to emacs: |
|---|
| 961 | |
|---|
| 962 | Type the search string in 'Replace:' and hit enter. Type the replacement |
|---|
| 963 | string in 'with:' and hit enter. Then, use the keyboard to control the |
|---|
| 964 | replacements: |
|---|
| 965 | |
|---|
| 966 | 'y' or SPACE: replace one match and advance to the next match |
|---|
| 967 | ',': replace the current match but not advance to the next match |
|---|
| 968 | 'n' or DELETE: skip to the next match |
|---|
| 969 | 'p' or '^': move to the previous match |
|---|
| 970 | 'q': quit matching and remove the replace bar |
|---|
| 971 | '.': replace one match and quit matching |
|---|
| 972 | '!': replace all remaining matches and quit matching |
|---|
| 973 | """ |
|---|
| 974 | help_status = "y: replace, n: skip, q: exit, !:replace all, f: edit find, r: edit replace, ?: help" |
|---|
| 975 | |
|---|
| 976 | def __init__(self, *args, **kwargs): |
|---|
| 977 | FindBar.__init__(self, *args, **kwargs) |
|---|
| 978 | |
|---|
| 979 | if 'on_exit' in kwargs: |
|---|
| 980 | self.on_exit = kwargs['on_exit'] |
|---|
| 981 | else: |
|---|
| 982 | self.on_exit = None |
|---|
| 983 | |
|---|
| 984 | # Count of number of replacements |
|---|
| 985 | self.count = 0 |
|---|
| 986 | |
|---|
| 987 | # Last cursor position, tracked during replace all |
|---|
| 988 | self.last_cursor = 0 |
|---|
| 989 | |
|---|
| 990 | # focus tracker to make sure that focus events come from us |
|---|
| 991 | self.focus_tracker = None |
|---|
| 992 | |
|---|
| 993 | def createCtrls(self): |
|---|
| 994 | text = self.service.forward |
|---|
| 995 | grid = wx.GridBagSizer(2, 2) |
|---|
| 996 | self.label = wx.StaticText(self, -1, _(text) + u":") |
|---|
| 997 | grid.Add(self.label, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) |
|---|
| 998 | self.find = wx.TextCtrl(self, -1, size=(-1,-1), style=wx.TE_PROCESS_ENTER) |
|---|
| 999 | grid.Add(self.find, (0, 1), flag=wx.EXPAND) |
|---|
| 1000 | self.replace = wx.TextCtrl(self, -1, size=(-1,-1), style=wx.TE_PROCESS_ENTER) |
|---|
| 1001 | grid.Add(self.replace, (1, 1), flag=wx.EXPAND) |
|---|
| 1002 | |
|---|
| 1003 | # Windows doesn't process char events through a real button, so we use |
|---|
| 1004 | # this fake button as a label and process events through it. It's |
|---|
| 1005 | # added here so that it will be last in the tab order. |
|---|
| 1006 | self.command = FakeButton(self, -1, _("Replace with") + u":") |
|---|
| 1007 | grid.Add(self.command, (1, 0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) |
|---|
| 1008 | |
|---|
| 1009 | grid.AddGrowableCol(1) |
|---|
| 1010 | self.SetSizer(grid) |
|---|
| 1011 | |
|---|
| 1012 | self.find.Bind(wx.EVT_TEXT_ENTER, self.OnTabToReplace) |
|---|
| 1013 | self.find.Bind(wx.EVT_SET_FOCUS, self.OnFindSetFocus) |
|---|
| 1014 | self.replace.Bind(wx.EVT_TEXT_ENTER, self.OnTabToCommand) |
|---|
| 1015 | self.replace.Bind(wx.EVT_KILL_FOCUS, self.OnReplaceLoseFocus) |
|---|
| 1016 | self.replace.Bind(wx.EVT_SET_FOCUS, self.OnReplaceSetFocus) |
|---|
| 1017 | self.command.Bind(wx.EVT_BUTTON, self.OnReplace) |
|---|
| 1018 | self.command.Bind(wx.EVT_KEY_DOWN, self.OnCommandKeyDown) |
|---|
| 1019 | self.command.Bind(wx.EVT_CHAR, self.OnCommandChar) |
|---|
| 1020 | self.command.Bind(wx.EVT_SET_FOCUS, self.OnSearchStart) |
|---|
| 1021 | |
|---|
| 1022 | def OnReplaceError(self, msg=None): |
|---|
| 1023 | self.replace.SetForegroundColour(wx.RED) |
|---|
| 1024 | self.frame.SetStatusText(msg) |
|---|
| 1025 | self.Refresh() |
|---|
| 1026 | |
|---|
| 1027 | def resetColor(self): |
|---|
| 1028 | if self.replace.GetBackgroundColour() != wx.WHITE: |
|---|
| 1029 | self.replace.SetBackgroundColour(wx.WHITE) |
|---|
| 1030 | self.replace.Refresh() |
|---|
| 1031 | FindBar.resetColor(self) |
|---|
| 1032 | |
|---|
| 1033 | def setDirection(self, dir=1): |
|---|
| 1034 | self.label.SetLabel(_(self.service.forward) + u":") |
|---|
| 1035 | self.Layout() |
|---|
| 1036 | |
|---|
| 1037 | def OnFindSetFocus(self, evt): |
|---|
| 1038 | self.dprint(evt.GetWindow()) |
|---|
| 1039 | self.cancel() |
|---|
| 1040 | self.frame.SetStatusText("Enter search text and press Return") |
|---|
| 1041 | |
|---|
| 1042 | def OnReplaceSetFocus(self, evt): |
|---|
| 1043 | self.dprint(evt.GetWindow()) |
|---|
| 1044 | self.frame.SetStatusText("Enter replacement text and press Return") |
|---|
| 1045 | self.focus_tracker = True |
|---|
| 1046 | |
|---|
| 1047 | def OnReplaceLoseFocus(self, evt): |
|---|
| 1048 | self.dprint(evt.GetWindow()) |
|---|
| 1049 | # Using tab after changing color when a selection exists in |
|---|
| 1050 | # self.replace causes the text to appear white on a white background. |
|---|
| 1051 | # Force the selection to disappear when tabbing off of self.replace |
|---|
| 1052 | self.replace.SetInsertionPointEnd() |
|---|
| 1053 | |
|---|
| 1054 | def OnTabToFind(self, evt): |
|---|
| 1055 | self.find.SetFocus() |
|---|
| 1056 | |
|---|
| 1057 | def OnTabToReplace(self, evt): |
|---|
| 1058 | self.replace.SetFocus() |
|---|
| 1059 | |
|---|
| 1060 | def OnTabToCommand(self, evt): |
|---|
| 1061 | # Using tab after changing color when a selection exists in |
|---|
| 1062 | # self.replace causes the text to appear white on a white background. |
|---|
| 1063 | # Force the selection to disappear when tabbing off of self.replace |
|---|
| 1064 | self.replace.SetInsertionPointEnd() |
|---|
| 1065 | self.command.SetFocus() |
|---|
| 1066 | |
|---|
| 1067 | def OnSearchStart(self, evt): |
|---|
| 1068 | if self.focus_tracker: |
|---|
| 1069 | self.service.setFindString(self.find.GetValue()) |
|---|
| 1070 | self.service.setReplaceString(self.replace.GetValue()) |
|---|
| 1071 | self.OnFindN(evt, help=self.help_status) |
|---|
| 1072 | self.focus_tracker = False |
|---|
| 1073 | |
|---|
| 1074 | def OnReplace(self, evt, find_next=True, interactive=True): |
|---|
| 1075 | """Replace the selection |
|---|
| 1076 | |
|---|
| 1077 | The bulk of this algorithm is from PyPE. |
|---|
| 1078 | """ |
|---|
| 1079 | if self.stc.GetReadOnly(): |
|---|
| 1080 | return False |
|---|
| 1081 | |
|---|
| 1082 | allow_wrap = interactive |
|---|
| 1083 | sel = self.stc.GetSelection() |
|---|
| 1084 | if sel[0] == sel[1]: |
|---|
| 1085 | if find_next: |
|---|
| 1086 | self.OnFindN(None, allow_wrap=allow_wrap, help=self.help_status, interactive=interactive) |
|---|
| 1087 | return True |
|---|
| 1088 | |
|---|
| 1089 | try: |
|---|
| 1090 | self.service.doReplace() |
|---|
| 1091 | if find_next: |
|---|
| 1092 | self.OnFindN(None, allow_wrap=allow_wrap, help=self.help_status, interactive=interactive) |
|---|
| 1093 | self.count += 1 |
|---|
| 1094 | |
|---|
| 1095 | return True |
|---|
| 1096 | except ReplacementError, e: |
|---|
| 1097 | self.OnReplaceError(str(e)) |
|---|
| 1098 | return False |
|---|
| 1099 | |
|---|
| 1100 | def OnReplaceAll(self, evt): |
|---|
| 1101 | self.count = 0 |
|---|
| 1102 | self.loop = 0 |
|---|
| 1103 | self.last_cursor = 0 |
|---|
| 1104 | |
|---|
| 1105 | # FIXME: this takes more time than it should, probably because there's |
|---|
| 1106 | # a lot of redundant stuff going on in OnReplace. This should be |
|---|
| 1107 | # rewritten for speed at some point. |
|---|
| 1108 | if hasattr(self.stc, 'showBusy'): |
|---|
| 1109 | self.stc.showBusy(True) |
|---|
| 1110 | wx.Yield() |
|---|
| 1111 | self.stc.BeginUndoAction() |
|---|
| 1112 | valid = True |
|---|
| 1113 | try: |
|---|
| 1114 | while self.loop == 0 and valid: |
|---|
| 1115 | valid = self.OnReplace(None, interactive=False) |
|---|
| 1116 | self.stc.GotoPos(self.last_cursor) |
|---|
| 1117 | finally: |
|---|
| 1118 | self.stc.EndUndoAction() |
|---|
| 1119 | |
|---|
| 1120 | if hasattr(self.stc, 'showBusy'): |
|---|
| 1121 | self.stc.showBusy(False) |
|---|
| 1122 | if valid: |
|---|
| 1123 | if self.count == 1: |
|---|
| 1124 | occurrences = _("Replaced %d occurrence") |
|---|
| 1125 | else: |
|---|
| 1126 | occurrences = _("Replaced %d occurrences") |
|---|
| 1127 | self.OnExit(msg=_(occurrences) % self.count) |
|---|
| 1128 | |
|---|
| 1129 | def OnCommandKeyDown(self, evt): |
|---|
| 1130 | key = evt.GetKeyCode() |
|---|
| 1131 | mods = evt.GetModifiers() |
|---|
| 1132 | |
|---|
| 1133 | # FIXME: windows doesn't receive the return character, so this code is |
|---|
| 1134 | # never reached when the return key is pressed. |
|---|
| 1135 | |
|---|
| 1136 | #dprint("key=%s mods=%s" % (key, mods)) |
|---|
| 1137 | if key == wx.WXK_TAB and not mods & (wx.MOD_CMD|wx.MOD_SHIFT|wx.MOD_ALT): |
|---|
| 1138 | self.find.SetFocus() |
|---|
| 1139 | elif key == wx.WXK_RETURN or key == wx.WXK_DELETE: |
|---|
| 1140 | self.OnFindN(None, help=self.help_status) |
|---|
| 1141 | else: |
|---|
| 1142 | evt.Skip() |
|---|
| 1143 | |
|---|
| 1144 | def OnCommandChar(self, evt): |
|---|
| 1145 | uchar = unichr(evt.GetKeyCode()) |
|---|
| 1146 | #dprint("uchar = %s" % uchar) |
|---|
| 1147 | if uchar in u'nN': |
|---|
| 1148 | self.OnFindN(None, help=self.help_status) |
|---|
| 1149 | elif uchar in u'yY ': |
|---|
| 1150 | self.OnReplace(None) |
|---|
| 1151 | elif uchar in u'pP^': |
|---|
| 1152 | self.OnFindP(None, help=self.help_status) |
|---|
| 1153 | elif uchar in u'qQ': |
|---|
| 1154 | self.OnExit() |
|---|
| 1155 | elif uchar in u'.': |
|---|
| 1156 | self.OnReplace(None, find_next=False) |
|---|
| 1157 | self.OnExit() |
|---|
| 1158 | elif uchar in u',': |
|---|
| 1159 | self.OnReplace(None, find_next=False) |
|---|
| 1160 | elif uchar in u'!': |
|---|
| 1161 | self.OnReplaceAll(None) |
|---|
| 1162 | elif uchar in u'fF': |
|---|
| 1163 | self.OnTabToFind(None) |
|---|
| 1164 | elif uchar in u'rR': |
|---|
| 1165 | self.OnTabToReplace(None) |
|---|
| 1166 | elif uchar in u'?': |
|---|
| 1167 | Publisher().sendMessage('peppy.log.info', (self.frame, self.__doc__)) |
|---|
| 1168 | else: |
|---|
| 1169 | evt.Skip() |
|---|
| 1170 | |
|---|
| 1171 | def OnExit(self, msg=''): |
|---|
| 1172 | self.cancel(pos_at_end=True) |
|---|
| 1173 | if msg: |
|---|
| 1174 | self.frame.SetStatusText(msg) |
|---|
| 1175 | if self.on_exit: |
|---|
| 1176 | self.on_exit() |
|---|
| 1177 | |
|---|
| 1178 | def repeatLastUserInput(self): |
|---|
| 1179 | """Load the user interface with the last saved user input |
|---|
| 1180 | |
|---|
| 1181 | @return: True if user input was loaded from the storage space |
|---|
| 1182 | """ |
|---|
| 1183 | # disable the repeat search when using the replace buffer |
|---|
| 1184 | self._lastcall = None |
|---|
| 1185 | |
|---|
| 1186 | # Set the focus to the find string to prevent the focus from staying on |
|---|
| 1187 | # the command control. Unless this is done, the focus tends to stay |
|---|
| 1188 | # on the command control which highlights the last successful search. |
|---|
| 1189 | self.find.SetFocus() |
|---|
| 1190 | |
|---|
| 1191 | if not self.settings.find_user: |
|---|
| 1192 | self.service.unserialize(self.storage) |
|---|
| 1193 | self.find.ChangeValue(self.settings.find_user) |
|---|
| 1194 | self.find.SetInsertionPointEnd() |
|---|
| 1195 | self.replace.ChangeValue(self.settings.replace_user) |
|---|
| 1196 | self.replace.SetInsertionPointEnd() |
|---|
| 1197 | return True |
|---|
| 1198 | return False |
|---|
| 1199 | |
|---|
| 1200 | |
|---|
| 1201 | class ReplaceMinibuffer(FindMinibuffer): |
|---|
| 1202 | """ |
|---|
| 1203 | Adapter for PyPE findbar. Maps findbar callbacks to our stuff. |
|---|
| 1204 | """ |
|---|
| 1205 | search_storage = {} |
|---|
| 1206 | |
|---|
| 1207 | def createWindow(self, parent, **kwargs): |
|---|
| 1208 | self.win = ReplaceBar(parent, self.mode.frame, self.mode, |
|---|
| 1209 | self.search_storage, on_exit=self.removeFromParent, |
|---|
| 1210 | direction=self.action.find_direction, |
|---|
| 1211 | service=self.action.find_service) |
|---|
| 1212 | |
|---|
| 1213 | |
|---|
| 1214 | |
|---|
| 1215 | class Replace(MinibufferRepeatAction): |
|---|
| 1216 | name = "Replace..." |
|---|
| 1217 | tooltip = "Replace a string in the text." |
|---|
| 1218 | default_menu = ("Edit", 410) |
|---|
| 1219 | icon = "icons/text_replace.png" |
|---|
| 1220 | key_bindings = {'win': 'C-H', 'emacs': 'F6', } |
|---|
| 1221 | minibuffer = ReplaceMinibuffer |
|---|
| 1222 | find_service = FindService |
|---|
| 1223 | find_direction = 1 |
|---|
| 1224 | |
|---|
| 1225 | class ReplaceRegex(MinibufferRepeatAction): |
|---|
| 1226 | name = "Replace Regex..." |
|---|
| 1227 | tooltip = "Replace using python regular expressions." |
|---|
| 1228 | default_menu = ("Edit", 411) |
|---|
| 1229 | key_bindings = {'emacs': 'S-F6', } |
|---|
| 1230 | minibuffer = ReplaceMinibuffer |
|---|
| 1231 | find_service = FindRegexService |
|---|
| 1232 | find_direction = 1 |
|---|
| 1233 | |
|---|
| 1234 | class ReplaceWildcard(MinibufferRepeatAction): |
|---|
| 1235 | name = "Replace Wildcard..." |
|---|
| 1236 | tooltip = "Replace using shell-style wildcards." |
|---|
| 1237 | default_menu = ("Edit", 411) |
|---|
| 1238 | key_bindings = {'emacs': 'M-F6', } |
|---|
| 1239 | minibuffer = ReplaceMinibuffer |
|---|
| 1240 | find_service = FindWildcardService |
|---|
| 1241 | find_direction = 1 |
|---|
| 1242 | |
|---|
| 1243 | |
|---|
| 1244 | class CaseSensitiveSearch(ToggleAction): |
|---|
| 1245 | """Should search string require match by case""" |
|---|
| 1246 | name = "Case Sensitive Search" |
|---|
| 1247 | default |
|---|