/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "qmljsfindreferences.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "qmljseditorconstants.h" #include #include #include #include #include #include #include #include #include #include using namespace Core; using namespace QmlJS; using namespace QmlJS::AST; using namespace QmlJSEditor; namespace { // ### These visitors could be useful in general class FindUsages: protected Visitor { public: using Result = QList; FindUsages(Document::Ptr doc, const ContextPtr &context) : _doc(doc) , _scopeChain(doc, context) , _builder(&_scopeChain) { } Result operator()(const QString &name, const ObjectValue *scope) { _name = name; _scope = scope; _usages.clear(); if (_doc) Node::accept(_doc->ast(), this); return _usages; } protected: void accept(AST::Node *node) { AST::Node::acceptChild(node, this); } using Visitor::visit; bool visit(AST::UiPublicMember *node) override { if (node->name == _name && _scopeChain.qmlScopeObjects().contains(_scope)) { _usages.append(node->identifierToken); } if (AST::cast(node->statement)) { _builder.push(node); Node::accept(node->statement, this); _builder.pop(); return false; } return true; } bool visit(AST::UiObjectDefinition *node) override { _builder.push(node); Node::accept(node->initializer, this); _builder.pop(); return false; } bool visit(AST::UiObjectBinding *node) override { if (node->qualifiedId && !node->qualifiedId->next && node->qualifiedId->name == _name && checkQmlScope()) { _usages.append(node->qualifiedId->identifierToken); } _builder.push(node); Node::accept(node->initializer, this); _builder.pop(); return false; } bool visit(AST::UiScriptBinding *node) override { if (node->qualifiedId && !node->qualifiedId->next && node->qualifiedId->name == _name && checkQmlScope()) { _usages.append(node->qualifiedId->identifierToken); } if (AST::cast(node->statement)) { Node::accept(node->qualifiedId, this); _builder.push(node); Node::accept(node->statement, this); _builder.pop(); return false; } return true; } bool visit(AST::UiArrayBinding *node) override { if (node->qualifiedId && !node->qualifiedId->next && node->qualifiedId->name == _name && checkQmlScope()) { _usages.append(node->qualifiedId->identifierToken); } return true; } bool visit(AST::IdentifierExpression *node) override { if (node->name.isEmpty() || node->name != _name) return false; const ObjectValue *scope; _scopeChain.lookup(_name, &scope); if (!scope) return false; if (check(scope)) { _usages.append(node->identifierToken); return false; } // the order of scopes in 'instantiatingComponents' is undefined, // so it might still be a use - we just found a different value in a different scope first // if scope is one of these, our match wasn't inside the instantiating components list const ScopeChain &chain = _scopeChain; if (chain.jsScopes().contains(scope) || chain.qmlScopeObjects().contains(scope) || chain.qmlTypes() == scope || chain.globalScope() == scope) return false; if (contains(chain.qmlComponentChain().data())) _usages.append(node->identifierToken); return false; } bool visit(AST::FieldMemberExpression *node) override { if (node->name != _name) return true; Evaluate evaluate(&_scopeChain); const Value *lhsValue = evaluate(node->base); if (!lhsValue) return true; if (check(lhsValue->asObjectValue())) // passing null is ok _usages.append(node->identifierToken); return true; } bool visit(AST::FunctionDeclaration *node) override { return visit(static_cast(node)); } bool visit(AST::FunctionExpression *node) override { if (node->name == _name) { if (checkLookup()) _usages.append(node->identifierToken); } Node::accept(node->formals, this); _builder.push(node); Node::accept(node->body, this); _builder.pop(); return false; } bool visit(AST::PatternElement *node) override { if (node->isVariableDeclaration() && node->bindingIdentifier == _name) { if (checkLookup()) _usages.append(node->identifierToken); } return true; } void throwRecursionDepthError() override { qWarning("Warning: Hit maximum recursion depth while visitin AST in FindUsages"); } private: bool contains(const QmlComponentChain *chain) { if (!chain || !chain->document() || !chain->document()->bind()) return false; const ObjectValue *idEnv = chain->document()->bind()->idEnvironment(); if (idEnv && idEnv->lookupMember(_name, _scopeChain.context())) return idEnv == _scope; const ObjectValue *root = chain->document()->bind()->rootObjectValue(); if (root && root->lookupMember(_name, _scopeChain.context())) return check(root); foreach (const QmlComponentChain *parent, chain->instantiatingComponents()) { if (contains(parent)) return true; } return false; } bool check(const ObjectValue *s) { if (!s) return false; const ObjectValue *definingObject; s->lookupMember(_name, _scopeChain.context(), &definingObject); return definingObject == _scope; } bool checkQmlScope() { foreach (const ObjectValue *s, _scopeChain.qmlScopeObjects()) { if (check(s)) return true; } return false; } bool checkLookup() { const ObjectValue *scope = nullptr; _scopeChain.lookup(_name, &scope); return check(scope); } Result _usages; Document::Ptr _doc; ScopeChain _scopeChain; ScopeBuilder _builder; QString _name; const ObjectValue *_scope = nullptr; }; class FindTypeUsages: protected Visitor { public: using Result = QList; FindTypeUsages(Document::Ptr doc, const ContextPtr &context) : _doc(doc) , _context(context) , _scopeChain(doc, context) , _builder(&_scopeChain) { } Result operator()(const QString &name, const ObjectValue *typeValue) { _name = name; _typeValue = typeValue; _usages.clear(); if (_doc) Node::accept(_doc->ast(), this); return _usages; } protected: void accept(AST::Node *node) { AST::Node::acceptChild(node, this); } using Visitor::visit; bool visit(AST::UiPublicMember *node) override { if (UiQualifiedId *memberType = node->memberType) { if (memberType->name == _name) { const ObjectValue * tVal = _context->lookupType(_doc.data(), QStringList(_name)); if (tVal == _typeValue) _usages.append(node->typeToken); } } if (AST::cast(node->statement)) { _builder.push(node); Node::accept(node->statement, this); _builder.pop(); return false; } return true; } bool visit(AST::UiObjectDefinition *node) override { checkTypeName(node->qualifiedTypeNameId); _builder.push(node); Node::accept(node->initializer, this); _builder.pop(); return false; } bool visit(AST::UiObjectBinding *node) override { checkTypeName(node->qualifiedTypeNameId); _builder.push(node); Node::accept(node->initializer, this); _builder.pop(); return false; } bool visit(AST::UiScriptBinding *node) override { if (AST::cast(node->statement)) { Node::accept(node->qualifiedId, this); _builder.push(node); Node::accept(node->statement, this); _builder.pop(); return false; } return true; } bool visit(AST::IdentifierExpression *node) override { if (node->name != _name) return false; const ObjectValue *scope; const Value *objV = _scopeChain.lookup(_name, &scope); if (objV == _typeValue) _usages.append(node->identifierToken); return false; } bool visit(AST::FieldMemberExpression *node) override { if (node->name != _name) return true; Evaluate evaluate(&_scopeChain); const Value *lhsValue = evaluate(node->base); if (!lhsValue) return true; const ObjectValue *lhsObj = lhsValue->asObjectValue(); if (lhsObj && lhsObj->lookupMember(_name, _context) == _typeValue) _usages.append(node->identifierToken); return true; } bool visit(AST::FunctionDeclaration *node) override { return visit(static_cast(node)); } bool visit(AST::FunctionExpression *node) override { Node::accept(node->formals, this); _builder.push(node); Node::accept(node->body, this); _builder.pop(); return false; } bool visit(AST::PatternElement *node) override { if (node->isVariableDeclaration()) Node::accept(node->initializer, this); return false; } bool visit(UiImport *ast) override { if (ast && ast->importId == _name) { const Imports *imp = _context->imports(_doc.data()); if (!imp) return false; if (_context->lookupType(_doc.data(), QStringList(_name)) == _typeValue) _usages.append(ast->importIdToken); } return false; } void throwRecursionDepthError() override { qWarning("Warning: Hit maximum recursion depth while visitin AST in FindTypeUsages"); } private: bool checkTypeName(UiQualifiedId *id) { for (UiQualifiedId *att = id; att; att = att->next){ if (att->name == _name) { const ObjectValue *objectValue = _context->lookupType(_doc.data(), id, att->next); if (_typeValue == objectValue){ _usages.append(att->identifierToken); return true; } } } return false; } Result _usages; Document::Ptr _doc; ContextPtr _context; ScopeChain _scopeChain; ScopeBuilder _builder; QString _name; const ObjectValue *_typeValue = nullptr; }; class FindTargetExpression: protected Visitor { public: enum Kind { ExpKind, TypeKind }; FindTargetExpression(Document::Ptr doc, const ScopeChain *scopeChain) : _doc(doc), _scopeChain(scopeChain) { } void operator()(quint32 offset) { _name.clear(); _scope = nullptr; _objectNode = nullptr; _offset = offset; _typeKind = ExpKind; if (_doc) Node::accept(_doc->ast(), this); } QString name() const { return _name; } const ObjectValue *scope() { if (!_scope) _scopeChain->lookup(_name, &_scope); return _scope; } Kind typeKind(){ return _typeKind; } const Value *targetValue(){ return _targetValue; } protected: void accept(AST::Node *node) { AST::Node::acceptChild(node, this); } using Visitor::visit; bool preVisit(Node *node) override { if (Statement *stmt = node->statementCast()) return containsOffset(stmt->firstSourceLocation(), stmt->lastSourceLocation()); else if (ExpressionNode *exp = node->expressionCast()) return containsOffset(exp->firstSourceLocation(), exp->lastSourceLocation()); else if (UiObjectMember *ui = node->uiObjectMemberCast()) return containsOffset(ui->firstSourceLocation(), ui->lastSourceLocation()); return true; } bool visit(IdentifierExpression *node) override { if (containsOffset(node->identifierToken)) { _name = node->name.toString(); if ((!_name.isEmpty()) && _name.at(0).isUpper()) { // a possible type _targetValue = _scopeChain->lookup(_name, &_scope); if (value_cast(_targetValue)) _typeKind = TypeKind; } } return true; } bool visit(FieldMemberExpression *node) override { if (containsOffset(node->identifierToken)) { setScope(node->base); _name = node->name.toString(); if ((!_name.isEmpty()) && _name.at(0).isUpper()) { // a possible type Evaluate evaluate(_scopeChain); const Value *lhsValue = evaluate(node->base); if (!lhsValue) return true; const ObjectValue *lhsObj = lhsValue->asObjectValue(); if (lhsObj) { _scope = lhsObj; _targetValue = lhsObj->lookupMember(_name, _scopeChain->context()); _typeKind = TypeKind; } } return false; } return true; } bool visit(UiScriptBinding *node) override { return !checkBindingName(node->qualifiedId); } bool visit(UiArrayBinding *node) override { return !checkBindingName(node->qualifiedId); } bool visit(UiObjectBinding *node) override { if ((!checkTypeName(node->qualifiedTypeNameId)) && (!checkBindingName(node->qualifiedId))) { Node *oldObjectNode = _objectNode; _objectNode = node; accept(node->initializer); _objectNode = oldObjectNode; } return false; } bool visit(UiObjectDefinition *node) override { if (!checkTypeName(node->qualifiedTypeNameId)) { Node *oldObjectNode = _objectNode; _objectNode = node; accept(node->initializer); _objectNode = oldObjectNode; } return false; } bool visit(UiPublicMember *node) override { if (containsOffset(node->typeToken)){ if (node->defaultToken.isValid()) { _name = node->memberType->name.toString(); _targetValue = _scopeChain->context()->lookupType(_doc.data(), QStringList(_name)); _scope = nullptr; _typeKind = TypeKind; } return false; } else if (containsOffset(node->identifierToken)) { _scope = _doc->bind()->findQmlObject(_objectNode); _name = node->name.toString(); return false; } return true; } bool visit(FunctionDeclaration *node) override { return visit(static_cast(node)); } bool visit(FunctionExpression *node) override { if (containsOffset(node->identifierToken)) { _name = node->name.toString(); return false; } return true; } bool visit(PatternElement *node) override { if (node->isVariableDeclaration() && containsOffset(node->identifierToken)) { _name = node->bindingIdentifier.toString(); return false; } return true; } void throwRecursionDepthError() override { qWarning("Warning: Hit maximum recursion depth visiting AST in FindUsages"); } private: bool containsOffset(SourceLocation start, SourceLocation end) { return _offset >= start.begin() && _offset <= end.end(); } bool containsOffset(SourceLocation loc) { return _offset >= loc.begin() && _offset <= loc.end(); } bool checkBindingName(UiQualifiedId *id) { if (id && !id->name.isEmpty() && !id->next && containsOffset(id->identifierToken)) { _scope = _doc->bind()->findQmlObject(_objectNode); _name = id->name.toString(); return true; } return false; } bool checkTypeName(UiQualifiedId *id) { for (UiQualifiedId *att = id; att; att = att->next) { if (!att->name.isEmpty() && containsOffset(att->identifierToken)) { _targetValue = _scopeChain->context()->lookupType(_doc.data(), id, att->next); _scope = nullptr; _name = att->name.toString(); _typeKind = TypeKind; return true; } } return false; } void setScope(Node *node) { Evaluate evaluate(_scopeChain); const Value *v = evaluate(node); if (v) _scope = v->asObjectValue(); } QString _name; const ObjectValue *_scope = nullptr; const Value *_targetValue = nullptr; Node *_objectNode = nullptr; Document::Ptr _doc; const ScopeChain *_scopeChain = nullptr; quint32 _offset = 0; Kind _typeKind = ExpKind; }; static QString matchingLine(unsigned position, const QString &source) { int start = source.lastIndexOf(QLatin1Char('\n'), position); start += 1; int end = source.indexOf(QLatin1Char('\n'), position); return source.mid(start, end - start); } class ProcessFile { ContextPtr context; using Usage = FindReferences::Usage; const QString name; const ObjectValue *scope; QFutureInterface *future; public: // needed by QtConcurrent using argument_type = const QString &; using result_type = QList; ProcessFile(const ContextPtr &context, const QString &name, const ObjectValue *scope, QFutureInterface *future) : context(context), name(name), scope(scope), future(future) { } QList operator()(const QString &fileName) { QList usages; if (future->isPaused()) future->waitForResume(); if (future->isCanceled()) return usages; Document::Ptr doc = context->snapshot().document(fileName); if (!doc) return usages; // find all idenfifier expressions, try to resolve them and check if the result is in scope FindUsages findUsages(doc, context); FindUsages::Result results = findUsages(name, scope); foreach (const SourceLocation &loc, results) usages.append(Usage(fileName, matchingLine(loc.offset, doc->source()), loc.startLine, loc.startColumn - 1, loc.length)); if (future->isPaused()) future->waitForResume(); return usages; } }; class SearchFileForType { ContextPtr context; using Usage = FindReferences::Usage; const QString name; const ObjectValue *scope; QFutureInterface *future; public: // needed by QtConcurrent using argument_type = const QString &; using result_type = QList; SearchFileForType(const ContextPtr &context, const QString &name, const ObjectValue *scope, QFutureInterface *future) : context(context), name(name), scope(scope), future(future) { } QList operator()(const QString &fileName) { QList usages; if (future->isPaused()) future->waitForResume(); if (future->isCanceled()) return usages; Document::Ptr doc = context->snapshot().document(fileName); if (!doc) return usages; // find all idenfifier expressions, try to resolve them and check if the result is in scope FindTypeUsages findUsages(doc, context); FindTypeUsages::Result results = findUsages(name, scope); foreach (const SourceLocation &loc, results) usages.append(Usage(fileName, matchingLine(loc.offset, doc->source()), loc.startLine, loc.startColumn - 1, loc.length)); if (future->isPaused()) future->waitForResume(); return usages; } }; class UpdateUI { using Usage = FindReferences::Usage; QFutureInterface *future; public: // needed by QtConcurrent using first_argument_type = QList &; using second_argument_type = const QList &; using result_type = void; UpdateUI(QFutureInterface *future): future(future) {} void operator()(QList &, const QList &usages) { foreach (const Usage &u, usages) future->reportResult(u); future->setProgressValue(future->progressValue() + 1); } }; } // end of anonymous namespace FindReferences::FindReferences(QObject *parent) : QObject(parent) { m_watcher.setPendingResultsLimit(1); connect(&m_watcher, &QFutureWatcherBase::resultsReadyAt, this, &FindReferences::displayResults); connect(&m_watcher, &QFutureWatcherBase::finished, this, &FindReferences::searchFinished); } FindReferences::~FindReferences() = default; static void find_helper(QFutureInterface &future, const ModelManagerInterface::WorkingCopy &workingCopy, Snapshot snapshot, const QString &fileName, quint32 offset, QString replacement) { // update snapshot from workingCopy to make sure it's up to date // ### remove? // ### this is a great candidate for map-reduce const ModelManagerInterface::WorkingCopy::Table &all = workingCopy.all(); for (auto it = all.cbegin(), end = all.cend(); it != end; ++it) { const QString fileName = it.key(); Document::Ptr oldDoc = snapshot.document(fileName); if (oldDoc && oldDoc->editorRevision() == it.value().second) continue; Dialect language; if (oldDoc) language = oldDoc->language(); else language = ModelManagerInterface::guessLanguageOfFile(fileName); if (language == Dialect::NoLanguage) { qCDebug(qmljsLog) << "NoLanguage in qmljsfindreferences.cpp find_helper for " << fileName; language = Dialect::AnyLanguage; } Document::MutablePtr newDoc = snapshot.documentFromSource( it.value().first, fileName, language); newDoc->parse(); snapshot.insert(newDoc); } // find the scope for the name we're searching Document::Ptr doc = snapshot.document(fileName); if (!doc) return; ModelManagerInterface *modelManager = ModelManagerInterface::instance(); Link link(snapshot, modelManager->defaultVContext(doc->language(), doc), modelManager->builtins(doc)); ContextPtr context = link(); ScopeChain scopeChain(doc, context); ScopeBuilder builder(&scopeChain); ScopeAstPath astPath(doc); builder.push(astPath(offset)); FindTargetExpression findTarget(doc, &scopeChain); findTarget(offset); const QString &name = findTarget.name(); if (name.isEmpty()) return; if (!replacement.isNull() && replacement.isEmpty()) replacement = name; QStringList files; foreach (const Document::Ptr &doc, snapshot) { // ### skip files that don't contain the name token files.append(doc->fileName()); } future.setProgressRange(0, files.size()); // report a dummy usage to indicate the search is starting FindReferences::Usage searchStarting(replacement, name, 0, 0, 0); if (findTarget.typeKind() == findTarget.TypeKind){ const ObjectValue *typeValue = value_cast(findTarget.targetValue()); if (!typeValue) return; future.reportResult(searchStarting); SearchFileForType process(context, name, typeValue, &future); UpdateUI reduce(&future); QtConcurrent::blockingMappedReduced > (files, process, reduce); } else { const ObjectValue *scope = findTarget.scope(); if (!scope) return; scope->lookupMember(name, context, &scope); if (!scope) return; if (!scope->className().isEmpty()) searchStarting.lineText.prepend(scope->className() + QLatin1Char('.')); future.reportResult(searchStarting); ProcessFile process(context, name, scope, &future); UpdateUI reduce(&future); QtConcurrent::blockingMappedReduced > (files, process, reduce); } future.setProgressValue(files.size()); } void FindReferences::findUsages(const QString &fileName, quint32 offset) { ModelManagerInterface *modelManager = ModelManagerInterface::instance(); QFuture result = Utils::runAsync(&find_helper, modelManager->workingCopy(), modelManager->snapshot(), fileName, offset, QString()); m_watcher.setFuture(result); } void FindReferences::renameUsages(const QString &fileName, quint32 offset, const QString &replacement) { ModelManagerInterface *modelManager = ModelManagerInterface::instance(); // an empty non-null string asks the future to use the current name as base QString newName = replacement; if (newName.isNull()) newName = QLatin1String(""); QFuture result = Utils::runAsync(&find_helper, modelManager->workingCopy(), modelManager->snapshot(), fileName, offset, newName); m_watcher.setFuture(result); } QList FindReferences::findUsageOfType(const QString &fileName, const QString &typeName) { QList usages; ModelManagerInterface *modelManager = ModelManagerInterface::instance(); Document::Ptr doc = modelManager->snapshot().document(fileName); if (!doc) return usages; Link link(modelManager->snapshot(), modelManager->defaultVContext(doc->language(), doc), modelManager->builtins(doc)); ContextPtr context = link(); ScopeChain scopeChain(doc, context); const ObjectValue *targetValue = scopeChain.context()->lookupType(doc.data(), QStringList(typeName)); QmlJS::Snapshot snapshot = modelManager->snapshot(); foreach (const QmlJS::Document::Ptr &doc, snapshot) { FindTypeUsages findUsages(doc, context); FindTypeUsages::Result results = findUsages(typeName, targetValue); foreach (const SourceLocation &loc, results) { usages.append(Usage(doc->fileName(), matchingLine(loc.offset, doc->source()), loc.startLine, loc.startColumn - 1, loc.length)); } } return usages; } void FindReferences::displayResults(int first, int last) { // the first usage is always a dummy to indicate we now start searching if (first == 0) { Usage dummy = m_watcher.future().resultAt(0); const QString replacement = dummy.path; const QString symbolName = dummy.lineText; const QString label = tr("QML/JS Usages:"); if (replacement.isEmpty()) { m_currentSearch = SearchResultWindow::instance()->startNewSearch( label, QString(), symbolName, SearchResultWindow::SearchOnly); } else { m_currentSearch = SearchResultWindow::instance()->startNewSearch( label, QString(), symbolName, SearchResultWindow::SearchAndReplace, SearchResultWindow::PreserveCaseDisabled); m_currentSearch->setTextToReplace(replacement); connect(m_currentSearch.data(), &SearchResult::replaceButtonClicked, this, &FindReferences::onReplaceButtonClicked); } connect(m_currentSearch.data(), &SearchResult::activated, [](const Core::SearchResultItem& item) { Core::EditorManager::openEditorAtSearchResult(item); }); connect(m_currentSearch.data(), &SearchResult::cancelled, this, &FindReferences::cancel); connect(m_currentSearch.data(), &SearchResult::paused, this, &FindReferences::setPaused); SearchResultWindow::instance()->popup(IOutputPane::Flags(IOutputPane::ModeSwitch | IOutputPane::WithFocus)); FutureProgress *progress = ProgressManager::addTask(m_watcher.future(), tr("Searching for Usages"), "QmlJSEditor.TaskSearch"); connect(progress, &FutureProgress::clicked, m_currentSearch.data(), &SearchResult::popup); ++first; } if (!m_currentSearch) { m_watcher.cancel(); return; } for (int index = first; index != last; ++index) { Usage result = m_watcher.future().resultAt(index); m_currentSearch->addResult(result.path, result.line, result.lineText, result.col, result.len); } } void FindReferences::searchFinished() { if (m_currentSearch) m_currentSearch->finishSearch(m_watcher.isCanceled()); m_currentSearch = nullptr; emit changed(); } void FindReferences::cancel() { m_watcher.cancel(); } void FindReferences::setPaused(bool paused) { if (!paused || m_watcher.isRunning()) // guard against pausing when the search is finished m_watcher.setPaused(paused); } void FindReferences::onReplaceButtonClicked(const QString &text, const QList &items, bool preserveCase) { const QStringList fileNames = TextEditor::BaseFileFind::replaceAll(text, items, preserveCase); // files that are opened in an editor are changed, but not saved QStringList changedOnDisk; QStringList changedUnsavedEditors; foreach (const QString &fileName, fileNames) { if (DocumentModel::documentForFilePath(fileName)) changedOnDisk += fileName; else changedUnsavedEditors += fileName; } if (!changedOnDisk.isEmpty()) ModelManagerInterface::instance()->updateSourceFiles(changedOnDisk, true); if (!changedUnsavedEditors.isEmpty()) ModelManagerInterface::instance()->updateSourceFiles(changedUnsavedEditors, false); SearchResultWindow::instance()->hide(); }