kateprojectplugin.cpp 16.3 KB
Newer Older
1
2
/*  This file is part of the Kate project.
 *
3
 *  SPDX-FileCopyrightText: 2010 Christoph Cullmann <cullmann@kde.org>
4
 *
5
 *  SPDX-License-Identifier: LGPL-2.0-or-later
6
7
 */

8
#include "kateprojectplugin.h"
9

10
#include "kateprojectconfigpage.h"
Christoph Cullmann's avatar
Christoph Cullmann committed
11
#include "kateprojectpluginview.h"
Christoph Cullmann's avatar
Christoph Cullmann committed
12

Christoph Cullmann's avatar
Christoph Cullmann committed
13
#include <ktexteditor/application.h>
14
#include <ktexteditor/editor.h>
15
#include <ktexteditor/view.h>
16

17
#include <KConfigGroup>
18
#include <KLocalizedString>
19
20
#include <KSharedConfig>

21
#include <QCoreApplication>
22
#include <QFileInfo>
23
#include <QTime>
24
25
#include <QMessageBox>
#include <QString>
26

27
28
#include <vector>

29
30
#ifdef HAVE_CTERMID
#include <fcntl.h>
31
32
#include <sys/stat.h>
#include <sys/types.h>
33
34
35
36
#include <termios.h>
#include <unistd.h>
#endif

37
namespace
38
{
39
40
41
42
43
44
45
46
47
48
const QString ProjectFileName = QStringLiteral(".kateproject");
const QString GitFolderName = QStringLiteral(".git");
const QString SubversionFolderName = QStringLiteral(".svn");
const QString MercurialFolderName = QStringLiteral(".hg");

const QString GitConfig = QStringLiteral("git");
const QString SubversionConfig = QStringLiteral("subversion");
const QString MercurialConfig = QStringLiteral("mercurial");

const QStringList DefaultConfig = QStringList() << GitConfig << SubversionConfig << MercurialConfig;
49
50
}

51
KateProjectPlugin::KateProjectPlugin(QObject *parent, const QList<QVariant> &)
52
53
    : KTextEditor::Plugin(parent)
    , m_completion(this)
54
{
55
    qRegisterMetaType<KateProjectSharedQStandardItem>("KateProjectSharedQStandardItem");
56
    qRegisterMetaType<KateProjectSharedQHashStringItem>("KateProjectSharedQHashStringItem");
57
58
    qRegisterMetaType<KateProjectSharedProjectIndex>("KateProjectSharedProjectIndex");

59
60
    connect(KTextEditor::Editor::instance()->application(), &KTextEditor::Application::documentCreated, this, &KateProjectPlugin::slotDocumentCreated);
    connect(&m_fileWatcher, &QFileSystemWatcher::directoryChanged, this, &KateProjectPlugin::slotDirectoryChanged);
61

62
63
    // read configuration prior to cwd project setup below
    readConfig();
64
65
66
67
68
69
70
71
72
73
    QStringList args = qApp->arguments();
    bool projectSpecified = false;
    args.removeFirst(); // The first argument is the executable name
    for (const QString &arg : qAsConst(args)) {
        QFileInfo info(arg);
        if (info.isDir()) {
            projectForDir(info.absoluteFilePath());
            projectSpecified = true;
        }
    }
74

75
#ifdef HAVE_CTERMID
76
    /**
77
     * open project for our current working directory, if this kate has a terminal
78
     * https://stackoverflow.com/questions/1312922/detect-if-stdin-is-a-terminal-or-pipe-in-c-c-qt
79
     */
80
81
82
    char tty[L_ctermid + 1] = {0};
    ctermid(tty);
    int fd = ::open(tty, O_RDONLY);
83

84
    if (fd >= 0) {
85
86
87
        if (!projectSpecified) {
            projectForDir(QDir::current());
        }
88
89
90
91
        ::close(fd);
    }
#endif

Michal Humpula's avatar
Michal Humpula committed
92
    for (auto document : KTextEditor::Editor::instance()->application()->documents()) {
93
94
        slotDocumentCreated(document);
    }
95
96

    registerVariables();
97
98
}

99
KateProjectPlugin::~KateProjectPlugin()
100
{
101
102
    unregisterVariables();

Michal Humpula's avatar
Michal Humpula committed
103
    for (KateProject *project : m_projects) {
104
105
106
107
        m_fileWatcher.removePath(QFileInfo(project->fileName()).canonicalPath());
        delete project;
    }
    m_projects.clear();
108
109
}

110
QObject *KateProjectPlugin::createView(KTextEditor::MainWindow *mainWindow)
111
{
112
    return new KateProjectPluginView(this, mainWindow);
113
}
114

115
116
117
118
119
120
121
122
int KateProjectPlugin::configPages() const
{
    return 1;
}

KTextEditor::ConfigPage *KateProjectPlugin::configPage(int number, QWidget *parent)
{
    if (number != 0) {
Michal Humpula's avatar
Michal Humpula committed
123
        return nullptr;
124
125
126
127
    }
    return new KateProjectConfigPage(parent, this);
}

128
KateProject *KateProjectPlugin::createProjectForFileName(const QString &fileName)
129
{
130
    KateProject *project = new KateProject(m_threadPool, this);
131
    if (!project->loadFromFile(fileName)) {
132
        delete project;
Michal Humpula's avatar
Michal Humpula committed
133
        return nullptr;
134
135
    }

136
137
    m_projects.append(project);
    m_fileWatcher.addPath(QFileInfo(fileName).canonicalPath());
Christoph Cullmann's avatar
Christoph Cullmann committed
138
    Q_EMIT projectCreated(project);
139
140
    return project;
}
141

142
143
KateProject *KateProjectPlugin::projectForDir(QDir dir)
{
144
    /**
145
     * search project file upwards
146
     * with recursion guard
147
148
     * do this first for all level and only after this fails try to invent projects
     * otherwise one e.g. invents projects for .kateproject tree structures with sub .git clones
149
     */
150
    QSet<QString> seenDirectories;
151
    std::vector<QString> directoryStack;
152
    while (!seenDirectories.contains(dir.absolutePath())) {
153
        // update guard
154
155
        seenDirectories.insert(dir.absolutePath());

156
157
        // remember directory for later project creation based on other criteria
        directoryStack.push_back(dir.absolutePath());
158

159
160
161
        // check for project and load it if found
        const QString canonicalPath = dir.canonicalPath();
        const QString canonicalFileName = dir.filePath(ProjectFileName);
Michal Humpula's avatar
Michal Humpula committed
162
        for (KateProject *project : m_projects) {
163
164
165
166
167
            if (project->baseDir() == canonicalPath || project->fileName() == canonicalFileName) {
                return project;
            }
        }

168
        // project file found => done
169
170
171
172
        if (dir.exists(ProjectFileName)) {
            return createProjectForFileName(canonicalFileName);
        }

173
        // else: cd up, if possible or abort
174
175
176
177
178
        if (!dir.cdUp()) {
            break;
        }
    }

179
180
181
182
183
184
185
186
187
188
189
190
191
    /**
     * if we arrive here, we found no .kateproject
     * => we want to invent a project based on e.g. version control system info
     */
    for (const QString &dir : directoryStack) {
        // try to invent project based on version control stuff
        KateProject *project = nullptr;
        if ((project = detectGit(dir)) || (project = detectSubversion(dir)) || (project = detectMercurial(dir))) {
            return project;
        }
    }

    // no project found, bad luck
192
    return nullptr;
Christoph Cullmann's avatar
Christoph Cullmann committed
193
194
}

195
bool KateProjectPlugin::closeProject(KateProject *project)
196
{
Krzysztof Stokop's avatar
Krzysztof Stokop committed
197
    QList< KTextEditor::Document* > documents = KTextEditor::Editor::instance()->application()->documents();
198
    QVector< KTextEditor::Document* > projectDocuments;
199
    QWidget* window = KTextEditor::Editor::instance()->application()->activeMainWindow()->window();
200
    
Krzysztof Stokop's avatar
Krzysztof Stokop committed
201
202
    for(int i = 0; i<documents.size(); i++)
        if(QUrl(project->baseDir()).isParentOf(documents[i]->url().adjusted(QUrl::RemoveScheme)))
203
            projectDocuments.push_back(documents[i]);
204
205
206
        
    QString title = i18n("Confirm project closing: ") + project->name();
    QString text = i18n("Do you want to close ") + QString::number(projectDocuments.size()) + i18n(" documents and ") + project->name() + i18n(" project?");
207

208
    QMessageBox confirmationBox;
Krzysztof Stokop's avatar
Krzysztof Stokop committed
209
    
210
    if(QMessageBox::Yes == confirmationBox.question(window, title, text, QMessageBox::No | QMessageBox::Yes, QMessageBox::No))
211
    {
212
213
214
215
216
217
218
219
220
221
        for(int i = 0; i<projectDocuments.size(); i++)
            KTextEditor::Editor::instance()->application()->closeDocument(projectDocuments[i]);
        
        Q_EMIT pluginViewProjectClosing(project);
        if(m_projects.removeOne(project))
        {
            m_fileWatcher.removePath(QFileInfo(project->fileName()).canonicalPath());
            delete project;
            return true;
        }
222
    }
223
224
    
    return false;
225
226
}

227
KateProject *KateProjectPlugin::projectForUrl(const QUrl &url)
228
{
229
    if (url.isEmpty() || !url.isLocalFile()) {
Michal Humpula's avatar
Michal Humpula committed
230
        return nullptr;
231
    }
232

233
    return projectForDir(QFileInfo(url.toLocalFile()).absoluteDir());
234
235
}

236
void KateProjectPlugin::slotDocumentCreated(KTextEditor::Document *document)
Christoph Cullmann's avatar
Christoph Cullmann committed
237
{
238
239
    connect(document, &KTextEditor::Document::documentUrlChanged, this, &KateProjectPlugin::slotDocumentUrlChanged);
    connect(document, &KTextEditor::Document::destroyed, this, &KateProjectPlugin::slotDocumentDestroyed);
240
241

    slotDocumentUrlChanged(document);
Christoph Cullmann's avatar
Christoph Cullmann committed
242
243
}

244
void KateProjectPlugin::slotDocumentDestroyed(QObject *document)
245
{
246
247
248
249
250
    if (KateProject *project = m_document2Project.value(document)) {
        project->unregisterDocument(static_cast<KTextEditor::Document *>(document));
    }

    m_document2Project.remove(document);
251
252
}

253
void KateProjectPlugin::slotDocumentUrlChanged(KTextEditor::Document *document)
Christoph Cullmann's avatar
Christoph Cullmann committed
254
{
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
    KateProject *project = projectForUrl(document->url());

    if (KateProject *project = m_document2Project.value(document)) {
        project->unregisterDocument(document);
    }

    if (!project) {
        m_document2Project.remove(document);
    } else {
        m_document2Project[document] = project;
    }

    if (KateProject *project = m_document2Project.value(document)) {
        project->registerDocument(document);
    }
270
271
}

272
void KateProjectPlugin::slotDirectoryChanged(const QString &path)
273
{
274
    QString fileName = QDir(path).filePath(ProjectFileName);
275
    for (KateProject *project : m_projects) {
276
        if (project->fileName() == fileName) {
277
278
279
280
            QDateTime lastModified = QFileInfo(fileName).lastModified();
            if (project->fileLastModified().isNull() || (lastModified > project->fileLastModified())) {
                project->reload();
            }
281
282
            break;
        }
283
    }
284
}
285

286
KateProject *KateProjectPlugin::detectGit(const QDir &dir)
287
{
288
289
    // allow .git as dir and file (file for git worktree stuff, https://git-scm.com/docs/git-worktree)
    if (m_autoGit && dir.exists(GitFolderName)) {
290
291
292
293
294
295
        return createProjectForRepository(QStringLiteral("git"), dir);
    }

    return nullptr;
}

296
KateProject *KateProjectPlugin::detectSubversion(const QDir &dir)
297
298
299
300
301
302
303
304
{
    if (m_autoSubversion && dir.exists(SubversionFolderName) && QFileInfo(dir, SubversionFolderName).isDir()) {
        return createProjectForRepository(QStringLiteral("svn"), dir);
    }

    return nullptr;
}

305
KateProject *KateProjectPlugin::detectMercurial(const QDir &dir)
306
307
308
309
310
311
312
313
314
315
316
317
{
    if (m_autoMercurial && dir.exists(MercurialFolderName) && QFileInfo(dir, MercurialFolderName).isDir()) {
        return createProjectForRepository(QStringLiteral("hg"), dir);
    }

    return nullptr;
}

KateProject *KateProjectPlugin::createProjectForRepository(const QString &type, const QDir &dir)
{
    QVariantMap cnf, files;
    files[type] = 1;
Laurent Montel's avatar
Laurent Montel committed
318
319
    cnf[QStringLiteral("name")] = dir.dirName();
    cnf[QStringLiteral("files")] = (QVariantList() << files);
320

321
    KateProject *project = new KateProject(m_threadPool, this);
322
323
324
325
    project->loadFromData(cnf, dir.canonicalPath());

    m_projects.append(project);

Christoph Cullmann's avatar
Christoph Cullmann committed
326
    Q_EMIT projectCreated(project);
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
    return project;
}

void KateProjectPlugin::setAutoRepository(bool onGit, bool onSubversion, bool onMercurial)
{
    m_autoGit = onGit;
    m_autoSubversion = onSubversion;
    m_autoMercurial = onMercurial;
    writeConfig();
}

bool KateProjectPlugin::autoGit() const
{
    return m_autoGit;
}

bool KateProjectPlugin::autoSubversion() const
{
    return m_autoSubversion;
}

bool KateProjectPlugin::autoMercurial() const
{
    return m_autoMercurial;
}

353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
void KateProjectPlugin::setIndex(bool enabled, const QUrl &directory)
{
    m_indexEnabled = enabled;
    m_indexDirectory = directory;
    writeConfig();
}

bool KateProjectPlugin::getIndexEnabled() const
{
    return m_indexEnabled;
}

QUrl KateProjectPlugin::getIndexDirectory() const
{
    return m_indexDirectory;
}

370
371
372
373
374
375
376
377
378
379
bool KateProjectPlugin::multiProjectCompletion() const
{
    return m_multiProjectCompletion;
}

bool KateProjectPlugin::multiProjectGoto() const
{
    return m_multiProjectGoto;
}

380
381
382
383
384
385
386
387
388
389
390
void KateProjectPlugin::setGitStatusShowNumStat(bool show)
{
    m_gitNumStat = show;
    writeConfig();
}

bool KateProjectPlugin::showGitStatusWithNumStat()
{
    return m_gitNumStat;
}

391
392
void KateProjectPlugin::setSingleClickAction(ClickAction cb)
{
Waqar Ahmed's avatar
Waqar Ahmed committed
393
    m_singleClickAction = cb;
394
395
396
397
398
    writeConfig();
}

ClickAction KateProjectPlugin::singleClickAcion()
{
Waqar Ahmed's avatar
Waqar Ahmed committed
399
    return m_singleClickAction;
400
401
402
403
}

void KateProjectPlugin::setDoubleClickAction(ClickAction cb)
{
Waqar Ahmed's avatar
Waqar Ahmed committed
404
    m_doubleClickAction = cb;
405
406
407
408
409
    writeConfig();
}

ClickAction KateProjectPlugin::doubleClickAcion()
{
Waqar Ahmed's avatar
Waqar Ahmed committed
410
    return m_doubleClickAction;
411
412
}

413
414
415
416
417
418
419
void KateProjectPlugin::setMultiProject(bool completion, bool gotoSymbol)
{
    m_multiProjectCompletion = completion;
    m_multiProjectGoto = gotoSymbol;
    writeConfig();
}

420
421
422
423
void KateProjectPlugin::readConfig()
{
    KConfigGroup config(KSharedConfig::openConfig(), "project");

424
425
426
427
    const QStringList autorepository = config.readEntry("autorepository", DefaultConfig);
    m_autoGit = autorepository.contains(GitConfig);
    m_autoSubversion = autorepository.contains(SubversionConfig);
    m_autoMercurial = autorepository.contains(MercurialConfig);
428
429
430

    m_indexEnabled = config.readEntry("index", false);
    m_indexDirectory = config.readEntry("indexDirectory", QUrl());
431
432
433
434

    m_multiProjectCompletion = config.readEntry("multiProjectCompletion", false);
    m_multiProjectGoto = config.readEntry("multiProjectCompletion", false);

435
    m_gitNumStat = config.readEntry("gitStatusNumStat", true);
Waqar Ahmed's avatar
Waqar Ahmed committed
436
437
    m_singleClickAction = (ClickAction)config.readEntry("gitStatusSingleClick", (int)ClickAction::ShowDiff);
    m_doubleClickAction = (ClickAction)config.readEntry("gitStatusDoubleClick", (int)ClickAction::StageUnstage);
438

Christoph Cullmann's avatar
Christoph Cullmann committed
439
    Q_EMIT configUpdated();
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
}

void KateProjectPlugin::writeConfig()
{
    KConfigGroup config(KSharedConfig::openConfig(), "project");
    QStringList repos;

    if (m_autoGit) {
        repos << GitConfig;
    }

    if (m_autoSubversion) {
        repos << SubversionConfig;
    }

    if (m_autoMercurial) {
        repos << MercurialConfig;
    }

    config.writeEntry("autorepository", repos);
460
461
462

    config.writeEntry("index", m_indexEnabled);
    config.writeEntry("indexDirectory", m_indexDirectory);
463
464
465
466

    config.writeEntry("multiProjectCompletion", m_multiProjectCompletion);
    config.writeEntry("multiProjectGoto", m_multiProjectGoto);

467
    config.writeEntry("gitStatusNumStat", m_gitNumStat);
Waqar Ahmed's avatar
Waqar Ahmed committed
468
469
    config.writeEntry("gitStatusSingleClick", (int)m_singleClickAction);
    config.writeEntry("gitStatusDoubleClick", (int)m_doubleClickAction);
470

Christoph Cullmann's avatar
Christoph Cullmann committed
471
    Q_EMIT configUpdated();
472
}
473
474
475
476

static KateProjectPlugin *findProjectPlugin()
{
    auto plugin = KTextEditor::Editor::instance()->application()->plugin(QStringLiteral("kateprojectplugin"));
477
    return qobject_cast<KateProjectPlugin *>(plugin);
478
479
480
481
482
}

void KateProjectPlugin::registerVariables()
{
    auto editor = KTextEditor::Editor::instance();
Alexander Lohnau's avatar
Alexander Lohnau committed
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
    editor->registerVariableMatch(QStringLiteral("Project:Path"),
                                  i18n("Full path to current project excluding the file name."),
                                  [](const QStringView &, KTextEditor::View *view) {
                                      if (!view) {
                                          return QString();
                                      }
                                      auto projectPlugin = findProjectPlugin();
                                      if (!projectPlugin) {
                                          return QString();
                                      }
                                      auto kateProject = findProjectPlugin()->projectForUrl(view->document()->url());
                                      if (!kateProject) {
                                          return QString();
                                      }
                                      return QDir(kateProject->baseDir()).absolutePath();
                                  });

    editor->registerVariableMatch(QStringLiteral("Project:NativePath"),
                                  i18n("Full path to current project excluding the file name, with native path separator (backslash on Windows)."),
                                  [](const QStringView &, KTextEditor::View *view) {
                                      if (!view) {
                                          return QString();
                                      }
                                      auto projectPlugin = findProjectPlugin();
                                      if (!projectPlugin) {
                                          return QString();
                                      }
                                      auto kateProject = findProjectPlugin()->projectForUrl(view->document()->url());
                                      if (!kateProject) {
                                          return QString();
                                      }
                                      return QDir::toNativeSeparators(QDir(kateProject->baseDir()).absolutePath());
                                  });
516
517
518
519
520
521
522
523
}

void KateProjectPlugin::unregisterVariables()
{
    auto editor = KTextEditor::Editor::instance();
    editor->unregisterVariableMatch(QStringLiteral("Project:Path"));
    editor->unregisterVariableMatch(QStringLiteral("Project:NativePath"));
}