kateprojectplugin.cpp 17 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
24
#include <QMessageBox>
#include <QString>
Christoph Cullmann's avatar
Christoph Cullmann committed
25
#include <QTime>
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
    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()) {
70
            projectForDir(info.absoluteFilePath(), true);
71
72
73
            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
KateProject *KateProjectPlugin::projectForDir(QDir dir, bool userSpecified)
143
{
144
145
146
147
148
    /**
     * Save dir to create a project from directory if nothing works
     */
    const QDir originalDir = dir;

149
    /**
150
     * search project file upwards
151
     * with recursion guard
152
153
     * 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
154
     */
155
    QSet<QString> seenDirectories;
156
    std::vector<QString> directoryStack;
157
    while (!seenDirectories.contains(dir.absolutePath())) {
158
        // update guard
159
160
        seenDirectories.insert(dir.absolutePath());

161
162
        // remember directory for later project creation based on other criteria
        directoryStack.push_back(dir.absolutePath());
163

164
165
166
        // 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
167
        for (KateProject *project : m_projects) {
168
169
170
171
172
            if (project->baseDir() == canonicalPath || project->fileName() == canonicalFileName) {
                return project;
            }
        }

173
        // project file found => done
174
175
176
177
        if (dir.exists(ProjectFileName)) {
            return createProjectForFileName(canonicalFileName);
        }

178
        // else: cd up, if possible or abort
179
180
181
182
183
        if (!dir.cdUp()) {
            break;
        }
    }

184
185
186
187
188
189
190
191
192
193
194
195
    /**
     * 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;
        }
    }

196
197
198
    /**
     * Version control not found? Load the directory as project
     */
199
200
201
202
203
204
205
206
    if (userSpecified) {
        return createProjectForDirectory(originalDir);
    }

    /**
     * Give up
     */
    return nullptr;
Christoph Cullmann's avatar
Christoph Cullmann committed
207
208
}

209
bool KateProjectPlugin::closeProject(KateProject *project)
210
{
Christoph Cullmann's avatar
Christoph Cullmann committed
211
212
213
214
215
216
    QList<KTextEditor::Document *> documents = KTextEditor::Editor::instance()->application()->documents();
    QVector<KTextEditor::Document *> projectDocuments;
    QWidget *window = KTextEditor::Editor::instance()->application()->activeMainWindow()->window();

    for (int i = 0; i < documents.size(); i++)
        if (QUrl(project->baseDir()).isParentOf(documents[i]->url().adjusted(QUrl::RemoveScheme)))
217
            projectDocuments.push_back(documents[i]);
Christoph Cullmann's avatar
Christoph Cullmann committed
218

219
    const QString title = i18n("Confirm project closing: %1", project->name());
Christoph Cullmann's avatar
Christoph Cullmann committed
220
    const QString text = i18n("Do you want to close the project %1 and the related %2 open documents?", project->name(), projectDocuments.size());
221
    if (QMessageBox::Yes == QMessageBox::question(window, title, text, QMessageBox::No | QMessageBox::Yes, QMessageBox::Yes)) {
Christoph Cullmann's avatar
Christoph Cullmann committed
222
        for (int i = 0; i < projectDocuments.size(); i++)
223
            KTextEditor::Editor::instance()->application()->closeDocument(projectDocuments[i]);
Christoph Cullmann's avatar
Christoph Cullmann committed
224

225
        Q_EMIT pluginViewProjectClosing(project);
Christoph Cullmann's avatar
Christoph Cullmann committed
226
        if (m_projects.removeOne(project)) {
227
228
229
230
            m_fileWatcher.removePath(QFileInfo(project->fileName()).canonicalPath());
            delete project;
            return true;
        }
231
    }
Christoph Cullmann's avatar
Christoph Cullmann committed
232

233
    return false;
234
235
}

236
KateProject *KateProjectPlugin::projectForUrl(const QUrl &url)
237
{
238
    if (url.isEmpty() || !url.isLocalFile()) {
Michal Humpula's avatar
Michal Humpula committed
239
        return nullptr;
240
    }
241

242
    return projectForDir(QFileInfo(url.toLocalFile()).absoluteDir());
243
244
}

245
void KateProjectPlugin::slotDocumentCreated(KTextEditor::Document *document)
Christoph Cullmann's avatar
Christoph Cullmann committed
246
{
247
248
    connect(document, &KTextEditor::Document::documentUrlChanged, this, &KateProjectPlugin::slotDocumentUrlChanged);
    connect(document, &KTextEditor::Document::destroyed, this, &KateProjectPlugin::slotDocumentDestroyed);
249
250

    slotDocumentUrlChanged(document);
Christoph Cullmann's avatar
Christoph Cullmann committed
251
252
}

253
void KateProjectPlugin::slotDocumentDestroyed(QObject *document)
254
{
255
256
257
258
259
    if (KateProject *project = m_document2Project.value(document)) {
        project->unregisterDocument(static_cast<KTextEditor::Document *>(document));
    }

    m_document2Project.remove(document);
260
261
}

262
void KateProjectPlugin::slotDocumentUrlChanged(KTextEditor::Document *document)
Christoph Cullmann's avatar
Christoph Cullmann committed
263
{
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
    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);
    }
279
280
}

281
void KateProjectPlugin::slotDirectoryChanged(const QString &path)
282
{
283
    QString fileName = QDir(path).filePath(ProjectFileName);
284
    for (KateProject *project : m_projects) {
285
        if (project->fileName() == fileName) {
286
287
288
289
            QDateTime lastModified = QFileInfo(fileName).lastModified();
            if (project->fileLastModified().isNull() || (lastModified > project->fileLastModified())) {
                project->reload();
            }
290
291
            break;
        }
292
    }
293
}
294

295
KateProject *KateProjectPlugin::detectGit(const QDir &dir)
296
{
297
298
    // allow .git as dir and file (file for git worktree stuff, https://git-scm.com/docs/git-worktree)
    if (m_autoGit && dir.exists(GitFolderName)) {
299
300
301
302
303
304
        return createProjectForRepository(QStringLiteral("git"), dir);
    }

    return nullptr;
}

305
KateProject *KateProjectPlugin::detectSubversion(const QDir &dir)
306
307
308
309
310
311
312
313
{
    if (m_autoSubversion && dir.exists(SubversionFolderName) && QFileInfo(dir, SubversionFolderName).isDir()) {
        return createProjectForRepository(QStringLiteral("svn"), dir);
    }

    return nullptr;
}

314
KateProject *KateProjectPlugin::detectMercurial(const QDir &dir)
315
316
317
318
319
320
321
322
323
324
325
326
{
    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
327
328
    cnf[QStringLiteral("name")] = dir.dirName();
    cnf[QStringLiteral("files")] = (QVariantList() << files);
329

330
    KateProject *project = new KateProject(m_threadPool, this);
331
332
333
334
    project->loadFromData(cnf, dir.canonicalPath());

    m_projects.append(project);

Christoph Cullmann's avatar
Christoph Cullmann committed
335
    Q_EMIT projectCreated(project);
336
337
338
    return project;
}

339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
KateProject *KateProjectPlugin::createProjectForDirectory(const QDir &dir)
{
    QVariantMap cnf, files;
    files[QStringLiteral("directory")] = QStringLiteral("./");
    cnf[QStringLiteral("name")] = dir.dirName();
    cnf[QStringLiteral("files")] = (QVariantList() << files);

    KateProject *project = new KateProject(m_threadPool, this);
    project->loadFromData(cnf, dir.canonicalPath());

    m_projects.append(project);

    Q_EMIT projectCreated(project);
    return project;
}

355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
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;
}

378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
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;
}

395
396
397
398
399
400
401
402
403
404
bool KateProjectPlugin::multiProjectCompletion() const
{
    return m_multiProjectCompletion;
}

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

405
406
407
408
409
410
411
412
413
414
415
void KateProjectPlugin::setGitStatusShowNumStat(bool show)
{
    m_gitNumStat = show;
    writeConfig();
}

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

416
417
void KateProjectPlugin::setSingleClickAction(ClickAction cb)
{
Waqar Ahmed's avatar
Waqar Ahmed committed
418
    m_singleClickAction = cb;
419
420
421
422
423
    writeConfig();
}

ClickAction KateProjectPlugin::singleClickAcion()
{
Waqar Ahmed's avatar
Waqar Ahmed committed
424
    return m_singleClickAction;
425
426
427
428
}

void KateProjectPlugin::setDoubleClickAction(ClickAction cb)
{
Waqar Ahmed's avatar
Waqar Ahmed committed
429
    m_doubleClickAction = cb;
430
431
432
433
434
    writeConfig();
}

ClickAction KateProjectPlugin::doubleClickAcion()
{
Waqar Ahmed's avatar
Waqar Ahmed committed
435
    return m_doubleClickAction;
436
437
}

438
439
440
441
442
443
444
void KateProjectPlugin::setMultiProject(bool completion, bool gotoSymbol)
{
    m_multiProjectCompletion = completion;
    m_multiProjectGoto = gotoSymbol;
    writeConfig();
}

445
446
447
448
void KateProjectPlugin::readConfig()
{
    KConfigGroup config(KSharedConfig::openConfig(), "project");

449
450
451
452
    const QStringList autorepository = config.readEntry("autorepository", DefaultConfig);
    m_autoGit = autorepository.contains(GitConfig);
    m_autoSubversion = autorepository.contains(SubversionConfig);
    m_autoMercurial = autorepository.contains(MercurialConfig);
453
454
455

    m_indexEnabled = config.readEntry("index", false);
    m_indexDirectory = config.readEntry("indexDirectory", QUrl());
456
457
458
459

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

460
    m_gitNumStat = config.readEntry("gitStatusNumStat", true);
Waqar Ahmed's avatar
Waqar Ahmed committed
461
462
    m_singleClickAction = (ClickAction)config.readEntry("gitStatusSingleClick", (int)ClickAction::ShowDiff);
    m_doubleClickAction = (ClickAction)config.readEntry("gitStatusDoubleClick", (int)ClickAction::StageUnstage);
463

Christoph Cullmann's avatar
Christoph Cullmann committed
464
    Q_EMIT configUpdated();
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
}

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);
485
486
487

    config.writeEntry("index", m_indexEnabled);
    config.writeEntry("indexDirectory", m_indexDirectory);
488
489
490
491

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

492
    config.writeEntry("gitStatusNumStat", m_gitNumStat);
Waqar Ahmed's avatar
Waqar Ahmed committed
493
494
    config.writeEntry("gitStatusSingleClick", (int)m_singleClickAction);
    config.writeEntry("gitStatusDoubleClick", (int)m_doubleClickAction);
495

Christoph Cullmann's avatar
Christoph Cullmann committed
496
    Q_EMIT configUpdated();
497
}
498
499
500
501

static KateProjectPlugin *findProjectPlugin()
{
    auto plugin = KTextEditor::Editor::instance()->application()->plugin(QStringLiteral("kateprojectplugin"));
502
    return qobject_cast<KateProjectPlugin *>(plugin);
503
504
505
506
507
}

void KateProjectPlugin::registerVariables()
{
    auto editor = KTextEditor::Editor::instance();
Alexander Lohnau's avatar
Alexander Lohnau committed
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
    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());
                                  });
541
542
543
544
545
546
547
548
}

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