// SPDX-License-Identifier: LGPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of libnymea-app. * * libnymea-app is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * libnymea-app is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnymea-app. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "codecompletion.h" #include "engine.h" #include "types/interfaces.h" #include "types/interface.h" #include #include #include #include CodeCompletion::CodeCompletion(QObject *parent): QObject(parent) { m_classes.insert("Item", ClassInfo("Item", {"id"})); m_classes.insert("ThingAction", ClassInfo("ThingAction", {"id", "thingId", "actionTypeId", "actionName"}, {}, {"execute"}, {"onExecuted"})); m_classes.insert("ThingState", ClassInfo("ThingState", {"id", "thingId", "stateTypeId", "stateName", "value"}, {"minimumValue", "maximumValue"}, {}, {"onValueChanged"})); m_classes.insert("ThingEvent", ClassInfo("ThingEvent", {"id", "thingId", "eventTypeId", "eventName"}, {}, {}, {"onTriggered"})); m_classes.insert("InterfaceAction", ClassInfo("InterfaceAction", {"id", "interfaceName", "actionName"}, {}, {"execute"})); m_classes.insert("InterfaceEvent", ClassInfo("InterfaceEvent", {"id", "interfaceName", "eventName"}, {}, {}, {"onTriggered"})); m_classes.insert("InterfaceState", ClassInfo("InterfaceState", {"id", "interfaceName", "stateName"}, {}, {}, {"onStateChanged"})); m_classes.insert("Thing", ClassInfo("Thing", {"id", "thingId"}, {"name"}, {"executeAction", "setStateValue"}, {"onEventTriggered", "onStateValueChanged", "onActionExecuted"})); m_classes.insert("Things", ClassInfo("Things", {"id", "filterInterface"}, {"count"}, {"get", "getThing"}, {"onThingAdded", "onThingRemoved", "onCountChanged"})); m_classes.insert("Timer", ClassInfo("Timer", {"id", "interval", "running", "repeat"}, {}, {"start", "stop", "restart"}, {"onTriggered"})); m_classes.insert("Alarm", ClassInfo("Alarm", {"id", "time", "endTime", "weekDays"}, {"active"}, {}, {"onTriggered", "onActiveChanged"})); m_classes.insert("PropertyAnimation", ClassInfo("PropertyAnimation", {"id", "target", "targets", "property", "properties", "value", "from", "to", "easing", "exclude", "duration", "alwaysRunToEnd", "loops"}, {"paused", "running"}, {"start", "stop", "pause", "resume", "complete"}, {"onStarted", "onStopped", "onFinished", "onRunningChanged"})); m_classes.insert("ColorAnimation", ClassInfo("ColorAnimation", {"id", "target", "targets", "property", "properties", "value", "from", "to", "easing", "exclude", "duration", "alwaysRunToEnd", "loops"}, {"paused", "running"}, {"start", "stop", "pause", "resume", "complete"}, {"onStarted", "onStopped", "onFinished", "onRunningChanged"})); m_classes.insert("SequentialAnimation", ClassInfo("SequentialAnimation", {"id", "alwaysRunToEnd", "loops"}, {"paused", "running"}, {"start", "stop", "pause", "resume", "complete"}, {"onStarted", "onStopped", "onFinished", "onRunningChanged"})); m_classes.insert("ParallelAnimation", ClassInfo("ParallelAnimation", {"id", "alwaysRunToEnd", "loops"}, {"paused", "running"}, {"start", "stop", "pause", "resume", "complete"}, {"onStarted", "onStopped", "onFinished", "onRunningChanged"})); m_classes.insert("PauseAnimation", ClassInfo("PauseAnimation", {"id", "duration", "alwaysRunToEnd", "loops"}, {"paused", "running"}, {"start", "stop", "pause", "resume", "complete"}, {"onStarted", "onStopped", "onFinished", "onRunningChanged"})); m_classes.insert("ListModel", ClassInfo("ListModel", {}, {"count"}, {"clear", "append", "get"}, {})); m_classes.insert("ListElement", ClassInfo("ListElement", {}, {}, {}, {})); m_classes.insert("Repeater", ClassInfo("Repeater", {"model", "delegate"}, {"count"}, {"itemAt"}, {"onCountChanged"})); m_attachedClasses.insert("Component", ClassInfo("Component", {}, {}, {}, {"onCompleted", "onDestruction", "onDestroyed"})); m_attachedClasses.insert("Alarm", ClassInfo("Alarm", {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday", "AllDays"}, {}, {})); m_attachedClasses.insert("Animation", ClassInfo("Animation", {"Infinite"}, {}, {})); m_attachedClasses.insert("Action", ClassInfo("Action", {"TriggeredByUser", "TriggeredByRule", "TriggeredByScript"}, {}, {})); m_attachedClasses.insert("Thing", ClassInfo("Thing", {"ThingErrorNoError", "ThingErrorPluginNotFound", "ThingErrorVendorNotFound", "ThingErrorThingNotFound", "ThingErrorThingClassNotFound", "ThingErrorActionTypeNotFound", "ThingErrorStateTypeNotFound", "ThingErrorEventTypeNotFound", "ThingErrorThingDescriptorNotFound", "ThingErrorMissingParameter", "ThingErrorInvalidParameter", "ThingErrorSetupFailed", "ThingErrorDuplicateUuid", "ThingErrorCreationMethodNotSupported", "ThingErrorSetupMethodNotSupported", "ThingErrorHardwareNotAvailable", "ThingErrorHardwareFailure", "ThingErrorAuthenticationFailure", "ThingErrorThingInUse", "ThingErrorThingInRule", "ThingErrorThingIsChild", "ThingErrorPairingTransactionIdNotFound", "ThingErrorParameterNotWritable", "ThingErrorItemNotFound", "ThingErrorItemNotExecutable", "ThingErrorUnsupportedFeature", "ThingErrorTimeout"}, {}, {})); m_genericSyntax.insert("property", "property "); m_genericSyntax.insert("function", "function "); m_genericJsSyntax.insert("for", "for"); m_genericJsSyntax.insert("var", "var"); m_genericJsSyntax.insert("while", "while "); m_genericJsSyntax.insert("do", "do "); m_genericJsSyntax.insert("if", "if "); m_genericJsSyntax.insert("else", "else "); m_genericJsSyntax.insert("print", "print"); m_jsClasses.insert("console", ClassInfo("console", {}, {}, {"log", "warn"})); m_jsClasses.insert("JSON", ClassInfo("JSON", {}, {}, {"stringify", "parse", "hasOwnProperty", "isPrototypeOf", "toString", "valueOf", "toLocaleString", "propertyIsEnumerable"})); m_model = new CompletionModel(this); m_proxy = new CompletionProxyModel(m_model, this); connect(m_proxy, &CompletionProxyModel::filterChanged, this, &CodeCompletion::currentWordChanged); } Engine *CodeCompletion::engine() const { return m_engine; } void CodeCompletion::setEngine(Engine *engine) { if (m_engine != engine) { m_engine = engine; emit engineChanged(); } } QQuickTextDocument *CodeCompletion::document() const { return m_document; } void CodeCompletion::setDocument(QQuickTextDocument *document) { if (m_document != document) { m_document = document; emit documentChanged(); m_cursor = QTextCursor(m_document->textDocument()); emit cursorPositionChanged(); connect(m_document->textDocument(), &QTextDocument::cursorPositionChanged, this, [this](const QTextCursor &cursor){ m_cursor = cursor; update(); }); } } int CodeCompletion::cursorPosition() const { return m_cursor.position(); } void CodeCompletion::setCursorPosition(int position) { // This is a bit tricky: As our cursor works on the same textDocument as the view, // our cursor will already have the position set to the new one by the time we // receive the update from the View when the document is changed. // But we can't just connect to our cursor's updates as that will miss out events // generated in the UI without changing the document (e.g. move cursor with kbd/mouse) if (m_cursor.position() != position) { m_cursor.setPosition(position); // NOTE: Don't emit cursorPositionChanged here, it will break selections // because the view thinks we've edited the document. // If we actually edit the document, the view will sync up automatically // through the document. So we must *only* emit cursorPositionChanged when // we actually want to move it without changing the document. } } QString CodeCompletion::currentWord() const { return m_proxy->filter(); } CompletionProxyModel *CodeCompletion::model() const { return m_proxy; } void CodeCompletion::update() { if (!m_engine || !m_document) { return; } static int lastUpdatePos = -1; if (lastUpdatePos == m_cursor.position()) { return; } lastUpdatePos = m_cursor.position(); QTextCursor tmp = m_cursor; tmp.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor); QString blockText = tmp.selectedText(); QList entries; QRegularExpression thingIdExp(".*thingId: \"[a-zA-ZÀ-ž0-9- ]*"); if (thingIdExp.match(blockText).hasMatch()) { for (int i = 0; i < m_engine->thingManager()->things()->rowCount(); i++) { Thing *thing = m_engine->thingManager()->things()->get(i); entries.append(CompletionModel::Entry(thing->id().toString() + "\" // " + thing->name(), thing->name(), "thing", thing->thingClass()->interfaces().join(","))); } blockText.remove(QRegularExpression(".*thingId: \"")); m_model->update(entries); m_proxy->setFilter(blockText, false); emit hint(); return; } QRegularExpression stateTypeIdExp(".*stateTypeId: \"[a-zA-Z0-9-]*"); if (stateTypeIdExp.match(blockText).hasMatch()) { BlockInfo info = getBlockInfo(m_cursor.position()); QString thingId; if (!info.properties.contains("thingId")) { return; } thingId = info.properties.value("thingId"); qDebug() << "selected thingId" << thingId; Thing *thing = m_engine->thingManager()->things()->getThing(QUuid(thingId)); if (!thing) { return; } for (int i = 0; i < thing->thingClass()->stateTypes()->rowCount(); i++) { StateType *stateType = thing->thingClass()->stateTypes()->get(i); entries.append(CompletionModel::Entry(stateType->id().toString() + "\" // " + stateType->name(), stateType->name(), "stateType")); } blockText.remove(QRegularExpression(".*stateTypeId: \"")); m_model->update(entries); m_proxy->setFilter(blockText); emit hint(); return; } QRegularExpression stateNameExp(".*stateName: \"[a-zA-Z0-9-]*"); // qDebug() << "block text" << blockText << stateNameExp.exactMatch(blockText); if (stateNameExp.match(blockText).hasMatch()) { BlockInfo info = getBlockInfo(m_cursor.position()); qDebug() << "stateName block info" << info.name << info.properties; QString thingId; Interfaces ifaces; //StateTypes *stateTypes = nullptr; if (info.properties.contains("thingId")) { thingId = info.properties.value("thingId"); qDebug() << "selected thingId" << thingId; Thing *thing = m_engine->thingManager()->things()->getThing(QUuid(thingId)); if (!thing) { return; } //stateTypes = thing->thingClass()->stateTypes(); } else if (info.properties.contains("interfaceName")) { QString interfaceName = info.properties.value("interfaceName"); Interface *iface = ifaces.findByName(interfaceName); if (!iface) { return; } //stateTypes = iface->stateTypes(); } else { return; } thingId = info.properties.value("thingId"); qDebug() << "selected thingId" << thingId; Thing *thing = m_engine->thingManager()->things()->getThing(QUuid(thingId)); if (!thing) { return; } qDebug() << "Thing is" << thing->name(); for (int i = 0; i < thing->thingClass()->stateTypes()->rowCount(); i++) { StateType *stateType = thing->thingClass()->stateTypes()->get(i); entries.append(CompletionModel::Entry(stateType->name() + "\"", stateType->name(), "stateType")); } blockText.remove(QRegularExpression(".*stateName: \"")); m_model->update(entries); m_proxy->setFilter(blockText); emit hint(); return; } QRegularExpression actionTypeIdExp(".*actionTypeId: \"[a-zA-Z0-9-]*"); if (actionTypeIdExp.match(blockText).hasMatch()) { BlockInfo info = getBlockInfo(m_cursor.position()); QString thingId; if (!info.properties.contains("thingId")) { return; } thingId = info.properties.value("thingId"); qDebug() << "selected thingId" << thingId; Thing *thing = m_engine->thingManager()->things()->getThing(QUuid(thingId)); if (!thing) { return; } for (int i = 0; i < thing->thingClass()->actionTypes()->rowCount(); i++) { ActionType *actionType = thing->thingClass()->actionTypes()->get(i); entries.append(CompletionModel::Entry(actionType->id().toString() + "\" // " + actionType->name(), actionType->name(), "actionType")); } blockText.remove(QRegularExpression(".*actionTypeId: \"")); m_model->update(entries); m_proxy->setFilter(blockText); emit hint(); return; } QRegularExpression actionNameExp(".*actionName: \"[a-zA-Z0-9-]*"); if (actionNameExp.match(blockText).hasMatch()) { BlockInfo info = getBlockInfo(m_cursor.position()); Interfaces ifaces; ActionTypes *actionTypes = nullptr; if (info.properties.contains("thingId")) { QString thingId = info.properties.value("thingId"); qDebug() << "selected thingId" << thingId; Thing *thing = m_engine->thingManager()->things()->getThing(QUuid(thingId)); if (!thing) { return; } actionTypes = thing->thingClass()->actionTypes(); } else if (info.properties.contains("interfaceName")) { QString interfaceName = info.properties.value("interfaceName"); Interface *iface = ifaces.findByName(interfaceName); if (!iface) { return; } actionTypes = iface->actionTypes(); } else { return; } for (int i = 0; i < actionTypes->rowCount(); i++) { ActionType *actionType = actionTypes->get(i); entries.append(CompletionModel::Entry(actionType->name() + "\"", actionType->name(), "actionType")); } blockText.remove(QRegularExpression(".*actionName: \"")); m_model->update(entries); m_proxy->setFilter(blockText); emit hint(); return; } QRegularExpression eventTypeIdExp(".*eventTypeId: \"[a-zA-Z0-9-]*"); if (eventTypeIdExp.match(blockText).hasMatch()) { BlockInfo info = getBlockInfo(m_cursor.position()); QString thingId; if (!info.properties.contains("thingId")) { return; } thingId = info.properties.value("thingId"); qDebug() << "selected thingId" << thingId; Thing *thing= m_engine->thingManager()->things()->getThing(QUuid(thingId)); if (!thing) { return; } for (int i = 0; i < thing->thingClass()->eventTypes()->rowCount(); i++) { EventType *eventType = thing->thingClass()->eventTypes()->get(i); entries.append(CompletionModel::Entry(eventType->id().toString() + "\" // " + eventType->name(), eventType->name(), "eventType")); } blockText.remove(QRegularExpression(".*eventTypeId: \"")); m_model->update(entries); m_proxy->setFilter(blockText); emit hint(); return; } QRegularExpression eventNameExp(".*eventName: \"[a-zA-Z0-9-]*"); if (eventNameExp.match(blockText).hasMatch()) { BlockInfo info = getBlockInfo(m_cursor.position()); Interfaces ifaces; EventTypes *eventTypes = nullptr; if (info.properties.contains("thingId")) { QString thingId = info.properties.value("thingId"); Thing *thing = m_engine->thingManager()->things()->getThing(QUuid(thingId)); if (!thing) { return; } eventTypes = thing->thingClass()->eventTypes(); } else if (info.properties.contains("interfaceName")) { QString interfaceName = info.properties.value("interfaceName"); Interface *iface = ifaces.findByName(interfaceName); if (!iface) { return; } eventTypes = iface->eventTypes(); } else { return; } for (int i = 0; i < eventTypes->rowCount(); i++) { EventType *eventType = eventTypes->get(i); entries.append(CompletionModel::Entry(eventType->name() + "\"", eventType->name(), "eventType")); } blockText.remove(QRegularExpression(".*eventName: \"")); m_model->update(entries); m_proxy->setFilter(blockText); emit hint(); return; } QRegularExpression interfaceNameExp(".*interfaceName: \"[a-zA-Z]*"); if (interfaceNameExp.match(blockText).hasMatch()) { BlockInfo info = getBlockInfo(m_cursor.position()); Interfaces ifaces; for (int i = 0; i < ifaces.rowCount(); i++) { Interface *iface = ifaces.get(i); entries.append(CompletionModel::Entry(iface->name() + "\"", iface->name(), "interface", iface->name())); } m_model->update(entries); blockText.remove(QRegularExpression(".*interfaceName: \"")); m_proxy->setFilter(blockText); emit hint(); return; } QRegularExpression importExp("imp(o|or)?"); if (importExp.match(blockText).hasMatch()) { entries.append(CompletionModel::Entry("import ", "import", "keyword", "")); m_model->update(entries); m_proxy->setFilter(blockText); return; } QRegularExpression importExp2("import [a-zA-Z]*"); if (importExp2.match(blockText).hasMatch()) { entries.append(CompletionModel::Entry("QtQuick 2.0")); entries.append(CompletionModel::Entry("nymea 1.0")); m_model->update(entries); blockText.remove("import "); m_proxy->setFilter(blockText); return; } QRegularExpression rValueExp(" *[\\.a-zA-Z0-0]+[^id]:[ a-zA-Z0-0]*"); if (rValueExp.match(blockText).hasMatch()) { QTextCursor tmp = m_cursor; tmp.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); QString word = tmp.selectedText(); tmp.movePosition(QTextCursor::PreviousWord, QTextCursor::MoveAnchor, 2); tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); QString previousWord = tmp.selectedText(); if (previousWord.isEmpty()) { m_model->update({}); return; } qDebug() << "rValue" << previousWord << word; entries.append(getIds()); foreach (const QString &s, m_jsClasses.keys()) { entries.append(CompletionModel::Entry(s, s, "type")); } foreach (const QString &s, m_attachedClasses.keys()) { entries.append(CompletionModel::Entry(s, s, "type")); } m_model->update(entries); m_proxy->setFilter(word); return; } QRegularExpression dotExp(".*[a-zA-Z0-9]+\\.[a-zA-Z0-9]*"); if (dotExp.match(blockText).hasMatch()) { QString id = blockText; id.remove(QRegularExpression(".* ")).remove(QRegularExpression("\\.[a-zA-Z0-9]*")); QString type = getIdTypes().value(id); int blockPosition = getBlockPosition(id); BlockInfo blockInfo = getBlockInfo(blockPosition); qDebug() << "dot expression:" << id << type; qDebug() << "lvalue info:" << blockInfo.properties.keys() << blockInfo.properties.value("actionName") << blockInfo.properties.value("actionTypeId"); // Append properties of the type foreach (const QString &property, m_classes.value(type).properties) { entries.append(CompletionModel::Entry(property, property, "property")); } foreach (const QString &property, m_classes.value(type).readOnlyProperties) { entries.append(CompletionModel::Entry(property, property, "property")); } // Append user-defined properties of the type foreach (const QString &property, blockInfo.properties.keys()) { CompletionModel::Entry entry(property, property, "property"); if (!entries.contains(entry)) { entries.append(entry); } } // Append methods of the item foreach (const QString &method, m_classes.value(type).methods) { QString paramString; // If it's an execute() call, also autocomplete the params if (method == "execute") { qDebug() << "Blockposition:" << blockPosition; if (blockPosition >= 0) { if (blockInfo.valid) { QString thingId = blockInfo.properties.value("thingId"); Thing *d = m_engine->thingManager()->things()->getThing(QUuid(thingId)); if (d) { ActionType *at = nullptr; if (blockInfo.properties.contains("actionTypeId")) { at = d->thingClass()->actionTypes()->getActionType(QUuid(blockInfo.properties.value("actionTypeId"))); } else if (blockInfo.properties.contains("actionName")) { at = d->thingClass()->actionTypes()->findByName(blockInfo.properties.value("actionName")); } if (at) { QStringList params; QStringList nonEscapeTypes = {"Int", "Bool", "Double"}; for (int i = 0; i < at->paramTypes()->rowCount(); i++) { ParamType *pt = at->paramTypes()->get(i); QString escapeChar = nonEscapeTypes.contains(pt->type()) ? "" : "\""; params.append("\"" + pt->name() + "\": " + escapeChar + pt->defaultValue().toString() + escapeChar); } paramString = "{" + params.join(", ") + "}"; } } } } } entries.append(CompletionModel::Entry(method + "(", method, "method", "", paramString + ")")); } // User-defined functions of the item foreach (const QString &function, blockInfo.functions) { entries.append(CompletionModel::Entry(function + "(", function, "method", "", ")")); } // Attached classes/properties foreach (const QString &property, m_attachedClasses.value(id).readOnlyProperties) { entries.append(CompletionModel::Entry(property, property, "property")); } foreach (const QString &property, m_attachedClasses.value(id).properties) { entries.append(CompletionModel::Entry(property, property, "property")); } foreach (const QString &method, m_attachedClasses.value(id).methods) { entries.append(CompletionModel::Entry(method + "(", method, "method", "", ")")); } foreach (const QString &event, m_attachedClasses.value(id).events) { entries.append(CompletionModel::Entry(event + ": ", event, "event")); } // JS global objects foreach (const QString &property, m_jsClasses.value(id).properties) { entries.append(CompletionModel::Entry(property, property, "property")); } foreach (const QString &method, m_jsClasses.value(id).methods) { entries.append(CompletionModel::Entry(method + "(", method, "method", "", ")")); } m_model->update(entries); m_proxy->setFilter(blockText.remove(QRegularExpression(".*\\."))); return; } // Are we in a JS block? int pos = m_cursor.position(); BlockInfo jsBlock = getBlockInfo(pos); bool isImperative = jsBlock.name.endsWith(":") || jsBlock.name.endsWith("()"); bool atStart = false; while (!isImperative && jsBlock.valid && !atStart) { // qDebug() << "is imperative block?" << isImperative << jsBlock.name << "blockText" << blockText; BlockInfo tmp = getBlockInfo(jsBlock.start - 1); if (tmp.valid) { jsBlock = tmp; isImperative = jsBlock.name.endsWith(":") || jsBlock.name.endsWith("()"); } else { atStart = true; } } if (isImperative) { // qDebug() << "Is imperative!"; // Starting a new expression? QRegularExpression newExpressionExp("(.*; [a-zA-Z0-9]*| *[a-zA-Z0-9]*)"); if (newExpressionExp.match(blockText).hasMatch()) { // Add generic qml syntax foreach (const QString &s, m_genericJsSyntax.keys()) { entries.append(CompletionModel::Entry(m_genericJsSyntax.value(s), s, "keyword", "")); } // Add js global objects foreach (const QString &s, m_jsClasses.keys()) { entries.append(CompletionModel::Entry(s, s, "type")); } entries.append(getIds()); } m_model->update(entries); m_proxy->setFilter(blockText.remove(QRegularExpression(".* "))); return; } QRegularExpression lValueStartExp(" *[a-zA-Z0-9]*"); if (lValueStartExp.match(blockText).hasMatch()) { BlockInfo blockInfo = getBlockInfo(m_cursor.position()); // If we're inside a class, add properties // qDebug() << "Block name" << blockInfo.name; if (!blockInfo.name.isEmpty()) { foreach (const QString &s, m_classes.value(blockInfo.name).properties) { if (!blockInfo.properties.contains(s)) { entries.append(CompletionModel::Entry(s + ": ", s, "property")); } } foreach (const QString &s, m_classes.value(blockInfo.name).events) { if (!blockInfo.properties.contains(s)) { entries.append(CompletionModel::Entry(s + ": ", s, "event")); } } } // Always append class names foreach (const QString &s, m_classes.keys()) { entries.append(CompletionModel::Entry(s + " {", s, "type", "", "}")); } // Always append attached class names foreach (const QString &s, m_attachedClasses.keys()) { entries.append(CompletionModel::Entry(s, s, "attachedProperty")); } // Add generic qml syntax foreach (const QString &s, m_genericSyntax.keys()) { entries.append(CompletionModel::Entry(m_genericSyntax.value(s), s, "keyword", "")); } m_model->update(entries); blockText.remove(QRegularExpression(".* ")); m_proxy->setFilter(blockText); // qDebug() << "Model has" << m_model->rowCount() << "Filtered:" << m_proxy->rowCount() << "filter:" << blockText; return; } m_model->update({}); m_proxy->setFilter(QString()); } CodeCompletion::BlockInfo CodeCompletion::getBlockInfo(int position) const { BlockInfo info; // Find start of block QTextCursor blockStart = m_document->textDocument()->find("{", position, QTextDocument::FindBackward); QTextCursor blockEnd = m_document->textDocument()->find("}", position, QTextDocument::FindBackward); while (blockEnd.position() > blockStart.position() && !blockStart.isNull()) { blockStart = m_document->textDocument()->find("{", blockStart, QTextDocument::FindBackward); blockEnd = m_document->textDocument()->find("}", blockEnd, QTextDocument::FindBackward); } if (blockStart.isNull()) { return info; } // Find end of block QTextCursor tmp = blockStart; blockEnd = blockStart; while (!tmp.isNull() && blockEnd >= tmp) { tmp = m_document->textDocument()->find("{", tmp.position()); blockEnd = m_document->textDocument()->find("}", blockEnd.position()); } info.start = blockStart.position(); info.end = blockEnd.position(); info.valid = true; // qDebug() << "Block start:" << info.start << "end:" << info.end; info.name = blockStart.block().text(); info.name.remove(QRegularExpression(" *\\{ *")); while (info.name.contains(" ")) { info.name.remove(QRegularExpression(".* ")); } int childBlocks = 0; while (!blockStart.isNull() && blockStart.position() < info.end) { QString line = blockStart.block().text(); // qDebug() << "line:" << line; if (line.contains("{") && !line.contains("}")) { childBlocks++; } if (line.contains("}") && !line.contains("{")) { childBlocks--; if (!blockStart.movePosition(QTextCursor::NextBlock)) { break; } continue; } // \n if (childBlocks > 1 && !(childBlocks == 2 && line.trimmed().startsWith("function"))) { // Skip all stuff in child blocks // qDebug() << "skipping line in child block" << childBlocks; blockStart.movePosition(QTextCursor::NextBlock); continue; } foreach (const QString &statement, blockStart.block().text().split(";")) { // qDebug() << "analysing statement:" << statement; QStringList parts = statement.split(":"); if (parts.length() == 2) { // Properties must be "foo: bar" QString propName = parts.first().trimmed(); if (propName.split(" ").count() > 1) { // trim modifiers e.g. "property bool foo: bar" propName = propName.split(" ").last(); } if (propName.contains(".")) { // skip attached properties e.g. "Component.onCompleted: ..." continue; } QString propValue = parts.last().split("//").first().trimmed().remove("\""); info.properties.insert(propName, propValue); } parts = statement.trimmed().split(" "); if (parts.count() >= 2 && parts.first().trimmed() == "function") { QString functionHeader = parts.at(1).trimmed(); info.functions.append(functionHeader.split("(").first()); } } if (!blockStart.movePosition(QTextCursor::NextBlock)) { break; } } return info; } QList CodeCompletion::getIds() const { // Find all ids in the doc QList entries; QTextCursor tmp = QTextCursor(m_document->textDocument()); while (!tmp.atEnd()) { tmp.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor); tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); QString word = tmp.selectedText(); if (word == "id") { tmp.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor); tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); QString idName = tmp.selectedText(); entries.append(CompletionModel::Entry(idName, idName, "id", "")); } tmp.movePosition(QTextCursor::NextWord); } return entries; } QHash CodeCompletion::getIdTypes() const { QHash ret; QTextCursor tmp = QTextCursor(m_document->textDocument()); while (!tmp.atEnd()) { tmp.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor); tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); QString word = tmp.selectedText(); if (word == "id") { tmp.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor); tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); QString idName = tmp.selectedText(); BlockInfo info = getBlockInfo(tmp.position()); if (!info.name.isEmpty()) { ret.insert(idName, info.name); } } tmp.movePosition(QTextCursor::NextWord); } return ret; } int CodeCompletion::openingBlocksBefore(int position) const { int opening = 0; int closing = 0; QTextCursor tmp = m_cursor; tmp.setPosition(position); do { tmp = m_document->textDocument()->find(QRegularExpression("[{}]"), tmp, QTextDocument::FindBackward); if (tmp.selectedText() == "{") opening++; if (tmp.selectedText() == "}") closing++; } while (!tmp.isNull()); return opening - closing; } int CodeCompletion::closingBlocksAfter(int position) const { int opening = 0; int closing = 0; QTextCursor tmp = m_cursor; tmp.setPosition(position); do { tmp = m_document->textDocument()->find(QRegularExpression("[{}]"), tmp); if (tmp.selectedText() == "{") opening++; if (tmp.selectedText() == "}") closing++; } while (!tmp.isNull()); return closing - opening; } void CodeCompletion::complete(int index) { if (index < 0 || index >= m_proxy->rowCount()) { qWarning() << "Invalid index for completion"; return; } CompletionModel::Entry entry = m_proxy->get(index); m_cursor.select(QTextCursor::WordUnderCursor); m_cursor.removeSelectedText(); // Do we need to remove more? Thing names may contain spaces... QTextCursor tmp = m_cursor; tmp.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor); QString blockText = tmp.selectedText(); QRegularExpression thingIdExp(".*thingId: \"[a-zA-ZÀ-ž0-9- ]*"); if (thingIdExp.match(blockText).hasMatch()) { QTextCursor tmp = m_document->textDocument()->find("\"", m_cursor.position(), QTextDocument::FindBackward); m_cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_cursor.position() - tmp.position()); m_cursor.removeSelectedText(); } qDebug() << "inserting:" << entry.text; m_cursor.insertText(entry.text); qDebug() << "inserting after cursor:" << entry.trailingText; insertAfterCursor(entry.trailingText); } void CodeCompletion::newLine() { qDebug() << "Newline" << m_cursor.position(); QString line = m_cursor.block().text(); if (line.endsWith("{") && openingBlocksBefore(m_cursor.position()) > closingBlocksAfter(m_cursor.position())) { m_cursor.insertText("}"); m_cursor.movePosition(QTextCursor::PreviousCharacter); } QString trimmedLine = line; trimmedLine.remove(QRegularExpression("^[ ]+")); int indent = line.length() - trimmedLine.length(); m_cursor.insertText(QString("\n").leftJustified(indent + 1, ' ')); if (m_cursor.block().previous().text().endsWith("{")) { m_cursor.insertText(" "); if (m_cursor.block().text().trimmed().endsWith("}")) { m_cursor.insertText(QString("\n").leftJustified(indent + 1, ' ')); m_cursor.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor, 1); m_cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::MoveAnchor, 1); emit cursorPositionChanged(); } } } void CodeCompletion::indent(int from, int to) { QTextCursor tmp = QTextCursor(m_document->textDocument()); tmp.setPosition(from); if (from == to) { tmp.insertText(" "); } else { while (tmp.position() < to) { tmp.insertText(" "); to += 4; if (!tmp.movePosition(QTextCursor::NextBlock)) { break; } } } } void CodeCompletion::unindent(int from, int to) { QTextCursor tmp = QTextCursor(m_document->textDocument()); tmp.setPosition(from); tmp.movePosition(QTextCursor::StartOfLine); if (from == to) { if (tmp.block().text().startsWith(" ")) { tmp.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 4); tmp.removeSelectedText(); } } else { // Make sure all selected lines start with 4 empty spaces before we start editing bool ok = true; while (tmp.position() < to) { if (!tmp.block().text().startsWith(" ")) { ok = false; break; } if (!tmp.movePosition(QTextCursor::NextBlock)) { ok = false; break; } } if (ok) { tmp.setPosition(from); tmp.movePosition(QTextCursor::StartOfLine); while (tmp.position() < to) { tmp.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 4); tmp.removeSelectedText(); to -= 4; if (!tmp.movePosition(QTextCursor::NextBlock)) { break; } } } } } void CodeCompletion::closeBlock() { m_cursor.insertText("}"); if (m_cursor.block().text().trimmed() == "}") { unindent(m_cursor.position(), m_cursor.position()); } } void CodeCompletion::insertBeforeCursor(const QString &text) { m_cursor.insertText(text); } void CodeCompletion::insertAfterCursor(const QString &text) { m_cursor.insertText(text); m_cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, text.length()); emit cursorPositionChanged(); } void CodeCompletion::toggleComment(int from, int to) { bool textSelected = from != to; QTextCursor tmp = QTextCursor(m_document->textDocument()); tmp.setPosition(from); tmp.movePosition(QTextCursor::StartOfLine); bool allLinesHaveComments = true; do { QTextCursor nextComment = m_document->textDocument()->find(QRegularExpression("^[ ]*//"), tmp.position()); nextComment.movePosition(QTextCursor::StartOfLine); bool lineHasComment = tmp.position() == nextComment.position(); allLinesHaveComments &= lineHasComment; tmp.movePosition(QTextCursor::EndOfLine); tmp.movePosition(QTextCursor::NextCharacter); } while (tmp.position() < to); qDebug() << "All lines have comments:" << allLinesHaveComments; tmp.setPosition(from); tmp.movePosition(QTextCursor::StartOfLine); do { if (allLinesHaveComments) { QTextCursor nextComment = m_document->textDocument()->find(QRegularExpression("//"), tmp.position()); nextComment.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 2); nextComment.removeSelectedText(); nextComment.insertText(" "); to -= 2; } else { tmp.insertText("//"); to += 2; } tmp.movePosition(QTextCursor::EndOfLine); tmp.movePosition(QTextCursor::NextCharacter); } while (tmp.position() < to); if (textSelected) { emit select(from, to); } } void CodeCompletion::moveCursor(CodeCompletion::MoveOperation moveOperation, int count) { switch (moveOperation) { case MoveOperationPreviousLine: m_cursor.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor, count); emit cursorPositionChanged(); return; case MoveOperationNextLine: m_cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, count); emit cursorPositionChanged(); return; case MoveOperationPreviousWord: { // We're not using the cursors next/previos word because we want camelCase word fragments QTextCursor tmp = m_document->textDocument()->find(QRegularExpression("[A-Z\\.:\"'\\(\\)\\[\\]^ ]"), m_cursor.position() - 1, QTextDocument::FindBackward); qWarning() << "found at" << tmp.position() << "starting at" << m_cursor.position(); m_cursor.setPosition(tmp.position()); emit cursorPositionChanged(); return; } case MoveOperationNextWord: { // We're not using the cursors next/previos word because we want camelCase word fragments QTextCursor tmp = m_document->textDocument()->find(QRegularExpression("[A-Z\\.:\"'\\(\\)\\[\\]$ ]"), m_cursor.position() + 1); m_cursor.setPosition(tmp.position() - 1); emit cursorPositionChanged(); return; } case MoveOperationAbsoluteLine: m_cursor.movePosition(QTextCursor::Start); m_cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, count - 1); emit cursorPositionChanged(); break; } } int CodeCompletion::getBlockPosition(const QString &id) const { // Find block with id "id" in the doc QTextCursor tmp = QTextCursor(m_document->textDocument()); while (!tmp.atEnd()) { tmp.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor); tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); QString word = tmp.selectedText(); if (word == "id") { tmp.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor); tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); QString idName = tmp.selectedText(); if (idName == id) { return tmp.position(); } } tmp.movePosition(QTextCursor::NextWord); } return -1; }