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