def change_indent(cursor, direction): """Changes the indent in the desired direction (-1 for left and +1 for right). Returns True if the indent operation was applied. The cursor may contain a selection. """ # get some variables from the document indent_vars = indent_variables(cursor.document()) blocks = list(cursortools.blocks(cursor)) block = blocks[0] pos = cursor.selectionStart() - block.position() token = tokeniter.tokens(block)[0] if tokeniter.tokens(block) else None if cursor.hasSelection() or pos == 0 or (token and isinstance(token, ly.lex.Space) and token.end >= pos): # decrease the indent state = tokeniter.state(block) current_indent = get_indent(block) new_indent = current_indent + direction * indent_vars['indent-width'] if state.mode() in ('lilypond', 'scheme'): computed_indent = compute_indent(block) if cmp(computed_indent, new_indent) == direction: new_indent = computed_indent diff = new_indent - current_indent with cursortools.compress_undo(cursor): for block in blocks: set_indent(block, get_indent(block) + diff) return True
def html_selection(self, cursor): """Return HTML for the cursor's selection.""" d = cursor.document() start = d.findBlock(cursor.selectionStart()) startpos = cursor.selectionStart() - start.position() end = d.findBlock(cursor.selectionEnd()) endpos = cursor.selectionEnd() - end.position() html = [] # first block, skip tokens before selection block = start source = iter(tokeniter.tokens(block)) for t in source: if t.end > startpos: startslice = max(0, startpos - t.pos) endslice = None if block == end and t.end > endpos: endslice = endpos - t.pos html.append(self.html_for_token(t[startslice:endslice], type(t))) break while block != end: html.extend(map(self.html_for_token, source)) html.append('\n') block = block.next() source = iter(tokeniter.tokens(block)) # last block, go to end of selection for t in source: if t.end > endpos: if t.pos < endpos: html.append(self.html_for_token(t[:endpos-t.pos], type(t))) break html.append(self.html_for_token(t)) return self.html_wrapper("".join(html))
def reformat(cursor): """Reformats the selection or the whole document, adjusting the whitespace.""" def newlinebefore(t): editor.insertText(tokeniter.cursor(block, t, end=0), '\n') def newlineafter(t): editor.insertText(tokeniter.cursor(block, t, start=len(t)), '\n') indent_vars = indent.indent_variables(cursor.document()) with cursortools.compress_undo(cursor): indent.re_indent(cursor) with cursortools.Editor() as editor: for block in get_blocks(cursor): denters = [] tokens = tokeniter.tokens(block) nonspace_index = -1 for i, t in enumerate(tokens): if isinstance(t, ly.lex.Indent) and t in ('{', '<<'): denters.append(i) elif isinstance(t, ly.lex.Dedent) and t in ('}', '>>'): if denters: denters.pop() elif nonspace_index != -1: newlinebefore(t) elif not isinstance(t, ly.lex.Space): nonspace_index = i for i in denters: if i < nonspace_index: newlineafter(tokens[i]) # TODO: wrap long lines indent.re_indent(cursor) with cursortools.Editor() as editor: for block in get_blocks(cursor): tokens = tokeniter.tokens(block) if (len(tokens) == 2 and isinstance(tokens[0], ly.lex.Space) and isinstance(tokens[1], ( ly.lex.lilypond.LineComment, ly.lex.scheme.LineComment)) and len(tokens[1]) > 2 and len(set(tokens[1][:3])) == 1): # move commented lines with more than 2 comment characters # to column 0 editor.removeSelectedText(tokeniter.cursor(block, tokens[0])) else: # remove trialing whitespace for t in tokens[::-1]: if isinstance(t, ly.lex.Space): editor.removeSelectedText(tokeniter.cursor(block, t)) else: break
def tokens(cursor): """Yield the tokens tuple for every block from the beginning of the document until the cursor.""" end = cursor.block() block = cursor.document().firstBlock() while block < end: yield tokeniter.tokens(block) block = block.next()
def actionTriggered(self, name): # convert arpeggio_normal to arpeggioNormal, etc. name = _arpeggioTypes[name] cursor = self.mainwindow().textCursor() # which arpeggio type is last used? lastused = '\\arpeggioNormal' types = set(_arpeggioTypes.values()) block = cursor.block() while block.isValid(): s = types.intersection(tokeniter.tokens(block)) if s: lastused = s.pop() break block = block.previous() # where to insert c = lydocument.cursor(cursor) c.select_end_of_block() with cursortools.compress_undo(cursor): for item in ly.rhythm.music_items(c, partial=ly.document.OUTSIDE): c = QTextCursor(cursor.document()) c.setPosition(item.end) c.insertText('\\arpeggio') if name != lastused: cursortools.strip_indent(c) indent = c.block().text()[:c.position() - c.block().position()] c.insertText(name + '\n' + indent) # just pick the first place return
def fold_events(self, block): """Provides folding information by looking at indent/dedent tokens.""" for t in tokeniter.tokens(block): if isinstance(t, ly.lex.Indent): yield widgets.folding.START elif isinstance(t, ly.lex.Dedent): yield widgets.folding.STOP
def open_file_at_cursor(cursor, mainwin): """Opens the filename mentioned at the text cursor.""" # take either the selection or the include-args found by ly.parse if cursor.hasSelection(): fnames = [cursor.selection().toPlainText()] else: fnames = list(ly.parse.includeargs(iter(tokeniter.tokens(cursor.block())))) # detemine search path: doc dir and other include path names filename = cursor.document().url().toLocalFile() if filename: path = [os.path.dirname(filename)] else: path = [] path.extend(documentinfo.info(cursor.document()).includepath()) # load all docs, trying all include paths d = None for f in fnames: for p in path: name = os.path.normpath(os.path.join(p, f)) if os.access(name, os.R_OK): d = mainwin.openUrl(QUrl.fromLocalFile(name)) break if d: mainwin.setCurrentDocument(d, True)
def actionTriggered(self, name): # convert arpeggio_normal to arpeggioNormal, etc. name = _arpeggioTypes[name] cursor = self.mainwindow().textCursor() # which arpeggio type is last used? lastused = '\\arpeggioNormal' types = set(_arpeggioTypes.values()) block = cursor.block() while block.isValid(): s = types.intersection(tokeniter.tokens(block)) if s: lastused = s.pop() break block = block.previous() # where to insert c = lydocument.cursor(cursor) c.select_end_of_block() source = lydocument.Source(c, True, ly.document.OUTSIDE, True) with cursortools.compress_undo(cursor): for p in ly.rhythm.music_tokens(source): c = source.cursor(p[-1], start=len(p[-1])) c.insertText('\\arpeggio') if name != lastused: cursortools.strip_indent(c) indent = c.block().text()[:c.position()-c.block().position()] c.insertText(name + '\n' + indent) # just pick the first place return
def fold_events(self, block): """Provides folding information by looking at indent/dedent tokens.""" for t in tokeniter.tokens(block): if isinstance(t, (ly.lex.Indent, ly.lex.BlockCommentStart)): yield widgets.folding.START elif isinstance(t, (ly.lex.Dedent, ly.lex.BlockCommentEnd)): yield widgets.folding.STOP
def actionTriggered(self, name): # convert arpeggio_normal to arpeggioNormal, etc. name = _arpeggioTypes[name] cursor = self.mainwindow().textCursor() # which arpeggio type is last used? lastused = '\\arpeggioNormal' types = set(_arpeggioTypes.values()) block = cursor.block() while block.isValid(): s = types.intersection(tokeniter.tokens(block)) if s: lastused = s.pop() break block = block.previous() # where to insert source = tokeniter.Source.from_cursor(cursor, True, -1) with cursortools.compress_undo(cursor): for p in music.music_items(source, tokens=source.tokens): c = source.cursor(p[-1], start=len(p[-1])) c.insertText('\\arpeggio') if name != lastused: cursortools.strip_indent(c) import indent indent.insert_text(c, name + '\n') # just pick the first place return
def back(cursor): """Yields per-block token iters in backward direction from the cursor.""" yield reversed(tokeniter.partition(cursor).left) block = cursor.block() while block.previous().isValid(): block = block.previous() yield reversed(tokeniter.tokens(block))
def cut_assign(cursor): """Cuts selected text and assigns it to a LilyPond variable.""" # ask the variable name name = inputdialog.getText(None, _("Cut and Assign"), _( "Please enter the name for the variable to assign the selected " "text to:"), regexp="[A-Za-z]+") if not name: return cursortools.strip_selection(cursor) # determine state at cursor block = cursortools.block(cursor) state = tokeniter.state(block) for t in tokeniter.partition(cursor).left: state.follow(t) mode = "" for p in state.parsers(): if isinstance(p, ly.lex.lilypond.ParseInputMode): if isinstance(p, ly.lex.lilypond.ParseLyricMode): mode = " \\lyricmode" elif isinstance(p, ly.lex.lilypond.ParseChordMode): mode = " \\chordmode" elif isinstance(p, ly.lex.lilypond.ParseFigureMode): mode = " \\figuremode" elif isinstance(p, ly.lex.lilypond.ParseDrumMode): mode = " \\drummode" break # find insertion place: found = False while block.previous().isValid(): block = block.previous() state = tokeniter.state(block) if isinstance(state.parser(), ly.lex.lilypond.ParseGlobal): found = True break tokens = tokeniter.tokens(block) for t in tokens: if isinstance(t, ly.lex.lilypond.Name): found = True break elif not isinstance(t, (ly.lex.Space, ly.lex.Comment)): break if found: break insert = QTextCursor(block) text = cursor.selection().toPlainText() space = '\n' if '\n' in text else ' ' text = ''.join((name, ' =', mode, ' {', space, text, space, '}\n\n')) with cursortools.compress_undo(cursor): cursor.insertText('\\' + name) pos = insert.selectionStart() insert.insertText(text) if metainfo.info(cursor.document()).auto_indent: insert.setPosition(pos, QTextCursor.KeepAnchor) with cursortools.compress_undo(insert, True): indent.re_indent(insert)
def indentable(cursor): """Returns True if the cursor is at a dedent token and running the auto-indenter makes sense.""" block = cursor.block() pos = cursor.position() - block.position() for token in tokeniter.tokens(block): if token.end >= pos: return isinstance(token, (ly.lex.Dedent, ly.lex.BlockCommentEnd)) elif not isinstance(token, (ly.lex.Space, ly.lex.Dedent)): return
def get_indent(block): """Returns the indent of the given block.""" # get some variables from the document indent_vars = indent_variables(block.document()) tokens = tokeniter.tokens(block) if tokens and isinstance(tokens[0], ly.lex.Space): return column_position(tokens[0], tabwidth = indent_vars['tab-width']) return 0
def names(cursor): """Harvests names from assignments until the cursor.""" end = cursor.block() block = cursor.document().firstBlock() while block.isValid() and block != end: for t in tokeniter.tokens(block)[:2]: if type(t) is ly.lex.lilypond.Name: yield t break block = block.next()
def get_definition(cursor): block = cursor.block() while block.isValid(): state = tokeniter.state(block) if isinstance(state.parser(), ly.lex.lilypond.ParseGlobal): for t in tokeniter.tokens(block)[:2]: if type(t) is ly.lex.lilypond.Name: return t[:] elif isinstance(t, ly.lex.lilypond.Keyword) and t == '\\score': return '\\score' block = block.previous()
def state(cursor): """Returns the simplestate string for the position of the cursor.""" import simplestate pos = cursor.selectionStart() block = cursor.document().findBlock(pos) tokens = tokeniter.tokens(block) state = tokeniter.state(block) column = pos - block.position() for t in tokens: if t.end > column: break state.follow(t) return simplestate.state(state)
def globalStaffSize(self, default=20): """Returns the global staff size, if set, else the default value.""" for block in cursortools.all_blocks(self.document()): tokens = tokeniter.tokens(block) try: i = tokens.index('set-global-staff-size') except ValueError: continue try: return int(tokens[i+2], 10) except (IndexError, ValueError): pass return default
def get_definition(cursor): """Return the variable name the cursor's music expression is assigned to. If the music is in a \\score instead, "\\score" is returned. Returns None if no variable name can be found. """ block = cursor.block() while block.isValid(): state = tokeniter.state(block) if isinstance(state.parser(), ly.lex.lilypond.ParseGlobal): for t in tokeniter.tokens(block)[:2]: if type(t) is ly.lex.lilypond.Name: return t[:] elif isinstance(t, ly.lex.lilypond.Keyword) and t == '\\score': return '\\score' block = block.previous()
def pitchLanguage(self): """Returns the pitchname language used in the document, if defined.""" languages = ly.pitch.pitchInfo.keys() for block in cursortools.all_blocks(self.document()): tokens = tokeniter.tokens(block) try: i = tokens.index('\\language') except ValueError: try: i = tokens.index('\\include') except ValueError: continue if isinstance(tokens[i], ly.lex.lilypond.Keyword): for t in tokens[i+1:]: if isinstance(t, ly.lex.Space): continue elif t == '"': continue lang = t[:-3] if t.endswith('.ly') else t[:] if lang in languages: return lang
def set_indent(block, indent): """Sets the indent of block to tabs/spaces of length indent. Does not change the document if the indent does not need a change. Returns True if the indent was changed. """ # get some variables from the document indent_vars = indent_variables(block.document()) space = make_indent(indent, indent_vars['tab-width'], indent_vars['indent-tabs']) tokens = tokeniter.tokens(block) if tokens and isinstance(tokens[0], ly.lex.Space): changed = tokens[0] != space cursor = tokeniter.cursor(block, tokens[0]) else: changed = indent != 0 cursor = QTextCursor(block) if changed: cursor.insertText(space) return changed
def analyze(self, cursor): """Do the analyzing work; set the attributes column and model.""" self.cursor = cursor block = cursor.block() self.column = column = cursor.position() - block.position() self.text = text = block.text()[:column] self.model = None # make a list of tokens exactly ending at the cursor position # and let state follow state = self.state = tokeniter.state(block) tokens = self.tokens = [] for t in tokeniter.tokens(cursor.block()): if t.end > column: # cut off the last token and run the parser on it tokens.extend(state.tokens(text, t.pos)) break tokens.append(t) state.follow(t) if t.end == column: break self.last = tokens[-1] if tokens else '' self.lastpos = self.last.pos if self.last else column parser = state.parser() # Map the parser class to a group of tests to return the model. # Try the tests until a model is returned. try: tests = self.tests[parser.__class__] except KeyError: return else: for function in tests: model = function(self) if model: self.model = model return
def insertLanguage(document, language): """Inserts a language command in the document. The command is inserted at the top or just below the version line. If the document uses LilyPond < 2.13.38, the \\include command is used, otherwise the newer \\language command. """ version = (documentinfo.info(document).version() or lilypondinfo.preferred().version()) if version and version < (2, 13, 38): text = '\\include "{0}.ly"' else: text = '\\language "{0}"' # insert language command on top of file, but below version block = document.firstBlock() c = QTextCursor(block) if '\\version' in tokeniter.tokens(block): c.movePosition(QTextCursor.EndOfBlock) text = '\n' + text else: text += '\n' c.insertText(text.format(language))
def tokens(self, block): """Return the tuple of tokens of the specified block.""" return tokeniter.tokens(block)
def cut_assign(cursor): """Cuts selected text and assigns it to a LilyPond variable.""" # ask the variable name name = inputdialog.getText( None, _("Cut and Assign"), _("Please enter the name for the variable to assign the selected " "text to:"), regexp="[A-Za-z]+") if not name: return cursortools.strip_selection(cursor) # determine state at cursor block = cursortools.block(cursor) state = tokeniter.state(block) for t in tokeniter.partition(cursor).left: state.follow(t) mode = "" for p in state.parsers(): if isinstance(p, ly.lex.lilypond.ParseInputMode): if isinstance(p, ly.lex.lilypond.ParseLyricMode): mode = " \\lyricmode" elif isinstance(p, ly.lex.lilypond.ParseChordMode): mode = " \\chordmode" elif isinstance(p, ly.lex.lilypond.ParseFigureMode): mode = " \\figuremode" elif isinstance(p, ly.lex.lilypond.ParseDrumMode): mode = " \\drummode" break # find insertion place: found = False while block.previous().isValid(): block = block.previous() state = tokeniter.state(block) if isinstance(state.parser(), ly.lex.lilypond.ParseGlobal): found = True break tokens = tokeniter.tokens(block) for t in tokens: if isinstance(t, ly.lex.lilypond.Name): found = True break elif not isinstance(t, (ly.lex.Space, ly.lex.Comment)): break if found: break insert = QTextCursor(block) text = cursor.selection().toPlainText() space = '\n' if '\n' in text else ' ' text = ''.join((name, ' =', mode, ' {', space, text, space, '}\n\n')) with cursortools.compress_undo(cursor): cursor.insertText('\\' + name) pos = insert.selectionStart() insert.insertText(text) if metainfo.info(cursor.document()).auto_indent: insert.setPosition(pos, QTextCursor.KeepAnchor) with cursortools.compress_undo(insert, True): indent.re_indent(insert)
def compute_indent(block): """Returns the indent the given block should have.""" # get some variables from the document indent_vars = indent_variables(block.document()) # count the dedent tokens at the beginning of the block indents = 0 for token in tokeniter.tokens(block): # dont dedent scheme dedent tokens at beginning of lines (unusual) if isinstance(token, ly.lex.Dedent) and not isinstance(token, ly.lex.scheme.CloseParen): indents -= 1 elif not isinstance(token, ly.lex.Space): break # these variables control the position (yet to be translated to tabbed (real) columns) # and how much to add indent_pos = None indent_add = 0 # look backwards for the token that starts this indent prev = block.previous() while prev.isValid(): # skip empty blocks if prev.length() <= 1: prev = prev.previous() continue closers = 0 found = False lasttokens = [] token = None # in case of empty line for token in reversed(tokeniter.tokens(prev)): if isinstance(token, ly.lex.Dedent): indents -= 1 if isinstance(token, ly.lex.scheme.CloseParen): closers = 0 # scheme close parens are not moved else: closers += 1 elif isinstance(token, ly.lex.Indent): indents += 1 closers = 0 if not found: if indents == 1: found = token else: lasttokens.append(token) elif not isinstance(token, ly.lex.Space): closers = 0 if not found: lasttokens.append(token) if found: # the token that started the current indent has been found # if there are no tokens after the indent-opener, take indent of current line and increase, # else set indent to the same indent of the token after the indent-opener. if isinstance(found, ly.lex.scheme.OpenParen): # scheme if lasttokens: if len(lasttokens) == 1 or isinstance(lasttokens[-1], ly.lex.Indent): indent_pos = lasttokens[-1].pos elif lasttokens[-1] in scheme_sync_args: indent_pos = lasttokens[-2].pos else: indent_pos = found.pos indent_add = indent_vars['indent-width'] else: indent_pos = found.pos indent_add = 1 else: # no scheme (lilypond) if lasttokens: indent_pos = lasttokens[-1].pos else: # just use current indent + INDENT_WIDTH indent_pos = token.end if isinstance(token, ly.lex.Space) else 0 indent_add = indent_vars['indent-width'] elif indents + closers == 0: # take over indent of current line indent_pos = token.end if isinstance(token, ly.lex.Space) else 0 else: prev = prev.previous() continue # translate indent to real columns (expanding tabs) return column_position(prev.text(), indent_pos, indent_vars['tab-width']) + indent_add # e.g. on first line return 0
def tokens(): end = cursor.block() block = cursor.document().firstBlock() while block < end: yield tokeniter.tokens(block) block = block.next()
def html(): block = doc.firstBlock() while block.isValid(): yield "".join(map(self.html_for_token, tokeniter.tokens(block))) block = block.next()