1# This file is part of the Frescobaldi project, http://www.frescobaldi.org/ 2# 3# Copyright (c) 2011 - 2014 by Wilbert Berendsen 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU General Public License 7# as published by the Free Software Foundation; either version 2 8# of the License, or (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18# See http://www.gnu.org/licenses/ for more information. 19 20""" 21Analyze text to determine suitable completions. 22""" 23 24 25import re 26import os 27 28import ly.lex as lx 29import ly.lex.lilypond as lp 30import ly.lex.scheme as scm 31import ly.words 32import tokeniter 33 34from . import completiondata 35from . import documentdata 36 37 38class Analyzer(object): 39 """Analyzes text at some cursor position and gives suitable completions.""" 40 def analyze(self, cursor): 41 """Do the analyzing work; set the attributes column and model.""" 42 self.cursor = cursor 43 block = cursor.block() 44 self.column = column = cursor.position() - block.position() 45 self.text = text = block.text()[:column] 46 self.model = None 47 48 # make a list of tokens exactly ending at the cursor position 49 # and let state follow 50 state = self.state = tokeniter.state(block) 51 tokens = self.tokens = [] 52 for t in tokeniter.tokens(cursor.block()): 53 if t.end > column: 54 # cut off the last token and run the parser on it 55 tokens.extend(state.tokens(text, t.pos)) 56 break 57 tokens.append(t) 58 state.follow(t) 59 if t.end == column: 60 break 61 62 self.last = tokens[-1] if tokens else '' 63 self.lastpos = self.last.pos if self.last else column 64 65 parser = state.parser() 66 67 # Map the parser class to a group of tests to return the model. 68 # Try the tests until a model is returned. 69 try: 70 tests = self.tests[parser.__class__] 71 except KeyError: 72 return 73 else: 74 for function in tests: 75 model = function(self) 76 if model: 77 self.model = model 78 return 79 80 def completions(self, cursor): 81 """Analyzes text at cursor and returns a tuple (position, model). 82 83 The position is an integer specifying the column in the line where the last 84 text starts that should be completed. 85 86 The model list the possible completions. If the model is None, there are no 87 suitable completions. 88 89 This function does its best to return extremely meaningful completions 90 for the context the cursor is in. 91 92 """ 93 self.analyze(cursor) 94 return self.column, self.model 95 96 def document_cursor(self): 97 """Return the current QTextCursor, to harvest info from its document. 98 99 By default this is simply the cursor given on analyze() or completions() 100 but you can override this method to provide another cursor. This can 101 be useful when the completion occurs in a small QTextDocument, which is 102 in fact a part of the main document. 103 104 """ 105 return self.cursor 106 107 def tokenclasses(self): 108 """Return the list of classes of the tokens.""" 109 return list(map(type, self.tokens)) 110 111 def backuntil(self, *classes): 112 """Move self.column back until a token of *classes is encountered.""" 113 for t in self.tokens[::-1]: 114 if isinstance(t, classes): 115 break 116 self.column = t.pos 117 118 # Test functions that return a model or None 119 def toplevel(self): 120 """LilyPond toplevel document contents.""" 121 self.backuntil(lx.Space) 122 return completiondata.lilypond_toplevel 123 # maybe: check if behind \version or \language 124 125 def book(self): 126 """\\book {""" 127 self.backuntil(lx.Space) 128 cursor = self.document_cursor() 129 return documentdata.doc(cursor.document()).bookcommands(cursor) 130 131 def bookpart(self): 132 """\\bookpart {""" 133 self.backuntil(lx.Space) 134 cursor = self.document_cursor() 135 return documentdata.doc(cursor.document()).bookpartcommands(cursor) 136 137 def score(self): 138 """\\score {""" 139 self.backuntil(lx.Space) 140 cursor = self.document_cursor() 141 return documentdata.doc(cursor.document()).scorecommands(cursor) 142 143 def tweak(self): 144 """complete property after \\tweak""" 145 try: 146 i = self.tokens.index('\\tweak') 147 except ValueError: 148 return 149 tokens = self.tokens[i+1:] 150 tokenclasses = self.tokenclasses()[i+1:] 151 if tokenclasses == [lx.Space, lp.SchemeStart]: 152 self.column -= 1 153 return completiondata.lilypond_all_grob_properties 154 elif tokenclasses == [lx.Space, lp.SchemeStart, scm.Quote]: 155 self.column -= 2 156 return completiondata.lilypond_all_grob_properties 157 elif tokenclasses[:-1] == [lx.Space, lp.SchemeStart, scm.Quote]: 158 self.column = self.lastpos - 2 159 return completiondata.lilypond_all_grob_properties 160 # 2.18-style [GrobName.]propertyname tweak 161 if lp.GrobName in tokenclasses: 162 self.backuntil(lx.Space, lp.DotPath) 163 return completiondata.lilypond_grob_properties(tokens[1], False) 164 if tokens: 165 self.backuntil(lx.Space) 166 return completiondata.lilypond_all_grob_properties_and_grob_names 167 168 def key(self): 169 """complete mode argument of '\\key'""" 170 tokenclasses = self.tokenclasses() 171 if '\\key' in self.tokens[-5:-2] and lp.Note in tokenclasses[-3:]: 172 if self.last.startswith('\\'): 173 self.column = self.lastpos 174 return completiondata.lilypond_modes 175 176 def clef(self): 177 """complete \\clef names""" 178 if '\\clef' in self.tokens[-4:-1]: 179 self.backuntil(lx.Space, lp.StringQuotedStart) 180 return completiondata.lilypond_clefs 181 182 def repeat(self): 183 """complete \\repeat types""" 184 if '\\repeat' in self.tokens[-4:-1]: 185 self.backuntil(lx.Space, lp.StringQuotedStart) 186 return completiondata.lilypond_repeat_types 187 188 def language(self): 189 """complete \\language "name" """ 190 if '\\language' in self.tokens[-4:-1]: 191 self.backuntil(lp.StringQuotedStart) 192 return completiondata.language_names 193 194 def include(self): 195 """complete \\include """ 196 if '\\include' in self.tokens[-4:-2]: 197 self.backuntil(lp.StringQuotedStart) 198 sep = '/' # Even on Windows, LilyPond uses the forward slash 199 dir = self.last[:self.last.rfind(sep)] if sep in self.last else None 200 cursor = self.document_cursor() 201 return documentdata.doc(cursor.document()).includenames(cursor, dir) 202 203 def general_music(self): 204 """fall back: generic music commands and user-defined commands.""" 205 if self.last.startswith('\\'): 206 self.column = self.lastpos 207 cursor = self.document_cursor() 208 return documentdata.doc(cursor.document()).musiccommands(cursor) 209 210 def lyricmode(self): 211 """Commands inside lyric mode.""" 212 if self.last.startswith('\\'): 213 self.column = self.lastpos 214 cursor = self.document_cursor() 215 return documentdata.doc(cursor.document()).lyriccommands(cursor) 216 217 def music_glyph(self): 218 r"""Complete \markup \musicglyph names.""" 219 try: 220 i = self.tokens.index('\\musicglyph', -5, -3) 221 except ValueError: 222 return 223 for t, cls in zip(self.tokens[i:], ( 224 lp.MarkupCommand, lx.Space, lp.SchemeStart, scm.StringQuotedStart, scm.String)): 225 if type(t) is not cls: 226 return 227 if i + 4 < len(self.tokens): 228 self.column = self.tokens[i + 4].pos 229 return completiondata.music_glyphs 230 231 def midi_instrument(self): 232 """Complete midiInstrument = #"... """ 233 try: 234 i = self.tokens.index('midiInstrument', -7, -2) 235 except ValueError: 236 return 237 if self.last != '"': 238 self.column = self.lastpos 239 return completiondata.midi_instruments 240 241 def font_name(self): 242 """Complete #'font-name = #"...""" 243 try: 244 i = self.tokens.index('font-name', -7, -3) 245 except ValueError: 246 return 247 if self.last != '"': 248 self.column = self.lastpos 249 return completiondata.font_names() 250 251 def scheme_word(self): 252 """Complete scheme word from scheme functions, etc.""" 253 if isinstance(self.last, scm.Word): 254 self.column = self.lastpos 255 cursor = self.document_cursor() 256 return documentdata.doc(cursor.document()).schemewords() 257 258 def markup(self): 259 """\\markup {""" 260 if self.last.startswith('\\'): 261 if (self.last[1:] not in ly.words.markupcommands 262 and self.last != '\\markup'): 263 self.column = self.lastpos 264 else: 265 return completiondata.lilypond_markup_commands 266 else: 267 m = re.search(r'\w+$', self.last) 268 if m: 269 self.column = self.lastpos + m.start() 270 cursor = self.document_cursor() 271 return documentdata.doc(cursor.document()).markup(cursor) 272 273 def markup_top(self): 274 """\\markup ... in music or toplevel""" 275 if self.last.startswith('\\') and isinstance(self.last, 276 (ly.lex.lilypond.MarkupCommand, ly.lex.lilypond.MarkupUserCommand)): 277 self.column = self.lastpos 278 cursor = self.document_cursor() 279 return documentdata.doc(cursor.document()).markup(cursor) 280 281 def header(self): 282 """\\header {""" 283 if '=' in self.tokens[-3:] or self.last.startswith('\\'): 284 if self.last.startswith('\\'): 285 self.column = self.lastpos 286 return completiondata.lilypond_markup 287 if self.last[:1].isalpha(): 288 self.column = self.lastpos 289 return completiondata.lilypond_header_variables 290 291 def paper(self): 292 """\\paper {""" 293 if '=' in self.tokens[-3:] or self.last.startswith('\\'): 294 if self.last.startswith('\\'): 295 self.column = self.lastpos 296 return completiondata.lilypond_markup 297 if self.last[:1].isalpha(): 298 self.column = self.lastpos 299 return completiondata.lilypond_paper_variables 300 301 def layout(self): 302 """\\layout {""" 303 self.backuntil(lx.Space) 304 return completiondata.lilypond_layout_variables 305 306 def midi(self): 307 """\\midi {""" 308 self.backuntil(lx.Space) 309 return completiondata.lilypond_midi_variables 310 311 def engraver(self): 312 """Complete engraver names.""" 313 cmd_in = lambda tokens: '\\remove' in tokens or '\\consists' in tokens 314 if isinstance(self.state.parser(), lp.ParseString): 315 if not cmd_in(self.tokens[-5:-2]): 316 return 317 if self.last != '"': 318 if '"' not in self.tokens[-2:-1]: 319 return 320 self.column = self.lastpos 321 return completiondata.lilypond_engravers 322 if cmd_in(self.tokens[-3:-1]): 323 self.backuntil(lx.Space) 324 return completiondata.lilypond_engravers 325 326 def context_variable_set(self): 327 if '=' in self.tokens[-4:]: 328 if isinstance(self.last, scm.Word): 329 self.column = self.lastpos 330 cursor = self.document_cursor() 331 return documentdata.doc(cursor.document()).schemewords() 332 if self.last.startswith('\\'): 333 self.column = self.lastpos 334 return completiondata.lilypond_markup 335 336 def context(self): 337 self.backuntil(lx.Space) 338 return completiondata.lilypond_context_contents 339 340 def with_(self): 341 self.backuntil(lx.Space) 342 return completiondata.lilypond_with_contents 343 344 def translator(self): 345 """complete context name after \\new, \\change or \\context in music""" 346 for t in self.tokens[-2::-1]: 347 if isinstance(t, lp.ContextName): 348 return 349 elif isinstance(t, lp.Translator): 350 break 351 self.backuntil(lx.Space) 352 return completiondata.lilypond_contexts 353 354 def override(self): 355 """\\override and \\revert""" 356 tokenclasses = self.tokenclasses() 357 try: 358 # check if there is a GrobName in the last 5 tokens 359 i = tokenclasses.index(lp.GrobName, -5) 360 except ValueError: 361 # not found, then complete Contexts and or Grobs 362 # (only if we are in the override parser and there's no "=") 363 if isinstance(self.state.parser(), scm.ParseScheme): 364 return 365 self.backuntil(lp.DotPath, lx.Space) 366 if (isinstance(self.state.parsers()[1], ( 367 lp.ParseWith, 368 lp.ParseContext, 369 )) 370 or lp.DotPath in tokenclasses): 371 return completiondata.lilypond_grobs 372 return completiondata.lilypond_contexts_and_grobs 373 # yes, there is a GrobName at i 374 count = len(self.tokens) - i - 1 # tokens after grobname 375 if count == 0: 376 self.column = self.lastpos 377 return completiondata.lilypond_grobs 378 elif count >= 2: 379 # set the place of the scheme-start "#" as the column 380 self.column = self.tokens[i+2].pos 381 test = [lx.Space, lp.SchemeStart, scm.Quote, scm.Word] 382 if tokenclasses[i+1:] == test[:count]: 383 return completiondata.lilypond_grob_properties(self.tokens[i]) 384 self.backuntil(lp.DotPath, lx.Space) 385 return completiondata.lilypond_grob_properties(self.tokens[i], False) 386 387 def revert(self): 388 """test for \\revert in general music expressions 389 390 (because the revert parser drops out of invalid constructs, which happen 391 during typing). 392 393 """ 394 if '\\revert' in self.tokens: 395 return self.override() 396 397 def set_unset(self): 398 """\\set and \\unset""" 399 tokenclasses = self.tokenclasses() 400 self.backuntil(lx.Space, lp.DotPath) 401 if lp.ContextProperty in tokenclasses and isinstance(self.last, lx.Space): 402 return # fall back to music? 403 elif lp.DotPath in tokenclasses: 404 return completiondata.lilypond_context_properties 405 return completiondata.lilypond_contexts_and_properties 406 407 def markup_override(self): 408 """test for \\markup \\override inside scheme""" 409 try: 410 i = self.tokens.index('\\override', -6, -4) 411 except ValueError: 412 return 413 for t, cls in zip(self.tokens[i:], ( 414 lp.MarkupCommand, lx.Space, lp.SchemeStart, scm.Quote, scm.OpenParen)): 415 if type(t) is not cls: 416 return 417 if len(self.tokens) > i + 5: 418 self.column = self.lastpos 419 return completiondata.lilypond_markup_properties 420 421 def scheme_other(self): 422 """test for other scheme words""" 423 if isinstance(self.last, ( 424 lp.SchemeStart, 425 scm.OpenParen, 426 scm.Word, 427 )): 428 if isinstance(self.last, scm.Word): 429 self.column = self.lastpos 430 cursor = self.document_cursor() 431 return documentdata.doc(cursor.document()).schemewords() 432 433 def accidental_style(self): 434 """test for \accidentalStyle""" 435 try: 436 i = self.tokens.index("\\accidentalStyle") 437 except ValueError: 438 return 439 self.backuntil(lx.Space, lp.DotPath) 440 tokens = self.tokens[i+1:] 441 tokenclasses = self.tokenclasses()[i+1:] 442 try: 443 i = tokenclasses.index(lp.AccidentalStyleSpecifier) 444 except ValueError: 445 pass 446 else: 447 if lx.Space in tokenclasses[i+1:]: 448 return 449 if lp.ContextName in tokenclasses: 450 return completiondata.lilypond_accidental_styles 451 return completiondata.lilypond_accidental_styles_contexts 452 453 def hide_omit(self): 454 r"""test for \omit and \hide""" 455 indices = [] 456 for t in "\\omit", "\\hide": 457 try: 458 indices.append(self.tokens.index(t, -6)) 459 except ValueError: 460 pass 461 if not indices: 462 return 463 self.backuntil(lx.Space, lp.DotPath) 464 i = max(indices) 465 tokens = self.tokens[i+1:] 466 tokenclasses = self.tokenclasses()[i+1:] 467 if lp.GrobName not in tokenclasses[:-1]: 468 if lp.ContextName in tokenclasses: 469 return completiondata.lilypond_grobs 470 return completiondata.lilypond_contexts_and_grobs 471 472 473 # Mapping from Parsers to the lists of functions to run. 474 tests = { 475 lp.ParseGlobal: ( 476 markup_top, 477 repeat, 478 toplevel, 479 ), 480 lp.ParseBook: ( 481 markup_top, 482 book, 483 ), 484 lp.ParseBookPart: ( 485 markup_top, 486 bookpart, 487 ), 488 lp.ParseScore: ( 489 score, 490 ), 491 lp.ParseMusic: ( 492 markup_top, 493 tweak, 494 scheme_word, 495 key, 496 clef, 497 repeat, 498 accidental_style, 499 hide_omit, 500 revert, 501 general_music, 502 ), 503 lp.ParseNoteMode: ( 504 markup_top, 505 tweak, 506 scheme_word, 507 key, 508 clef, 509 repeat, 510 accidental_style, 511 hide_omit, 512 revert, 513 general_music, 514 ), 515 lp.ParseChordMode: ( 516 markup_top, 517 tweak, 518 scheme_word, 519 key, 520 clef, 521 repeat, 522 accidental_style, 523 hide_omit, 524 revert, 525 general_music, 526 ), 527 lp.ParseDrumMode: ( 528 markup_top, 529 tweak, 530 scheme_word, 531 key, 532 clef, 533 repeat, 534 hide_omit, 535 revert, 536 general_music, 537 ), 538 lp.ParseFigureMode: ( 539 markup_top, 540 tweak, 541 scheme_word, 542 key, 543 clef, 544 repeat, 545 accidental_style, 546 hide_omit, 547 revert, 548 general_music, 549 ), 550 lp.ParseMarkup: ( 551 markup, 552 ), 553 lp.ParseHeader: ( 554 markup_top, 555 header, 556 ), 557 lp.ParsePaper: ( 558 paper, 559 ), 560 lp.ParseLayout: ( 561 accidental_style, 562 hide_omit, 563 layout, 564 ), 565 lp.ParseMidi: ( 566 midi, 567 ), 568 lp.ParseContext: ( 569 engraver, 570 context_variable_set, 571 context, 572 ), 573 lp.ParseWith: ( 574 markup_top, 575 engraver, 576 context_variable_set, 577 with_, 578 ), 579 lp.ParseTranslator: ( 580 translator, 581 ), 582 lp.ExpectTranslatorId: ( 583 translator, 584 ), 585 lp.ParseOverride: ( 586 override, 587 ), 588 lp.ParseRevert: ( 589 override, 590 ), 591 lp.ParseSet: ( 592 set_unset, 593 ), 594 lp.ParseUnset: ( 595 set_unset, 596 ), 597 lp.ParseTweak: ( 598 tweak, 599 ), 600 lp.ParseTweakGrobProperty: ( 601 tweak, 602 ), 603 lp.ParseString: ( 604 engraver, 605 clef, 606 repeat, 607 midi_instrument, 608 include, 609 language, 610 ), 611 lp.ParseClef: ( 612 clef, 613 ), 614 lp.ParseRepeat: ( 615 repeat, 616 ), 617 scm.ParseScheme: ( 618 override, 619 tweak, 620 markup_override, 621 scheme_other, 622 ), 623 scm.ParseString: ( 624 music_glyph, 625 midi_instrument, 626 font_name, 627 ), 628 lp.ParseLyricMode: ( 629 markup_top, 630 repeat, 631 lyricmode, 632 ), 633 lp.ParseAccidentalStyle: ( 634 accidental_style, 635 ), 636 lp.ParseScriptAbbreviationOrFingering: ( 637 accidental_style, 638 ), 639 lp.ParseHideOmit: ( 640 hide_omit, 641 ), 642 lp.ParseGrobPropertyPath: ( 643 revert, 644 ), 645 } 646