1 /*
2     SPDX-FileCopyrightText: 2007 Hamish Rodda <rodda@kde.org>
3     SPDX-FileCopyrightText: 2008 Vladimir Prus <ghost@cs.msu.su>
4     SPDX-FileCopyrightText: 2009 Niko Sams <niko.sams@gmail.com>
5 
6     SPDX-License-Identifier: GPL-2.0-or-later
7 */
8 
9 #include "variablecollection.h"
10 
11 #include <QFont>
12 #include <QApplication>
13 
14 #include <KColorScheme>
15 #include <KLocalizedString>
16 #include <KTextEditor/TextHintInterface>
17 #include <KTextEditor/Document>
18 #include <KTextEditor/View>
19 
20 #include "../../interfaces/icore.h"
21 #include "../../interfaces/idocumentcontroller.h"
22 #include "../../interfaces/iuicontroller.h"
23 #include "../../sublime/controller.h"
24 #include "../../sublime/view.h"
25 #include "../../interfaces/idebugcontroller.h"
26 #include "../interfaces/idebugsession.h"
27 #include "../interfaces/ivariablecontroller.h"
28 #include <debug.h>
29 #include "util/texteditorhelpers.h"
30 #include "variabletooltip.h"
31 #include <sublime/area.h>
32 
33 namespace KDevelop {
34 
currentSession()35 IDebugSession* currentSession()
36 {
37     return ICore::self()->debugController()->currentSession();
38 }
39 
currentSessionState()40 IDebugSession::DebuggerState currentSessionState()
41 {
42     if (!currentSession()) return IDebugSession::NotStartedState;
43     return currentSession()->state();
44 }
45 
hasStartedSession()46 bool hasStartedSession()
47 {
48     IDebugSession::DebuggerState s = currentSessionState();
49     return s != IDebugSession::NotStartedState && s != IDebugSession::EndedState;
50 }
51 
Variable(TreeModel * model,TreeItem * parent,const QString & expression,const QString & display)52 Variable::Variable(TreeModel* model, TreeItem* parent,
53                    const QString& expression,
54                    const QString& display)
55   : TreeItem(model, parent)
56   , m_expression(expression)
57   , m_inScope(true)
58   , m_topLevel(true)
59   , m_changed(false)
60   , m_showError(false)
61   , m_format(Natural)
62 {
63     // FIXME: should not duplicate the data, instead overload 'data'
64     // and return expression_ directly.
65     if (display.isEmpty())
66         setData(QVector<QVariant>{expression, QString(), QString()});
67     else
68         setData(QVector<QVariant>{display, QString(), QString()});
69 }
70 
expression() const71 QString Variable::expression() const
72 {
73     return m_expression;
74 }
75 
inScope() const76 bool Variable::inScope() const
77 {
78     return m_inScope;
79 }
80 
setValue(const QString & v)81 void Variable::setValue(const QString& v)
82 {
83     itemData[VariableCollection::ValueColumn] = v;
84     reportChange();
85 }
86 
value() const87 QString Variable::value() const
88 {
89     return itemData[VariableCollection::ValueColumn].toString();
90 }
91 
setType(const QString & type)92 void Variable::setType(const QString& type)
93 {
94     itemData[VariableCollection::TypeColumn] = type;
95     reportChange();
96 }
97 
type() const98 QString Variable::type() const
99 {
100     return itemData[VariableCollection::TypeColumn].toString();
101 }
102 
setTopLevel(bool v)103 void Variable::setTopLevel(bool v)
104 {
105     m_topLevel = v;
106 }
107 
setInScope(bool v)108 void Variable::setInScope(bool v)
109 {
110     m_inScope = v;
111     for (int i=0; i < childCount(); ++i) {
112         if (auto *var = qobject_cast<Variable*>(child(i))) {
113             var->setInScope(v);
114         }
115     }
116     reportChange();
117 }
118 
setShowError(bool v)119 void Variable::setShowError (bool v)
120 {
121     m_showError = v;
122     reportChange();
123 }
124 
showError()125 bool Variable::showError()
126 {
127     return m_showError;
128 }
129 
130 
~Variable()131 Variable::~Variable()
132 {
133 }
134 
die()135 void Variable::die()
136 {
137     removeSelf();
138     deleteLater();
139 }
140 
141 
setChanged(bool c)142 void Variable::setChanged(bool c)
143 {
144     m_changed=c;
145     reportChange();
146 }
147 
resetChanged()148 void Variable::resetChanged()
149 {
150     setChanged(false);
151     for (int i=0; i<childCount(); ++i) {
152         TreeItem* childItem = child(i);
153         if (qobject_cast<Variable*>(childItem)) {
154             static_cast<Variable*>(childItem)->resetChanged();
155         }
156     }
157 }
158 
str2format(const QString & str)159 Variable::format_t Variable::str2format(const QString& str)
160 {
161     if(str==QLatin1String("Binary") || str==QLatin1String("binary"))          return Binary;
162     if(str==QLatin1String("Octal") || str==QLatin1String("octal"))            return Octal;
163     if(str==QLatin1String("Decimal") || str==QLatin1String("decimal"))        return Decimal;
164     if(str==QLatin1String("Hexadecimal") || str==QLatin1String("hexadecimal"))return Hexadecimal;
165 
166     return Natural; // maybe most reasonable default
167 }
168 
format2str(format_t format)169 QString Variable::format2str(format_t format)
170 {
171     switch(format) {
172         case Natural:       return QStringLiteral("natural");
173         case Binary:        return QStringLiteral("binary");
174         case Octal:         return QStringLiteral("octal");
175         case Decimal:       return QStringLiteral("decimal");
176         case Hexadecimal:   return QStringLiteral("hexadecimal");
177     }
178     return QString();
179 }
180 
181 
setFormat(Variable::format_t format)182 void Variable::setFormat(Variable::format_t format)
183 {
184     if (m_format != format) {
185         m_format = format;
186         formatChanged();
187     }
188 }
189 
formatChanged()190 void Variable::formatChanged()
191 {
192 }
193 
isPotentialProblematicValue() const194 bool Variable::isPotentialProblematicValue() const
195 {
196     const auto value = data(VariableCollection::ValueColumn, Qt::DisplayRole).toString();
197     return value == QLatin1String("0x0");
198 }
199 
data(int column,int role) const200 QVariant Variable::data(int column, int role) const
201 {
202     if (m_showError) {
203         if (role == Qt::FontRole) {
204             QVariant ret = TreeItem::data(column, role);
205             QFont font = ret.value<QFont>();
206             font.setStyle(QFont::StyleItalic);
207             return font;
208         } else if (column == 1 && role == Qt::DisplayRole) {
209             return i18n("Error");
210         }
211     }
212     if (column == 1 && role == Qt::ForegroundRole)
213     {
214         KColorScheme scheme(QPalette::Active);
215         if (!m_inScope) {
216             return scheme.foreground(KColorScheme::InactiveText).color();
217         } else if (isPotentialProblematicValue()) {
218             return scheme.foreground(KColorScheme::NegativeText).color();
219         } else if (m_changed) {
220             return scheme.foreground(KColorScheme::NeutralText).color();
221         }
222     }
223     if (role == Qt::ToolTipRole) {
224         return TreeItem::data(column, Qt::DisplayRole);
225     }
226 
227     return TreeItem::data(column, role);
228 }
229 
Watches(TreeModel * model,TreeItem * parent)230 Watches::Watches(TreeModel* model, TreeItem* parent)
231 : TreeItem(model, parent), finishResult_(nullptr)
232 {
233     setData(QVector<QVariant>{i18n("Auto"), QString()});
234 }
235 
add(const QString & expression)236 Variable* Watches::add(const QString& expression)
237 {
238     if (!hasStartedSession()) return nullptr;
239 
240     Variable* v = currentSession()->variableController()->createVariable(
241         model(), this, expression);
242     appendChild(v);
243     v->attachMaybe();
244     if (childCount() == 1 && !isExpanded()) {
245         setExpanded(true);
246     }
247     return v;
248 }
249 
addFinishResult(const QString & convenienceVarible)250 Variable *Watches::addFinishResult(const QString& convenienceVarible)
251 {
252     if( finishResult_ )
253     {
254         removeFinishResult();
255     }
256     finishResult_ = currentSession()->variableController()->createVariable(
257         model(), this, convenienceVarible, QStringLiteral("$ret"));
258     appendChild(finishResult_);
259     finishResult_->attachMaybe();
260     if (childCount() == 1 && !isExpanded()) {
261         setExpanded(true);
262     }
263     return finishResult_;
264 }
265 
removeFinishResult()266 void Watches::removeFinishResult()
267 {
268     if (finishResult_)
269     {
270         finishResult_->die();
271         finishResult_ = nullptr;
272     }
273 }
274 
resetChanged()275 void Watches::resetChanged()
276 {
277     for (int i=0; i<childCount(); ++i) {
278         TreeItem* childItem = child(i);
279         if (qobject_cast<Variable*>(childItem)) {
280             static_cast<Variable*>(childItem)->resetChanged();
281         }
282     }
283 }
284 
data(int column,int role) const285 QVariant Watches::data(int column, int role) const
286 {
287 #if 0
288     if (column == 0 && role == Qt::FontRole)
289     {
290         /* FIXME: is creating font again and again efficient? */
291         QFont f = font();
292         f.setBold(true);
293         return f;
294     }
295 #endif
296     return TreeItem::data(column, role);
297 }
298 
reinstall()299 void Watches::reinstall()
300 {
301     for (int i = 0; i < childItems.size(); ++i)
302     {
303         auto* v = static_cast<Variable*>(child(i));
304         v->attachMaybe();
305     }
306 }
307 
Locals(TreeModel * model,TreeItem * parent,const QString & name)308 Locals::Locals(TreeModel* model, TreeItem* parent, const QString &name)
309 : TreeItem(model, parent)
310 {
311     setData(QVector<QVariant>{name, QString()});
312 }
313 
updateLocals(const QStringList & locals)314 QList<Variable*> Locals::updateLocals(const QStringList& locals)
315 {
316     QSet<QString> existing, current;
317     for (int i = 0; i < childItems.size(); i++)
318     {
319         Q_ASSERT(qobject_cast<KDevelop::Variable*>(child(i)));
320         auto* var= static_cast<KDevelop::Variable*>(child(i));
321         existing << var->expression();
322     }
323 
324     for (const QString& var : locals) {
325         current << var;
326         // If we currently don't display this local var, add it.
327         if( !existing.contains( var ) ) {
328             // FIXME: passing variableCollection this way is awkward.
329             // In future, variableCollection probably should get a
330             // method to create variable.
331             Variable* v =
332                 currentSession()->variableController()->createVariable(
333                     ICore::self()->debugController()->variableCollection(),
334                     this, var );
335             appendChild( v, false );
336         }
337     }
338 
339     for (int i = 0; i < childItems.size(); ++i) {
340         auto* v = static_cast<KDevelop::Variable*>(child(i));
341         if (!current.contains(v->expression())) {
342             removeChild(i);
343             --i;
344             // FIXME: check that -var-delete is sent.
345             delete v;
346         }
347     }
348 
349 
350     if (hasMore()) {
351         setHasMore(false);
352     }
353 
354     // Variables which changed just value are updated by a call to -var-update.
355     // Variables that changed type -- likewise.
356 
357     QList<Variable*> ret;
358     ret.reserve(childItems.size());
359     for (TreeItem* i : qAsConst(childItems)) {
360         Q_ASSERT(qobject_cast<Variable*>(i));
361         ret << static_cast<Variable*>(i);
362     }
363     return ret;
364 }
365 
resetChanged()366 void Locals::resetChanged()
367 {
368     for (int i=0; i<childCount(); ++i) {
369         TreeItem* childItem = child(i);
370         if (qobject_cast<Variable*>(childItem)) {
371             static_cast<Variable*>(childItem)->resetChanged();
372         }
373     }
374 }
375 
VariablesRoot(TreeModel * model)376 VariablesRoot::VariablesRoot(TreeModel* model)
377     : TreeItem(model)
378     , m_watches(new Watches(model, this))
379 {
380     appendChild(m_watches, true);
381 }
382 
383 
locals(const QString & name)384 Locals* VariablesRoot::locals(const QString& name)
385 {
386     auto localsIt = m_locals.find(name);
387     if (localsIt == m_locals.end()) {
388         localsIt = m_locals.insert(name, new Locals(model(), this, name));
389         appendChild(*localsIt);
390     }
391     return *localsIt;
392 }
393 
allLocals() const394 QHash<QString, Locals*> VariablesRoot::allLocals() const
395 {
396     return m_locals;
397 }
398 
resetChanged()399 void VariablesRoot::resetChanged()
400 {
401     m_watches->resetChanged();
402     for (Locals* l : qAsConst(m_locals)) {
403         l->resetChanged();
404     }
405 }
406 
VariableCollection(IDebugController * controller)407 VariableCollection::VariableCollection(IDebugController* controller)
408     : TreeModel({i18n("Name"), i18n("Value"), i18n("Type")}, controller)
409     , m_widgetVisible(false)
410     , m_textHintProvider(this)
411 {
412     m_universe = new VariablesRoot(this);
413     setRootItem(m_universe);
414 
415     //new ModelTest(this);
416 
417     connect (ICore::self()->documentController(),
418          &IDocumentController::textDocumentCreated,
419          this,
420          &VariableCollection::textDocumentCreated );
421 
422     connect(controller, &IDebugController::currentSessionChanged,
423              this, &VariableCollection::updateAutoUpdate);
424 
425     // Qt5 signal slot syntax does not support default arguments
__anon64739e870102null426     auto callUpdateAutoUpdate = [&] { updateAutoUpdate(); };
427 
428     connect(locals(), &Locals::expanded, this, callUpdateAutoUpdate);
429     connect(locals(), &Locals::collapsed, this, callUpdateAutoUpdate);
430     connect(watches(), &Watches::expanded, this, callUpdateAutoUpdate);
431     connect(watches(), &Watches::collapsed, this, callUpdateAutoUpdate);
432 }
433 
variableWidgetHidden()434 void VariableCollection::variableWidgetHidden()
435 {
436     m_widgetVisible = false;
437     updateAutoUpdate();
438 }
439 
variableWidgetShown()440 void VariableCollection::variableWidgetShown()
441 {
442     m_widgetVisible = true;
443     updateAutoUpdate();
444 }
445 
updateAutoUpdate(IDebugSession * session)446 void VariableCollection::updateAutoUpdate(IDebugSession* session)
447 {
448     if (!session) session = currentSession();
449     qCDebug(DEBUGGER) << session;
450     if (!session) return;
451 
452     if (!m_widgetVisible) {
453         session->variableController()->setAutoUpdate(IVariableController::UpdateNone);
454     } else {
455         QFlags<IVariableController::UpdateType> t = IVariableController::UpdateNone;
456         if (locals()->isExpanded()) t |= IVariableController::UpdateLocals;
457         if (watches()->isExpanded()) t |= IVariableController::UpdateWatches;
458         session->variableController()->setAutoUpdate(t);
459     }
460 }
461 
~VariableCollection()462 VariableCollection::~ VariableCollection()
463 {
464     for (auto* view : qAsConst(m_textHintProvidedViews)) {
465         auto* iface = qobject_cast<KTextEditor::TextHintInterface*>(view);
466         iface->unregisterTextHintProvider(&m_textHintProvider);
467     }
468 }
469 
textDocumentCreated(IDocument * doc)470 void VariableCollection::textDocumentCreated(IDocument* doc)
471 {
472   connect( doc->textDocument(),
473        &KTextEditor::Document::viewCreated,
474        this, &VariableCollection::viewCreated );
475 
476   const auto views = doc->textDocument()->views();
477   for (KTextEditor::View* view : views) {
478     viewCreated( doc->textDocument(), view );
479   }
480 }
481 
viewCreated(KTextEditor::Document * doc,KTextEditor::View * view)482 void VariableCollection::viewCreated(KTextEditor::Document* doc,
483                                      KTextEditor::View* view)
484 {
485     Q_UNUSED(doc);
486     using namespace KTextEditor;
487     auto* iface = qobject_cast<TextHintInterface*>(view);
488     if( !iface )
489         return;
490 
491     if (m_textHintProvidedViews.contains(view)) {
492         return;
493     }
494     connect(view, &View::destroyed, this, [this, view](QObject* obj) {
495         Q_ASSERT(obj == view);
496         m_textHintProvidedViews.removeOne(view);
497     });
498 
499     iface->registerTextHintProvider(&m_textHintProvider);
500     m_textHintProvidedViews.append(view);
501 }
502 
locals(const QString & name) const503 Locals* VariableCollection::locals(const QString &name) const
504 {
505     return m_universe->locals(name.isEmpty() ? i18n("Locals") : name);
506 }
507 
VariableProvider(VariableCollection * collection)508 VariableProvider::VariableProvider(VariableCollection* collection)
509     : KTextEditor::TextHintProvider()
510     , m_collection(collection)
511 {
512 }
513 
textHint(KTextEditor::View * view,const KTextEditor::Cursor & cursor)514 QString VariableProvider::textHint(KTextEditor::View* view, const KTextEditor::Cursor& cursor)
515 {
516     if (!hasStartedSession())
517         return QString();
518 
519     if (ICore::self()->uiController()->activeArea()->objectName() != QLatin1String("debug"))
520         return QString();
521 
522     //TODO: These keyboardModifiers should also hide already opened tooltip, and show another one for code area.
523     if (QApplication::keyboardModifiers() == Qt::ControlModifier ||
524         QApplication::keyboardModifiers() == Qt::AltModifier){
525         return QString();
526     }
527 
528     KTextEditor::Document* doc = view->document();
529 
530     KTextEditor::Range expressionRange = currentSession()->variableController()->expressionRangeUnderCursor(doc, cursor);
531 
532     if (!expressionRange.isValid())
533         return QString();
534     QString expression = doc->text(expressionRange).trimmed();
535 
536     // Don't do anything if there's already an open tooltip with matching range
537     if (m_collection->m_activeTooltip && m_collection->m_activeTooltip->variable()->expression() == expression)
538         return QString();
539     if (expression.isEmpty())
540         return QString();
541 
542     QPoint local = view->cursorToCoordinate(cursor);
543     QPoint global = view->mapToGlobal(local);
544     QWidget* w = view->childAt(local);
545     if (!w)
546         w = view;
547 
548     m_collection->m_activeTooltip = new VariableToolTip(w, global+QPoint(30,30), expression);
549     m_collection->m_activeTooltip->setHandleRect(KTextEditorHelpers::itemBoundingRect(view, expressionRange));
550     return QString();
551 }
552 
553 }
554 
555