1
# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
3
# Copyright (c) 2008, 2009, 2010 by Wilbert Berendsen
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.
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.
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.
20
from __future__ import unicode_literals
23
LilyPond auto completion
28
from PyQt4.QtCore import QModelIndex, Qt
29
from PyQt4.QtGui import QBrush, QColor, QTextFormat
30
from PyKDE4.kdecore import KGlobal
31
from PyKDE4.ktexteditor import KTextEditor
33
import ly.tokenize, ly.version, ly.words, ly.colors
34
import frescobaldi_app.version
35
from frescobaldi_app.mainapp import lilyPondCommand
38
class CompletionHelper(object):
40
Helper class that contains a list of completions.
42
# roles on the Name column
44
KTextEditor.CodeCompletionModel.CompletionRole:
45
KTextEditor.CodeCompletionModel.FirstProperty |
46
KTextEditor.CodeCompletionModel.Public |
47
KTextEditor.CodeCompletionModel.LastProperty,
48
KTextEditor.CodeCompletionModel.ScopeIndex: 0,
49
KTextEditor.CodeCompletionModel.MatchQuality: 10,
50
KTextEditor.CodeCompletionModel.HighlightingMethod: None,
51
KTextEditor.CodeCompletionModel.InheritanceDepth: 0,
54
def __init__(self, model, resultList=None):
56
model is the KTextEditor.CodeCompletionModel helped
60
self.resultList = resultList or []
62
def index(self, row, column, parent):
63
if (row < 0 or row >= len(self.resultList) or
64
column < 0 or column >= KTextEditor.CodeCompletionModel.ColumnCount or
67
return self.model.createIndex(row, column, 0)
69
def rowCount(self, parent):
71
return 0 # Do not make the model look hierarchical
73
return len(self.resultList)
75
def data(self, index, role):
76
if index.column() == KTextEditor.CodeCompletionModel.Name:
77
if role == Qt.DisplayRole:
78
return self.resultList[index.row()]
80
return self.roles[role]
84
def executeCompletionItem(self, doc, word, row):
88
class CompletionList(CompletionHelper):
90
Contains completions presented as a simple list.
92
def executeCompletionItem(self, doc, word, row):
93
text = self.resultList[row]
95
text = text.replace('{}', '{\n(|)\n}')
96
self.model.doc.manipulator().insertTemplate(text, word.start(), word)
100
class VarCompletions(CompletionHelper):
102
List of vars, that get ' = ' after themselves.
104
def executeCompletionItem(self, doc, word, row):
105
text = self.resultList[row]
106
line = doc.line(word.end().line())[word.end().column():]
107
if not line.lstrip().startswith('='):
109
doc.replaceText(word, text)
113
class ColorCompletions(CompletionHelper):
115
Completions with color, that show the color name highlighted
117
roles = CompletionHelper.roles.copy()
119
KTextEditor.CodeCompletionModel.HighlightingMethod:
120
KTextEditor.CodeCompletionModel.CustomHighlighting
123
def data(self, index, role):
124
if index.column() == KTextEditor.CodeCompletionModel.Name:
125
name, (r, g, b) = self.resultList[index.row()]
126
if role == Qt.DisplayRole:
128
elif role == KTextEditor.CodeCompletionModel.CustomHighlight:
129
format = QTextFormat()
130
color = QColor.fromRgbF(r, g, b)
131
format.setBackground(QBrush(color))
132
return [0, len(name), format]
133
return super(ColorCompletions, self).data(index, role)
136
class ExpansionCompletions(CompletionHelper):
138
Looks in the expansions, but skips expansions.
140
def __init__(self, model):
141
self.mgr = model.doc.app.mainwin.expandManager()
142
self.expansions = self.mgr.expansionsList()
143
descriptions = [self.mgr.description(name) for name in self.expansions]
144
result = ['{0} ({1})'.format(e, d) for e, d in zip(self.expansions, descriptions)]
145
super(ExpansionCompletions, self).__init__(model, result)
147
def executeCompletionItem(self, doc, word, row):
148
self.mgr.doExpand(self.expansions[row], word)
152
def getCompletions(model, view, word, invocationType):
154
Returns an object that describes the matches that
155
are useful in the current context.
157
matches = findMatches(model, view, word, invocationType)
158
if isinstance(matches, CompletionHelper):
161
return CompletionList(model, matches)
163
def findMatches(model, view, word, invocationType):
165
Return either a simple list of matches that are useful in the current
166
context, or a CompletionHelper instance that can handle specialized
169
doc = view.document()
170
line, col = word.start().line(), word.start().column()
171
text = doc.line(line)
172
text = text[:col] if text else "" # cause KatePart returns None i.s.o. ""
174
# determine what the user tries to type
175
# very specific situations:
176
if re.search(r'\\(consists|remove)\s*"?$', text):
177
return ly.words.engravers
178
if re.search(r'\bmidiInstrument\s*=\s*#?"$', text):
179
return ly.words.midi_instruments
180
if re.search(r'\\musicglyph\s*#"$', text):
181
return musicglyph_names()
182
if re.search(r'\bmarkFormatter\s*=\s*#$', text):
183
return ly.words.mark_formatters
184
if re.search(r'\\key\s+[a-z]+\s*\\$', text):
185
return ly.words.modes
186
if re.search(r'\\(un)?set\b\s*$', text):
187
return ly.words.contexts + ly.words.contextproperties
188
if re.search(r'\\(new|change|context)\s+$', text):
189
return ly.words.contexts
190
if ly.words.set_context_re.search(text):
191
return ly.words.contextproperties
192
if ly.words.context_re.search(text):
193
return ly.words.grobs
194
if text.endswith("#'"):
195
m = ly.words.grob_re.search(text[:-2])
197
return ly.words.schemeprops(m.group(1))
198
if re.search(r"\\tweak\b\s*$", text[:-2]):
199
return ly.words.schemeprops()
200
if re.search(r"\\(override|revert)\s+$", text):
201
return ly.words.contexts + ly.words.grobs
202
if re.search(r'\\repeat\s+"?$', text):
203
return ly.words.repeat_types
204
if re.search(r'\\clef\s*"$', text):
205
return ly.words.clefs
206
if re.search(r"\\clef\s+$", text):
207
return ly.words.clefs_plain
208
if re.search(r"\bcolor\s*=?\s*#$", text):
209
return ColorCompletions(model, ly.colors.colors_predefined)
210
if re.search(r"\bx11-color\s*'$", text):
211
return ColorCompletions(model, ly.colors.colors_x11)
212
if re.search(r"#'break-visibility\s*=\s*#$", text):
213
return ly.words.break_visibility
214
# parse to get current context
215
fragment = doc.text(KTextEditor.Range(KTextEditor.Cursor(0, 0),
216
word.start())) or "" # KatePart returns None instead of empty string
217
tokenizer = ly.tokenize.Tokenizer()
218
token = None # in case the next loop does not run at all
219
for token in tokenizer.tokens(fragment):
221
# don't bother if we are inside a string or comment
222
if isinstance(token, (tokenizer.String, tokenizer.Comment)):
225
if text.endswith("\\"):
226
if isinstance(tokenizer.parser(), tokenizer.MarkupParser):
227
if tokenizer.parser().token == "\\markuplines":
228
return ly.words.markupcommands + ly.words.markuplistcommands
230
return ly.words.markupcommands
231
commands = (ly.words.keywords + ly.words.keywords_completion
232
+ ly.words.musiccommands + ly.words.musiccommands_completion
234
if tokenizer.parser().token == "\\context":
235
return commands + ly.words.contexts
239
if isinstance(tokenizer.parser(), tokenizer.SchemeParser):
240
# is the last token the scheme-introducing '#' ?
241
if token is tokenizer.parser().token:
242
return ('UP', 'DOWN', 'CENTER', 'LEFT', 'RIGHT')
244
if text.endswith("#("):
245
if tokenizer.parser(-2).token == "\\paper":
246
return ('set-paper-size',)
247
elif text.endswith("#:"):
248
return ly.words.markupcommands
249
elif text.endswith("#(set-accidental-style '"):
250
return ly.words.accidentalstyles
251
return ly.words.schemefuncs
253
if col == 0 or text[-1] in " \t":
254
# all kinds of variables only at start of line or after whitespace
255
# the VarCompletions model can add ' = ' after them
256
if tokenizer.parser().token == "\\header":
257
return VarCompletions(model, ly.words.headervars)
258
if tokenizer.parser().token == "\\paper":
259
return VarCompletions(model, ly.words.papervars)
260
if tokenizer.parser().token == "\\layout":
261
return VarCompletions(model, ly.words.layoutvars)
262
if tokenizer.parser().token in ("\\context", "\\with"):
263
return VarCompletions(model, ly.words.contextproperties)
266
# only on an empty line: show the expansions
267
return ExpansionCompletions(model)
270
# load some (cached) data
271
def musicglyph_names():
272
font = ly.version.LilyPondInstance(lilyPondCommand()).fontInfo("emmentaler-20")
274
return tuple(font.glyphs())
277
def lilyPondVersion():
278
ver = frescobaldi_app.version.defaultVersion()
279
return ('version "{0}"'.format(ver),) if ver else ()