Coding is a very pleasant experience, but writing about the code is at least as pleasant, especially when there are many things to show off!
This week-end, I finished the support for import statements in the QML/JS KDevelop language support plugin. After that, I decided to have a look at how code-completion works. In fact, one of the nicest feature of KDevelop is its code completion, and having it right is very important. This blog post will therefore explain how I implemented several features concerning the code-completion of QML files. For those who prefer to see things instead of reading a technical description of them, I have put several screenshots that show how everything fits together.
Note that all I will show in this blog post is still very experimental and has not even yet been subject to review requests. I still need to clean up the code (and the Git history, full of commits partly reverting what others have done), and after that I hope to have all of this merged into master.
Namespaces and import statements
Last week, I implemented the support for plugin.qmltypes
files. These files contain the complete description of the contents of a QML module, and can be obtained by running qmlplugindump
on the module. This is needed because QML modules are distributed in binary form (most of them are shipped by Qt and are implemented in C++). QML does not have a notion of "header files" like other languages have, and unlike Python, the complete source-code of modules is not available.
Parsing these plugin.qmltypes
files was fairly easy as they are very complete and contain all the information needed. Moreover, they are valid QML files, so I did not have to implement a new parser for them. When the user imports a QML module, the plugin finds its plugin.qmltypes
file (the plugin ships module files for all the QML modules found in Qt 5), and parses it. The module file contains many declarations (classes, methods, properties, enums, etc), and the plugin puts all of them in a namespace.
In order for at least every C++ developer to be able to understand what I will explain, I will always show how the QML/JS parser sees things using a C++-ish syntax. This way, you don't have to be familiar with specific KDevelop concepts, like definition-use chains, declarations and contexts. You can simply think of the QML/JS plugin as a mean to convert QML into a subset of C++ that is somewhat understood by KDevelop. Note that the conversion does not have to be exact because the goal is not to execute QML in KDevelop, but only to display nice code-completion popups and have a great syntax highlighting.
So, my first example is an import statement. When the QML/JS plugin sees an import statement, it parses the corresponding module file and puts all of its declarations in a namespace. As QML does not support namespaces (you use Button
, not QtQuick.Controls.Button
), a "namespace alias" needs to be used. Namespace alias are simply "using" clauses in C++.
1 | import QtQuick.Controls
|
Becomes (note that namespaces with a dot in their name is not valid C++, but the DUChain supports such namespaces):
1 2 3 4 5 | namespace QtQuick.Controls {
#include "$DATA_DIR/kdevqmljssupport/QtQuick.Controls.qml"
}
using namespace QtQuick.Controls;
|
Code completion for namespaces
When a QML module is parsed and correctly imported, everything it declares becomes available in the current file. The previous section explained how it works, now it is time for a screenshot:
A "wrapper" is a C++ class exposed to the QML world (for instance QQmlAnchors). A "component" is an actual QML component that can be instantiated. In the screenshot above, it happens that every wrapper has a associated component (for instantiation), but most of the time, the wrapper (QQmlItem) and the component (Item) don't have the same name. There is even a large bunch of wrappers without an associated component (QQmlMargins for instance, the type of "margins"; you never instantiate a margin).
When the user uses a QML component, KDevelop is now able to display from where it comes (and even where its declaration lies in the module file, even if it is useless):
Here, you see that Label is an interface inherited by Label (another one ;-) ), and inheriting from QQuickText. Why so many classes? Because QQuickText (an interface) is a wrapper around the C++ QQuickText class, Label (the interface) is the wrapper around the Label C++ class, and Label (the class) is the actual QML component. I still have to make KDevelop resolve "Label" as the component, not the wrapper, but it is a small technical detail.
The class hierarchy
Nice. Now the user is able to see the list of all the QML components that he/she can use. It's already very useful, but more can be done. The second new feature of the QML/JS plugin is code-completion for the signals, slots, methods, properties and enumerations of all the standard QML components. This feature is allowed by the fact that plugin.qmltypes
files describe everything in great detail (it even gives the types of function parameters, and whether a property is read-only or not!)
The problem is that using this information is difficult, because the object hierarchy in QML is a bit more complex than it looks. For instance, if I create a QML component that inherits from Button, I can see all the attributes of Button. Now, I can add new properties and attributes to my custom component. If I give a name to my button and someone else uses it, this person must be able to see the properties of Button and the properties I have added.
Let's look at an example:
1 2 3 4 5 | Button {
id: my_button
text: "Hello!"
property int foo
}
|
Now, an object named "foo" is available, and it has all the properties of Button, plus the one that I've just declared. The QML/JS parser handles that by using an anonymous class. This class has no name and inherits from Button, and it overloads its text property and adds a "foo" property. Now, my_button
inherits from this anonymous class, and everythings starts to work.
1 2 3 4 5 6 | class __anon : public Button {
string text;
int foo;
}
__anon my_button;
|
Things are in fact a bit harder than that, because "my_button" is visible in the QML component that I declare. For instance, I can have a QML component named "my_button" that contains a Loader that has "my_button" as proxy, or something like that. Moreover, QML component names are global, and they can be used even before they are declared. The parser has to handle that.
Code completion for QML components
Now that every QML component knows who its parents are, it is easy to add code-completion to them. The user can therefore see which properties, signals, slots, enumerations and methods are available in the QML component being edited:
There is still one problem to fix: QML component instances like "my_button" in my previous example inherit from an anonymous class. This means that their type is a class having no name. By default, KDevelops identifies my_button as "
I have not yet found a solution of this problem for the tooltip shown in the code editor (but as eveything is customizable in KDevelop, a solution should exist somewhere). However, the code-completion list is fully and easily customisable.
When the code-completion list has to display a QML component instance, the QML/JS plugin looks at the anonymous class of the QML component that has to be displayed, and replaces "
This ends my week-end of work. I now have to cleanup all of this and to have it merged into master. After that, I will work on more advanced QML constructs, like behaviors ("Behavior on ... { }") and animations, and more advanced code-completion (being able to type "my_button." and to see all the properties of my_button, for instance). Then it will be high time to give some love to Javascript arrays. Finally, I would like to be able to have this kind of code-completion:
1 2 3 4 5 6 7 8 9 10 11 | Column {
Text {
id: text
}
Button {
text: "Click me!"
onClick: {
text. // List of all the methods of text
}
}
}
|