1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "qmljscomponentfromobjectdef.h"
27 #include "qmljscomponentnamedialog.h"
28 #include "qmljsquickfixassist.h"
29 
30 #include <coreplugin/icore.h>
31 #include <coreplugin/idocument.h>
32 #include <coreplugin/iversioncontrol.h>
33 #include <coreplugin/vcsmanager.h>
34 
35 #include <qmljs/parser/qmljsast_p.h>
36 #include <qmljs/qmljsdocument.h>
37 #include <qmljs/qmljsmodelmanagerinterface.h>
38 #include <qmljs/qmljsutils.h>
39 #include <qmljs/qmljspropertyreader.h>
40 #include <qmljs/qmljsrewriter.h>
41 #include <qmljstools/qmljsrefactoringchanges.h>
42 #include <projectexplorer/project.h>
43 #include <projectexplorer/projectnodes.h>
44 #include <projectexplorer/projecttree.h>
45 
46 #include <utils/fileutils.h>
47 
48 #include <QCoreApplication>
49 #include <QDir>
50 #include <QFileInfo>
51 #include <QMessageBox>
52 
53 using namespace QmlJS::AST;
54 using QmlJS::SourceLocation;
55 using namespace QmlJSTools;
56 using namespace Utils;
57 
58 namespace QmlJSEditor {
59 
60 using namespace Internal;
61 
62 namespace {
63 
64 class Operation: public QmlJSQuickFixOperation
65 {
66     QString m_idName, m_componentName;
67     SourceLocation m_firstSourceLocation;
68     SourceLocation m_lastSourceLocation;
69     UiObjectInitializer *m_initializer;
70 public:
init()71     void init()
72     {
73         if (!m_idName.isEmpty()) {
74             m_componentName = m_idName;
75             m_componentName[0] = m_componentName.at(0).toUpper();
76             m_componentName.prepend("My");
77         }
78 
79         setDescription(QCoreApplication::translate("QmlJSEditor::ComponentFromObjectDef",
80                                                    "Move Component into Separate File"));
81     }
82 
Operation(const QSharedPointer<const QmlJSQuickFixAssistInterface> & interface,UiObjectDefinition * objDef)83     Operation(const QSharedPointer<const QmlJSQuickFixAssistInterface> &interface,
84               UiObjectDefinition *objDef)
85         : QmlJSQuickFixOperation(interface, 0),
86           m_idName(idOfObject(objDef)),
87           m_firstSourceLocation(objDef->firstSourceLocation()),
88           m_lastSourceLocation(objDef->lastSourceLocation()),
89           m_initializer(objDef->initializer)
90     {
91         init();
92     }
93 
Operation(const QSharedPointer<const QmlJSQuickFixAssistInterface> & interface,UiObjectBinding * objDef)94     Operation(const QSharedPointer<const QmlJSQuickFixAssistInterface> &interface,
95               UiObjectBinding *objDef)
96         : QmlJSQuickFixOperation(interface, 0),
97           m_idName(idOfObject(objDef)),
98           m_firstSourceLocation(objDef->qualifiedTypeNameId->firstSourceLocation()),
99           m_lastSourceLocation(objDef->lastSourceLocation()),
100           m_initializer(objDef->initializer)
101     {
102         init();
103     }
104 
performChanges(QmlJSRefactoringFilePtr currentFile,const QmlJSRefactoringChanges & refactoring)105     void performChanges(QmlJSRefactoringFilePtr currentFile,
106                         const QmlJSRefactoringChanges &refactoring) override
107     {
108         QString componentName = m_componentName;
109 
110         const QString currentFileName = currentFile->qmljsDocument()->fileName();
111         QString path = QFileInfo(currentFileName).path();
112 
113         QmlJS::PropertyReader propertyReader(currentFile->qmljsDocument(), m_initializer);
114         QStringList result;
115         QStringList sourcePreview;
116 
117         QString suffix;
118 
119         if (!m_idName.isEmpty())
120             sourcePreview.append(QLatin1String("    id: ") + m_idName);
121         else
122             sourcePreview.append(QString());
123 
124         QStringList sortedPropertiesWithoutId;
125 
126         foreach (const QString &property, propertyReader.properties())
127             if (property != QLatin1String("id"))
128                 sortedPropertiesWithoutId.append(property);
129 
130         sortedPropertiesWithoutId.sort();
131 
132         foreach (const QString &property, sortedPropertiesWithoutId)
133             sourcePreview.append(QLatin1String("    ") + property + QLatin1String(": ") + propertyReader.readAstValue(property));
134 
135         const bool confirm = ComponentNameDialog::go(&componentName, &path, &suffix,
136                                                sortedPropertiesWithoutId,
137                                                sourcePreview,
138                                                QFileInfo(currentFileName).fileName(),
139                                                &result,
140                                                Core::ICore::dialogParent());
141         if (!confirm)
142             return;
143 
144         if (componentName.isEmpty() || path.isEmpty())
145             return;
146 
147         const QString newFileName = path + QLatin1Char('/') + componentName
148                 + QLatin1String(".") + suffix;
149 
150         QString imports;
151         UiProgram *prog = currentFile->qmljsDocument()->qmlProgram();
152         if (prog && prog->headers) {
153             const unsigned int start = currentFile->startOf(prog->headers->firstSourceLocation());
154             const unsigned int end = currentFile->startOf(prog->members->member->firstSourceLocation());
155             imports = currentFile->textOf(start, end);
156         }
157 
158         const unsigned int start = currentFile->startOf(m_firstSourceLocation);
159         const unsigned int end = currentFile->startOf(m_lastSourceLocation);
160         QString newComponentSource = imports + currentFile->textOf(start, end)
161                 + QLatin1String("}\n");
162 
163         //Remove properties from resulting code...
164 
165         Utils::ChangeSet changeSet;
166         QmlJS::Rewriter rewriter(newComponentSource, &changeSet, QStringList());
167 
168         QmlJS::Dialect dialect = QmlJS::Dialect::Qml;
169 
170         QmlJS::Document::MutablePtr doc = QmlJS::Document::create(newFileName, dialect);
171         doc->setSource(newComponentSource);
172         doc->parseQml();
173 
174         if (doc->isParsedCorrectly()) {
175 
176             UiObjectMember *astRootNode = nullptr;
177             if (UiProgram *program = doc->qmlProgram())
178                 if (program->members)
179                     astRootNode = program->members->member;
180 
181             foreach (const QString &property, result)
182                 rewriter.removeBindingByName(initializerOfObject(astRootNode), property);
183         } else {
184             qWarning() << Q_FUNC_INFO << "parsing failed:" << newComponentSource;
185         }
186 
187         changeSet.apply(&newComponentSource);
188 
189         // stop if we can't create the new file
190         const bool reindent = true;
191         const bool openEditor = false;
192         const Utils::FilePath newFilePath = Utils::FilePath::fromString(newFileName);
193         if (!refactoring.createFile(newFilePath, newComponentSource, reindent, openEditor))
194             return;
195 
196         if (path == QFileInfo(currentFileName).path()) {
197             // hack for the common case, next version should use the wizard
198             ProjectExplorer::Node * oldFileNode =
199                     ProjectExplorer::ProjectTree::nodeForFile(Utils::FilePath::fromString(currentFileName));
200             if (oldFileNode) {
201                 ProjectExplorer::FolderNode *containingFolder = oldFileNode->parentFolderNode();
202                 if (containingFolder)
203                     containingFolder->addFiles({FilePath::fromString(newFileName)});
204             }
205         }
206 
207         QString replacement = componentName + QLatin1String(" {\n");
208         if (!m_idName.isEmpty())
209             replacement += QLatin1String("id: ") + m_idName + QLatin1Char('\n');
210 
211         foreach (const QString &property, result)
212             replacement += property + QLatin1String(": ") + propertyReader.readAstValue(property) + QLatin1Char('\n');
213 
214         Utils::ChangeSet changes;
215         changes.replace(start, end, replacement);
216         currentFile->setChangeSet(changes);
217         currentFile->appendIndentRange(Range(start, end + 1));
218         currentFile->apply();
219 
220         Core::IVersionControl *versionControl = Core::VcsManager::findVersionControlForDirectory(path);
221         if (versionControl
222                 && versionControl->supportsOperation(Core::IVersionControl::AddOperation)) {
223             const QMessageBox::StandardButton button = QMessageBox::question(
224                 Core::ICore::dialogParent(),
225                 Core::VcsManager::msgAddToVcsTitle(),
226                 Core::VcsManager::msgPromptToAddToVcs(QStringList(newFileName), versionControl),
227                 QMessageBox::Yes | QMessageBox::No);
228             if (button == QMessageBox::Yes && !versionControl->vcsAdd(newFileName)) {
229                 QMessageBox::warning(Core::ICore::dialogParent(),
230                                      Core::VcsManager::msgAddToVcsFailedTitle(),
231                                      Core::VcsManager::msgToAddToVcsFailed(QStringList(newFileName),
232                                                                            versionControl));
233             }
234         }
235     }
236 };
237 
238 } // end of anonymous namespace
239 
240 
matchComponentFromObjectDefQuickFix(const QmlJSQuickFixInterface & interface,QuickFixOperations & result)241 void matchComponentFromObjectDefQuickFix(const QmlJSQuickFixInterface &interface, QuickFixOperations &result)
242 {
243     const int pos = interface->currentFile()->cursor().position();
244 
245     QList<Node *> path = interface->semanticInfo().rangePath(pos);
246     for (int i = path.size() - 1; i >= 0; --i) {
247         Node *node = path.at(i);
248         if (auto objDef = cast<UiObjectDefinition *>(node)) {
249 
250             if (!interface->currentFile()->isCursorOn(objDef->qualifiedTypeNameId))
251                 return;
252              // check that the node is not the root node
253             if (i > 0 && !cast<UiProgram*>(path.at(i - 1))) {
254                 result << new Operation(interface, objDef);
255                 return;
256             }
257         } else if (auto objBinding = cast<UiObjectBinding *>(node)) {
258             if (!interface->currentFile()->isCursorOn(objBinding->qualifiedTypeNameId))
259                 return;
260             result << new Operation(interface, objBinding);
261             return;
262         }
263     }
264 }
265 
performComponentFromObjectDef(const QString & fileName,QmlJS::AST::UiObjectDefinition * objDef)266 void performComponentFromObjectDef(const QString &fileName, QmlJS::AST::UiObjectDefinition *objDef)
267 {
268     QmlJSRefactoringChanges refactoring(QmlJS::ModelManagerInterface::instance(),
269                                         QmlJS::ModelManagerInterface::instance()->snapshot());
270     QmlJSRefactoringFilePtr current = refactoring.file(Utils::FilePath::fromString(fileName));
271 
272     QmlJSQuickFixInterface interface;
273     Operation operation(interface, objDef);
274 
275     operation.performChanges(current, refactoring);
276 }
277 
278 } //namespace QmlJSEditor
279