diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..c64ae331b --- /dev/null +++ b/.clang-format @@ -0,0 +1,82 @@ +# Copyright (C) 2016 Olivier Goffart +# +# You may use this file under the terms of the 3-clause BSD license. +# See the file LICENSE from this package for details. + +# This is the clang-format configuration style to be used by Qt, +# based on the rules from https://wiki.qt.io/Qt_Coding_Style and +# https://wiki.qt.io/Coding_Conventions + +--- +# Webkit style was loosely based on the Qt style +BasedOnStyle: WebKit + +Standard: Cpp11 + +# Column width is limited to 100 in accordance with Qt Coding Style. +# https://wiki.qt.io/Qt_Coding_Style +# Note that this may be changed at some point in the future. +ColumnLimit: 100 +# How much weight do extra characters after the line length limit have. +# PenaltyExcessCharacter: 4 + +# Disable reflow of qdoc comments: indentation rules are different. +# Translation comments are also excluded. +CommentPragmas: "^!|^:" + +# We want a space between the type and the star for pointer types. +PointerBindsToType: false + +# We use template< without space. +SpaceAfterTemplateKeyword: false + +# We want to break before the operators, but not before a '='. +BreakBeforeBinaryOperators: NonAssignment + +# Braces are usually attached, but not after functions or class declarations. +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + +# When constructor initializers do not fit on one line, put them each on a new line. +ConstructorInitializerAllOnOneLineOrOnePerLine: true +# Indent initializers by 4 spaces +ConstructorInitializerIndentWidth: 4 + +# Indent width for line continuations. +ContinuationIndentWidth: 8 + +# No indentation for namespaces. +NamespaceIndentation: None + +# Horizontally align arguments after an open bracket. +# The coding style does not specify the following, but this is what gives +# results closest to the existing code. +AlignAfterOpenBracket: true +AlwaysBreakTemplateDeclarations: true + +# Ideally we should also allow less short function in a single line, but +# clang-format does not handle that. +AllowShortFunctionsOnASingleLine: Inline + +# The coding style specifies some include order categories, but also tells to +# separate categories with an empty line. It does not specify the order within +# the categories. Since the SortInclude feature of clang-format does not +# re-order includes separated by empty lines, the feature is not used. +SortIncludes: false + +# macros for which the opening brace stays attached. +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH, forever, Q_FOREVER, QBENCHMARK, QBENCHMARK_ONCE ] + +# Break constructor initializers before the colon and after the commas. +BreakConstructorInitializers: BeforeColon diff --git a/.gitignore b/.gitignore index 8a79cd50f..05bf748e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,22 @@ # general -build +/build*/ *.rej *.orig *.out +CMakeLists.txt.user* # kate *~ *-swp # kdevelop *.kdev4 # from kdiff3 *.BACKUP.* *.BASE.* *.LOCAL.* *.REMOTE.* # from dolphin .directory diff --git a/CMakeLists.txt b/CMakeLists.txt index b15ccad08..db64adb8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,88 +1,40 @@ -# minimal requirements -cmake_minimum_required (VERSION 3.0 FATAL_ERROR) +# 3.1 is required for `target_sources`. +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) -# Kate project -project (kate) +project(kate) -set (QT_MIN_VERSION "5.4.0") +set(QT_MIN_VERSION "5.4.0") set(KF5_DEP_VERSION "5.40.0") -# KDE Application Version, managed by release script -set (KDE_APPLICATIONS_VERSION_MAJOR "19") -set (KDE_APPLICATIONS_VERSION_MINOR "07") -set (KDE_APPLICATIONS_VERSION_MICRO "70") -set (KDE_APPLICATIONS_VERSION "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}.${KDE_APPLICATIONS_VERSION_MICRO}") +# KDE Applications version, managed by release script. +set(KDE_APPLICATIONS_VERSION_MAJOR "19") +set(KDE_APPLICATIONS_VERSION_MINOR "11") +set(KDE_APPLICATIONS_VERSION_MICRO "70") +set(KDE_APPLICATIONS_VERSION "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}.${KDE_APPLICATIONS_VERSION_MICRO}") -# we need some parts of the ECM CMake helpers -find_package (ECM ${KF5_DEP_VERSION} REQUIRED NO_MODULE) -set (CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) -include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) -include(ECMOptionalAddSubdirectory) -include(ECMInstallIcons) -include(ECMSetupVersion) -include(ECMMarkNonGuiExecutable) -include(ECMGenerateHeaders) -include(ECMAddAppIcon) -include(GenerateExportHeader) +# We need some parts of the ECM CMake helpers. +find_package(ECM ${KF5_DEP_VERSION} QUIET REQUIRED NO_MODULE) -include(CMakePackageConfigHelpers) -include(FeatureSummary) -include(WriteBasicConfigVersionFile) -include (CheckFunctionExists) +# We append to the module path so modules can be overriden from the command line. +list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) +# Allow adding Qt resource files with `add_executable` or `target_sources` instead of +# `qt5_add_resources`. See https://cmake.org/cmake/help/v3.0/manual/cmake-qt.7.html#autorcc. +set(CMAKE_AUTORCC ON) + +include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(KDEInstallDirs) include(KDECMakeSettings) -find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Core DBus Widgets Sql) - -if(BUILD_TESTING) - find_package(Qt5Test ${QT_MIN_VERSION} CONFIG REQUIRED) -endif() - -# Load the frameworks we need -find_package(KF5 "${KF5_DEP_VERSION}" REQUIRED COMPONENTS - Config - Crash - I18n - JobWidgets - KIO - Parts - TextEditor - WindowSystem - XmlGui - IconThemes -) - -# some optional deps, to make compiling on some OSes easier -find_package(KF5 "${KF5_DEP_VERSION}" COMPONENTS - Activities - DocTools -) - -if (KF5Activities_FOUND) - add_definitions(-DKActivities_FOUND) -endif () - -# config.h -check_function_exists (ctermid HAVE_CTERMID) -configure_file (config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h) - -# let our config.h be found first in any case -include_directories (BEFORE ${CMAKE_CURRENT_BINARY_DIR}) - -# kwrite -ecm_optional_add_subdirectory (kwrite) - -# kate application -ecm_optional_add_subdirectory (kate) +include(ECMOptionalAddSubdirectory) +include(ECMAddAppIcon) +include(ECMInstallIcons) -# addons, e.g. kate plugins, plasma applets, ... -ecm_optional_add_subdirectory (addons) +include(FeatureSummary) -# docs, if doc tools around -if (KF5DocTools_FOUND) - ecm_optional_add_subdirectory (doc) -endif () +ecm_optional_add_subdirectory(addons) +ecm_optional_add_subdirectory(kwrite) +ecm_optional_add_subdirectory(kate) +ecm_optional_add_subdirectory(doc) -# tell about our features (and what is missing) -feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) +feature_summary(INCLUDE_QUIET_PACKAGES WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/addons/CMakeLists.txt b/addons/CMakeLists.txt index 29dd75c6f..c2f72cb72 100644 --- a/addons/CMakeLists.txt +++ b/addons/CMakeLists.txt @@ -1,94 +1,28 @@ -# detect additional frameworks -find_package(KF5 "${KF5_DEP_VERSION}" OPTIONAL_COMPONENTS Wallet Plasma Service ItemModels ThreadWeaver NewStuff IconThemes GuiAddons) - -set_package_properties(KF5Wallet PROPERTIES PURPOSE "Required to build the katesql addon") -set_package_properties(KF5Plasma PROPERTIES PURPOSE "Required to build the sessionapplet addon") -set_package_properties(KF5Service PROPERTIES PURPOSE "Required to build the sessionapplet addon") -set_package_properties(KF5ItemModels PROPERTIES PURPOSE "Required to build the project, konsole addon") -set_package_properties(KF5ThreadWeaver PROPERTIES PURPOSE "Required to build the project addon") -set_package_properties(KF5NewStuff PROPERTIES PURPOSE "Required to build the snippets and project addons") - -# document switcher -ecm_optional_add_subdirectory (filetree) - -# search in open documents and files -ecm_optional_add_subdirectory (search) - -# ALT+Tab like tab switcher -ecm_optional_add_subdirectory (tabswitcher) - -# ctags -ecm_optional_add_subdirectory (kate-ctags) - -# backtrace -ecm_optional_add_subdirectory (backtracebrowser) - -# file browser -ecm_optional_add_subdirectory (filebrowser) - -# xml completion -ecm_optional_add_subdirectory (xmltools) - -# XML Validation plugin -ecm_optional_add_subdirectory (xmlcheck) - -# open header matching to current file -ecm_optional_add_subdirectory (openheader) - -# debugger plugin, needs windows love, guarded until ported to win32 -if (NOT WIN32) - ecm_optional_add_subdirectory (gdbplugin) -endif () - -# list symbols and functions in a file -ecm_optional_add_subdirectory (symbolviewer) - -# replicode integration -ecm_optional_add_subdirectory (replicode) - -# pipe text through some external command -ecm_optional_add_subdirectory (textfilter) - -if(NOT KF5TextEditor_VERSION VERSION_LESS 5.57.0) - # external tools - ecm_optional_add_subdirectory (externaltools) -endif() - -# Rust complection plugin -ecm_optional_add_subdirectory (rustcompletion) - -# D completion plugin -ecm_optional_add_subdirectory (lumen) - -# build plugin -ecm_optional_add_subdirectory (katebuild-plugin) - -# close document except this one (or similar) -ecm_optional_add_subdirectory (close-except-like) - -if(KF5Wallet_FOUND) - # kate sql - ecm_optional_add_subdirectory (katesql) -endif() - -if(KF5NewStuff_FOUND) - # snippets - ecm_optional_add_subdirectory (snippets) -endif() - -# live preview of sources in target format -ecm_optional_add_subdirectory (preview) - -# terminal tool view -if(KF5Service_FOUND AND NOT WIN32) - ecm_optional_add_subdirectory (konsole) -endif() - -if(KF5ItemModels_FOUND AND KF5ThreadWeaver_FOUND AND KF5NewStuff_FOUND) - # small & smart project manager - ecm_optional_add_subdirectory (project) -endif() - -if (KF5Plasma_FOUND AND KF5Service_FOUND) - ecm_optional_add_subdirectory (sessionapplet) -endif() +# Most plugins will need to link against KF5TextEditor to have access to its plugin interface. +find_package(KF5TextEditor QUIET REQUIRED) + +ecm_optional_add_subdirectory(backtracebrowser) +ecm_optional_add_subdirectory(close-except-like) # Close all documents except this one (or similar). +ecm_optional_add_subdirectory(externaltools) +ecm_optional_add_subdirectory(filebrowser) +ecm_optional_add_subdirectory(filetree) +ecm_optional_add_subdirectory(gdbplugin) +ecm_optional_add_subdirectory(kate-ctags) +ecm_optional_add_subdirectory(katebuild-plugin) +ecm_optional_add_subdirectory(katesql) +ecm_optional_add_subdirectory(konsole) +ecm_optional_add_subdirectory(lspclient) # Language Server Protocol (LSP) client plugin. +ecm_optional_add_subdirectory(lumen) # D completion +ecm_optional_add_subdirectory(openheader) # Open header matching to current file. +ecm_optional_add_subdirectory(preview) # Live preview of sources in target format. +ecm_optional_add_subdirectory(project) # Small & smart project manager. +ecm_optional_add_subdirectory(replicode) +ecm_optional_add_subdirectory(rustcompletion) +ecm_optional_add_subdirectory(search) +ecm_optional_add_subdirectory(sessionapplet) +ecm_optional_add_subdirectory(snippets) +ecm_optional_add_subdirectory(symbolviewer) # List symbols and functions in a file. +ecm_optional_add_subdirectory(tabswitcher) # ALT+Tab like tab switcher. +ecm_optional_add_subdirectory(textfilter) # Pipe text through some external command. +ecm_optional_add_subdirectory(xmlcheck) # XML Validation plugin +ecm_optional_add_subdirectory(xmltools) # XML completion diff --git a/addons/backtracebrowser/CMakeLists.txt b/addons/backtracebrowser/CMakeLists.txt index 72e7afb5f..8ca06ff4f 100644 --- a/addons/backtracebrowser/CMakeLists.txt +++ b/addons/backtracebrowser/CMakeLists.txt @@ -1,34 +1,22 @@ -########### next target ############### -add_definitions(-DTRANSLATION_DOMAIN=\"katebacktracebrowserplugin\") +add_library(katebacktracebrowserplugin MODULE "") +target_compile_definitions(katebacktracebrowserplugin PRIVATE TRANSLATION_DOMAIN="katebacktracebrowserplugin") +target_link_libraries(katebacktracebrowserplugin PRIVATE KF5::TextEditor) -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) +ki18n_wrap_ui(UI_SOURCES btbrowserwidget.ui btconfigwidget.ui) +target_sources(katebacktracebrowserplugin PRIVATE ${UI_SOURCES}) -set(katebacktracebrowserplugin_PART_SRCS +target_sources( + katebacktracebrowserplugin + PRIVATE katebacktracebrowser.cpp btparser.cpp btfileindexer.cpp btdatabase.cpp ) -set(katebacktracebrowserplugin_PART_UI - btbrowserwidget.ui - btconfigwidget.ui -) -ki18n_wrap_ui(katebacktracebrowserplugin_PART_SRCS ${katebacktracebrowserplugin_PART_UI} ) -add_library(katebacktracebrowserplugin MODULE ${katebacktracebrowserplugin_PART_SRCS}) - -# we compile in the .desktop file -kcoreaddons_desktop_to_json (katebacktracebrowserplugin katebacktracebrowserplugin.desktop) - -target_link_libraries(katebacktracebrowserplugin - KF5::TextEditor - KF5::I18n -) - -########### install files ############### -install( TARGETS katebacktracebrowserplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) +kcoreaddons_desktop_to_json(katebacktracebrowserplugin katebacktracebrowserplugin.desktop) +install(TARGETS katebacktracebrowserplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) -############# unit tests ################ -if (BUILD_TESTING) - add_subdirectory(autotests) +if(BUILD_TESTING) + add_subdirectory(autotests) endif() diff --git a/addons/backtracebrowser/autotests/CMakeLists.txt b/addons/backtracebrowser/autotests/CMakeLists.txt index cd1894986..cea0093ea 100644 --- a/addons/backtracebrowser/autotests/CMakeLists.txt +++ b/addons/backtracebrowser/autotests/CMakeLists.txt @@ -1,12 +1,20 @@ include(ECMMarkAsTest) -include_directories( - ${CMAKE_CURRENT_SOURCE_DIR}/.. +add_executable(btbrowser_test "") +target_include_directories(btbrowser_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + +find_package(Qt5Test QUIET REQUIRED) +target_link_libraries( + btbrowser_test + PRIVATE + Qt5::Widgets + Qt5::Test +) + +target_sources(btbrowser_test PRIVATE + btbrowsertest.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../btparser.cpp ) -# Plugin Kate Backtrace Browser -set(BtBrowserSrc btbrowsertest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../btparser.cpp) -add_executable(btbrowser_test ${BtBrowserSrc}) add_test(NAME plugin-btbrowser_test COMMAND btbrowser_test) -target_link_libraries(btbrowser_test kdeinit_kate Qt5::Test) ecm_mark_as_test(btbrowser_test) diff --git a/addons/backtracebrowser/katebacktracebrowserplugin.desktop b/addons/backtracebrowser/katebacktracebrowserplugin.desktop index 6c051f8df..aa822e40a 100644 --- a/addons/backtracebrowser/katebacktracebrowserplugin.desktop +++ b/addons/backtracebrowser/katebacktracebrowserplugin.desktop @@ -1,89 +1,90 @@ [Desktop Entry] Type=Service ServiceTypes=KTextEditor/Plugin X-KDE-Library=katebacktracebrowserplugin Name=Backtrace Browser Name[ar]=متصفّح التّتبّع الخلفيّ Name[ast]=Restolador de llistes de llamaes de función Name[bg]=Преглед на backtrace Name[bs]=Pregledač kontratraga Name[ca]=Navegador de traça inversa Name[ca@valencia]=Navegador de traça inversa Name[cs]=Prohlížeč backtrace Name[da]=Backtrace-browser Name[de]=Backtrace-Browser Name[el]=Περιήγηση Backtrace Name[en_GB]=Backtrace Browser Name[es]=Navegador de trazado inverso Name[et]=Tagasijälituse brauser Name[eu]=Aztarnen arakatzailea Name[fi]=Pinolistausselain Name[fr]=Explorateur de piles d'appels Name[ga]=Brabhsálaí Cúl-Loirg Name[gl]=Navegador de trazados inversos Name[hu]=Visszakövetési böngésző Name[ia]=Navigator de tracia de retro +Name[id]=Penelusur Backtrace Name[is]=Afturrakningarvafri Name[it]=Navigatore di backtrace Name[ja]=バックトレースブラウザ Name[kk]=Жаңылыс хаттама шолғышы Name[km]=កម្ម​វិធី​រុករក​ដាន​ Name[ko]=역추적 탐색기 Name[lt]=Derinimo informacijos naršyklė Name[lv]=Atpakaļceļa pārlūks Name[mr]=बॅकट्रेस ब्राऊजर Name[nb]=Tilbakelogg-viser Name[nds]=Fehlerspoorkieker Name[nl]=Backtrace bladerprogramma Name[nn]=Tilbakelogg-visar Name[pa]=ਬੈਕਟਰੇਸ ਬਰਾਊਜ਼ਰ Name[pl]=Przeglądarka śladu Name[pt]=Navegação na Lista de Chamadas Name[pt_BR]=Navegador de backtrace Name[ro]=Navigator de backtrace-uri Name[ru]=Просмотр стека вызовов Name[si]=පසුසෙවුම් ගවේශකය Name[sk]=Prehliadač Backtrace Name[sl]=Brskalnik po povratnih sledeh Name[sr]=Прегледач контратрага Name[sr@ijekavian]=Прегледач контратрага Name[sr@ijekavianlatin]=Pregledač kontratraga Name[sr@latin]=Pregledač kontratraga Name[sv]=Bläddring i bakåtspårning Name[tg]=Браузери ақиббар Name[tr]=Geri İz Tarayıcı Name[ug]=ئارقا ئىزلاش كۆرگۈ Name[uk]=Переглядач зворотного трасування Name[wa]=Naivieu d' backtraces Name[x-test]=xxBacktrace Browserxx Name[zh_CN]=回溯跟踪浏览器 Name[zh_TW]=回溯追蹤瀏覽器 Comment=C/C++ Backtrace navigation tool view Comment[ca]=Vista d'eina del navegador per a la traça inversa del C/C++ Comment[ca@valencia]=Vista d'eina del navegador per a la traça inversa del C/C++ Comment[cs]=Nástroj pro procházení backtrace v C/C++ Comment[de]=Ansicht zur Navigation in C/C++ Backtraces Comment[el]=Προβολή C/C++ εργαλείου πλοήγησης Backtrace Comment[en_GB]=C/C++ Backtrace navigation tool view Comment[es]=Vista de la herramienta del navegador de trazado inverso de C/C++ Comment[eu]=C/C++ aztarnetan nabigatzeko tresnaren ikuspegia Comment[fi]=C/C++-pinolistausnavigointinäkymä Comment[fr]=Vue des outils de navigation dans les piles d'appels C/C++ Comment[gl]=Vista da ferramenta de navegación por trazados inversos de C e C++ Comment[id]=Tampilan alat navigasi Backtrace C/C++ Comment[it]=Vista dello strumento di navigazione di backtrace C/C++ Comment[ko]=C/C++ 역추적 탐색기 도구 보기 Comment[nl]=C/C++ Backtrace hulpmiddel voor navigatieweergave Comment[nn]=Navigeringsverktøy for C-/C++-tilbakeloggar Comment[pl]=Widok narzędzia nawigacji śladu C/C++ Comment[pt]=Área da ferramenta de navegação de Chamadas de C/C++ Comment[pt_BR]=Janela da ferramenta de navegação de backtrace C/C++ Comment[ru]=Инструмент для просмотра стека вызовов C и C++ Comment[sk]=Zobrazenie nástroja backtrace navigácie Comment[sv]=C/C++ navigeringsverktyg med vy av bakåtspårning Comment[tr]=C/C++ geri iz tarayıcı aracı görünümü Comment[uk]=Панель навігації зворотним трасуванням C++ Comment[x-test]=xxC/C++ Backtrace navigation tool viewxx Comment[zh_CN]=C/C++ 回溯导航工具视图 Comment[zh_TW]=C/C++ 回溯追蹤導覽工具檢視 author=Dominik Haumann, dhaumann@kde.org diff --git a/addons/close-except-like/.kateconfig b/addons/close-except-like/.kateconfig deleted file mode 100644 index 6049b6ed9..000000000 --- a/addons/close-except-like/.kateconfig +++ /dev/null @@ -1 +0,0 @@ -kate: space-indent on; tab-width 4; indent-width 4; replace-tabs on; hl C++/Qt4; diff --git a/addons/close-except-like/CMakeLists.txt b/addons/close-except-like/CMakeLists.txt index 1922d446c..fce5ef292 100644 --- a/addons/close-except-like/CMakeLists.txt +++ b/addons/close-except-like/CMakeLists.txt @@ -1,37 +1,30 @@ -project(katecloseexceptplugin) -add_definitions(-DTRANSLATION_DOMAIN=\"katecloseexceptplugin\") -set(VERSION_MAJOR 0) -set(VERSION_MINOR 3) -set(VERSION_PATCH 5) -set(VERSION_STRING ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}) +find_package(KF5IconThemes QUIET) +set_package_properties(KF5IconThemes PROPERTIES PURPOSE "Required to build the close-except-like addon") -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) +if(NOT KF5IconThemes_FOUND) + return() +endif() -set(KATE_CLOSE_EXCEPT_PLUGIN_SOURCES - close_confirm_dialog.cpp - close_except_plugin.cpp - ) +add_library(katecloseexceptplugin MODULE "") +target_compile_definitions(katecloseexceptplugin PRIVATE TRANSLATION_DOMAIN="katecloseexceptplugin") -set(KATE_CLOSE_EXCEPT_PLUGIN_UI - close_confirm_dialog.ui +target_link_libraries( + katecloseexceptplugin + PRIVATE + KF5::TextEditor + KF5::IconThemes ) -ki18n_wrap_ui(KATE_CLOSE_EXCEPT_PLUGIN_SOURCES ${KATE_CLOSE_EXCEPT_PLUGIN_UI}) -# resource for ui file and stuff -qt5_add_resources(KATE_CLOSE_EXCEPT_PLUGIN_SOURCES plugin.qrc) +ki18n_wrap_ui(UI_SOURCES close_confirm_dialog.ui) +target_sources(katecloseexceptplugin PRIVATE ${UI_SOURCES}) -add_library(katecloseexceptplugin MODULE ${KATE_CLOSE_EXCEPT_PLUGIN_SOURCES}) +target_sources( + katecloseexceptplugin + PRIVATE + close_confirm_dialog.cpp + close_except_plugin.cpp + plugin.qrc +) -# we compile in the .desktop file kcoreaddons_desktop_to_json(katecloseexceptplugin katecloseexceptplugin.desktop) - -target_link_libraries(katecloseexceptplugin - KF5::TextEditor - KF5::Parts - KF5::I18n - KF5::IconThemes - ) - -configure_file(config.h.in config.h) - install(TARGETS katecloseexceptplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/close-except-like/close_confirm_dialog.cpp b/addons/close-except-like/close_confirm_dialog.cpp index db8bc6689..d2d418ef7 100644 --- a/addons/close-except-like/close_confirm_dialog.cpp +++ b/addons/close-except-like/close_confirm_dialog.cpp @@ -1,131 +1,131 @@ /** * \file * * \brief Class \c kate::CloseConfirmDialog (implementation) * * Copyright (C) 2012 Alex Turbov * * \date Sun Jun 24 16:29:13 MSK 2012 -- Initial design */ /* * KateCloseExceptPlugin is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * KateCloseExceptPlugin is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ // Project specific includes #include "close_confirm_dialog.h" // Standard includes #include #include /// \todo Where is \c i18n() defined? #include #include #include #include #include #include #include #include namespace kate { namespace { class KateDocItem : public QTreeWidgetItem { public: KateDocItem(KTextEditor::Document* doc, QTreeWidget* tw) : QTreeWidgetItem(tw) , document(doc) { setText(0, doc->documentName()); setText(1, doc->url().toString()); setCheckState(0, Qt::Checked); } KTextEditor::Document* document; }; } // anonymous namespace CloseConfirmDialog::CloseConfirmDialog( QList& docs , KToggleAction* show_confirmation_action , QWidget* const parent ) : QDialog(parent) , m_docs(docs) { assert("Documents container expected to be non empty" && !docs.isEmpty()); setupUi(this); setWindowTitle(i18nc("@title:window", "Close files confirmation")); setModal(true); buttonBox->button(QDialogButtonBox::Ok)->setDefault(true); icon->setPixmap(KIconLoader::global()->loadIcon(QStringLiteral("dialog-warning"),KIconLoader::Desktop,KIconLoader::SizeLarge)); text->setText( i18nc("@label:listbox", "You are about to close the following documents:") ); QStringList headers; headers << i18nc("@title:column", "Document") << i18nc("@title:column", "Location"); m_docs_tree->setHeaderLabels(headers); m_docs_tree->setSelectionMode(QAbstractItemView::SingleSelection); m_docs_tree->setRootIsDecorated(false); - for (int i = 0; i < m_docs.size(); i++) + for (auto& doc : qAsConst(m_docs)) { - new KateDocItem(m_docs[i], m_docs_tree); + new KateDocItem(doc, m_docs_tree); } m_docs_tree->header()->setStretchLastSection(false); m_docs_tree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); m_docs_tree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); m_dont_ask_again->setText(i18nc("option:check", "Do not ask again")); // NOTE If we are here, it means that 'Show Confirmation' action is enabled, // so not needed to read config... assert("Sanity check" && show_confirmation_action->isChecked()); m_dont_ask_again->setCheckState(Qt::Unchecked); connect(m_dont_ask_again, &QCheckBox::toggled, show_confirmation_action, &KToggleAction::toggle); // Update documents list according checkboxes connect(this, &CloseConfirmDialog::accepted, this, &CloseConfirmDialog::updateDocsList); KConfigGroup gcg(KSharedConfig::openConfig(), "kate-close-except-like-CloseConfirmationDialog"); KWindowConfig::restoreWindowSize(windowHandle(),gcg); // restore dialog geometry from config } CloseConfirmDialog::~CloseConfirmDialog() { KConfigGroup gcg(KSharedConfig::openConfig(), "kate-close-except-like-CloseConfirmationDialog"); KWindowConfig::saveWindowSize(windowHandle(),gcg); // write dialog geometry to config gcg.sync(); } /** * Going to remove unchecked files from the given documents list */ void CloseConfirmDialog::updateDocsList() { for ( QTreeWidgetItemIterator it(m_docs_tree, QTreeWidgetItemIterator::NotChecked) ; *it ; ++it ) { KateDocItem* item = static_cast(*it); m_docs.removeAll(item->document); qDebug() << "do not close the file " << item->document->url().toString(); } } } // namespace kate diff --git a/addons/close-except-like/close_except_plugin.cpp b/addons/close-except-like/close_except_plugin.cpp index 66af5268b..bcd559bd5 100644 --- a/addons/close-except-like/close_except_plugin.cpp +++ b/addons/close-except-like/close_except_plugin.cpp @@ -1,363 +1,350 @@ /** * \file * * \brief Kate Close Except/Like plugin implementation * * Copyright (C) 2012 Alex Turbov * * \date Thu Mar 8 08:13:43 MSK 2012 -- Initial design */ /* * KateCloseExceptPlugin is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * KateCloseExceptPlugin is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ // Project specific includes -#include "config.h" #include "close_except_plugin.h" #include "close_confirm_dialog.h" // Standard includes #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(CloseExceptPluginFactory, "katecloseexceptplugin.json", registerPlugin();) -/*K_EXPORT_PLUGIN( - CloseExceptPluginFactory( - KAboutData( - "katecloseexceptplugin" - , "katecloseexceptplugin" - , ki18n("Close Except/Like Plugin") - , PLUGIN_VERSION - , ki18n("Close all documents started from specified path") - , KAboutData::License_LGPL_V3 - ) - ) - )*/ namespace kate { //BEGIN CloseExceptPlugin CloseExceptPlugin::CloseExceptPlugin( QObject* application , const QList& ) : KTextEditor::Plugin(application) { } QObject* CloseExceptPlugin::createView(KTextEditor::MainWindow* parent) { return new CloseExceptPluginView(parent, this); } void CloseExceptPlugin::readSessionConfig(const KConfigGroup& config) { const KConfigGroup scg(&config, QStringLiteral("menu")); m_show_confirmation_needed = scg.readEntry(QStringLiteral("ShowConfirmation"), true); } void CloseExceptPlugin::writeSessionConfig(KConfigGroup& config) { KConfigGroup scg(&config, QStringLiteral("menu")); scg.writeEntry(QStringLiteral("ShowConfirmation"), m_show_confirmation_needed); scg.sync(); } //END CloseExceptPlugin //BEGIN CloseExceptPluginView CloseExceptPluginView::CloseExceptPluginView( KTextEditor::MainWindow* mw , CloseExceptPlugin* plugin ) : QObject(mw) , KXMLGUIClient() , m_plugin(plugin) , m_show_confirmation_action(new KToggleAction(i18nc("@action:inmenu", "Show Confirmation"), this)) , m_except_menu(new KActionMenu( i18nc("@action:inmenu close docs except the following...", "Close Except") , this )) , m_like_menu(new KActionMenu( i18nc("@action:inmenu close docs like the following...", "Close Like") , this )) , m_mainWindow(mw) { KXMLGUIClient::setComponentName (QStringLiteral("katecloseexceptplugin"), i18n("Close Except/Like Plugin")); setXMLFile( QStringLiteral("ui.rc") ); actionCollection()->addAction(QStringLiteral("file_close_except"), m_except_menu); actionCollection()->addAction(QStringLiteral("file_close_like"), m_like_menu); connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::documentCreated, this, &CloseExceptPluginView::documentCreated); // Configure toggle action and connect it to update state m_show_confirmation_action->setChecked(m_plugin->showConfirmationNeeded()); connect(m_show_confirmation_action.data(), &KToggleAction::toggled, m_plugin, &CloseExceptPlugin::toggleShowConfirmation); // connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, &CloseExceptPluginView::viewCreated); // Fill menu w/ currently opened document masks/groups updateMenu(); m_mainWindow->guiFactory()->addClient(this); } CloseExceptPluginView::~CloseExceptPluginView() { m_mainWindow->guiFactory()->removeClient(this); } void CloseExceptPluginView::viewCreated(KTextEditor::View* view) { connectToDocument(view->document()); updateMenu(); } void CloseExceptPluginView::documentCreated(KTextEditor::Editor*, KTextEditor::Document* document) { connectToDocument(document); updateMenu(); } void CloseExceptPluginView::connectToDocument(KTextEditor::Document* document) { // Subscribe self to document close and name changes connect(document, &KTextEditor::Document::aboutToClose, this, &CloseExceptPluginView::updateMenuSlotStub); connect(document, &KTextEditor::Document::documentNameChanged, this, &CloseExceptPluginView::updateMenuSlotStub); connect(document, &KTextEditor::Document::documentUrlChanged, this, &CloseExceptPluginView::updateMenuSlotStub); } void CloseExceptPluginView::updateMenuSlotStub(KTextEditor::Document*) { updateMenu(); } void CloseExceptPluginView::appendActionsFrom( const std::set& paths , actions_map_type& actions , KActionMenu* menu , CloseFunction closeFunction ) { Q_FOREACH(const QUrl& path, paths) { QString action = path.path() + QLatin1Char('*'); actions[action] = QPointer(new QAction(action, menu)); menu->addAction(actions[action]); connect(actions[action].data(), &QAction::triggered, this, [this, closeFunction, action]() { (this->*closeFunction)(action); }); } } void CloseExceptPluginView::appendActionsFrom( const std::set& masks , actions_map_type& actions , KActionMenu* menu , CloseFunction closeFunction ) { Q_FOREACH(const QString& mask, masks) { QString action = mask.startsWith(QLatin1Char('*')) ? mask : mask + QLatin1Char('*'); actions[action] = QPointer(new QAction(action, menu)); menu->addAction(actions[action]); connect(actions[action].data(), &QAction::triggered, this, [this, closeFunction, action]() { (this->*closeFunction)(action); }); } } void CloseExceptPluginView::updateMenu( const std::set& paths , const std::set& masks , actions_map_type& actions , KActionMenu* menu , CloseFunction closeFunction ) { // turn menu ON or OFF depending on collected results menu->setEnabled(!paths.empty()); // Clear previous menus for (actions_map_type::iterator it = actions.begin(), last = actions.end(); it !=last;) { menu->removeAction(*it); actions.erase(it++); } // Form a new one appendActionsFrom(paths, actions, menu, closeFunction); if (!masks.empty()) { if (!paths.empty()) menu->addSeparator(); // Add separator between paths and file's ext filters appendActionsFrom(masks, actions, menu, closeFunction); } // Append 'Show Confirmation' toggle menu item menu->addSeparator(); // Add separator between paths and show confirmation menu->addAction(m_show_confirmation_action); } void CloseExceptPluginView::updateMenu() { const QList& docs = KTextEditor::Editor::instance()->application()->documents(); if (docs.size() < 2) { //qDebug() << "No docs r (or the only) opened right now --> disable menu"; m_except_menu->setEnabled(false); m_except_menu->addSeparator(); m_like_menu->setEnabled(false); m_like_menu->addSeparator(); /// \note It seems there is always a document present... it named \em 'Untitled' } else { // Iterate over documents and form a set of candidates typedef std::set paths_set_type; typedef std::set paths_set_type_masks; paths_set_type doc_paths; paths_set_type_masks masks; Q_FOREACH(KTextEditor::Document* document, docs) { const QString& ext = QFileInfo(document->url().path()).completeSuffix(); if (!ext.isEmpty()) masks.insert(QStringLiteral("*.") + ext); doc_paths.insert(KIO::upUrl(document->url())); } paths_set_type paths = doc_paths; //qDebug() << "stage #1: Collected" << paths.size() << "paths and" << masks.size() << "masks"; // Add common paths to the collection for (paths_set_type::iterator it = doc_paths.begin(), last = doc_paths.end(); it != last; ++it) { for ( QUrl url = *it ; (!url.path().isEmpty()) && url.path() != QStringLiteral("/") ; url = KIO::upUrl(url) ) { paths_set_type::iterator not_it = it; for (++not_it; not_it != last; ++not_it) if (!not_it->path().startsWith(url.path())) break; if (not_it == last) { paths.insert(url); break; } } } //qDebug() << "stage #2: Collected" << paths.size() << "paths and" << masks.size() << "masks"; // updateMenu(paths, masks, m_except_actions, m_except_menu, &CloseExceptPluginView::closeExcept); updateMenu(paths, masks, m_like_actions, m_like_menu, &CloseExceptPluginView::closeLike); } } void CloseExceptPluginView::close(const QString& item, const bool close_if_match) { QChar asterisk=QLatin1Char('*'); assert( "Parameter seems invalid! Is smth has changed in the code?" && !item.isEmpty() && (item[0] == asterisk || item[item.size() - 1] == asterisk) ); const bool is_path = item[0] != asterisk; const QString mask = is_path ? item.left(item.size() - 1) : item; //qDebug() << "Going to close items [" << close_if_match << "/" << is_path << "]: " << mask; QList docs2close; const QList& docs = KTextEditor::Editor::instance()->application()->documents(); Q_FOREACH(KTextEditor::Document* document, docs) { const QString& path = KIO::upUrl(document->url()).path(); /// \note Take a dot in account, so \c *.c would not match for \c blah.kcfgc const QString& ext = QLatin1Char('.') + QFileInfo(document->url().fileName()).completeSuffix(); const bool match = (!is_path && mask.endsWith(ext)) || (is_path && path.startsWith(mask)) ; if (match == close_if_match) { //qDebug() << "*** Will close: " << document->url(); docs2close.push_back(document); } } if (docs2close.isEmpty()) { displayMessage( i18nc("@title:window", "Error") , i18nc("@info:tooltip", "No files to close ...") , KTextEditor::Message::Error ); return; } // Show confirmation dialog if needed const bool removeNeeded = !m_plugin->showConfirmationNeeded() || CloseConfirmDialog(docs2close, m_show_confirmation_action, qobject_cast(this)).exec(); if (removeNeeded) { if (docs2close.isEmpty()) { displayMessage( i18nc("@title:window", "Error") , i18nc("@info:tooltip", "No files to close ...") , KTextEditor::Message::Error ); } else { // Close 'em all! KTextEditor::Editor::instance()->application()->closeDocuments(docs2close); updateMenu(); displayMessage( i18nc("@title:window", "Done") , i18np("%1 file closed", "%1 files closed", docs2close.size()) , KTextEditor::Message::Positive ); } } } void CloseExceptPluginView::displayMessage(const QString &title, const QString &msg, KTextEditor::Message::MessageType level) { KTextEditor::View *kv = m_mainWindow->activeView(); if (!kv) return; delete m_infoMessage; m_infoMessage = new KTextEditor::Message(xi18nc("@info", "%1%2", title, msg), level); m_infoMessage->setWordWrap(true); m_infoMessage->setPosition(KTextEditor::Message::TopInView); m_infoMessage->setAutoHide(5000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(kv); kv->document()->postMessage(m_infoMessage); } //END CloseExceptPluginView } // namespace kate #include "close_except_plugin.moc" // kate: hl C++11/Qt4; diff --git a/addons/close-except-like/config.h.in b/addons/close-except-like/config.h.in deleted file mode 100644 index 37da1548d..000000000 --- a/addons/close-except-like/config.h.in +++ /dev/null @@ -1,31 +0,0 @@ -/** - * \file - * - * \brief Class \c kate::config (interface) - * - * Copyright (C) 2012 Alex Turbov - * - * \date Thu Mar 8 08:19:57 MSK 2012 -- Initial design - * - * \attention DO NOT EDIT THIS FILE! IT WAS GENERATED BY CMAKE. - */ -/* - * KateCloseExceptPlugin is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KateCloseExceptPlugin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -#ifndef __SRC__CONFIG_H__ -# define __SRC__CONFIG_H__ -# define PLUGIN_VERSION "@VERSION_STRING@" -#endif // __SRC__CONFIG_H__ -// kate: hl c++; diff --git a/addons/externaltools/CMakeLists.txt b/addons/externaltools/CMakeLists.txt index caa80d445..909ec694e 100644 --- a/addons/externaltools/CMakeLists.txt +++ b/addons/externaltools/CMakeLists.txt @@ -1,42 +1,46 @@ +if(KF5TextEditor_VERSION VERSION_LESS 5.57.0) + return() +endif() + project(externaltoolsplugin) add_definitions(-DTRANSLATION_DOMAIN=\"kateexternaltoolsplugin\") include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) set(externaltoolsplugin_PART_SRCS externaltoolsplugin.cpp kateexternaltoolsview.cpp katetoolrunner.cpp kateexternaltool.cpp kateexternaltoolscommand.cpp kateexternaltoolsconfigwidget.cpp ) # resource for ui file and stuff qt5_add_resources(externaltoolsplugin_PART_SRCS plugin.qrc) set(externaltoolsplugin_PART_UI configwidget.ui tooldialog.ui toolview.ui ) ki18n_wrap_ui(externaltoolsplugin_PART_SRCS ${externaltoolsplugin_PART_UI} ) add_library(externaltoolsplugin MODULE ${externaltoolsplugin_PART_SRCS}) # we compile in the .desktop file kcoreaddons_desktop_to_json (externaltoolsplugin externaltoolsplugin.desktop) target_link_libraries(externaltoolsplugin KF5::CoreAddons KF5::IconThemes KF5::TextEditor KF5::I18n ) ########### install files ############### install(TARGETS externaltoolsplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) ############# unit tests ################ if (BUILD_TESTING) add_subdirectory(autotests) endif() diff --git a/addons/filebrowser/CMakeLists.txt b/addons/filebrowser/CMakeLists.txt index 61d8dc34d..0e181d780 100644 --- a/addons/filebrowser/CMakeLists.txt +++ b/addons/filebrowser/CMakeLists.txt @@ -1,20 +1,28 @@ -project(katefilebrowserplugin) -add_definitions(-DTRANSLATION_DOMAIN=\"katefilebrowserplugin\") +find_package(KF5KIO QUIET) +set_package_properties(KF5KIO PROPERTIES PURPOSE "Required to build the filebrowser addon") -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) +if(NOT KF5KIO_FOUND) + return() +endif() -set(katefilebrowserplugin_PART_SRCS +add_library(katefilebrowserplugin MODULE "") +target_compile_definitions(katefilebrowserplugin PRIVATE TRANSLATION_DOMAIN="katefilebrowserplugin") + +target_link_libraries( + katefilebrowserplugin + PRIVATE + KF5::TextEditor + KF5::KIOFileWidgets +) + +target_sources( + katefilebrowserplugin + PRIVATE katefilebrowserplugin.cpp katefilebrowserconfig.cpp katefilebrowser.cpp katebookmarkhandler.cpp ) -add_library(katefilebrowserplugin MODULE ${katefilebrowserplugin_PART_SRCS}) - -# we compile in the .desktop file -kcoreaddons_desktop_to_json (katefilebrowserplugin katefilebrowserplugin.desktop) - -target_link_libraries(katefilebrowserplugin KF5::TextEditor KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::I18n) - -install(TARGETS katefilebrowserplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) +kcoreaddons_desktop_to_json(katefilebrowserplugin katefilebrowserplugin.desktop) +install(TARGETS katefilebrowserplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/filebrowser/katefilebrowserconfig.cpp b/addons/filebrowser/katefilebrowserconfig.cpp index 9ed2e1712..a476b879e 100644 --- a/addons/filebrowser/katefilebrowserconfig.cpp +++ b/addons/filebrowser/katefilebrowserconfig.cpp @@ -1,192 +1,204 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2001 Anders Lund Copyright (C) 2007 Mirko Stocker This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "katefilebrowserconfig.h" #include "katefilebrowser.h" #include #include #include #include #include #include #include #include #include #include #include #include #include //BEGIN ACtionLBItem /* QListboxItem that can store and return a string, used for the toolbar action selector. */ class ActionLBItem : public QListWidgetItem { public: ActionLBItem( QListWidget *lb = nullptr, const QIcon &pm = QIcon(), const QString &text = QString(), const QString &str = QString() ) : QListWidgetItem(pm, text, lb, 0 ), _str(str) {} QString idstring() { return _str; } private: QString _str; }; //END ActionLBItem //BEGIN KateFileBrowserConfigPage KateFileBrowserConfigPage::KateFileBrowserConfigPage( QWidget *parent, KateFileBrowser *kfb ) : KTextEditor::ConfigPage( parent ), fileBrowser( kfb ), m_changed( false ) { QVBoxLayout *lo = new QVBoxLayout( this ); int spacing = QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); lo->setSpacing( spacing ); lo->setContentsMargins(0, 0, 0, 0); // Toolbar - a lot for a little... QGroupBox *gbToolbar = new QGroupBox(i18n("Toolbar"), this ); acSel = new KActionSelector( gbToolbar ); acSel->setAvailableLabel( i18n("A&vailable actions:") ); acSel->setSelectedLabel( i18n("S&elected actions:") ); QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(acSel); gbToolbar->setLayout(vbox); lo->addWidget( gbToolbar ); connect(acSel, &KActionSelector::added, this, &KateFileBrowserConfigPage::slotMyChanged); connect(acSel, &KActionSelector::removed, this, &KateFileBrowserConfigPage::slotMyChanged); connect(acSel, &KActionSelector::movedUp, this, &KateFileBrowserConfigPage::slotMyChanged); connect(acSel, &KActionSelector::movedDown, this, &KateFileBrowserConfigPage::slotMyChanged); // make it look nice lo->addStretch( 1 ); init(); } QString KateFileBrowserConfigPage::name() const { return i18n("Filesystem Browser"); } QString KateFileBrowserConfigPage::fullName() const { return i18n("Filesystem Browser Settings"); } QIcon KateFileBrowserConfigPage::icon() const { return QIcon::fromTheme(QStringLiteral("document-open")); } void KateFileBrowserConfigPage::apply() { if ( ! m_changed ) return; m_changed = false; KConfigGroup config(KSharedConfig::openConfig(), "filebrowser"); QStringList l; ActionLBItem *aItem; QList list = acSel->selectedListWidget()->findItems(QStringLiteral("*"), Qt::MatchWildcard); foreach(QListWidgetItem *item, list) { aItem = static_cast(item); l << aItem->idstring(); } config.writeEntry( "toolbar actions", l ); fileBrowser->setupToolbar(); } void KateFileBrowserConfigPage::reset() { // hmm, what is this supposed to do, actually?? init(); m_changed = false; } void KateFileBrowserConfigPage::init() { KConfigGroup config(KSharedConfig::openConfig(), "filebrowser"); // toolbar QStringList l = config.readEntry( "toolbar actions", QStringList() ); if ( l.isEmpty() ) // default toolbar l << QStringLiteral("back") << QStringLiteral("forward") << QStringLiteral("bookmarks") << QStringLiteral("sync_dir") << QStringLiteral("configure"); // actions from diroperator + two of our own - QStringList allActions; - allActions << QStringLiteral("up") << QStringLiteral("back") << QStringLiteral("forward") << QStringLiteral("home") - << QStringLiteral("reload") << QStringLiteral("mkdir") << QStringLiteral("delete") - << QStringLiteral("short view") << QStringLiteral("detailed view") - << QStringLiteral("tree view") << QStringLiteral("detailed tree view") - << QStringLiteral("show hidden") /*<< QStringLiteral("view menu") << QStringLiteral("properties")*/ - << QStringLiteral("bookmarks") << QStringLiteral("sync_dir") << QStringLiteral("configure"); + const QStringList allActions{ + QStringLiteral("up"), + QStringLiteral("back"), + QStringLiteral("forward"), + QStringLiteral("home"), + QStringLiteral("reload"), + QStringLiteral("mkdir"), + QStringLiteral("delete"), + QStringLiteral("short view"), + QStringLiteral("detailed view"), + QStringLiteral("tree view"), + QStringLiteral("detailed tree view"), + QStringLiteral("show hidden"), + //QStringLiteral("view menu"), + //QStringLiteral("properties"), + QStringLiteral("bookmarks"), + QStringLiteral("sync_dir"), + QStringLiteral("configure")}; + QRegularExpression re(QStringLiteral("&(?=[^&])")); QAction *ac = nullptr; QListWidget *lb; - for ( QStringList::Iterator it = allActions.begin(); it != allActions.end(); ++it ) + for (const auto& actionName : allActions) { - lb = l.contains( *it ) ? acSel->selectedListWidget() : acSel->availableListWidget(); + lb = l.contains( actionName ) ? acSel->selectedListWidget() : acSel->availableListWidget(); - if ( *it == QStringLiteral ("bookmarks") || *it == QStringLiteral ("sync_dir") || *it == QStringLiteral ("configure") ) - ac = fileBrowser->actionCollection()->action( *it ); + if ( actionName == QStringLiteral ("bookmarks") || actionName == QStringLiteral ("sync_dir") || actionName == QStringLiteral ("configure") ) + ac = fileBrowser->actionCollection()->action( actionName ); else - ac = fileBrowser->dirOperator()->actionCollection()->action( *it ); + ac = fileBrowser->dirOperator()->actionCollection()->action( actionName ); if ( ac ) { QString text = ac->text().remove( re ); // CJK languages need a filtering message for action texts in lists, // to remove special accelerators that they use. // The exact same filtering message exists in kdelibs; hence, // avoid extraction here and let it be sourced from kdelibs. #define i18ncX i18nc text = i18ncX( "@item:intable Action name in toolbar editor", "%1", text ); - new ActionLBItem( lb, ac->icon(), text, *it ); + new ActionLBItem( lb, ac->icon(), text, actionName ); } } } void KateFileBrowserConfigPage::slotMyChanged() { m_changed = true; emit changed(); } //END KateFileBrowserConfigPage // kate: space-indent on; indent-width 2; replace-tabs on; diff --git a/addons/filebrowser/katefilebrowserplugin.desktop b/addons/filebrowser/katefilebrowserplugin.desktop index 9389c5a22..e71e2e257 100644 --- a/addons/filebrowser/katefilebrowserplugin.desktop +++ b/addons/filebrowser/katefilebrowserplugin.desktop @@ -1,90 +1,91 @@ [Desktop Entry] Type=Service ServiceTypes=KTextEditor/Plugin X-KDE-Library=katefilebrowserplugin Name=File System Browser Name[ast]=Restolador del sistema de ficheros Name[ca]=Navegador del sistema de fitxers Name[ca@valencia]=Navegador del sistema de fitxers Name[cs]=Prohlížeč souborového systému Name[de]=Dateisystem-Browser Name[el]=Περιηγητής συστήματος αρχείων Name[en_GB]=File System Browser Name[es]=Navegador del sistema de archivos Name[eu]=Fitxategi-sistemaren arakatzailea Name[fi]=Tiedostojärjestelmäselain Name[fr]=Explorateur du système de fichiers Name[gl]=Navegador do sistema de ficheiros +Name[id]=Penelusur Sistem File Name[it]=Selettore dei file Name[ko]=파일 시스템 탐색기 Name[nl]=Bestandssysteembrowser Name[nn]=Filsystemvisar Name[pl]=Przeglądarka systemu plików Name[pt]=Navegador do Sistema de Ficheiros Name[pt_BR]=Navegador do sistema de arquivos Name[ru]=Обзор файловой системы Name[sk]=Prehliadač súborového systému Name[sv]=Filsystembläddrare Name[tr]=Dosya Sistemi Tarayıcı Name[uk]=Навігатор файловою системою Name[x-test]=xxFile System Browserxx Name[zh_CN]=文件系统浏览器 Name[zh_TW]=檔案系統瀏覽器 Comment=File system browser tool view Comment[ar]=منظور أداة لتصفّح نظام الملفّات Comment[ast]=Vista de ferramientes del restolador del sistema de ficheros Comment[bg]=Инструмент за разглеждане на файловата система Comment[bs]=Prikaz pregledača datotečnog sistema Comment[ca]=Vista d'eina del navegador pel sistema de fitxers Comment[ca@valencia]=Vista d'eina del navegador pel sistema de fitxers Comment[cs]=Nástroj pro procházení souborového systému Comment[da]=Visning af værktøj til filsystembrowser Comment[de]=Werkzeugansicht für Dateisystem-Browser Comment[el]=Εργαλείο περιήγησης συστήματος αρχείων Comment[en_GB]=File system browser tool view Comment[es]=Herramienta de visualización del navegador del sistema de archivos Comment[et]=Failisüsteemi sirvija tööriistavaade Comment[eu]=Fitxategi-sistemaren arakatzailearen ikuspegia Comment[fi]=Tiedostojärjestelmäselain-työkalunäkymä Comment[fr]=Vue des outils de l'explorateur de systèmes de fichiers Comment[ga]=Amharc uirlisí brabhsálaí an chórais comhad Comment[gl]=Vista da utilidade de navegador do sistema de ficheiros Comment[he]=כלי לתצוגת מערכת קבצים Comment[hu]=Fájlböngésző - Eszköznézet Comment[ia]=Vista de instrumento de navigator de systema de file Comment[id]=Tampilan alat penelusur sistem file Comment[it]=Strumento di selezione dei file Comment[ja]=ファイルシステムブラウザ・ツールビュー Comment[kk]=Файл жүйесін шолу құралы Comment[km]=ទិដ្ឋភាព​ឧបករណ៍​កម្មវិធី​រុករក​ប្រព័ន្ធ​ឯកសារ Comment[ko]=파일 시스템 탐색기 도구 보기 Comment[lt]=Failų sistemos naršyklės įrankio rodinys Comment[lv]=Datņu sistēmas pārlūkošanas rīka skats Comment[mr]=फाईल प्रणाली ब्राऊजर साधन दृश्य Comment[nb]=Vising for filsystemviser Comment[nds]=De Warktüüchansicht Dateisysteem-Kieker Comment[ne]=फाइल प्रणाली ब्राउजर उपकरण दृश्य Comment[nl]=Blader door het bestandssysteem Comment[nn]=Vising for filsystemvisar Comment[pa]=ਫਾਇਲ ਸਿਸਟਮ ਬਰਾਊਜ਼ਰ ਟੂਲ ਝਲਕ Comment[pl]=Widok narzędzia przeglądarki systemu plików Comment[pt]=Área da ferramenta de navegação no sistema de ficheiros Comment[pt_BR]=Janela de navegação no sistema de arquivos Comment[ro]=Mod de vizualizare navigator sistem de fișiere Comment[ru]=Панель для просмотра файловой системы Comment[si]=ගොනු පද්ධති ගවේශක මෙවලම් දසුන Comment[sk]=Zobrazenie nástroja prehliadača súborového systému Comment[sl]=Orodni pogled za brskanje po datotečnem sistemu Comment[sr]=Приказ прегледача фајл система Comment[sr@ijekavian]=Приказ прегледача фајл система Comment[sr@ijekavianlatin]=Prikaz pregledača fajl sistema Comment[sr@latin]=Prikaz pregledača fajl sistema Comment[sv]=Verktygsvy för filsystembläddring Comment[tg]=Асбоби намоиши системаи файлҳои браузер Comment[tr]=Dosya sistemi tarayıcısı görünümü Comment[ug]=ھۆججەت سىستېما كۆرگۈ قورال كۆرۈنۈشى Comment[uk]=Панель навігатора файловою системою Comment[wa]=Voeyaedje d' usteye di naivieu di sistinme di fitchî Comment[x-test]=xxFile system browser tool viewxx Comment[zh_CN]=文件系统浏览器工具视图 Comment[zh_TW]=「檔案系統瀏覽器」工具檢視 diff --git a/addons/filetree/CMakeLists.txt b/addons/filetree/CMakeLists.txt index 985f0a1d8..8db595558 100644 --- a/addons/filetree/CMakeLists.txt +++ b/addons/filetree/CMakeLists.txt @@ -1,32 +1,35 @@ -add_definitions(-DTRANSLATION_DOMAIN=\"katefiletree\") -project(katefiletreeplugin) +find_package(KF5GuiAddons QUIET) +set_package_properties(KF5GuiAddons PROPERTIES PURPOSE "Required to build the katefiletree addon") -set(katefiletree_PART_SRCS katefiletree.cpp katefiletreemodel.cpp katefiletreeproxymodel.cpp ) -set(katefiletreeplugin_PART_SRCS katefiletreeplugin.cpp katefiletreeconfigpage.cpp katefiletreepluginsettings.cpp ) +if(NOT KF5GuiAddons_FOUND) + return() +endif() -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) +add_library(katefiletree MODULE "") +target_compile_definitions(katefiletree PRIVATE TRANSLATION_DOMAIN="katefiletree") -add_library (katefiletree STATIC ${katefiletree_PART_SRCS}) -set_target_properties(katefiletree PROPERTIES POSITION_INDEPENDENT_CODE TRUE) -target_link_libraries(katefiletree -PUBLIC - KF5::TextEditor - KF5::I18n - KF5::GuiAddons - KF5::IconThemes +target_link_libraries( + katefiletree + PUBLIC + KF5::TextEditor + KF5::GuiAddons ) -# resource for ui file and stuff -qt5_add_resources(katefiletreeplugin_PART_SRCS plugin.qrc) - -add_library (katefiletreeplugin MODULE ${katefiletreeplugin_PART_SRCS}) -target_link_libraries(katefiletreeplugin katefiletree) - -install(TARGETS katefiletreeplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) +target_sources( + katefiletree + PRIVATE + katefiletree.cpp + katefiletreeconfigpage.cpp + katefiletreemodel.cpp + katefiletreeplugin.cpp + katefiletreepluginsettings.cpp + katefiletreeproxymodel.cpp + plugin.qrc +) -kcoreaddons_desktop_to_json (katefiletreeplugin katefiletreeplugin.desktop) +kcoreaddons_desktop_to_json(katefiletree katefiletreeplugin.desktop) +install(TARGETS katefiletree DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) -############# unit tests ################ -if (BUILD_TESTING) - add_subdirectory(autotests) +if(BUILD_TESTING) + add_subdirectory(autotests) endif() diff --git a/addons/filetree/autotests/CMakeLists.txt b/addons/filetree/autotests/CMakeLists.txt index dca4fe0f9..6f85f07d7 100644 --- a/addons/filetree/autotests/CMakeLists.txt +++ b/addons/filetree/autotests/CMakeLists.txt @@ -1,15 +1,24 @@ - include(ECMMarkAsTest) -macro(filetree_executable_tests) - foreach(_testname ${ARGN}) - add_executable(${_testname} ${_testname}.cpp document_dummy.cpp) - add_test(NAME kateapp-${_testname} COMMAND ${_testname}) - target_link_libraries(${_testname} katefiletree Qt5::Test) - ecm_mark_as_test(${_testname}) - endforeach(_testname) -endmacro(filetree_executable_tests) - -filetree_executable_tests( - filetree_model_test +add_executable(filetree_model_test "") +target_include_directories(filetree_model_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + +find_package(Qt5Test QUIET REQUIRED) +target_link_libraries( + filetree_model_test + PRIVATE + KF5::GuiAddons + KF5::TextEditor + Qt5::Test ) + +target_sources( + filetree_model_test + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../katefiletreemodel.cpp + filetree_model_test.cpp + document_dummy.cpp +) + +add_test(NAME kateapp-filetree_model_test COMMAND filetree_model_test) +ecm_mark_as_test(filetree_model_test) diff --git a/addons/filetree/katefiletree.cpp b/addons/filetree/katefiletree.cpp index e3b7338d5..ee3d7d4fd 100644 --- a/addons/filetree/katefiletree.cpp +++ b/addons/filetree/katefiletree.cpp @@ -1,732 +1,731 @@ /* This file is part of the KDE project Copyright (C) 2010 Thomas Fjellstrom Copyright (C) 2014 Joseph Wenninger This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ //BEGIN Includes #include "katefiletree.h" #include "katefiletreemodel.h" #include "katefiletreeproxymodel.h" #include "katefiletreedebug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //END Includes //BEGIN KateFileTree KateFileTree::KateFileTree(QWidget *parent): QTreeView(parent) { setAcceptDrops(false); setIndentation(12); setAllColumnsShowFocus(true); setFocusPolicy(Qt::NoFocus); setDragEnabled(true); setDragDropMode(QAbstractItemView::DragOnly); // handle activated (e.g. for pressing enter) + clicked (to avoid to need to do double-click e.g. on Windows) connect(this, &KateFileTree::activated, this, &KateFileTree::mouseClicked); connect(this, &KateFileTree::clicked, this, &KateFileTree::mouseClicked); m_filelistReloadDocument = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action:inmenu", "Reloa&d"), this); connect(m_filelistReloadDocument, &QAction::triggered, this, &KateFileTree::slotDocumentReload); m_filelistReloadDocument->setWhatsThis(i18n("Reload selected document(s) from disk.")); m_filelistCloseDocument = new QAction(QIcon::fromTheme(QStringLiteral("document-close")), i18nc("@action:inmenu", "Close"), this); connect(m_filelistCloseDocument, &QAction::triggered, this, &KateFileTree::slotDocumentClose); m_filelistCloseDocument->setWhatsThis(i18n("Close the current document.")); m_filelistExpandRecursive = new QAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18nc("@action:inmenu", "Expand recursively"), this); connect(m_filelistExpandRecursive, &QAction::triggered, this, &KateFileTree::slotExpandRecursive); m_filelistExpandRecursive->setWhatsThis(i18n("Expand the file list sub tree recursively.")); m_filelistCollapseRecursive = new QAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18nc("@action:inmenu", "Collapse recursively"), this); connect(m_filelistCollapseRecursive, &QAction::triggered, this, &KateFileTree::slotCollapseRecursive); m_filelistCollapseRecursive->setWhatsThis(i18n("Collapse the file list sub tree recursively.")); m_filelistCloseOtherDocument = new QAction(QIcon::fromTheme(QStringLiteral("document-close")), i18nc("@action:inmenu", "Close Other"), this); connect(m_filelistCloseOtherDocument, &QAction::triggered, this, &KateFileTree::slotDocumentCloseOther); m_filelistCloseOtherDocument->setWhatsThis(i18n("Close other documents in this folder.")); m_filelistOpenContainingFolder = new QAction(QIcon::fromTheme(QStringLiteral("document-open-folder")), i18nc("@action:inmenu", "Open Containing Folder"), this); connect(m_filelistOpenContainingFolder, &QAction::triggered, this, &KateFileTree::slotOpenContainingFolder); m_filelistOpenContainingFolder->setWhatsThis(i18n("Open the folder this file is located in.")); m_filelistCopyFilename = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action:inmenu", "Copy File Path"), this); connect(m_filelistCopyFilename, &QAction::triggered, this, &KateFileTree::slotCopyFilename); m_filelistCopyFilename->setWhatsThis(i18n("Copy path and filename to the clipboard.")); m_filelistRenameFile = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18nc("@action:inmenu", "Rename..."), this); connect(m_filelistRenameFile, &QAction::triggered, this, &KateFileTree::slotRenameFile); m_filelistRenameFile->setWhatsThis(i18n("Rename the selected file.")); m_filelistPrintDocument = KStandardAction::print(this, SLOT(slotPrintDocument()), this); m_filelistPrintDocument->setWhatsThis(i18n("Print selected document.")); m_filelistPrintDocumentPreview = KStandardAction::printPreview(this, SLOT(slotPrintDocumentPreview()), this); m_filelistPrintDocumentPreview->setWhatsThis(i18n("Show print preview of current document")); m_filelistDeleteDocument = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action:inmenu", "Delete"), this); connect(m_filelistDeleteDocument, &QAction::triggered, this, &KateFileTree::slotDocumentDelete); m_filelistDeleteDocument->setWhatsThis(i18n("Close and delete selected file from storage.")); QActionGroup *modeGroup = new QActionGroup(this); m_treeModeAction = setupOption(modeGroup, QIcon::fromTheme(QStringLiteral("view-list-tree")), i18nc("@action:inmenu", "Tree Mode"), i18n("Set view style to Tree Mode"), SLOT(slotTreeMode()), true); m_listModeAction = setupOption(modeGroup, QIcon::fromTheme(QStringLiteral("view-list-text")), i18nc("@action:inmenu", "List Mode"), i18n("Set view style to List Mode"), SLOT(slotListMode()), false); QActionGroup *sortGroup = new QActionGroup(this); m_sortByFile = setupOption(sortGroup, QIcon(), i18nc("@action:inmenu sorting option", "Document Name"), i18n("Sort by Document Name"), SLOT(slotSortName()), true); m_sortByPath = setupOption(sortGroup, QIcon(), i18nc("@action:inmenu sorting option", "Document Path"), i18n("Sort by Document Path"), SLOT(slotSortPath()), false); m_sortByOpeningOrder = setupOption(sortGroup, QIcon(), i18nc("@action:inmenu sorting option", "Opening Order"), i18n("Sort by Opening Order"), SLOT(slotSortOpeningOrder()), false); m_resetHistory = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), i18nc("@action:inmenu", "Clear History"), this); connect(m_resetHistory, &QAction::triggered, this, &KateFileTree::slotResetHistory); m_resetHistory->setWhatsThis(i18n("Clear edit/view history.")); QPalette p = palette(); p.setColor(QPalette::Inactive, QPalette::Highlight, p.color(QPalette::Active, QPalette::Highlight)); p.setColor(QPalette::Inactive, QPalette::HighlightedText, p.color(QPalette::Active, QPalette::HighlightedText)); setPalette(p); } KateFileTree::~KateFileTree() {} void KateFileTree::setModel(QAbstractItemModel *model) { Q_ASSERT(qobject_cast(model)); // we don't really work with anything else QTreeView::setModel(model); } QAction *KateFileTree::setupOption( QActionGroup *group, const QIcon &icon, const QString &label, const QString &whatsThis, const char *slot, bool checked ) { QAction *new_action = new QAction(icon, label, this); new_action->setWhatsThis(whatsThis); new_action->setActionGroup(group); new_action->setCheckable(true); new_action->setChecked(checked); connect(new_action, SIGNAL(triggered()), this, slot); return new_action; } void KateFileTree::slotListMode() { emit viewModeChanged(true); } void KateFileTree::slotTreeMode() { emit viewModeChanged(false); } void KateFileTree::slotSortName() { emit sortRoleChanged(Qt::DisplayRole); } void KateFileTree::slotSortPath() { emit sortRoleChanged(KateFileTreeModel::PathRole); } void KateFileTree::slotSortOpeningOrder() { emit sortRoleChanged(KateFileTreeModel::OpeningOrderRole); } void KateFileTree::slotCurrentChanged(const QModelIndex ¤t, const QModelIndex &previous) { Q_UNUSED(previous); if (!current.isValid()) { return; } KTextEditor::Document *doc = model()->data(current, KateFileTreeModel::DocumentRole).value(); if (doc) { m_previouslySelected = current; } } void KateFileTree::mouseClicked(const QModelIndex &index) { if (auto doc = model()->data(index, KateFileTreeModel::DocumentRole).value()) { emit activateDocument(doc); } } void KateFileTree::contextMenuEvent(QContextMenuEvent *event) { m_indexContextMenu = selectionModel()->currentIndex(); selectionModel()->setCurrentIndex(m_indexContextMenu, QItemSelectionModel::ClearAndSelect); KateFileTreeProxyModel *ftpm = static_cast(model()); KateFileTreeModel *ftm = static_cast(ftpm->sourceModel()); bool listMode = ftm->listMode(); m_treeModeAction->setChecked(!listMode); m_listModeAction->setChecked(listMode); int sortRole = ftpm->sortRole(); m_sortByFile->setChecked(sortRole == Qt::DisplayRole); m_sortByPath->setChecked(sortRole == KateFileTreeModel::PathRole); m_sortByOpeningOrder->setChecked(sortRole == KateFileTreeModel::OpeningOrderRole); KTextEditor::Document *doc = m_indexContextMenu.data(KateFileTreeModel::DocumentRole).value(); const bool isFile = (nullptr != doc); QMenu menu; menu.addAction(m_filelistReloadDocument); menu.addAction(m_filelistCloseDocument); menu.addAction(m_filelistExpandRecursive); menu.addAction(m_filelistCollapseRecursive); if (isFile) { menu.addAction(m_filelistCloseOtherDocument); menu.addSeparator(); menu.addAction(m_filelistOpenContainingFolder); menu.addAction(m_filelistCopyFilename); menu.addAction(m_filelistRenameFile); menu.addAction(m_filelistPrintDocument); menu.addAction(m_filelistPrintDocumentPreview); QMenu *openWithMenu = menu.addMenu(i18nc("@action:inmenu", "Open With")); connect(openWithMenu, &QMenu::aboutToShow, this, &KateFileTree::slotFixOpenWithMenu); connect(openWithMenu, &QMenu::triggered, this, &KateFileTree::slotOpenWithMenuAction); const bool hasFileName = doc->url().isValid(); m_filelistOpenContainingFolder->setEnabled(hasFileName); m_filelistCopyFilename->setEnabled(hasFileName); m_filelistRenameFile->setEnabled(hasFileName); m_filelistDeleteDocument->setEnabled(hasFileName); menu.addAction(m_filelistDeleteDocument); } menu.addSeparator(); QMenu *view_menu = menu.addMenu(i18nc("@action:inmenu", "View Mode")); view_menu->addAction(m_treeModeAction); view_menu->addAction(m_listModeAction); QMenu *sort_menu = menu.addMenu(QIcon::fromTheme(QStringLiteral("view-sort")), i18nc("@action:inmenu", "Sort By")); sort_menu->addAction(m_sortByFile); sort_menu->addAction(m_sortByPath); sort_menu->addAction(m_sortByOpeningOrder); menu.addAction(m_resetHistory); menu.exec(viewport()->mapToGlobal(event->pos())); if (m_previouslySelected.isValid()) { selectionModel()->setCurrentIndex(m_previouslySelected, QItemSelectionModel::ClearAndSelect); } event->accept(); } void KateFileTree::slotFixOpenWithMenu() { QMenu *menu = (QMenu *)sender(); menu->clear(); KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); if (!doc) { return; } // get a list of appropriate services. QMimeDatabase db; QMimeType mime = db.mimeTypeForName(doc->mimeType()); QAction *a = nullptr; - KService::List offers = KMimeTypeTrader::self()->query(mime.name(), QStringLiteral("Application")); + const KService::List offers = KMimeTypeTrader::self()->query(mime.name(), QStringLiteral("Application")); // for each one, insert a menu item... - for (KService::List::Iterator it = offers.begin(); it != offers.end(); ++it) { - KService::Ptr service = *it; + for (const auto& service : offers) { if (service->name() == QLatin1String("Kate")) { continue; } a = menu->addAction(QIcon::fromTheme(service->icon()), service->name()); a->setData(service->entryPath()); } // append "Other..." to call the KDE "open with" dialog. a = menu->addAction(i18n("&Other...")); a->setData(QString()); } void KateFileTree::slotOpenWithMenuAction(QAction *a) { QList list; KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); if (!doc) { return; } list.append(doc->url()); const QString openWith = a->data().toString(); if (openWith.isEmpty()) { // display "open with" dialog KOpenWithDialog dlg(list); if (dlg.exec()) { KRun::runService(*dlg.service(), list, this); } return; } KService::Ptr app = KService::serviceByDesktopPath(openWith); if (app) { KRun::runService(*app, list, this); } else { KMessageBox::error(this, i18n("Application '%1' not found.", openWith), i18n("Application not found")); } } Q_DECLARE_METATYPE(QList) void KateFileTree::slotDocumentClose() { m_previouslySelected = QModelIndex(); QVariant v = m_indexContextMenu.data(KateFileTreeModel::DocumentTreeRole); if (!v.isValid()) { return; } QList closingDocuments = v.value >(); KTextEditor::Editor::instance()->application()->closeDocuments(closingDocuments); } void KateFileTree::slotExpandRecursive() { if (! m_indexContextMenu.isValid()) { return; } // Work list for DFS walk over sub tree QList worklist = { m_indexContextMenu }; while (! worklist.isEmpty()) { QPersistentModelIndex index = worklist.takeLast(); // Expand current item expand(index); // Append all children of current item for (int i=0 ; i < model()->rowCount(index) ; ++i) { worklist.append(index.child(i,0)); } } } void KateFileTree::slotCollapseRecursive() { if (! m_indexContextMenu.isValid()) { return; } // Work list for DFS walk over sub tree QList worklist = { m_indexContextMenu }; while (! worklist.isEmpty()) { QPersistentModelIndex index = worklist.takeLast(); // Expand current item collapse(index); // Prepend all children of current item for (int i=0 ; i < model()->rowCount(index) ; ++i) { worklist.append(index.child(i,0)); } } } void KateFileTree::slotDocumentCloseOther() { QVariant v = model()->data(m_indexContextMenu.parent(), KateFileTreeModel::DocumentTreeRole); if (!v.isValid()) { return; } QList closingDocuments = v.value >(); KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); closingDocuments.removeOne(doc); KTextEditor::Editor::instance()->application()->closeDocuments(closingDocuments); } void KateFileTree::slotDocumentReload() { QVariant v = m_indexContextMenu.data(KateFileTreeModel::DocumentTreeRole); if (!v.isValid()) { return; } QList docs = v.value >(); foreach(KTextEditor::Document * doc, docs) { doc->documentReload(); } } void KateFileTree::slotOpenContainingFolder() { KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); if (doc) { KIO::highlightInFileManager({doc->url()}); } } void KateFileTree::slotCopyFilename() { KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); // TODO: the following code was improved in kate/katefileactions.cpp and should be reused here // (make sure that the mentioned bug 381052 does not reappear) if (doc) { // ensure we prefer native separators, bug 381052 if (doc->url().isLocalFile()) { QApplication::clipboard()->setText(QDir::toNativeSeparators(doc->url().toLocalFile())); } else { QApplication::clipboard()->setText(doc->url().url()); } } } void KateFileTree::slotRenameFile() { KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); // TODO: the following code was improved in kate/katefileactions.cpp and should be reused here if (!doc) { return; } const QUrl oldFileUrl = doc->url(); const QString oldFileName = doc->url().fileName(); bool ok; QString newFileName = QInputDialog::getText(this, i18n("Rename file"), i18n("New file name"), QLineEdit::Normal, oldFileName, &ok); if (!ok) { return; } QUrl newFileUrl = oldFileUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); newFileUrl.setPath(newFileUrl.path() + QLatin1Char('/') + newFileName); if (!newFileUrl.isValid()) { return; } if (!doc->closeUrl()) { return; } doc->waitSaveComplete(); KIO::CopyJob* job = KIO::move(oldFileUrl, newFileUrl); QSharedPointer sc(new QMetaObject::Connection()); auto success = [doc, sc] (KIO::Job*, const QUrl&, const QUrl &realNewFileUrl, const QDateTime&, bool, bool) { doc->openUrl(realNewFileUrl); doc->documentSavedOrUploaded(doc, true); QObject::disconnect(*sc); }; *sc = connect(job, &KIO::CopyJob::copyingDone, doc, success); if (!job->exec()) { KMessageBox::sorry(this, i18n("File \"%1\" could not be moved to \"%2\"", oldFileUrl.toDisplayString(), newFileUrl.toDisplayString())); doc->openUrl(oldFileUrl); } } void KateFileTree::slotDocumentFirst() { KTextEditor::Document *doc = model()->data(model()->index(0, 0), KateFileTreeModel::DocumentRole).value(); if (doc) { emit activateDocument(doc); } } void KateFileTree::slotDocumentLast() { int count = model()->rowCount(model()->parent(currentIndex())); KTextEditor::Document *doc = model()->data(model()->index(count - 1, 0), KateFileTreeModel::DocumentRole).value(); if (doc) { emit activateDocument(doc); } } void KateFileTree::slotDocumentPrev() { KateFileTreeProxyModel *ftpm = static_cast(model()); QModelIndex current_index = currentIndex(); QModelIndex prev; // scan up the tree skipping any dir nodes while (current_index.isValid()) { if (current_index.row() > 0) { current_index = ftpm->sibling(current_index.row() - 1, current_index.column(), current_index); if (!current_index.isValid()) { break; } if (ftpm->isDir(current_index)) { // try and select the last child in this parent int children = ftpm->rowCount(current_index); current_index = ftpm->index(children - 1, 0, current_index); if (ftpm->isDir(current_index)) { // since we're a dir, keep going while (ftpm->isDir(current_index)) { children = ftpm->rowCount(current_index); current_index = ftpm->index(children - 1, 0, current_index); } if (!ftpm->isDir(current_index)) { prev = current_index; break; } continue; } else { // we're the previous file, set prev prev = current_index; break; } } else { // found document item prev = current_index; break; } } else { // just select the parent, the logic above will handle the rest current_index = ftpm->parent(current_index); if (!current_index.isValid()) { // paste the root node here, try and wrap around int children = ftpm->rowCount(current_index); QModelIndex last_index = ftpm->index(children - 1, 0, current_index); if (!last_index.isValid()) { break; } if (ftpm->isDir(last_index)) { // last node is a dir, select last child row int last_children = ftpm->rowCount(last_index); prev = ftpm->index(last_children - 1, 0, last_index); // bug here? break; } else { // got last file node prev = last_index; break; } } } } if (prev.isValid()) { KTextEditor::Document *doc = model()->data(prev, KateFileTreeModel::DocumentRole).value(); emit activateDocument(doc); } } void KateFileTree::slotDocumentNext() { KateFileTreeProxyModel *ftpm = static_cast(model()); QModelIndex current_index = currentIndex(); int parent_row_count = ftpm->rowCount(ftpm->parent(current_index)); QModelIndex next; // scan down the tree skipping any dir nodes while (current_index.isValid()) { if (current_index.row() < parent_row_count - 1) { current_index = ftpm->sibling(current_index.row() + 1, current_index.column(), current_index); if (!current_index.isValid()) { break; } if (ftpm->isDir(current_index)) { // we have a dir node while (ftpm->isDir(current_index)) { current_index = ftpm->index(0, 0, current_index); } parent_row_count = ftpm->rowCount(ftpm->parent(current_index)); if (!ftpm->isDir(current_index)) { next = current_index; break; } } else { // found document item next = current_index; break; } } else { // select the parent's next sibling QModelIndex parent_index = ftpm->parent(current_index); int grandparent_row_count = ftpm->rowCount(ftpm->parent(parent_index)); current_index = parent_index; parent_row_count = grandparent_row_count; // at least if we're not past the last node if (!current_index.isValid()) { // paste the root node here, try and wrap around QModelIndex last_index = ftpm->index(0, 0, QModelIndex()); if (!last_index.isValid()) { break; } if (ftpm->isDir(last_index)) { // last node is a dir, select first child row while (ftpm->isDir(last_index)) { if (ftpm->rowCount(last_index)) { // has children, select first last_index = ftpm->index(0, 0, last_index); } } next = last_index; break; } else { // got first file node next = last_index; break; } } } } if (next.isValid()) { KTextEditor::Document *doc = model()->data(next, KateFileTreeModel::DocumentRole).value(); emit activateDocument(doc); } } void KateFileTree::slotPrintDocument() { KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); if (!doc) { return; } doc->print(); } void KateFileTree::slotPrintDocumentPreview() { KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); if (!doc) { return; } doc->printPreview(); } void KateFileTree::slotResetHistory() { KateFileTreeProxyModel *ftpm = static_cast(model()); KateFileTreeModel *ftm = static_cast(ftpm->sourceModel()); ftm->resetHistory(); } void KateFileTree::slotDocumentDelete() { KTextEditor::Document *doc = model()->data(m_indexContextMenu, KateFileTreeModel::DocumentRole).value(); // TODO: the following code was improved in kate/katefileactions.cpp and should be reused here if (!doc) { return; } QUrl url = doc->url(); bool go = (KMessageBox::warningContinueCancel(this, i18n("Do you really want to delete file \"%1\" from storage?", url.toDisplayString()), i18n("Delete file?"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("filetreedeletefile") ) == KMessageBox::Continue); if (!go) { return; } if (!KTextEditor::Editor::instance()->application()->closeDocument(doc)) { return; // no extra message, the internals of ktexteditor should take care of that. } if (url.isValid()) { KIO::DeleteJob *job = KIO::del(url); if (!job->exec()) { KMessageBox::sorry(this, i18n("File \"%1\" could not be deleted.", url.toDisplayString())); } } } //END KateFileTree diff --git a/addons/filetree/katefiletreeplugin.desktop b/addons/filetree/katefiletreeplugin.desktop index 131599a4c..703786a40 100644 --- a/addons/filetree/katefiletreeplugin.desktop +++ b/addons/filetree/katefiletreeplugin.desktop @@ -1,71 +1,72 @@ [Desktop Entry] Type=Service ServiceTypes=KTextEditor/Plugin X-KDE-Library=katefiletreeplugin Name=Document Tree View Name[ast]=Vista n'árbole de documentos Name[ca]=Vista en arbre de documents Name[ca@valencia]=Vista en arbre de documents Name[de]=Dokumentbaumansicht Name[el]=Προβολή δενδρικής δομής εγγράφων Name[en_GB]=Document Tree View Name[es]=Vista en árbol de documentos Name[eu]=Dokumentuen zuhaitz-ikuspegia Name[fi]=Tiedostopuunäkymä Name[fr]=Arborescence des documents Name[gl]=Vista da árbore de documentos +Name[id]=Tampilan Hierarki Dokumen Name[it]=Vista dell'albero del documento Name[ko]=문서 전환기 Name[nl]=Documentboomstructuur Name[nn]=Dokumenttrevising Name[pl]=Widok drzewa dokumentów Name[pt]=Árvore de Documentos Name[pt_BR]=Árvore de documentos Name[ru]=Иерархический просмотр документов Name[sk]=Prepínač dokumentov Name[sv]=Dokumentträdvy Name[tr]=Belge Ağacı Görünümü Name[uk]=Ієрархія документів Name[x-test]=xxDocument Tree Viewxx Name[zh_CN]=文档树视图 Name[zh_TW]=樹狀文件檢視 Comment=Displays the open documents in a file tree Comment[ar]=تعرض المستندات المفتوحة في شجرة ملفّات Comment[ast]=Amuesa los documentos abiertos nun árbole de ficheros Comment[ca]=Mostra els documents oberts en un arbre de fitxers Comment[ca@valencia]=Mostra els documents oberts en un arbre de fitxers Comment[cs]=Zobrazí otevřené dokumenty ve stromu souborů Comment[da]=Viser de åbne dokumenter som et filtræ Comment[de]=Zeigt geöffnete Dokumente als Dateibaum an Comment[el]=Εμφανίζει τα ανοιγμένα έγγραφα σε δενδρική δομή Comment[en_GB]=Displays the open documents in a file tree Comment[es]=Muestra los documentos abiertos en un árbol de archivos Comment[et]=Avatud dokumentide näitamine failipuuna Comment[eu]=Irekitako dokumentuak fitxategi-zuhaitz batean azaltzen ditu Comment[fi]=Näyttää avoimet tiedostot puuna Comment[fr]=Affiche les documents ouverts dans une arborescence de fichiers Comment[gl]=Mostra os documentos abertos nunha árbore de ficheiros. Comment[he]=הצג את המסמכים הפתוחים בעץ הקבצים Comment[hu]=Megnyitott dokumentumok megjelenítése fastruktúrában Comment[ia]=Monstra le documentos aperite in un arbore de file Comment[id]=Mendisplaikan dokumen yang terbuka dalam sebuah hierarki file Comment[it]=Visualizza i documenti aperti in un albero dei file Comment[ko]=열린 문서를 파일 트리 구조로 표시합니다 Comment[nl]=Toont de geopende documenten in een boomstructuur Comment[nn]=Viser dei opne dokumenta i eit filtre Comment[pl]=Wyświetla otwarte dokumenty w drzewie plików Comment[pt]=Mostra os documentos abertos numa árvore de ficheiros Comment[pt_BR]=Exibe os documentos abertos em uma árvore de arquivos Comment[ru]=Просмотр открытых документов в дереве папок Comment[sk]=Ukázať otvorené dokumenty v strome Comment[sl]=Odprte dokumente prikaže v drevesu datotek Comment[sr]=Приказује отворене документе као стабло фајлова Comment[sr@ijekavian]=Приказује отворене документе као стабло фајлова Comment[sr@ijekavianlatin]=Prikazuje otvorene dokumente kao stablo fajlova Comment[sr@latin]=Prikazuje otvorene dokumente kao stablo fajlova Comment[sv]=Visar öppna dokument i ett filträd Comment[tr]=Açık belgeleri bir dosya ağaç yapısı içinde gösterir Comment[uk]=Показує відкриті документи у ієрархії файлів Comment[x-test]=xxDisplays the open documents in a file treexx Comment[zh_CN]=用文件树显示打开的文档 Comment[zh_TW]=以樹狀圖顯示要開啟的文件 diff --git a/addons/gdbplugin/CMakeLists.txt b/addons/gdbplugin/CMakeLists.txt index 05ee4d633..6df148009 100644 --- a/addons/gdbplugin/CMakeLists.txt +++ b/addons/gdbplugin/CMakeLists.txt @@ -1,23 +1,26 @@ -add_definitions(-DTRANSLATION_DOMAIN=\"kategdbplugin\") -include_directories(${CMAKE_CURRENT_BINARY_DIR}) +# Hasn't been ported to Windows yet. +if(WIN32) + return() +endif() -set(kategdbplugin_PART_SRCS +add_library(kategdbplugin MODULE "") +target_compile_definitions(kategdbplugin PRIVATE TRANSLATION_DOMAIN="kategdbplugin") +target_link_libraries(kategdbplugin PRIVATE KF5::TextEditor) + +ki18n_wrap_ui(UI_SOURCES advanced_settings.ui) +target_sources(kategdbplugin PRIVATE ${UI_SOURCES}) + +target_sources( + kategdbplugin + PRIVATE plugin_kategdb.cpp debugview.cpp configview.cpp ioview.cpp localsview.cpp advanced_settings.cpp + plugin.qrc ) -ki18n_wrap_ui(kategdbplugin_PART_SRCS advanced_settings.ui) - -# resource for ui file and stuff -qt5_add_resources(kategdbplugin_PART_SRCS plugin.qrc) - -add_library(kategdbplugin MODULE ${kategdbplugin_PART_SRCS}) -kcoreaddons_desktop_to_json (kategdbplugin kategdbplugin.desktop) - -target_link_libraries(kategdbplugin KF5::TextEditor KF5::I18n KF5::IconThemes) - +kcoreaddons_desktop_to_json(kategdbplugin kategdbplugin.desktop) install(TARGETS kategdbplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/gdbplugin/debugview.cpp b/addons/gdbplugin/debugview.cpp index 59da507e8..c4dbe2299 100644 --- a/addons/gdbplugin/debugview.cpp +++ b/addons/gdbplugin/debugview.cpp @@ -1,696 +1,696 @@ // // debugview.cpp // // Description: Manages the interaction with GDB // // // Copyright (c) 2008-2010 Ian Wakeling // Copyright (c) 2011 Kåre Särs // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Library General Public // License version 2 as published by the Free Software Foundation. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Library General Public License for more details. // // You should have received a copy of the GNU Library General Public License // along with this library; see the file COPYING.LIB. If not, write to // the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, // Boston, MA 02110-1301, USA. #include "debugview.h" #include #include #include #include #include #include #include #include static const QString PromptStr = QStringLiteral("(prompt)"); DebugView::DebugView(QObject* parent) : QObject(parent), m_debugProcess(nullptr), m_state(none), m_subState(normal), m_debugLocationChanged(true), m_queryLocals(false) { } DebugView::~DebugView() { if ( m_debugProcess.state() != QProcess::NotRunning) { m_debugProcess.kill(); m_debugProcess.blockSignals(true); m_debugProcess.waitForFinished(); } } void DebugView::runDebugger(const GDBTargetConf &conf, const QStringList &ioFifos) { if (conf.executable.isEmpty()) { return; } m_targetConf = conf; if (ioFifos.size() == 3) { m_ioPipeString = QStringLiteral("< %1 1> %2 2> %3") .arg(ioFifos[0]) .arg(ioFifos[1]) .arg(ioFifos[2]); } if (m_state == none) { m_outBuffer.clear(); m_errBuffer.clear(); m_errorList.clear(); //create a process to control GDB m_debugProcess.setWorkingDirectory(m_targetConf.workDir); connect(&m_debugProcess, static_cast(&QProcess::error), this, &DebugView::slotError); connect(&m_debugProcess, &QProcess::readyReadStandardError, this, &DebugView::slotReadDebugStdErr); connect(&m_debugProcess, &QProcess::readyReadStandardOutput, this, &DebugView::slotReadDebugStdOut); connect(&m_debugProcess, static_cast(&QProcess::finished), this, &DebugView::slotDebugFinished); m_debugProcess.start(m_targetConf.gdbCmd); m_nextCommands << QStringLiteral("set pagination off"); m_state = ready; } else { // On startup the gdb prompt will trigger the "nextCommands", // here we have to trigger it manually. QTimer::singleShot(0, this, &DebugView::issueNextCommand); } m_nextCommands << QStringLiteral("file %1").arg(m_targetConf.executable); m_nextCommands << QStringLiteral("set args %1 %2").arg(m_targetConf.arguments).arg(m_ioPipeString); m_nextCommands << QStringLiteral("set inferior-tty /dev/null"); m_nextCommands << m_targetConf.customInit; m_nextCommands << QStringLiteral("(Q) info breakpoints"); } bool DebugView::debuggerRunning() const { return(m_state != none); } bool DebugView::debuggerBusy() const { return(m_state == executingCmd); } bool DebugView::hasBreakpoint(const QUrl& url, int line) { - for (int i = 0; i")) { m_outBuffer.clear(); processLine(PromptStr); } } void DebugView::slotReadDebugStdErr() { m_errBuffer += QString::fromLocal8Bit(m_debugProcess.readAllStandardError().data()); int end=0; // add whole lines at a time to the error list do { end = m_errBuffer.indexOf(QLatin1Char('\n')); if (end < 0) break; m_errorList << m_errBuffer.mid(0, end); m_errBuffer.remove(0,end+1); } while (1); processErrors(); } void DebugView::slotDebugFinished(int /*exitCode*/, QProcess::ExitStatus status) { if(status != QProcess::NormalExit) { emit outputText(i18n("*** gdb exited normally ***") + QLatin1Char('\n')); } m_state = none; emit readyForInput(false); // remove all old breakpoints BreakPoint bPoint; while (m_breakPointList.size() > 0) { bPoint = m_breakPointList.takeFirst(); emit breakPointCleared(bPoint.file, bPoint.line -1); } emit gdbEnded(); } void DebugView::movePC(QUrl const& url, int line) { if(m_state == ready) { QString cmd = QStringLiteral("tbreak %1:%2").arg(url.path()).arg(line); m_nextCommands << QStringLiteral("jump %1:%2").arg(url.path()).arg(line); issueCommand(cmd); } } void DebugView::runToCursor(QUrl const& url, int line) { if(m_state == ready) { QString cmd = QStringLiteral("tbreak %1:%2").arg(url.path()).arg(line); m_nextCommands << QStringLiteral("continue"); issueCommand(cmd); } } void DebugView::slotInterrupt() { if (m_state == executingCmd) { m_debugLocationChanged = true; } int pid = m_debugProcess.pid(); if (pid != 0) { ::kill(pid, SIGINT); } } void DebugView::slotKill() { if(m_state != ready) { slotInterrupt(); m_state = ready; } issueCommand(QStringLiteral("kill")); } void DebugView::slotReRun() { slotKill(); m_nextCommands << QStringLiteral("file %1").arg(m_targetConf.executable); m_nextCommands << QStringLiteral("set args %1 %2").arg(m_targetConf.arguments).arg(m_ioPipeString); m_nextCommands << QStringLiteral("set inferior-tty /dev/null"); m_nextCommands << m_targetConf.customInit; m_nextCommands << QStringLiteral("(Q) info breakpoints"); m_nextCommands << QStringLiteral("tbreak main"); m_nextCommands << QStringLiteral("run"); m_nextCommands << QStringLiteral("p setvbuf(stdout, 0, %1, 1024)").arg(_IOLBF); m_nextCommands << QStringLiteral("continue"); } void DebugView::slotStepInto() { issueCommand(QStringLiteral("step")); } void DebugView::slotStepOver() { issueCommand(QStringLiteral("next")); } void DebugView::slotStepOut() { issueCommand(QStringLiteral("finish")); } void DebugView::slotContinue() { issueCommand(QStringLiteral("continue")); } static QRegExp breakpointList(QStringLiteral("Num\\s+Type\\s+Disp\\s+Enb\\s+Address\\s+What.*")); static QRegExp breakpointListed(QStringLiteral("(\\d)\\s+breakpoint\\s+keep\\sy\\s+0x[\\da-f]+\\sin\\s.+\\sat\\s([^:]+):(\\d+).*")); static QRegExp stackFrameAny(QStringLiteral("#(\\d+)\\s(.*)")); static QRegExp stackFrameFile(QStringLiteral("#(\\d+)\\s+(?:0x[\\da-f]+\\s*in\\s)*(\\S+)(\\s\\(.*\\)) at ([^:]+):(\\d+).*")); static QRegExp changeFile(QStringLiteral("(?:(?:Temporary\\sbreakpoint|Breakpoint)\\s*\\d+,\\s*|0x[\\da-f]+\\s*in\\s*)?[^\\s]+\\s*\\([^)]*\\)\\s*at\\s*([^:]+):(\\d+).*")); static QRegExp changeLine(QStringLiteral("(\\d+)\\s+.*")); static QRegExp breakPointReg(QStringLiteral("Breakpoint\\s+(\\d+)\\s+at\\s+0x[\\da-f]+:\\s+file\\s+([^\\,]+)\\,\\s+line\\s+(\\d+).*")); static QRegExp breakPointMultiReg(QStringLiteral("Breakpoint\\s+(\\d+)\\s+at\\s+0x[\\da-f]+:\\s+([^\\,]+):(\\d+).*")); static QRegExp breakPointDel(QStringLiteral("Deleted\\s+breakpoint.*")); static QRegExp exitProgram(QStringLiteral("(?:Program|.*Inferior.*)\\s+exited.*")); static QRegExp threadLine(QStringLiteral("\\**\\s+(\\d+)\\s+Thread.*")); void DebugView::processLine(QString line) { if (line.isEmpty()) return; switch(m_state) { case none: case ready: if(PromptStr == line) { // we get here after initialization QTimer::singleShot(0, this, &DebugView::issueNextCommand); } break; case executingCmd: if(breakpointList.exactMatch(line)) { m_state = listingBreakpoints; emit clearBreakpointMarks(); m_breakPointList.clear(); } else if (line.contains(QStringLiteral("No breakpoints or watchpoints."))) { emit clearBreakpointMarks(); m_breakPointList.clear(); } else if (stackFrameAny.exactMatch(line)) { if (m_lastCommand.contains(QStringLiteral("info stack"))) { emit stackFrameInfo(stackFrameAny.cap(1), stackFrameAny.cap(2)); } else { m_subState = (m_subState == normal) ? stackFrameSeen : stackTraceSeen; m_newFrameLevel = stackFrameAny.cap(1).toInt(); if (stackFrameFile.exactMatch(line)) { m_newFrameFile = stackFrameFile.cap(4); } } } else if(changeFile.exactMatch(line)) { m_currentFile = changeFile.cap(1).trimmed(); int lineNum = changeFile.cap(2).toInt(); if (!m_nextCommands.contains(QStringLiteral("continue"))) { // GDB uses 1 based line numbers, kate uses 0 based... emit debugLocationChanged(resolveFileName(m_currentFile), lineNum - 1); } m_debugLocationChanged = true; } else if(changeLine.exactMatch(line)) { int lineNum = changeLine.cap(1).toInt(); if(m_subState == stackFrameSeen) { m_currentFile = m_newFrameFile; } if (!m_nextCommands.contains(QStringLiteral("continue"))) { // GDB uses 1 based line numbers, kate uses 0 based... emit debugLocationChanged(resolveFileName(m_currentFile), lineNum - 1); } m_debugLocationChanged = true; } else if (breakPointReg.exactMatch(line)) { BreakPoint breakPoint; breakPoint.number = breakPointReg.cap(1).toInt(); breakPoint.file = resolveFileName(breakPointReg.cap(2)); breakPoint.line = breakPointReg.cap(3).toInt(); m_breakPointList << breakPoint; emit breakPointSet(breakPoint.file, breakPoint.line -1); } else if (breakPointMultiReg.exactMatch(line)) { BreakPoint breakPoint; breakPoint.number = breakPointMultiReg.cap(1).toInt(); breakPoint.file = resolveFileName(breakPointMultiReg.cap(2)); breakPoint.line = breakPointMultiReg.cap(3).toInt(); m_breakPointList << breakPoint; emit breakPointSet(breakPoint.file, breakPoint.line -1); } else if (breakPointDel.exactMatch(line)) { line.remove(QStringLiteral("Deleted breakpoint")); line.remove(QLatin1Char('s')); // in case of multiple breakpoints QStringList numbers = line.split(QLatin1Char(' '), QString::SkipEmptyParts); for (int i=0; i 0) && !m_nextCommands[0].contains(QStringLiteral("file"))) { m_nextCommands.clear(); } m_debugLocationChanged = false; // do not insert (Q) commands emit programEnded(); } else if(PromptStr == line) { if(m_subState == stackFrameSeen) { emit stackFrameChanged(m_newFrameLevel); } m_state = ready; // Give the error a possibility get noticed since stderr and stdout are not in sync QTimer::singleShot(0, this, &DebugView::issueNextCommand); } break; case listingBreakpoints: if (breakpointListed.exactMatch(line)) { BreakPoint breakPoint; breakPoint.number = breakpointListed.cap(1).toInt(); breakPoint.file = resolveFileName(breakpointListed.cap(2)); breakPoint.line = breakpointListed.cap(3).toInt(); m_breakPointList << breakPoint; emit breakPointSet(breakPoint.file, breakPoint.line -1); } else if(PromptStr == line) { m_state = ready; QTimer::singleShot(0, this, &DebugView::issueNextCommand); } break; case infoArgs: if(PromptStr == line) { m_state = ready; QTimer::singleShot(0, this, &DebugView::issueNextCommand); } else { emit infoLocal(line); } break; case printThis: if(PromptStr == line) { m_state = ready; QTimer::singleShot(0, this, &DebugView::issueNextCommand); } else { emit infoLocal(line); } break; case infoLocals: if(PromptStr == line) { m_state = ready; emit infoLocal(QString()); QTimer::singleShot(0, this, &DebugView::issueNextCommand); } else { emit infoLocal(line); } break; case infoStack: if(PromptStr == line) { m_state = ready; emit stackFrameInfo(QString(), QString()); QTimer::singleShot(0, this, &DebugView::issueNextCommand); } else if (stackFrameAny.exactMatch(line)) { emit stackFrameInfo(stackFrameAny.cap(1), stackFrameAny.cap(2)); } break; case infoThreads: if(PromptStr == line) { m_state = ready; QTimer::singleShot(0, this, &DebugView::issueNextCommand); } else if (threadLine.exactMatch(line)) { emit threadInfo(threadLine.cap(1).toInt(), (line[0] == QLatin1Char('*'))); } break; } outputTextMaybe(line); } void DebugView::processErrors() { QString error; while (m_errorList.size() > 0) { error = m_errorList.takeFirst(); //qDebug() << error; if(error == QLatin1String("The program is not being run.")) { if (m_lastCommand == QLatin1String("continue")) { m_nextCommands.clear(); m_nextCommands << QStringLiteral("tbreak main"); m_nextCommands << QStringLiteral("run"); m_nextCommands << QStringLiteral("p setvbuf(stdout, 0, %1, 1024)").arg(_IOLBF); m_nextCommands << QStringLiteral("continue"); QTimer::singleShot(0, this, &DebugView::issueNextCommand); } else if ((m_lastCommand == QLatin1String("step")) || (m_lastCommand == QLatin1String("next")) || (m_lastCommand == QLatin1String("finish"))) { m_nextCommands.clear(); m_nextCommands << QStringLiteral("tbreak main"); m_nextCommands << QStringLiteral("run"); m_nextCommands << QStringLiteral("p setvbuf(stdout, 0, %1, 1024)").arg(_IOLBF); QTimer::singleShot(0, this, &DebugView::issueNextCommand); } else if ((m_lastCommand == QLatin1String("kill"))) { if (m_nextCommands.size() > 0) { if (!m_nextCommands[0].contains(QStringLiteral("file"))) { m_nextCommands.clear(); m_nextCommands << QStringLiteral("quit"); } // else continue with "ReRun" } else { m_nextCommands << QStringLiteral("quit"); } m_state = ready; QTimer::singleShot(0, this, &DebugView::issueNextCommand); } // else do nothing } else if (error.contains(QStringLiteral("No line ")) || error.contains(QStringLiteral("No source file named"))) { // setting a breakpoint failed. Do not continue. m_nextCommands.clear(); emit readyForInput(true); } else if (error.contains(QStringLiteral("No stack"))) { m_nextCommands.clear(); emit programEnded(); } if ((m_lastCommand == QLatin1String("(Q)print *this")) && error.contains(QStringLiteral("No symbol \"this\" in current context."))) { continue; } emit outputError(error + QLatin1Char('\n')); } } void DebugView::issueCommand(QString const& cmd) { if(m_state == ready) { emit readyForInput(false); m_state = executingCmd; if (cmd == QLatin1String("(Q)info locals")) { m_state = infoLocals; } else if (cmd == QLatin1String("(Q)info args")) { m_state = infoArgs; } else if (cmd == QLatin1String("(Q)print *this")) { m_state = printThis; } else if (cmd == QLatin1String("(Q)info stack")) { m_state = infoStack; } else if (cmd == QLatin1String("(Q)info thread")) { emit threadInfo(-1 , false); m_state = infoThreads; } m_subState = normal; m_lastCommand = cmd; if (cmd.startsWith(QStringLiteral("(Q)"))) { m_debugProcess.write(qPrintable(cmd.mid(3))); } else { emit outputText(QStringLiteral("(gdb) ") + cmd + QLatin1Char('\n')); m_debugProcess.write(qPrintable(cmd)); } m_debugProcess.write("\n"); } } void DebugView::issueNextCommand() { if(m_state == ready) { if(m_nextCommands.size() > 0) { QString cmd = m_nextCommands.takeFirst(); //qDebug() << "Next command" << cmd; issueCommand(cmd); } else { // FIXME "thread" needs a better generic solution if (m_debugLocationChanged || m_lastCommand.startsWith(QStringLiteral("thread"))) { m_debugLocationChanged = false; if (m_queryLocals && !m_lastCommand.startsWith(QStringLiteral("(Q)"))) { m_nextCommands << QStringLiteral("(Q)info stack"); m_nextCommands << QStringLiteral("(Q)frame"); m_nextCommands << QStringLiteral("(Q)info args"); m_nextCommands << QStringLiteral("(Q)print *this"); m_nextCommands << QStringLiteral("(Q)info locals"); m_nextCommands << QStringLiteral("(Q)info thread"); issueNextCommand(); return; } } emit readyForInput(true); } } } QUrl DebugView::resolveFileName(const QString &fileName) { QUrl url; QFileInfo fInfo = QFileInfo(fileName); //did we end up with an absolute path or a relative one? if (fInfo.exists()) { return QUrl::fromUserInput(fInfo.absoluteFilePath()); } if (fInfo.isAbsolute()) { // we can not do anything just return the fileName return QUrl::fromUserInput(fileName); } // Now try to add the working path fInfo = QFileInfo(m_targetConf.workDir + fileName); if (fInfo.exists()) { return QUrl::fromUserInput(fInfo.absoluteFilePath()); } // now try the executable path fInfo = QFileInfo(QFileInfo(m_targetConf.executable).absolutePath() + fileName); if (fInfo.exists()) { return QUrl::fromUserInput(fInfo.absoluteFilePath()); } foreach (QString srcPath, m_targetConf.srcPaths) { fInfo = QFileInfo(srcPath + QDir::separator() + fileName); if (fInfo.exists()) { return QUrl::fromUserInput(fInfo.absoluteFilePath()); } } // we can not do anything just return the fileName return QUrl::fromUserInput(fileName); } void DebugView::outputTextMaybe(const QString &text) { if (!m_lastCommand.startsWith(QStringLiteral("(Q)")) && !text.contains(PromptStr)) { emit outputText(text + QLatin1Char('\n')); } } void DebugView::slotQueryLocals(bool query) { m_queryLocals = query; if (query && (m_state == ready) && (m_nextCommands.size() == 0)) { m_nextCommands << QStringLiteral("(Q)info stack"); m_nextCommands << QStringLiteral("(Q)frame"); m_nextCommands << QStringLiteral("(Q)info args"); m_nextCommands << QStringLiteral("(Q)print *this"); m_nextCommands << QStringLiteral("(Q)info locals"); m_nextCommands << QStringLiteral("(Q)info thread"); issueNextCommand(); } } diff --git a/addons/kate-ctags/CMakeLists.txt b/addons/kate-ctags/CMakeLists.txt index 6ea0a67f3..80d586a5c 100644 --- a/addons/kate-ctags/CMakeLists.txt +++ b/addons/kate-ctags/CMakeLists.txt @@ -1,32 +1,29 @@ -add_definitions(-DTRANSLATION_DOMAIN=\"kate-ctags-plugin\") +add_library(katectagsplugin MODULE "") +target_compile_definitions(katectagsplugin PRIVATE TRANSLATION_DOMAIN="kate-ctags-plugin") +target_link_libraries(katectagsplugin PRIVATE KF5::TextEditor) -include(ECMQtDeclareLoggingCategory) +ki18n_wrap_ui(UI_SOURCES kate_ctags.ui CTagsGlobalConfig.ui) +target_sources(katectagsplugin PRIVATE ${UI_SOURCES}) -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category( + DEBUG_SOURCES + HEADER kate_ctags_debug.h + IDENTIFIER KTECTAGS + CATEGORY_NAME "katectagsplugin" +) +target_sources(katectagsplugin PRIVATE ${DEBUG_SOURCES}) -set(ctagsplugin_SRC +target_sources( + katectagsplugin + PRIVATE readtags.c tags.cpp ctagskinds.cpp kate_ctags_view.cpp kate_ctags_plugin.cpp + plugin.qrc ) -ecm_qt_declare_logging_category(ctagsplugin_SRC - HEADER kate_ctags_debug.h - IDENTIFIER KTECTAGS - CATEGORY_NAME "katectagsplugin" -) - -ki18n_wrap_ui(ctagsplugin_SRC kate_ctags.ui CTagsGlobalConfig.ui) - -# resource for ui file and stuff -qt5_add_resources(ctagsplugin_SRC plugin.qrc) - -add_library(katectagsplugin MODULE ${ctagsplugin_SRC}) - -kcoreaddons_desktop_to_json (katectagsplugin katectagsplugin.desktop) - -target_link_libraries(katectagsplugin KF5::TextEditor KF5::I18n KF5::IconThemes) - -install(TARGETS katectagsplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) +kcoreaddons_desktop_to_json(katectagsplugin katectagsplugin.desktop) +install(TARGETS katectagsplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/kate-ctags/kate_ctags_view.cpp b/addons/kate-ctags/kate_ctags_view.cpp index cbdbd9d47..f817fd9f6 100644 --- a/addons/kate-ctags/kate_ctags_view.cpp +++ b/addons/kate-ctags/kate_ctags_view.cpp @@ -1,643 +1,643 @@ /* Description : Kate CTags plugin * * Copyright (C) 2008-2011 by Kare Sars * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see . */ #include "kate_ctags_view.h" #include "kate_ctags_plugin.h" #include "kate_ctags_debug.h" #include #include #include #include #include #include #include #include #include #include #include /******************************************************************/ KateCTagsView::KateCTagsView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mainWin) : QObject(mainWin) , m_proc(nullptr) { KXMLGUIClient::setComponentName (QStringLiteral("katectags"), i18n ("Kate CTag")); setXMLFile( QStringLiteral("ui.rc") ); m_toolView = mainWin->createToolView(plugin, QStringLiteral("kate_plugin_katectagsplugin"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("application-x-ms-dos-executable")), i18n("CTags")); m_mWin = mainWin; QAction *back = actionCollection()->addAction(QStringLiteral("ctags_return_step")); back->setText(i18n("Jump back one step")); connect(back, &QAction::triggered, this, &KateCTagsView::stepBack); QAction *decl = actionCollection()->addAction(QStringLiteral("ctags_lookup_current_as_declaration")); decl->setText(i18n("Go to Declaration")); connect(decl, &QAction::triggered, this, &KateCTagsView::gotoDeclaration); QAction *defin = actionCollection()->addAction(QStringLiteral("ctags_lookup_current_as_definition")); defin->setText(i18n("Go to Definition")); connect(defin, &QAction::triggered, this, &KateCTagsView::gotoDefinition); QAction *lookup = actionCollection()->addAction(QStringLiteral("ctags_lookup_current")); lookup->setText(i18n("Lookup Current Text")); connect(lookup, &QAction::triggered, this, &KateCTagsView::lookupTag); QAction *updateDB = actionCollection()->addAction(QStringLiteral("ctags_update_global_db")); updateDB->setText(i18n("Configure ...")); connect(updateDB, &QAction::triggered, this, [this, plugin] (bool) { if (m_mWin) { KateCTagsPlugin *p = static_cast(plugin); QDialog *confWin = new QDialog(m_mWin->window()); confWin->setAttribute(Qt::WA_DeleteOnClose); auto confPage = p->configPage(0, confWin); auto controls = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel, Qt::Horizontal, confWin); connect(confWin, &QDialog::accepted, confPage, &KTextEditor::ConfigPage::apply); connect(controls, &QDialogButtonBox::accepted, confWin, &QDialog::accept); connect(controls, &QDialogButtonBox::rejected, confWin, &QDialog::reject); auto layout = new QVBoxLayout(confWin); layout->addWidget(confPage); layout->addWidget(controls); confWin->setLayout(layout); confWin->setWindowTitle(i18nc("@title:window", "Configure CTags Plugin")); confWin->setWindowIcon(confPage->icon()); confWin->show(); confWin->exec(); } }); // popup menu m_menu = new KActionMenu(i18n("CTags"), this); actionCollection()->addAction(QStringLiteral("popup_ctags"), m_menu); m_gotoDec=m_menu->menu()->addAction(i18n("Go to Declaration: %1",QString()), this, &KateCTagsView::gotoDeclaration); m_gotoDef=m_menu->menu()->addAction(i18n("Go to Definition: %1",QString()), this, &KateCTagsView::gotoDefinition); m_lookup=m_menu->menu()->addAction(i18n("Lookup: %1",QString()), this, &KateCTagsView::lookupTag); connect(m_menu->menu(), &QMenu::aboutToShow, this, &KateCTagsView::aboutToShow); QWidget *ctagsWidget = new QWidget(m_toolView.data()); m_ctagsUi.setupUi(ctagsWidget); m_ctagsUi.cmdEdit->setText(DEFAULT_CTAGS_CMD); m_ctagsUi.addButton->setToolTip(i18n("Add a directory to index.")); m_ctagsUi.addButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); m_ctagsUi.delButton->setToolTip(i18n("Remove a directory.")); m_ctagsUi.delButton->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); m_ctagsUi.updateButton->setToolTip(i18n("(Re-)generate the session specific CTags database.")); m_ctagsUi.updateButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); m_ctagsUi.updateButton2->setToolTip(i18n("(Re-)generate the session specific CTags database.")); m_ctagsUi.updateButton2->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); m_ctagsUi.resetCMD->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); m_ctagsUi.tagsFile->setToolTip(i18n("Select new or existing database file.")); m_ctagsUi.tagsFile->setMode(KFile::File); connect(m_ctagsUi.resetCMD, &QToolButton::clicked, this, &KateCTagsView::resetCMD); connect(m_ctagsUi.addButton, &QPushButton::clicked, this, &KateCTagsView::addTagTarget); connect(m_ctagsUi.delButton, &QPushButton::clicked, this, &KateCTagsView::delTagTarget); connect(m_ctagsUi.updateButton, &QPushButton::clicked, this, &KateCTagsView::updateSessionDB); connect(m_ctagsUi.updateButton2, &QPushButton::clicked, this, &KateCTagsView::updateSessionDB); connect(&m_proc, static_cast(&QProcess::finished), this, &KateCTagsView::updateDone); connect(m_ctagsUi.inputEdit, &QLineEdit::textChanged, this, &KateCTagsView::startEditTmr); m_editTimer.setSingleShot(true); connect(&m_editTimer, &QTimer::timeout, this, &KateCTagsView::editLookUp); connect(m_ctagsUi.tagTreeWidget, &QTreeWidget::itemActivated, this, &KateCTagsView::tagHitClicked); connect(m_mWin, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KateCTagsView::handleEsc); m_toolView->layout()->addWidget(ctagsWidget); m_toolView->installEventFilter(this); m_mWin->guiFactory()->addClient(this); m_commonDB = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1String("/katectags/common_db"); } /******************************************************************/ KateCTagsView::~KateCTagsView() { if (m_mWin && m_mWin->guiFactory()) { m_mWin->guiFactory()->removeClient( this ); } if (m_toolView) { delete m_toolView; } } /******************************************************************/ void KateCTagsView::aboutToShow() { QString currWord = currentWord(); if (currWord.isEmpty()) { return; } if (Tags::hasTag(m_commonDB, currWord) || Tags::hasTag(m_ctagsUi.tagsFile->text(), currWord)) { QString squeezed = KStringHandler::csqueeze(currWord, 30); m_gotoDec->setText(i18n("Go to Declaration: %1",squeezed)); m_gotoDef->setText(i18n("Go to Definition: %1",squeezed)); m_lookup->setText(i18n("Lookup: %1",squeezed)); } } /******************************************************************/ void KateCTagsView::readSessionConfig (const KConfigGroup& cg) { m_ctagsUi.cmdEdit->setText(cg.readEntry("TagsGenCMD", DEFAULT_CTAGS_CMD)); int numEntries = cg.readEntry("SessionNumTargets", 0); QString nr; QString target; for (int i=0; isetText(sessionDB); } /******************************************************************/ void KateCTagsView::writeSessionConfig (KConfigGroup& cg) { cg.writeEntry("TagsGenCMD", m_ctagsUi.cmdEdit->text()); cg.writeEntry("SessionNumTargets", m_ctagsUi.targetList->count()); QString nr; for (int i=0; icount(); i++) { nr = QStringLiteral("%1").arg(i,3); cg.writeEntry(QStringLiteral("SessionTarget_%1").arg(nr), m_ctagsUi.targetList->item(i)->text()); } cg.writeEntry("SessionDatabase", m_ctagsUi.tagsFile->text()); cg.sync(); } /******************************************************************/ void KateCTagsView::stepBack() { if (m_jumpStack.isEmpty()) { return; } TagJump back; back = m_jumpStack.pop(); m_mWin->openUrl(back.url); m_mWin->activeView()->setCursorPosition(back.cursor); m_mWin->activeView()->setFocus(); } /******************************************************************/ void KateCTagsView::lookupTag( ) { QString currWord = currentWord(); if (currWord.isEmpty()) { return; } setNewLookupText(currWord); Tags::TagList list = Tags::getExactMatches(m_ctagsUi.tagsFile->text(), currWord); if (list.size() == 0) list = Tags::getExactMatches(m_commonDB, currWord); displayHits(list); // activate the hits tab m_ctagsUi.tabWidget->setCurrentIndex(0); m_mWin->showToolView(m_toolView); } /******************************************************************/ void KateCTagsView::editLookUp() { Tags::TagList list = Tags::getPartialMatches(m_ctagsUi.tagsFile->text(), m_ctagsUi.inputEdit->text()); if (list.size() == 0) list = Tags::getPartialMatches(m_commonDB, m_ctagsUi.inputEdit->text()); displayHits(list); } /******************************************************************/ void KateCTagsView::gotoDefinition( ) { QString currWord = currentWord(); if (currWord.isEmpty()) { return; } QStringList types; types << QStringLiteral("S") << QStringLiteral("d") << QStringLiteral("f") << QStringLiteral("t") << QStringLiteral("v"); gotoTagForTypes(currWord, types); } /******************************************************************/ void KateCTagsView::gotoDeclaration( ) { QString currWord = currentWord(); if (currWord.isEmpty()) { return; } QStringList types; types << QStringLiteral("L") << QStringLiteral("c") << QStringLiteral("e") << QStringLiteral("g") << QStringLiteral("m") << QStringLiteral("n") << QStringLiteral("p") << QStringLiteral("s") << QStringLiteral("u") << QStringLiteral("x"); gotoTagForTypes(currWord, types); } /******************************************************************/ void KateCTagsView::gotoTagForTypes(const QString &word, const QStringList &types) { Tags::TagList list = Tags::getMatches(m_ctagsUi.tagsFile->text(), word, false, types); if (list.size() == 0) list = Tags::getMatches(m_commonDB, word, false, types); //qCDebug(KTECTAGS) << "found" << list.count() << word << types; setNewLookupText(word); if ( list.count() < 1) { m_ctagsUi.tagTreeWidget->clear(); new QTreeWidgetItem(m_ctagsUi.tagTreeWidget, QStringList(i18n("No hits found"))); m_ctagsUi.tabWidget->setCurrentIndex(0); m_mWin->showToolView(m_toolView); return; } displayHits(list); if (list.count() == 1) { Tags::TagEntry tag = list.first(); jumpToTag(tag.file, tag.pattern, word); } else { Tags::TagEntry tag = list.first(); jumpToTag(tag.file, tag.pattern, word); m_ctagsUi.tabWidget->setCurrentIndex(0); m_mWin->showToolView(m_toolView); } } /******************************************************************/ void KateCTagsView::setNewLookupText(const QString &newString) { m_ctagsUi.inputEdit->blockSignals( true ); m_ctagsUi.inputEdit->setText(newString); m_ctagsUi.inputEdit->blockSignals( false ); } /******************************************************************/ void KateCTagsView::displayHits(const Tags::TagList &list) { m_ctagsUi.tagTreeWidget->clear(); if (list.isEmpty()) { new QTreeWidgetItem(m_ctagsUi.tagTreeWidget, QStringList(i18n("No hits found"))); return; } m_ctagsUi.tagTreeWidget->setSortingEnabled(false); - for (int i=0; isetText(0, list[i].tag); - item->setText(1, list[i].type); - item->setText(2, list[i].file); - item->setData(0, Qt::UserRole, list[i].pattern); + item->setText(0, tag.tag); + item->setText(1, tag.type); + item->setText(2, tag.file); + item->setData(0, Qt::UserRole, tag.pattern); - QString pattern = list[i].pattern; + QString pattern = tag.pattern; pattern.replace( QStringLiteral("\\/"), QStringLiteral("/")); pattern = pattern.mid(2, pattern.length() - 4); pattern = pattern.trimmed(); item->setData(0, Qt::ToolTipRole, pattern); item->setData(1, Qt::ToolTipRole, pattern); item->setData(2, Qt::ToolTipRole, pattern); } m_ctagsUi.tagTreeWidget->setSortingEnabled(true); } /******************************************************************/ void KateCTagsView::tagHitClicked(QTreeWidgetItem *item) { // get stuff const QString file = item->data(2, Qt::DisplayRole).toString(); const QString pattern = item->data(0, Qt::UserRole).toString(); const QString word = item->data(0, Qt::DisplayRole).toString(); jumpToTag(file, pattern, word); } /******************************************************************/ QString KateCTagsView::currentWord( ) { KTextEditor::View *kv = m_mWin->activeView(); if (!kv) { qCDebug(KTECTAGS) << "no KTextEditor::View" << endl; return QString(); } if (kv->selection() && kv->selectionRange().onSingleLine()) { return kv->selectionText(); } if (!kv->cursorPosition().isValid()) { qCDebug(KTECTAGS) << "cursor not valid!" << endl; return QString(); } int line = kv->cursorPosition().line(); int col = kv->cursorPosition().column(); bool includeColon = m_ctagsUi.cmdEdit->text().contains(QLatin1String("--extra=+q")); QString linestr = kv->document()->line(line); int startPos = qMax(qMin(col, linestr.length()-1), 0); int endPos = startPos; while (startPos >= 0 && (linestr[startPos].isLetterOrNumber() || (linestr[startPos] == QLatin1Char(':') && includeColon) || linestr[startPos] == QLatin1Char('_') || linestr[startPos] == QLatin1Char('~'))) { startPos--; } while (endPos < (int)linestr.length() && (linestr[endPos].isLetterOrNumber() || (linestr[endPos] == QLatin1Char(':') && includeColon) || linestr[endPos] == QLatin1Char('_'))) { endPos++; } if (startPos == endPos) { qCDebug(KTECTAGS) << "no word found!" << endl; return QString(); } linestr = linestr.mid(startPos+1, endPos-startPos-1); while (linestr.endsWith(QLatin1Char(':'))) { linestr.remove(linestr.size()-1, 1); } while (linestr.startsWith(QLatin1Char(':'))) { linestr.remove(0, 1); } //qCDebug(KTECTAGS) << linestr; return linestr; } /******************************************************************/ void KateCTagsView::jumpToTag(const QString &file, const QString &pattern, const QString &word) { if (pattern.isEmpty()) return; // generate a regexp from the pattern // ctags interestingly escapes "/", but apparently nothing else. lets revert that QString unescaped = pattern; unescaped.replace( QStringLiteral("\\/"), QStringLiteral("/") ); // most of the time, the ctags pattern has the form /^foo$/ // but this isn't true for some macro definitions // where the form is only /^foo/ // I have no idea if this is a ctags bug or not, but we have to deal with it QString reduced; QString escaped; QString re_string; if (unescaped.endsWith(QStringLiteral("$/"))) { reduced = unescaped.mid(2, unescaped.length() - 4); escaped = QRegularExpression::escape(reduced); re_string = QStringLiteral("^%1$").arg(escaped); } else { reduced = unescaped.mid( 2, unescaped.length() -3 ); escaped = QRegularExpression::escape(reduced); re_string = QStringLiteral("^%1").arg(escaped); } QRegularExpression re(re_string); // save current location TagJump from; from.url = m_mWin->activeView()->document()->url(); from.cursor = m_mWin->activeView()->cursorPosition(); m_jumpStack.push(from); // open/activate the new file QFileInfo fInfo(file); //qCDebug(KTECTAGS) << pattern << file << fInfo.absoluteFilePath(); m_mWin->openUrl(QUrl::fromLocalFile(fInfo.absoluteFilePath())); // any view active? if (!m_mWin->activeView()) { return; } // look for the line QString linestr; int line; for (line =0; line < m_mWin->activeView()->document()->lines(); line++) { linestr = m_mWin->activeView()->document()->line(line); if (linestr.indexOf(re) > -1) break; } // activate the line if (line != m_mWin->activeView()->document()->lines()) { // line found now look for the column int column = linestr.indexOf(word) + (word.length()/2); m_mWin->activeView()->setCursorPosition(KTextEditor::Cursor(line, column)); } m_mWin->activeView()->setFocus(); } /******************************************************************/ void KateCTagsView::startEditTmr() { if (m_ctagsUi.inputEdit->text().size() > 3) { m_editTimer.start(500); } } /******************************************************************/ void KateCTagsView::updateSessionDB() { if (m_proc.state() != QProcess::NotRunning) { return; } QString targets; QString target; for (int i=0; icount(); i++) { target = m_ctagsUi.targetList->item(i)->text(); if (target.endsWith(QLatin1Char('/')) || target.endsWith(QLatin1Char('\\'))) { target = target.left(target.size() - 1); } targets += target + QLatin1Char(' '); } QString pluginFolder = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1String("/katectags"); QDir().mkpath(pluginFolder); if (m_ctagsUi.tagsFile->text().isEmpty()) { // FIXME we need a way to get the session name pluginFolder += QLatin1String("/session_db_"); pluginFolder += QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd_hhmmss")); m_ctagsUi.tagsFile->setText(pluginFolder); } if (targets.isEmpty()) { KMessageBox::error(nullptr, i18n("No folders or files to index")); QFile::remove(m_ctagsUi.tagsFile->text()); return; } QString command = QStringLiteral("%1 -f %2 %3").arg(m_ctagsUi.cmdEdit->text(), m_ctagsUi.tagsFile->text(), targets); m_proc.start(command); if(!m_proc.waitForStarted(500)) { KMessageBox::error(nullptr, i18n("Failed to run \"%1\". exitStatus = %2", command, m_proc.exitStatus())); return; } QApplication::setOverrideCursor(QCursor(Qt::BusyCursor)); m_ctagsUi.updateButton->setDisabled(true); m_ctagsUi.updateButton2->setDisabled(true); } /******************************************************************/ void KateCTagsView::updateDone(int exitCode, QProcess::ExitStatus status) { if (status == QProcess::CrashExit) { KMessageBox::error(m_toolView, i18n("The CTags executable crashed.")); } else if (exitCode != 0) { KMessageBox::error(m_toolView, i18n("The CTags program exited with code %1: %2" , exitCode , QString::fromLocal8Bit(m_proc.readAllStandardError()))); } m_ctagsUi.updateButton->setDisabled(false); m_ctagsUi.updateButton2->setDisabled(false); QApplication::restoreOverrideCursor(); } /******************************************************************/ void KateCTagsView::addTagTarget() { QFileDialog dialog; dialog.setDirectory(QFileInfo(m_mWin->activeView()->document()->url().path()).path()); dialog.setFileMode(QFileDialog::Directory); // i18n("CTags Database Location")); if (dialog.exec() != QDialog::Accepted) { return; } QStringList urls = dialog.selectedFiles(); for (int i=0; icurrentItem (); } /******************************************************************/ bool KateCTagsView::listContains(const QString &target) { for (int i=0; icount(); i++) { if (m_ctagsUi.targetList->item(i)->text() == target) { return true; } } return false; } /******************************************************************/ bool KateCTagsView::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *ke = static_cast(event); if ((obj == m_toolView) && (ke->key() == Qt::Key_Escape)) { m_mWin->hideToolView(m_toolView); event->accept(); return true; } } return QObject::eventFilter(obj, event); } /******************************************************************/ void KateCTagsView::resetCMD() { m_ctagsUi.cmdEdit->setText(DEFAULT_CTAGS_CMD); } /******************************************************************/ void KateCTagsView::handleEsc(QEvent *e) { if (!m_mWin) return; QKeyEvent *k = static_cast(e); if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) { if (m_toolView->isVisible()) { m_mWin->hideToolView(m_toolView); } } } diff --git a/addons/katebuild-plugin/CMakeLists.txt b/addons/katebuild-plugin/CMakeLists.txt index 4e4d83443..04c777eb4 100644 --- a/addons/katebuild-plugin/CMakeLists.txt +++ b/addons/katebuild-plugin/CMakeLists.txt @@ -1,22 +1,21 @@ -add_definitions(-DTRANSLATION_DOMAIN=\"katebuild-plugin\") +add_library(katebuildplugin MODULE "") +target_compile_definitions(katebuildplugin PRIVATE TRANSLATION_DOMAIN="katebuild-plugin") +target_link_libraries(katebuildplugin PRIVATE KF5::TextEditor) -set(katebuild_SRCS +ki18n_wrap_ui(UI_SOURCES build.ui SelectTargetUi.ui) +target_sources(katebuildplugin PRIVATE ${UI_SOURCES}) + +target_sources( + katebuildplugin + PRIVATE plugin_katebuild.cpp targets.cpp TargetHtmlDelegate.cpp TargetModel.cpp UrlInserter.cpp SelectTargetView.cpp + plugin.qrc ) -ki18n_wrap_ui(katebuild_SRCS build.ui SelectTargetUi.ui) - -# resource for ui file and stuff -qt5_add_resources(katebuild_SRCS plugin.qrc) - -add_library(katebuildplugin MODULE ${katebuild_SRCS}) kcoreaddons_desktop_to_json (katebuildplugin katebuildplugin.desktop) -target_link_libraries(katebuildplugin KF5::TextEditor KF5::I18n KF5::IconThemes) - install(TARGETS katebuildplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) - diff --git a/addons/katebuild-plugin/plugin_katebuild.cpp b/addons/katebuild-plugin/plugin_katebuild.cpp index 66f8f1520..02e55737f 100644 --- a/addons/katebuild-plugin/plugin_katebuild.cpp +++ b/addons/katebuild-plugin/plugin_katebuild.cpp @@ -1,1065 +1,1277 @@ /* plugin_katebuild.c Kate Plugin ** ** Copyright (C) 2013 by Alexander Neundorf ** Copyright (C) 2006-2015 by Kåre Särs ** Copyright (C) 2011 by Ian Wakeling ** ** This code is mostly a modification of the GPL'ed Make plugin ** by Adriaan de Groot. */ /* ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program in a file called COPYING; if not, write to ** the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, ** MA 02110-1301, USA. */ #include "plugin_katebuild.h" #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include +#include +#include + #include "SelectTargetView.h" K_PLUGIN_FACTORY_WITH_JSON (KateBuildPluginFactory, "katebuildplugin.json", registerPlugin();) static const QString DefConfigCmd = QStringLiteral("cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr/local ../"); static const QString DefConfClean; static const QString DefTargetName = QStringLiteral("all"); static const QString DefBuildCmd = QStringLiteral("make"); static const QString DefCleanCmd = QStringLiteral("make clean"); +static QIcon messageIcon(KateBuildView::ErrorCategory severity) +{ +#define RETURN_CACHED_ICON(name) \ +{ \ +static QIcon icon(QIcon::fromTheme(QStringLiteral(name))); \ +return icon; \ +} + switch (severity) + { + case KateBuildView::CategoryError: + RETURN_CACHED_ICON("dialog-error") + case KateBuildView::CategoryWarning: + RETURN_CACHED_ICON("dialog-warning") + default: + break; + } + return QIcon(); +} + +struct ItemData +{ + // ensure destruction, but not inadvertently so by a variant value copy + QSharedPointer cursor; +}; + +Q_DECLARE_METATYPE(ItemData) + /******************************************************************/ KateBuildPlugin::KateBuildPlugin(QObject *parent, const VariantList&): KTextEditor::Plugin(parent) { // KF5 FIXME KGlobal::locale()->insertCatalog("katebuild-plugin"); } /******************************************************************/ QObject *KateBuildPlugin::createView (KTextEditor::MainWindow *mainWindow) { return new KateBuildView(this, mainWindow); } /******************************************************************/ KateBuildView::KateBuildView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mw) : QObject (mw) , m_win(mw) , m_buildWidget(nullptr) , m_outputWidgetWidth(0) , m_proc(this) , m_stdOut() , m_stdErr() , m_buildCancelled(false) , m_displayModeBeforeBuild(1) // NOTE this will not allow spaces in file names. // e.g. from gcc: "main.cpp:14: error: cannot convert ‘std::string’ to ‘int’ in return" - , m_filenameDetector(QStringLiteral("(([a-np-zA-Z]:[\\\\/])?[a-zA-Z0-9_\\.\\-/\\\\]+\\.[a-zA-Z0-9]+):([0-9]+)(.*)")) + , m_filenameDetector(QStringLiteral("(([a-np-zA-Z]:[\\\\/])?[a-zA-Z0-9_\\.\\+\\-/\\\\]+\\.[a-zA-Z0-9]+):([0-9]+)(.*)")) // e.g. from icpc: "main.cpp(14): error: no suitable conversion function from "std::string" to "int" exists" - , m_filenameDetectorIcpc(QStringLiteral("(([a-np-zA-Z]:[\\\\/])?[a-zA-Z0-9_\\.\\-/\\\\]+\\.[a-zA-Z0-9]+)\\(([0-9]+)\\)(:.*)")) + , m_filenameDetectorIcpc(QStringLiteral("(([a-np-zA-Z]:[\\\\/])?[a-zA-Z0-9_\\.\\+\\-/\\\\]+\\.[a-zA-Z0-9]+)\\(([0-9]+)\\)(:.*)")) , m_filenameDetectorGccWorked(false) , m_newDirDetector(QStringLiteral("make\\[.+\\]: .+ `.*'")) { KXMLGUIClient::setComponentName (QStringLiteral("katebuild"), i18n ("Kate Build Plugin")); setXMLFile(QStringLiteral("ui.rc")); m_toolView = mw->createToolView(plugin, QStringLiteral("kate_plugin_katebuildplugin"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("application-x-ms-dos-executable")), i18n("Build Output")); QAction *a = actionCollection()->addAction(QStringLiteral("select_target")); a->setText(i18n("Select Target...")); a->setIcon(QIcon::fromTheme(QStringLiteral("select"))); connect(a, &QAction::triggered, this, &KateBuildView::slotSelectTarget); a = actionCollection()->addAction(QStringLiteral("build_default_target")); a->setText(i18n("Build Default Target")); connect(a, &QAction::triggered, this, &KateBuildView::slotBuildDefaultTarget); a = actionCollection()->addAction(QStringLiteral("build_previous_target")); a->setText(i18n("Build Previous Target")); connect(a, &QAction::triggered, this, &KateBuildView::slotBuildPreviousTarget); a = actionCollection()->addAction(QStringLiteral("stop")); a->setText(i18n("Stop")); a->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); connect(a, &QAction::triggered, this, &KateBuildView::slotStop); a = actionCollection()->addAction(QStringLiteral("goto_next")); a->setText(i18n("Next Error")); a->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); actionCollection()->setDefaultShortcut(a, Qt::SHIFT+Qt::ALT+Qt::Key_Right); connect(a, &QAction::triggered, this, &KateBuildView::slotNext); a = actionCollection()->addAction(QStringLiteral("goto_prev")); a->setText(i18n("Previous Error")); a->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); actionCollection()->setDefaultShortcut(a, Qt::SHIFT+Qt::ALT+Qt::Key_Left); connect(a, &QAction::triggered, this, &KateBuildView::slotPrev); + m_showMarks = a = actionCollection()->addAction(QStringLiteral("show_marks")); + a->setText(i18n("Show Marks")); + a->setCheckable(true); + connect(a, &QAction::triggered, this, &KateBuildView::slotDisplayOption); m_buildWidget = new QWidget(m_toolView); m_buildUi.setupUi(m_buildWidget); m_targetsUi = new TargetsUi(this, m_buildUi.u_tabWidget); m_buildUi.u_tabWidget->insertTab(0, m_targetsUi, i18nc("Tab label", "Target Settings")); m_buildUi.u_tabWidget->setCurrentWidget(m_targetsUi); m_buildWidget->installEventFilter(this); m_buildUi.buildAgainButton->setVisible(true); m_buildUi.cancelBuildButton->setVisible(true); m_buildUi.buildStatusLabel->setVisible(true); m_buildUi.buildAgainButton2->setVisible(false); m_buildUi.cancelBuildButton2->setVisible(false); m_buildUi.buildStatusLabel2->setVisible(false); m_buildUi.extraLineLayout->setAlignment(Qt::AlignRight); m_buildUi.cancelBuildButton->setEnabled(false); m_buildUi.cancelBuildButton2->setEnabled(false); connect(m_buildUi.errTreeWidget, &QTreeWidget::itemClicked, this, &KateBuildView::slotErrorSelected); m_buildUi.plainTextEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_buildUi.plainTextEdit->setReadOnly(true); slotDisplayMode(FullOutput); connect(m_buildUi.displayModeSlider, &QSlider::valueChanged, this, &KateBuildView::slotDisplayMode); connect(m_buildUi.buildAgainButton, &QPushButton::clicked, this, &KateBuildView::slotBuildPreviousTarget); connect(m_buildUi.cancelBuildButton, &QPushButton::clicked, this, &KateBuildView::slotStop); connect(m_buildUi.buildAgainButton2, &QPushButton::clicked, this, &KateBuildView::slotBuildPreviousTarget); connect(m_buildUi.cancelBuildButton2, &QPushButton::clicked, this, &KateBuildView::slotStop); connect(m_targetsUi->newTarget, &QToolButton::clicked, this, &KateBuildView::targetSetNew); connect(m_targetsUi->copyTarget, &QToolButton::clicked, this, &KateBuildView::targetOrSetCopy); connect(m_targetsUi->deleteTarget, &QToolButton::clicked, this, &KateBuildView::targetDelete); connect(m_targetsUi->addButton, &QToolButton::clicked, this, &KateBuildView::slotAddTargetClicked); connect(m_targetsUi->buildButton, &QToolButton::clicked, this, &KateBuildView::slotBuildActiveTarget); connect(m_targetsUi, &TargetsUi::enterPressed, this, &KateBuildView::slotBuildActiveTarget); m_proc.setOutputChannelMode(KProcess::SeparateChannels); connect(&m_proc, static_cast(&QProcess::finished), this, &KateBuildView::slotProcExited); connect(&m_proc, &KProcess::readyReadStandardError, this, &KateBuildView::slotReadReadyStdErr); connect(&m_proc, &KProcess::readyReadStandardOutput, this, &KateBuildView::slotReadReadyStdOut); connect(m_win, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KateBuildView::handleEsc); + connect(m_win, &KTextEditor::MainWindow::viewChanged, this, &KateBuildView::slotViewChanged); m_toolView->installEventFilter(this); m_win->guiFactory()->addClient(this); // watch for project plugin view creation/deletion connect(m_win, &KTextEditor::MainWindow::pluginViewCreated, this, &KateBuildView::slotPluginViewCreated); connect(m_win, &KTextEditor::MainWindow::pluginViewDeleted, this, &KateBuildView::slotPluginViewDeleted); // Connect signals from project plugin to our slots m_projectPluginView = m_win->pluginView(QStringLiteral("kateprojectplugin")); slotPluginViewCreated(QStringLiteral("kateprojectplugin"), m_projectPluginView); } /******************************************************************/ KateBuildView::~KateBuildView() { m_win->guiFactory()->removeClient( this ); delete m_toolView; } /******************************************************************/ void KateBuildView::readSessionConfig(const KConfigGroup& cg) { int numTargets = cg.readEntry(QStringLiteral("NumTargets"), 0); m_targetsUi->targetsModel.clear(); int tmpIndex; int tmpCmd; if (numTargets == 0 ) { // either the config is empty or uses the older format m_targetsUi->targetsModel.addTargetSet(i18n("Target Set"), QString()); m_targetsUi->targetsModel.addCommand(0, i18n("build"), cg.readEntry(QStringLiteral("Make Command"), DefBuildCmd)); m_targetsUi->targetsModel.addCommand(0, i18n("clean"), cg.readEntry(QStringLiteral("Clean Command"), DefCleanCmd)); m_targetsUi->targetsModel.addCommand(0, i18n("config"), DefConfigCmd); QString quickCmd = cg.readEntry(QStringLiteral("Quick Compile Command")); if (!quickCmd.isEmpty()) { m_targetsUi->targetsModel.addCommand(0, i18n("quick"), quickCmd); } tmpIndex = 0; tmpCmd = 0; } else { for (int i=0; itargetsModel.addTargetSet(targetSetName, buildDir); if (targetNames.isEmpty()) { QString quickCmd = cg.readEntry(QStringLiteral("%1 QuickCmd").arg(i)); m_targetsUi->targetsModel.addCommand(i, i18n("build"), cg.readEntry(QStringLiteral("%1 BuildCmd"), DefBuildCmd)); m_targetsUi->targetsModel.addCommand(i, i18n("clean"), cg.readEntry(QStringLiteral("%1 CleanCmd"), DefCleanCmd)); if (!quickCmd.isEmpty()) { m_targetsUi->targetsModel.addCommand(i, i18n("quick"), quickCmd); } m_targetsUi->targetsModel.setDefaultCmd(i, i18n("build")); } else { for (int tn=0; tntargetsModel.addCommand(i, targetName, cg.readEntry(QStringLiteral("%1 BuildCmd %2").arg(i).arg(targetName), DefBuildCmd)); } QString defCmd = cg.readEntry(QStringLiteral("%1 Target Default").arg(i), QString()); m_targetsUi->targetsModel.setDefaultCmd(i, defCmd); } } tmpIndex = cg.readEntry(QStringLiteral("Active Target Index"), 0); tmpCmd = cg.readEntry(QStringLiteral("Active Target Command"), 0); } m_targetsUi->targetsView->expandAll(); m_targetsUi->targetsView->resizeColumnToContents(0); m_targetsUi->targetsView->collapseAll(); QModelIndex root = m_targetsUi->targetsModel.index(tmpIndex); QModelIndex cmdIndex = m_targetsUi->targetsModel.index(tmpCmd, 0, root); m_targetsUi->targetsView->setCurrentIndex(cmdIndex); + auto showMarks = cg.readEntry(QStringLiteral("Show Marks"), false); + m_showMarks->setChecked(showMarks); + // Add project targets, if any slotAddProjectTarget(); } /******************************************************************/ void KateBuildView::writeSessionConfig(KConfigGroup& cg) { // Don't save project targets, is not our area of accountability m_targetsUi->targetsModel.deleteTargetSet(i18n("Project Plugin Targets")); QList targets = m_targetsUi->targetsModel.targetSets(); cg.writeEntry("NumTargets", targets.size()); for (int i=0; itargetsView->currentIndex(); if (ind.internalId() == TargetModel::InvalidIndex) { set = ind.row(); } else { set = ind.internalId(); setRow = ind.row(); } if (setRow < 0) setRow = 0; cg.writeEntry(QStringLiteral("Active Target Index"), set); cg.writeEntry(QStringLiteral("Active Target Command"), setRow); + cg.writeEntry(QStringLiteral("Show Marks"), m_showMarks->isChecked()); // Restore project targets, if any slotAddProjectTarget(); } /******************************************************************/ void KateBuildView::slotNext() { const int itemCount = m_buildUi.errTreeWidget->topLevelItemCount(); if (itemCount == 0) { return; } QTreeWidgetItem *item = m_buildUi.errTreeWidget->currentItem(); if (item && item->isHidden()) item = nullptr; int i = (item == nullptr) ? -1 : m_buildUi.errTreeWidget->indexOfTopLevelItem(item); while (++i < itemCount) { item = m_buildUi.errTreeWidget->topLevelItem(i); // Search item which fit view settings and has desired data if (!item->text(1).isEmpty() && !item->isHidden() && item->data(1, Qt::UserRole).toInt()) { m_buildUi.errTreeWidget->setCurrentItem(item); m_buildUi.errTreeWidget->scrollToItem(item); slotErrorSelected(item); return; } } } /******************************************************************/ void KateBuildView::slotPrev() { const int itemCount = m_buildUi.errTreeWidget->topLevelItemCount(); if (itemCount == 0) { return; } QTreeWidgetItem *item = m_buildUi.errTreeWidget->currentItem(); if (item && item->isHidden()) item = nullptr; int i = (item == nullptr) ? itemCount : m_buildUi.errTreeWidget->indexOfTopLevelItem(item); while (--i >= 0) { item = m_buildUi.errTreeWidget->topLevelItem(i); // Search item which fit view settings and has desired data if (!item->text(1).isEmpty() && !item->isHidden() && item->data(1, Qt::UserRole).toInt()) { m_buildUi.errTreeWidget->setCurrentItem(item); m_buildUi.errTreeWidget->scrollToItem(item); slotErrorSelected(item); return; } } } /******************************************************************/ void KateBuildView::slotErrorSelected(QTreeWidgetItem *item) { // any view active? if (!m_win->activeView()) { return; } // Avoid garish highlighting of the selected line m_win->activeView()->setFocus(); // Search the item where the data we need is stored while (!item->data(1, Qt::UserRole).toInt()) { item = m_buildUi.errTreeWidget->itemAbove(item); if (!item) { return; } } // get stuff const QString filename = item->data(0, Qt::UserRole).toString(); if (filename.isEmpty()) { return; } - const int line = item->data(1, Qt::UserRole).toInt(); - const int column = item->data(2, Qt::UserRole).toInt(); + int line = item->data(1, Qt::UserRole).toInt(); + int column = item->data(2, Qt::UserRole).toInt(); + // check with moving cursor + auto data = item->data(0, DataRole).value(); + if (data.cursor) { + line = data.cursor->line(); + column = data.cursor->column(); + } // open file (if needed, otherwise, this will activate only the right view...) m_win->openUrl(QUrl::fromLocalFile(filename)); // do it ;) m_win->activeView()->setCursorPosition(KTextEditor::Cursor(line-1, column-1)); } /******************************************************************/ void KateBuildView::addError(const QString &filename, const QString &line, const QString &column, const QString &message) { ErrorCategory errorCategory = CategoryInfo; QTreeWidgetItem* item = new QTreeWidgetItem(m_buildUi.errTreeWidget); item->setBackground(1, Qt::gray); // The strings are twice in case kate is translated but not make. if (message.contains(QStringLiteral("error")) || message.contains(i18nc("The same word as 'make' uses to mark an error.","error")) || message.contains(QStringLiteral("undefined reference")) || message.contains(i18nc("The same word as 'ld' uses to mark an ...","undefined reference")) ) { errorCategory = CategoryError; item->setForeground(1, Qt::red); m_numErrors++; item->setHidden(false); } if (message.contains(QStringLiteral("warning")) || message.contains(i18nc("The same word as 'make' uses to mark a warning.","warning")) ) { errorCategory = CategoryWarning; item->setForeground(1, Qt::yellow); m_numWarnings++; item->setHidden(m_buildUi.displayModeSlider->value() > 2); } item->setTextAlignment(1, Qt::AlignRight); // visible text //remove path from visible file name QFileInfo file(filename); item->setText(0, file.fileName()); item->setText(1, line); item->setText(2, message.trimmed()); // used to read from when activating an item item->setData(0, Qt::UserRole, filename); item->setData(1, Qt::UserRole, line); item->setData(2, Qt::UserRole, column); if (errorCategory == CategoryInfo) { item->setHidden(m_buildUi.displayModeSlider->value() > 1); } item->setData(0, ErrorRole, errorCategory); // add tooltips in all columns // The enclosing ... enables word-wrap for long error messages item->setData(0, Qt::ToolTipRole, filename); item->setData(1, Qt::ToolTipRole, QStringLiteral("%1").arg(message)); item->setData(2, Qt::ToolTipRole, QStringLiteral("%1").arg(message)); } +void KateBuildView::clearMarks() +{ + for (auto& doc: m_markedDocs) { + if (!doc) { + continue; + } + + KTextEditor::MarkInterface* iface = qobject_cast(doc); + if (iface) { + const QHash marks = iface->marks(); + QHashIterator i(marks); + while (i.hasNext()) { + i.next(); + auto markType = KTextEditor::MarkInterface::Error | KTextEditor::MarkInterface::Warning; + if (i.value()->type & markType) { + iface->removeMark(i.value()->line, markType); + } + } + } + } + + m_markedDocs.clear(); +} + +void KateBuildView::addMarks(KTextEditor::Document *doc, bool mark) +{ + KTextEditor::MarkInterface* iface = qobject_cast(doc); + KTextEditor::MovingInterface* miface = qobject_cast(doc); + if (!iface || m_markedDocs.contains(doc)) + return; + + QTreeWidgetItemIterator it(m_buildUi.errTreeWidget, QTreeWidgetItemIterator::All); + while (*it) { + QTreeWidgetItem *item = *it; + ++it; + + auto filename = item->data(0, Qt::UserRole).toString(); + auto url = QUrl::fromLocalFile(filename); + if (url != doc->url()) + continue; + + auto line = item->data(1, Qt::UserRole).toInt(); + if (mark) { + ErrorCategory category = (ErrorCategory)item->data(0, ErrorRole).toInt(); + KTextEditor::MarkInterface::MarkTypes markType {}; + + switch (category) { + case CategoryError: { + markType = KTextEditor::MarkInterface::Error; + iface->setMarkDescription(markType, i18n("Error")); + break; + } + case CategoryWarning: { + markType = KTextEditor::MarkInterface::Warning; + iface->setMarkDescription(markType, i18n("Warning")); + break; + } + default: + break; + } + + if (markType) { + const int ps = 32; + iface->setMarkPixmap(markType, messageIcon(category).pixmap(ps, ps)); + iface->addMark(line - 1, markType); + } + m_markedDocs.insert(doc, doc); + } + + // add moving cursor so link between message and location + // is not broken by document changes + if (miface) { + auto data = item->data(0, DataRole).value(); + if (!data.cursor) { + auto column = item->data(2, Qt::UserRole).toInt(); + data.cursor.reset(miface->newMovingCursor({line, column})); + QVariant var; + var.setValue(data); + item->setData(0, DataRole, var); + } + } + } + + // ensure cleanup + if (miface) { + auto conn = connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), + this, SLOT(slotInvalidateMoving(KTextEditor::Document*)), Qt::UniqueConnection); + conn = connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*)), + this, SLOT(slotInvalidateMoving(KTextEditor::Document*)), Qt::UniqueConnection); + } + + connect(doc, SIGNAL(markClicked(KTextEditor::Document*, KTextEditor::Mark, bool&)), + this, SLOT(slotMarkClicked(KTextEditor::Document*,KTextEditor::Mark, bool&)), Qt::UniqueConnection); +} + +void KateBuildView::slotInvalidateMoving(KTextEditor::Document* doc) +{ + QTreeWidgetItemIterator it(m_buildUi.errTreeWidget, QTreeWidgetItemIterator::All); + while (*it) { + QTreeWidgetItem *item = *it; + ++it; + + auto data = item->data(0, DataRole).value(); + if (data.cursor && data.cursor->document() == doc) { + item->setData(0, DataRole, 0); + } + } +} + +void KateBuildView::slotMarkClicked(KTextEditor::Document *doc, KTextEditor::Mark mark, bool &handled) +{ + auto tree = m_buildUi.errTreeWidget; + QTreeWidgetItemIterator it(tree, QTreeWidgetItemIterator::All); + while (*it) { + QTreeWidgetItem *item = *it; + ++it; + + auto filename = item->data(0, Qt::UserRole).toString(); + auto line = item->data(1, Qt::UserRole).toInt(); + // prefer moving cursor's opinion if so available + auto data = item->data(0, DataRole).value(); + if (data.cursor) { + line = data.cursor->line(); + } + if (line - 1 == mark.line && QUrl::fromLocalFile(filename) == doc->url()) { + tree->blockSignals(true); + tree->setCurrentItem(item); + tree->scrollToItem(item, QAbstractItemView::PositionAtCenter); + tree->blockSignals(false); + handled = true; + break; + } + } +} + +void KateBuildView::slotViewChanged() +{ + KTextEditor::View *activeView = m_win->activeView(); + auto doc = activeView ? activeView->document() : nullptr; + + if (doc) { + addMarks(doc, m_showMarks->isChecked()); + } +} + +void KateBuildView::slotDisplayOption() +{ + if (m_showMarks) { + if (!m_showMarks->isChecked()) { + clearMarks(); + } else { + slotViewChanged(); + } + } +} + /******************************************************************/ QUrl KateBuildView::docUrl() { KTextEditor::View *kv = m_win->activeView(); if (!kv) { qDebug() << "no KTextEditor::View" << endl; return QUrl(); } if (kv->document()->isModified()) kv->document()->save(); return kv->document()->url(); } /******************************************************************/ bool KateBuildView::checkLocal(const QUrl &dir) { if (dir.path().isEmpty()) { KMessageBox::sorry(nullptr, i18n("There is no file or directory specified for building.")); return false; } else if (!dir.isLocalFile()) { KMessageBox::sorry(nullptr, i18n("The file \"%1\" is not a local file. " "Non-local files cannot be compiled.", dir.path())); return false; } return true; } /******************************************************************/ void KateBuildView::clearBuildResults() { + clearMarks(); m_buildUi.plainTextEdit->clear(); m_buildUi.errTreeWidget->clear(); m_stdOut.clear(); m_stdErr.clear(); m_numErrors = 0; m_numWarnings = 0; m_make_dir_stack.clear(); } /******************************************************************/ bool KateBuildView::startProcess(const QString &dir, const QString &command) { if (m_proc.state() != QProcess::NotRunning) { return false; } // clear previous runs clearBuildResults(); // activate the output tab m_buildUi.u_tabWidget->setCurrentIndex(1); m_displayModeBeforeBuild = m_buildUi.displayModeSlider->value(); m_buildUi.displayModeSlider->setValue(0); m_win->showToolView(m_toolView); // set working directory m_make_dir = dir; m_make_dir_stack.push(m_make_dir); if (!QFile::exists(m_make_dir)) { KMessageBox::error(nullptr, i18n("Cannot run command: %1\nWork path does not exist: %2", command, m_make_dir)); return false; } m_proc.setWorkingDirectory(m_make_dir); m_proc.setShellCommand(command); m_proc.start(); if(!m_proc.waitForStarted(500)) { KMessageBox::error(nullptr, i18n("Failed to run \"%1\". exitStatus = %2", command, m_proc.exitStatus())); return false; } m_buildUi.cancelBuildButton->setEnabled(true); m_buildUi.cancelBuildButton2->setEnabled(true); m_buildUi.buildAgainButton->setEnabled(false); m_buildUi.buildAgainButton2->setEnabled(false); QApplication::setOverrideCursor(QCursor(Qt::BusyCursor)); return true; } /******************************************************************/ bool KateBuildView::slotStop() { if (m_proc.state() != QProcess::NotRunning) { m_buildCancelled = true; QString msg = i18n("Building %1 cancelled", m_currentlyBuildingTarget); m_buildUi.buildStatusLabel->setText(msg); m_buildUi.buildStatusLabel2->setText(msg); m_proc.terminate(); return true; } return false; } /******************************************************************/ void KateBuildView::slotBuildActiveTarget() { if (!m_targetsUi->targetsView->currentIndex().isValid()) { slotSelectTarget(); } else { buildCurrentTarget(); } } /******************************************************************/ void KateBuildView::slotBuildPreviousTarget() { if (!m_previousIndex.isValid()) { slotSelectTarget(); } else { m_targetsUi->targetsView->setCurrentIndex(m_previousIndex); buildCurrentTarget(); } } /******************************************************************/ void KateBuildView::slotBuildDefaultTarget() { QModelIndex defaultTarget = m_targetsUi->targetsModel.defaultTarget(m_targetsUi->targetsView->currentIndex()); m_targetsUi->targetsView->setCurrentIndex(defaultTarget); buildCurrentTarget(); } /******************************************************************/ void KateBuildView::slotSelectTarget() { SelectTargetView *dialog = new SelectTargetView(&(m_targetsUi->targetsModel)); dialog->setCurrentIndex(m_targetsUi->targetsView->currentIndex()); int result = dialog->exec(); if (result == QDialog::Accepted) { m_targetsUi->targetsView->setCurrentIndex(dialog->currentIndex()); buildCurrentTarget(); } delete dialog; dialog = nullptr; } /******************************************************************/ bool KateBuildView::buildCurrentTarget() { if (m_proc.state() != QProcess::NotRunning) { displayBuildResult(i18n("Already building..."), KTextEditor::Message::Warning); return false; } QFileInfo docFInfo = docUrl().toLocalFile(); // docUrl() saves the current document QModelIndex ind = m_targetsUi->targetsView->currentIndex(); m_previousIndex = ind; if (!ind.isValid()) { KMessageBox::sorry(nullptr, i18n("No target available for building.")); return false; } QString buildCmd = m_targetsUi->targetsModel.command(ind); QString cmdName = m_targetsUi->targetsModel.cmdName(ind); QString workDir = m_targetsUi->targetsModel.workDir(ind); QString targetSet = m_targetsUi->targetsModel.targetName(ind); QString dir = workDir; if (workDir.isEmpty()) { dir = docFInfo.absolutePath(); if (dir.isEmpty()) { KMessageBox::sorry(nullptr, i18n("There is no local file or directory specified for building.")); return false; } } + // a single target can serve to build lots of projects with similar directory layout + if (m_projectPluginView) { + QFileInfo baseDir = m_projectPluginView->property("projectBaseDir").toString(); + dir.replace(QStringLiteral("%B"), baseDir.absoluteFilePath()); + dir.replace(QStringLiteral("%b"), baseDir.baseName()); + } + // Check if the command contains the file name or directory if (buildCmd.contains(QStringLiteral("%f")) || buildCmd.contains(QStringLiteral("%d")) || buildCmd.contains(QStringLiteral("%n"))) { if (docFInfo.absoluteFilePath().isEmpty()) { return false; } buildCmd.replace(QStringLiteral("%n"), docFInfo.baseName()); buildCmd.replace(QStringLiteral("%f"), docFInfo.absoluteFilePath()); buildCmd.replace(QStringLiteral("%d"), docFInfo.absolutePath()); } m_filenameDetectorGccWorked = false; m_currentlyBuildingTarget = QStringLiteral("%1: %2").arg(targetSet, cmdName); m_buildCancelled = false; QString msg = i18n("Building target %1 ...", m_currentlyBuildingTarget); m_buildUi.buildStatusLabel->setText(msg); m_buildUi.buildStatusLabel2->setText(msg); return startProcess(dir, buildCmd); } /******************************************************************/ void KateBuildView::displayBuildResult(const QString &msg, KTextEditor::Message::MessageType level) { KTextEditor::View *kv = m_win->activeView(); if (!kv) return; delete m_infoMessage; m_infoMessage = new KTextEditor::Message(xi18nc("@info", "Make Results:%1", msg), level); m_infoMessage->setWordWrap(true); m_infoMessage->setPosition(KTextEditor::Message::BottomInView); m_infoMessage->setAutoHide(5000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(kv); kv->document()->postMessage(m_infoMessage); } /******************************************************************/ void KateBuildView::slotProcExited(int exitCode, QProcess::ExitStatus) { QApplication::restoreOverrideCursor(); m_buildUi.cancelBuildButton->setEnabled(false); m_buildUi.cancelBuildButton2->setEnabled(false); m_buildUi.buildAgainButton->setEnabled(true); m_buildUi.buildAgainButton2->setEnabled(true); QString buildStatus = i18n("Building %1 completed.", m_currentlyBuildingTarget); // did we get any errors? if (m_numErrors || m_numWarnings || (exitCode != 0)) { m_buildUi.u_tabWidget->setCurrentIndex(1); if (m_buildUi.displayModeSlider->value() == 0) { m_buildUi.displayModeSlider->setValue(m_displayModeBeforeBuild > 0 ? m_displayModeBeforeBuild: 1); } m_buildUi.errTreeWidget->resizeColumnToContents(0); m_buildUi.errTreeWidget->resizeColumnToContents(1); m_buildUi.errTreeWidget->resizeColumnToContents(2); m_buildUi.errTreeWidget->horizontalScrollBar()->setValue(0); //m_buildUi.errTreeWidget->setSortingEnabled(true); m_win->showToolView(m_toolView); } if (m_numErrors || m_numWarnings) { QStringList msgs; if (m_numErrors) { msgs << i18np("Found one error.", "Found %1 errors.", m_numErrors); buildStatus = i18n("Building %1 had errors.", m_currentlyBuildingTarget); } else if (m_numWarnings) { msgs << i18np("Found one warning.", "Found %1 warnings.", m_numWarnings); buildStatus = i18n("Building %1 had warnings.", m_currentlyBuildingTarget); } displayBuildResult(msgs.join(QLatin1Char('\n')), m_numErrors ? KTextEditor::Message::Error : KTextEditor::Message::Warning); } else if (exitCode != 0) { displayBuildResult(i18n("Build failed."), KTextEditor::Message::Warning); } else { displayBuildResult(i18n("Build completed without problems."), KTextEditor::Message::Positive); } if (!m_buildCancelled) { m_buildUi.buildStatusLabel->setText(buildStatus); m_buildUi.buildStatusLabel2->setText(buildStatus); m_buildCancelled = false; + // add marks + slotViewChanged(); } } /******************************************************************/ void KateBuildView::slotReadReadyStdOut() { // read data from procs stdout and add // the text to the end of the output // FIXME This works for utf8 but not for all charsets QString l = QString::fromUtf8(m_proc.readAllStandardOutput()); l.remove(QLatin1Char('\r')); m_stdOut += l; // handle one line at a time do { const int end = m_stdOut.indexOf(QLatin1Char('\n')); if (end < 0) break; const QString line = m_stdOut.mid(0, end); m_buildUi.plainTextEdit->appendPlainText(line); //qDebug() << line; if (m_newDirDetector.match(line).hasMatch()) { //qDebug() << "Enter/Exit dir found"; int open = line.indexOf(QLatin1Char('`')); int close = line.indexOf(QLatin1Char('\'')); QString newDir = line.mid(open+1, close-open-1); //qDebug () << "New dir = " << newDir; if ((m_make_dir_stack.size() > 1) && (m_make_dir_stack.top() == newDir)) { m_make_dir_stack.pop(); newDir = m_make_dir_stack.top(); } else { m_make_dir_stack.push(newDir); } m_make_dir = newDir; } m_stdOut.remove(0, end + 1); } while (1); } /******************************************************************/ void KateBuildView::slotReadReadyStdErr() { // FIXME This works for utf8 but not for all charsets QString l = QString::fromUtf8(m_proc.readAllStandardError()); l.remove(QLatin1Char('\r')); m_stdErr += l; do { const int end = m_stdErr.indexOf(QLatin1Char('\n')); if (end < 0) break; const QString line = m_stdErr.mid(0, end); m_buildUi.plainTextEdit->appendPlainText(line); processLine(line); m_stdErr.remove(0, end + 1); } while (1); } /******************************************************************/ void KateBuildView::processLine(const QString &line) { //qDebug() << line ; //look for a filename QRegularExpressionMatch match = m_filenameDetector.match(line); if (match.hasMatch()) { m_filenameDetectorGccWorked = true; } else { if (!m_filenameDetectorGccWorked) { // let's see whether the icpc regexp works: // so for icpc users error detection will be a bit slower, // since always both regexps are checked. // But this should be the minority, for gcc and clang users // both regexes will only be checked until the first regex // matched the first time. match = m_filenameDetectorIcpc.match(line); } } if (!match.hasMatch()) { addError(QString(), QStringLiteral("0"), QString(), line); //kDebug() << "A filename was not found in the line "; return; } QString filename = match.captured(1); const QString line_n = match.captured(3); const QString msg = match.captured(4); //qDebug() << "File Name:"<targetsView->currentIndex(); if (current.parent().isValid()) { current = current.parent(); } QModelIndex index = m_targetsUi->targetsModel.addCommand(current.row(), DefTargetName, DefBuildCmd); m_targetsUi->targetsView->setCurrentIndex(index); } /******************************************************************/ void KateBuildView::targetSetNew() { int row = m_targetsUi->targetsModel.addTargetSet(i18n("Target Set"), QString()); QModelIndex buildIndex = m_targetsUi->targetsModel.addCommand(row, i18n("Build"), DefBuildCmd); m_targetsUi->targetsModel.addCommand(row, i18n("Clean"), DefCleanCmd); m_targetsUi->targetsModel.addCommand(row, i18n("Config"), DefConfigCmd); m_targetsUi->targetsModel.addCommand(row, i18n("ConfigClean"), DefConfClean); m_targetsUi->targetsView->setCurrentIndex(buildIndex); } /******************************************************************/ void KateBuildView::targetOrSetCopy() { QModelIndex newIndex = m_targetsUi->targetsModel.copyTargetOrSet(m_targetsUi->targetsView->currentIndex()); if (m_targetsUi->targetsModel.hasChildren(newIndex)) { m_targetsUi->targetsView->setCurrentIndex(newIndex.child(0,0)); return; } m_targetsUi->targetsView->setCurrentIndex(newIndex); } /******************************************************************/ void KateBuildView::targetDelete() { QModelIndex current = m_targetsUi->targetsView->currentIndex(); m_targetsUi->targetsModel.deleteItem(current); if (m_targetsUi->targetsModel.rowCount() == 0) { targetSetNew(); } } /******************************************************************/ void KateBuildView::slotDisplayMode(int mode) { QTreeWidget *tree=m_buildUi.errTreeWidget; tree->setVisible(mode != 0); m_buildUi.plainTextEdit->setVisible(mode == 0); QString modeText; switch(mode) { case OnlyErrors: modeText = i18n("Only Errors"); break; case ErrorsAndWarnings: modeText = i18n("Errors and Warnings"); break; case ParsedOutput: modeText = i18n("Parsed Output"); break; case FullOutput: modeText = i18n("Full Output"); break; } m_buildUi.displayModeLabel->setText(modeText); if (mode < 1) { return; } const int itemCount = tree->topLevelItemCount(); for (int i=0;itopLevelItem(i); const ErrorCategory errorCategory = static_cast(item->data(0, ErrorRole).toInt()); switch (errorCategory) { case CategoryInfo: item->setHidden(mode > 1); break; case CategoryWarning: item->setHidden(mode > 2); break; case CategoryError: item->setHidden(false); break; } } } /******************************************************************/ void KateBuildView::slotPluginViewCreated(const QString &name, QObject *pluginView) { // add view if (pluginView && name == QLatin1String("kateprojectplugin")) { m_projectPluginView = pluginView; slotAddProjectTarget(); connect(pluginView, SIGNAL(projectMapChanged()), this, SLOT(slotProjectMapChanged()), Qt::UniqueConnection); } } /******************************************************************/ void KateBuildView::slotPluginViewDeleted(const QString &name, QObject *) { // remove view if (name == QLatin1String("kateprojectplugin")) { m_projectPluginView = nullptr; m_targetsUi->targetsModel.deleteTargetSet(i18n("Project Plugin Targets")); } } /******************************************************************/ void KateBuildView::slotProjectMapChanged() { // only do stuff with valid project if (!m_projectPluginView) { return; } m_targetsUi->targetsModel.deleteTargetSet(i18n("Project Plugin Targets")); slotAddProjectTarget(); } /******************************************************************/ void KateBuildView::slotAddProjectTarget() { // only do stuff with valid project if (!m_projectPluginView) { return; } // query new project map QVariantMap projectMap = m_projectPluginView->property("projectMap").toMap(); // do we have a valid map for build settings? QVariantMap buildMap = projectMap.value(QStringLiteral("build")).toMap(); if (buildMap.isEmpty()) { return; } // Delete any old project plugin targets m_targetsUi->targetsModel.deleteTargetSet(i18n("Project Plugin Targets")); int set = m_targetsUi->targetsModel.addTargetSet(i18n("Project Plugin Targets"), buildMap.value(QStringLiteral("directory")).toString()); QVariantList targetsets = buildMap.value(QStringLiteral("targets")).toList(); foreach (const QVariant &targetVariant, targetsets) { QVariantMap targetMap = targetVariant.toMap(); QString tgtName = targetMap[QStringLiteral("name")].toString(); QString buildCmd = targetMap[QStringLiteral("build_cmd")].toString(); if (tgtName.isEmpty() || buildCmd.isEmpty()) { continue; } m_targetsUi->targetsModel.addCommand(set, tgtName, buildCmd); } QModelIndex ind = m_targetsUi->targetsModel.index(set); if (!ind.child(0,0).data().isValid()) { QString buildCmd = buildMap.value(QStringLiteral("build")).toString(); QString cleanCmd = buildMap.value(QStringLiteral("clean")).toString(); QString quickCmd = buildMap.value(QStringLiteral("quick")).toString(); if (!buildCmd.isEmpty()) { // we have loaded an "old" project file (<= 4.12) m_targetsUi->targetsModel.addCommand(set, i18n("build"), buildCmd); } if (!cleanCmd.isEmpty()) { m_targetsUi->targetsModel.addCommand(set, i18n("clean"), cleanCmd); } if (!quickCmd.isEmpty()) { m_targetsUi->targetsModel.addCommand(set, i18n("quick"), quickCmd); } } } /******************************************************************/ bool KateBuildView::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *ke = static_cast(event); if ((obj == m_toolView) && (ke->key() == Qt::Key_Escape)) { m_win->hideToolView(m_toolView); event->accept(); return true; } } if ((event->type() == QEvent::Resize) && (obj == m_buildWidget)) { if (m_buildUi.u_tabWidget->currentIndex() == 1) { if ((m_outputWidgetWidth == 0) && m_buildUi.buildAgainButton->isVisible()) { QSize msh = m_buildWidget->minimumSizeHint(); m_outputWidgetWidth = msh.width(); } } bool useVertLayout = (m_buildWidget->width() < m_outputWidgetWidth); m_buildUi.buildAgainButton->setVisible(!useVertLayout); m_buildUi.cancelBuildButton->setVisible(!useVertLayout); m_buildUi.buildStatusLabel->setVisible(!useVertLayout); m_buildUi.buildAgainButton2->setVisible(useVertLayout); m_buildUi.cancelBuildButton2->setVisible(useVertLayout); m_buildUi.buildStatusLabel2->setVisible(useVertLayout); } return QObject::eventFilter(obj, event); } /******************************************************************/ void KateBuildView::handleEsc(QEvent *e) { if (!m_win) return; QKeyEvent *k = static_cast(e); if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) { if (m_toolView->isVisible()) { m_win->hideToolView(m_toolView); } } } #include "plugin_katebuild.moc" // kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/katebuild-plugin/plugin_katebuild.h b/addons/katebuild-plugin/plugin_katebuild.h index 43d37eb96..435060060 100644 --- a/addons/katebuild-plugin/plugin_katebuild.h +++ b/addons/katebuild-plugin/plugin_katebuild.h @@ -1,182 +1,194 @@ #ifndef PLUGIN_KATEBUILD_H #define PLUGIN_KATEBUILD_H /* plugin_katebuild.h Kate Plugin ** ** Copyright (C) 2008-2015 by Kåre Särs ** ** This code is almost a total rewrite of the GPL'ed Make plugin ** by Adriaan de Groot. */ /* ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program in a file called COPYING; if not, write to ** the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, ** MA 02110-1301, USA. */ #include #include #include #include +#include #include #include #include #include #include #include #include +#include #include #include #include "ui_build.h" #include "targets.h" /******************************************************************/ class KateBuildView : public QObject, public KXMLGUIClient, public KTextEditor::SessionConfigInterface { Q_OBJECT Q_INTERFACES(KTextEditor::SessionConfigInterface) Q_PROPERTY(QUrl docUrl READ docUrl) public: enum ResultDetails { FullOutput, ParsedOutput, ErrorsAndWarnings, OnlyErrors }; enum TreeWidgetRoles { - ErrorRole = Qt::UserRole+1 + ErrorRole = Qt::UserRole+1, + DataRole }; enum ErrorCategory { CategoryInfo, CategoryWarning, CategoryError }; KateBuildView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mw); ~KateBuildView() override; // reimplemented: read and write session config void readSessionConfig(const KConfigGroup& config) override; void writeSessionConfig(KConfigGroup& config) override; bool buildCurrentTarget(); QUrl docUrl(); private Q_SLOTS: // Building void slotSelectTarget(); void slotBuildActiveTarget(); void slotBuildPreviousTarget(); void slotBuildDefaultTarget(); bool slotStop(); // Parse output void slotProcExited(int exitCode, QProcess::ExitStatus exitStatus); void slotReadReadyStdErr(); void slotReadReadyStdOut(); // Selecting warnings/errors void slotNext(); void slotPrev(); void slotErrorSelected(QTreeWidgetItem *item); // Settings void targetSetNew(); void targetOrSetCopy(); void targetDelete(); void slotAddTargetClicked(); void slotDisplayMode(int mode); void handleEsc(QEvent *e); + void slotViewChanged(); + void slotDisplayOption(); + void slotMarkClicked(KTextEditor::Document *doc, KTextEditor::Mark mark, bool &handled); + void slotInvalidateMoving(KTextEditor::Document* doc); /** * keep track if the project plugin is alive and if the project map did change */ void slotPluginViewCreated(const QString &name, QObject *pluginView); void slotPluginViewDeleted(const QString &name, QObject *pluginView); void slotProjectMapChanged(); void slotAddProjectTarget(); protected: bool eventFilter(QObject *obj, QEvent *ev) override; private: void processLine(const QString &); void addError(const QString &filename, const QString &line, const QString &column, const QString &message); bool startProcess(const QString &dir, const QString &command); bool checkLocal(const QUrl &dir); void clearBuildResults(); void displayBuildResult(const QString &message, KTextEditor::Message::MessageType level); + void clearMarks(); + void addMarks(KTextEditor::Document *doc, bool mark); + KTextEditor::MainWindow *m_win; QWidget *m_toolView; Ui::build m_buildUi; QWidget *m_buildWidget; int m_outputWidgetWidth; TargetsUi *m_targetsUi; KProcess m_proc; QString m_stdOut; QString m_stdErr; QString m_currentlyBuildingTarget; bool m_buildCancelled; int m_displayModeBeforeBuild; QString m_make_dir; QStack m_make_dir_stack; QRegularExpression m_filenameDetector; QRegularExpression m_filenameDetectorIcpc; bool m_filenameDetectorGccWorked; QRegularExpression m_newDirDetector; unsigned int m_numErrors; unsigned int m_numWarnings; QString m_prevItemContent; QModelIndex m_previousIndex; QPointer m_infoMessage; + QPointer m_showMarks; + QHash> m_markedDocs; /** * current project plugin view, if any */ QObject *m_projectPluginView = nullptr; }; typedef QList VariantList; /******************************************************************/ class KateBuildPlugin : public KTextEditor::Plugin { Q_OBJECT public: explicit KateBuildPlugin(QObject* parent = nullptr, const VariantList& = VariantList()); ~KateBuildPlugin() override {} QObject *createView(KTextEditor::MainWindow *mainWindow) override; }; #endif diff --git a/addons/katebuild-plugin/ui.rc b/addons/katebuild-plugin/ui.rc index 242732cda..df1ffc7b7 100644 --- a/addons/katebuild-plugin/ui.rc +++ b/addons/katebuild-plugin/ui.rc @@ -1,17 +1,19 @@ - + &Build + + diff --git a/addons/katesql/CMakeLists.txt b/addons/katesql/CMakeLists.txt index c4fee13cd..a548130df 100644 --- a/addons/katesql/CMakeLists.txt +++ b/addons/katesql/CMakeLists.txt @@ -1,37 +1,45 @@ -project(katesqlplugin) +find_package(KF5Wallet QUIET) +set_package_properties(KF5Wallet PROPERTIES PURPOSE "Required to build the katesql addon") -add_definitions(-DTRANSLATION_DOMAIN=\"katesql\") -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) +find_package(Qt5Sql QUIET) +set_package_properties(Qt5Sql PROPERTIES PURPOSE "Required to build the katesql addon") -set(katesql_SRCS - katesqlplugin.cpp - katesqlview.cpp - connectionmodel.cpp - sqlmanager.cpp - cachedsqlquerymodel.cpp - dataoutputmodel.cpp - dataoutputview.cpp - dataoutputwidget.cpp - textoutputwidget.cpp - schemawidget.cpp - schemabrowserwidget.cpp - connectionwizard.cpp - katesqlconfigpage.cpp - exportwizard.cpp - outputstylewidget.cpp - outputwidget.cpp -) - -# resource for ui file and stuff -qt5_add_resources(katesql_SRCS plugin.qrc) - -add_library (katesqlplugin MODULE ${katesql_SRCS}) +if(NOT KF5Wallet_FOUND OR NOT Qt5Sql_FOUND) + return() +endif() -kcoreaddons_desktop_to_json (katesqlplugin katesql.desktop) +add_library(katesqlplugin MODULE "") +target_compile_definitions(katesqlplugin PRIVATE TRANSLATION_DOMAIN="katesql") -target_link_libraries(katesqlplugin +target_link_libraries( + katesqlplugin + PRIVATE KF5::TextEditor - KF5::Parts KF5::I18n KF5::Wallet - Qt5::Sql KF5::ItemViews KF5::IconThemes) + KF5::Wallet + Qt5::Sql +) + +target_sources( + katesqlplugin + PRIVATE + katesqlplugin.cpp + katesqlview.cpp + connectionmodel.cpp + sqlmanager.cpp + cachedsqlquerymodel.cpp + dataoutputmodel.cpp + dataoutputview.cpp + dataoutputwidget.cpp + textoutputwidget.cpp + schemawidget.cpp + schemabrowserwidget.cpp + connectionwizard.cpp + katesqlconfigpage.cpp + exportwizard.cpp + outputstylewidget.cpp + outputwidget.cpp + plugin.qrc +) -install(TARGETS katesqlplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) +kcoreaddons_desktop_to_json(katesqlplugin katesql.desktop) +install(TARGETS katesqlplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/konsole/CMakeLists.txt b/addons/konsole/CMakeLists.txt index 0c30cdc55..7dc6a8ecb 100644 --- a/addons/konsole/CMakeLists.txt +++ b/addons/konsole/CMakeLists.txt @@ -1,23 +1,17 @@ -project(katekonsoleplugin) -add_definitions(-DTRANSLATION_DOMAIN=\"katekonsoleplugin\") +if(WIN32) + return() +endif() -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) +add_library(katekonsoleplugin MODULE "") +target_compile_definitions(katekonsoleplugin PRIVATE TRANSLATION_DOMAIN="katekonsoleplugin") +target_link_libraries(katekonsoleplugin PRIVATE KF5::TextEditor) -set(katekonsoleplugin_PART_SRCS kateconsole.cpp ) - -# resource for ui file and stuff -qt5_add_resources(katekonsoleplugin_PART_SRCS plugin.qrc) - -add_library (katekonsoleplugin MODULE ${katekonsoleplugin_PART_SRCS}) - -kcoreaddons_desktop_to_json (katekonsoleplugin katekonsoleplugin.desktop) - -target_link_libraries(katekonsoleplugin - KF5::TextEditor - KF5::Parts - KF5::IconThemes - KF5::I18n - KF5::Service +target_sources( + katekonsoleplugin + PRIVATE + kateconsole.cpp + plugin.qrc ) -install(TARGETS katekonsoleplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) +kcoreaddons_desktop_to_json(katekonsoleplugin katekonsoleplugin.desktop) +install(TARGETS katekonsoleplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/konsole/kateconsole.cpp b/addons/konsole/kateconsole.cpp index 0f5ce730b..52cd0a48b 100644 --- a/addons/konsole/kateconsole.cpp +++ b/addons/konsole/kateconsole.cpp @@ -1,506 +1,506 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2002 Joseph Wenninger Copyright (C) 2002 Anders Lund Copyright (C) 2007 Anders Lund Copyright (C) 2017 Ederag This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateconsole.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON (KateKonsolePluginFactory, "katekonsoleplugin.json", registerPlugin();) KateKonsolePlugin::KateKonsolePlugin( QObject* parent, const QList& ): KTextEditor::Plugin ( parent ) { m_previousEditorEnv=qgetenv("EDITOR"); if (!KAuthorized::authorize(QStringLiteral("shell_access"))) { KMessageBox::sorry(nullptr, i18n ("You do not have enough karma to access a shell or terminal emulation")); } } KateKonsolePlugin::~KateKonsolePlugin() { qputenv("EDITOR", m_previousEditorEnv.data()); } QObject *KateKonsolePlugin::createView (KTextEditor::MainWindow *mainWindow) { KateKonsolePluginView *view = new KateKonsolePluginView (this, mainWindow); return view; } KTextEditor::ConfigPage *KateKonsolePlugin::configPage (int number, QWidget *parent) { if (number != 0) return nullptr; return new KateKonsoleConfigPage(parent, this); } void KateKonsolePlugin::readConfig() { foreach ( KateKonsolePluginView *view, mViews ) view->readConfig(); } KateKonsolePluginView::KateKonsolePluginView (KateKonsolePlugin* plugin, KTextEditor::MainWindow *mainWindow) : QObject(mainWindow),m_plugin(plugin) { // init console QWidget *toolview = mainWindow->createToolView (plugin, QStringLiteral("kate_private_plugin_katekonsoleplugin"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("utilities-terminal")), i18n("Terminal")); m_console = new KateConsole(m_plugin, mainWindow, toolview); // register this view m_plugin->mViews.append ( this ); } KateKonsolePluginView::~KateKonsolePluginView () { // unregister this view m_plugin->mViews.removeAll (this); // cleanup, kill toolview + console QWidget *toolview = m_console->parentWidget(); delete m_console; delete toolview; } void KateKonsolePluginView::readConfig() { m_console->readConfig(); } KateConsole::KateConsole (KateKonsolePlugin* plugin, KTextEditor::MainWindow *mw, QWidget *parent) : QWidget (parent) , m_part (nullptr) , m_mw (mw) , m_toolView (parent) , m_plugin(plugin) { KXMLGUIClient::setComponentName (QStringLiteral("katekonsole"), i18n ("Kate Terminal")); setXMLFile( QStringLiteral("ui.rc") ); // make sure we have a vertical layout new QVBoxLayout(this); layout()->setContentsMargins(0, 0, 0, 0); QAction* a = actionCollection()->addAction(QStringLiteral("katekonsole_tools_pipe_to_terminal")); a->setIcon(QIcon::fromTheme(QStringLiteral("utilities-terminal"))); a->setText(i18nc("@action", "&Pipe to Terminal")); connect(a, &QAction::triggered, this, &KateConsole::slotPipeToConsole); a = actionCollection()->addAction(QStringLiteral("katekonsole_tools_sync")); a->setText(i18nc("@action", "S&ynchronize Terminal with Current Document")); connect(a, &QAction::triggered, this, &KateConsole::slotManualSync); a = actionCollection()->addAction(QStringLiteral("katekonsole_tools_run")); a->setText(i18nc("@action", "Run Current Document")); connect(a, &QAction::triggered, this, &KateConsole::slotRun); a = actionCollection()->addAction(QStringLiteral("katekonsole_tools_toggle_focus")); a->setIcon(QIcon::fromTheme(QStringLiteral("utilities-terminal"))); a->setText(i18nc("@action", "&Focus Terminal")); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::Key_F4)); connect(a, &QAction::triggered, this, &KateConsole::slotToggleFocus); m_mw->guiFactory()->addClient (this); readConfig(); } KateConsole::~KateConsole () { m_mw->guiFactory()->removeClient (this); if (m_part) disconnect(m_part, &KParts::ReadOnlyPart::destroyed, this, &KateConsole::slotDestroyed); } void KateConsole::loadConsoleIfNeeded() { if (m_part) return; if (!window() || !parentWidget()) return; if (!window() || !isVisibleTo(window())) return; - /** * get konsole part factory */ KPluginFactory *factory = KPluginLoader(QStringLiteral("konsolepart")).factory(); if (!factory) return; m_part = factory->create(this, this); if (!m_part) return; layout()->addWidget(m_part->widget()); // start the terminal qobject_cast(m_part)->showShellInDir( QString() ); // KGlobal::locale()->insertCatalog("konsole"); // FIXME KF5: insert catalog setFocusProxy(m_part->widget()); m_part->widget()->show(); connect(m_part, &KParts::ReadOnlyPart::destroyed, this, &KateConsole::slotDestroyed); connect ( m_part, SIGNAL(overrideShortcut(QKeyEvent*,bool&)), this, SLOT(overrideShortcut(QKeyEvent*,bool&))); slotSync(); } void KateConsole::slotDestroyed () { m_part = nullptr; m_currentPath.clear (); + setFocusProxy(nullptr); // hide the dockwidget if (parentWidget()) { m_mw->hideToolView (m_toolView); } } void KateConsole::overrideShortcut (QKeyEvent *, bool &override) { /** * let konsole handle all shortcuts */ override = true; } void KateConsole::showEvent(QShowEvent *) { if (m_part) return; loadConsoleIfNeeded(); } void KateConsole::cd (const QString & path) { if (m_currentPath == path) return; if (!m_part) return; m_currentPath = path; QString command = QStringLiteral(" cd ") + KShell::quoteArg(m_currentPath) + QLatin1Char('\n'); // special handling for some interpreters TerminalInterface *t = qobject_cast(m_part); if (t) { // ghci doesn't allow \space dir names, does allow spaces in dir names // irb can take spaces or \space but doesn't allow " 'path' " if (t->foregroundProcessName() == QStringLiteral("irb") ) { command = QStringLiteral("Dir.chdir(\"") + path + QStringLiteral("\") \n") ; } else if(t->foregroundProcessName() == QStringLiteral("ghc")) { command = QStringLiteral(":cd ") + path + QStringLiteral("\n"); } } // Send prior Ctrl-E, Ctrl-U to ensure the line is empty sendInput(QStringLiteral("\x05\x15")); sendInput(command); } void KateConsole::sendInput( const QString& text ) { loadConsoleIfNeeded(); if (!m_part) return; TerminalInterface *t = qobject_cast(m_part); if (!t) return; t->sendInput (text); } void KateConsole::slotPipeToConsole () { if (KMessageBox::warningContinueCancel (m_mw->window() , i18n ("Do you really want to pipe the text to the console? This will execute any contained commands with your user rights.") , i18n ("Pipe to Terminal?") , KGuiItem(i18n("Pipe to Terminal")), KStandardGuiItem::cancel(), QStringLiteral("Pipe To Terminal Warning")) != KMessageBox::Continue) return; KTextEditor::View *v = m_mw->activeView(); if (!v) return; if (v->selection()) sendInput (v->selectionText()); else sendInput (v->document()->text()); } void KateConsole::slotSync(KTextEditor::View *) { if (m_mw->activeView()) { QUrl u = m_mw->activeView()->document()->url(); if (u.isValid() && u.isLocalFile()) { QFileInfo fi(u.toLocalFile()); cd(fi.absolutePath()); } else if (!u.isEmpty()) { sendInput( QStringLiteral("### ") + i18n("Sorry, cannot cd into '%1'", u.toLocalFile() ) + QLatin1Char('\n') ); } } } void KateConsole::slotManualSync() { m_currentPath.clear (); slotSync(); if ( ! m_part || ! m_part->widget()->isVisible() ) m_mw->showToolView( parentWidget() ); } void KateConsole::slotRun() { if ( m_mw->activeView() ) { KTextEditor::Document *document = m_mw->activeView()->document(); QUrl u = document->url(); if ( ! u.isLocalFile() ) { QPointer message = new KTextEditor::Message( i18n("Not a local file: '%1'", u.path()), KTextEditor::Message::Error ); // auto hide is enabled and set to a sane default value of several seconds. message->setAutoHide(2000); message->setAutoHideMode( KTextEditor::Message::Immediate ); document->postMessage( message ); return; } // ensure that file is saved if ( document->isModified() ) { document->save(); } // The string that should be output to terminal, upon acceptance QString output_str; // prefix first output_str += KConfigGroup( KSharedConfig::openConfig(), "Konsole" ).readEntry("RunPrefix", ""); // then filename if ( KConfigGroup(KSharedConfig::openConfig(), "Konsole" ).readEntry("RemoveExtension", true) ) { // append filename without extension (i.e. keep only the basename) output_str += QFileInfo( u.path() ).baseName() + QLatin1Char('\n'); } else { // append filename to the terminal output_str += QFileInfo( u.path() ).fileName() + QLatin1Char('\n'); } if ( KMessageBox::Continue != KMessageBox::warningContinueCancel( m_mw->window(), i18n("Do you really want to Run the document ?\n" "This will execute the following command,\n" "with your user rights, in the terminal:\n" "'%1'", output_str), i18n("Run in Terminal?"), KGuiItem( i18n("Run") ), KStandardGuiItem::cancel(), QStringLiteral("Konsole: Run in Terminal Warning") ) ) { return; } // echo to terminal sendInput( output_str ); } } void KateConsole::slotToggleFocus() { QAction *action = actionCollection()->action(QStringLiteral("katekonsole_tools_toggle_focus")); if ( ! m_part ) { m_mw->showToolView( parentWidget() ); action->setText( i18n("Defocus Terminal") ); return; // this shows and focuses the konsole } if ( ! m_part ) return; if (m_part->widget()->hasFocus()) { if (m_mw->activeView()) m_mw->activeView()->setFocus(); action->setText( i18n("Focus Terminal") ); } else { // show the view if it is hidden if (parentWidget()->isHidden()) m_mw->showToolView( parentWidget() ); else // should focus the widget too! m_part->widget()->setFocus( Qt::OtherFocusReason ); action->setText( i18n("Defocus Terminal") ); } } void KateConsole::readConfig() { disconnect(m_mw, &KTextEditor::MainWindow::viewChanged, this, &KateConsole::slotSync); if ( KConfigGroup(KSharedConfig::openConfig(), "Konsole").readEntry("AutoSyncronize", true) ) { connect(m_mw, &KTextEditor::MainWindow::viewChanged, this, &KateConsole::slotSync); } if ( KConfigGroup(KSharedConfig::openConfig(), "Konsole").readEntry("SetEditor", false) ) qputenv( "EDITOR", "kate -b"); else qputenv( "EDITOR", m_plugin->previousEditorEnv().data()); } KateKonsoleConfigPage::KateKonsoleConfigPage( QWidget* parent, KateKonsolePlugin *plugin ) : KTextEditor::ConfigPage( parent ) , mPlugin( plugin ) { QVBoxLayout *lo = new QVBoxLayout( this ); lo->setSpacing(QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); lo->setContentsMargins(0, 0, 0, 0); cbAutoSyncronize = new QCheckBox( i18n("&Automatically synchronize the terminal with the current document when possible"), this ); lo->addWidget( cbAutoSyncronize ); QVBoxLayout *vboxRun = new QVBoxLayout; QGroupBox *groupRun = new QGroupBox( i18n("Run in terminal"), this ); // Remove extension cbRemoveExtension = new QCheckBox( i18n("&Remove extension"), this ); vboxRun->addWidget( cbRemoveExtension ); // Prefix QFrame *framePrefix = new QFrame( this ); QHBoxLayout *hboxPrefix = new QHBoxLayout( framePrefix ); QLabel *label = new QLabel( i18n("Prefix:"), framePrefix ); hboxPrefix->addWidget( label ); lePrefix = new QLineEdit( framePrefix ); hboxPrefix->addWidget( lePrefix ); vboxRun->addWidget( framePrefix ); // show warning next time QFrame *frameWarn = new QFrame( this ); QHBoxLayout *hboxWarn = new QHBoxLayout( frameWarn ); QPushButton *buttonWarn = new QPushButton( i18n("&Show warning next time"), frameWarn); buttonWarn->setWhatsThis ( i18n ( "The next time '%1' is executed, " "make sure a warning window will pop up, " "displaying the command to be sent to terminal, " "for review.", i18n ("Run in terminal") ) ); connect( buttonWarn, &QPushButton::pressed, this, &KateKonsoleConfigPage::slotEnableRunWarning ); hboxWarn->addWidget( buttonWarn ); vboxRun->addWidget( frameWarn ); groupRun->setLayout( vboxRun ); lo->addWidget( groupRun ); cbSetEditor = new QCheckBox( i18n("Set &EDITOR environment variable to 'kate -b'"), this ); lo->addWidget( cbSetEditor ); QLabel *tmp = new QLabel(this); tmp->setText(i18n("Important: The document has to be closed to make the console application continue")); lo->addWidget(tmp); reset(); lo->addStretch(); connect(cbAutoSyncronize, &QCheckBox::stateChanged, this, &KateKonsoleConfigPage::changed); connect( cbRemoveExtension, &QCheckBox::stateChanged, this, &KTextEditor::ConfigPage::changed ); connect( lePrefix, &QLineEdit::textChanged, this, &KateKonsoleConfigPage::changed ); connect(cbSetEditor, &QCheckBox::stateChanged, this, &KateKonsoleConfigPage::changed); } void KateKonsoleConfigPage::slotEnableRunWarning () { KMessageBox::enableMessage(QStringLiteral("Konsole: Run in Terminal Warning")); } QString KateKonsoleConfigPage::name() const { return i18n("Terminal"); } QString KateKonsoleConfigPage::fullName() const { return i18n("Terminal Settings"); } QIcon KateKonsoleConfigPage::icon() const { return QIcon::fromTheme(QStringLiteral("utilities-terminal")); } void KateKonsoleConfigPage::apply() { KConfigGroup config(KSharedConfig::openConfig(), "Konsole"); config.writeEntry("AutoSyncronize", cbAutoSyncronize->isChecked()); config.writeEntry("RemoveExtension", cbRemoveExtension->isChecked()); config.writeEntry("RunPrefix", lePrefix->text()); config.writeEntry("SetEditor", cbSetEditor->isChecked()); config.sync(); mPlugin->readConfig(); } void KateKonsoleConfigPage::reset() { KConfigGroup config(KSharedConfig::openConfig(), "Konsole"); cbAutoSyncronize->setChecked(config.readEntry("AutoSyncronize", true)); cbRemoveExtension->setChecked(config.readEntry("RemoveExtension", false)); lePrefix->setText(config.readEntry("RunPrefix", "")); cbSetEditor->setChecked(config.readEntry("SetEditor", false)); } #include "kateconsole.moc" // kate: space-indent on; indent-width 2; replace-tabs on; diff --git a/addons/konsole/katekonsoleplugin.desktop b/addons/konsole/katekonsoleplugin.desktop index 093a6e462..71c836f39 100644 --- a/addons/konsole/katekonsoleplugin.desktop +++ b/addons/konsole/katekonsoleplugin.desktop @@ -1,58 +1,59 @@ [Desktop Entry] Type=Service ServiceTypes=KTextEditor/Plugin X-KDE-Library=katekonsoleplugin Name=Terminal Tool View Name[ca]=Vista d'eina del terminal Name[ca@valencia]=Vista d'eina del terminal Name[cs]=Nástroj zobrazující terminál Name[de]=Werkzeugansicht für Terminal Name[el]=Προβολή εργαλείου τερματικού Name[en_GB]=Terminal Tool View Name[es]=Vista de la herramienta de terminal Name[eu]=Terminalaren ikuspegi-tresna Name[fi]=Päätenäkymä Name[fr]=Vue des outils du terminal Name[gl]=Vista da utilidade de terminal +Name[id]=Tampilan Alat Terminal Name[it]=Vista dello strumento Terminale Name[ko]=터미널 도구 보기 Name[nl]=Terminalweergave Name[nn]=Terminal-verktøyvising Name[pl]=Widok narzędzia terminala Name[pt]=Área do Emulador de Terminal Name[pt_BR]=Área de ferramentas do Terminal Name[ru]=Встроенный терминал Name[sk]=Pohľad nástroja terminálu Name[sv]=Terminalverktygsvy Name[tr]=Uçbirim Araç Görünümü Name[uk]=Інструмент термінала Name[x-test]=xxTerminal Tool Viewxx Name[zh_CN]=终端工具视图 Name[zh_TW]=終端工具檢視 Comment=Have a terminal at the ready, using KDE's Konsole widget Comment[ca]=Té un terminal preparat, usant l'estri de Konsole del KDE -Comment[ca@valencia]=Té un terminal preparat, usant l'estri de Konsole del KDE +Comment[ca@valencia]=Té un terminal preparat, usant el giny («widget») de Konsole del KDE Comment[de]=Stellt ein Terminal mit KDE's Konsole bereit Comment[el]=Έτοιμο τερματικό, με χρήση του γραφικού συστατικού της κονσόλας του KDE Comment[en_GB]=Have a terminal at the ready, using KDE's Konsole widget Comment[es]=Tenga un terminal disponible usando el widget Konsole de KDE Comment[eu]=Izan terminal bat prest, KDEren Konsole trepeta erabiliz Comment[fi]=Pidä pääte valmiina KDE:n Konsole-sovelmalla Comment[fr]=Gardez un terminal à disposition grâce au composant graphique Konsole de KDE Comment[gl]=Teña un terminal a man co trebello de Konsole de KDE Comment[id]=Memiliki sebuah terminal yang sudah siap, menggunakan widget Konsole KDE punya Comment[it]=Avere un terminale pronto, usando l'oggetto Konsole di KDE Comment[ko]=KDE의 Konsole 위젯을 사용하여 곧바로 터미널 열기 Comment[nl]=Een terminal gereed hebben, met gebruik van de Konsole-widget van KDE Comment[nn]=Ha alltid terminalen for handa, med Konsole-modulen Comment[pl]=Miej terminal pod ręką przy użyciu elementu Konsoli KDE Comment[pt]=Tenha um terminal logo disponível, usando o elemento gráfico do Konsole Comment[pt_BR]=Tenha um terminal logo disponível, usando o widget do Konsole Comment[ru]=Быстрый вызов терминала, основанного на виджете Konsole Comment[sk]=Majte terminál poruke, pomocou Konsole widgetu od KDE Comment[sv]=Ha en terminal redo genom att använda KDE:s grafiska terminalkomponent Comment[tr]=KDE'nin Konsole gerecini kullanarak hazır bir uçbirime sahip ol Comment[uk]=Термінал на основі віджета Konsole KDE Comment[x-test]=xxHave a terminal at the ready, using KDE's Konsole widgetxx Comment[zh_CN]=立即可用的终端,使用 KDE 的 Konsole 小部件 Comment[zh_TW]=使用 KDE 的 Konsole 元件準備好終端器。 diff --git a/addons/lspclient/CMakeLists.txt b/addons/lspclient/CMakeLists.txt new file mode 100644 index 000000000..8a7177d08 --- /dev/null +++ b/addons/lspclient/CMakeLists.txt @@ -0,0 +1,46 @@ +find_package(KF5ItemModels QUIET) +set_package_properties(KF5ItemModels PROPERTIES PURPOSE "Required to build the lspclient addon") + +if(NOT KF5ItemModels_FOUND) + return() +endif() + +add_library(lspclientplugin MODULE "") +target_compile_definitions(lspclientplugin PRIVATE TRANSLATION_DOMAIN="lspclient") + +target_link_libraries( + lspclientplugin + PRIVATE + KF5::ItemModels + KF5::TextEditor +) + +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category( + DEBUG_SOURCES + HEADER lspclient_debug.h + IDENTIFIER LSPCLIENT + CATEGORY_NAME "katelspclientplugin" +) +target_sources(lspclientplugin PRIVATE ${DEBUG_SOURCES}) + +target_sources( + lspclientplugin + PRIVATE + lspclientcompletion.cpp + lspclientconfigpage.cpp + lspclienthover.cpp + lspclientplugin.cpp + lspclientpluginview.cpp + lspclientserver.cpp + lspclientservermanager.cpp + lspclientsymbolview.cpp + plugin.qrc +) + +kcoreaddons_desktop_to_json(lspclientplugin lspclientplugin.desktop) +install(TARGETS lspclientplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) + +if(BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/addons/lspclient/Messages.sh b/addons/lspclient/Messages.sh new file mode 100644 index 000000000..be91dd2df --- /dev/null +++ b/addons/lspclient/Messages.sh @@ -0,0 +1,3 @@ +#! /bin/sh +$EXTRACTRC *.rc >> rc.cpp +$XGETTEXT *.cpp -o $podir/lspclient.pot diff --git a/addons/lspclient/lspclientcompletion.cpp b/addons/lspclient/lspclientcompletion.cpp new file mode 100644 index 000000000..1e6614171 --- /dev/null +++ b/addons/lspclient/lspclientcompletion.cpp @@ -0,0 +1,340 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "lspclientcompletion.h" +#include "lspclientplugin.h" + +#include "lspclient_debug.h" + +#include +#include +#include + +#include +#include + +#include + +#define RETURN_CACHED_ICON(name) \ +{ \ + static QIcon icon(QIcon::fromTheme(QStringLiteral(name))); \ + return icon; \ +} + +static QIcon +kind_icon(LSPCompletionItemKind kind) +{ + switch (kind) + { + case LSPCompletionItemKind::Method: + case LSPCompletionItemKind::Function: + case LSPCompletionItemKind::Constructor: + RETURN_CACHED_ICON("code-function") + case LSPCompletionItemKind::Variable: + RETURN_CACHED_ICON("code-variable") + case LSPCompletionItemKind::Class: + case LSPCompletionItemKind::Interface: + case LSPCompletionItemKind::Struct: + RETURN_CACHED_ICON("code-class"); + case LSPCompletionItemKind::Module: + RETURN_CACHED_ICON("code-block"); + case LSPCompletionItemKind::Field: + case LSPCompletionItemKind::Property: + // align with symbolview + RETURN_CACHED_ICON("code-variable"); + case LSPCompletionItemKind::Enum: + case LSPCompletionItemKind::EnumMember: + RETURN_CACHED_ICON("enum"); + default: + break; + } + return QIcon(); +} + +static KTextEditor::CodeCompletionModel::CompletionProperty +kind_property(LSPCompletionItemKind kind) +{ + using CompletionProperty = KTextEditor::CodeCompletionModel::CompletionProperty; + auto p = CompletionProperty::NoProperty; + + switch (kind) + { + case LSPCompletionItemKind::Method: + case LSPCompletionItemKind::Function: + case LSPCompletionItemKind::Constructor: + p = CompletionProperty::Function; + break; + case LSPCompletionItemKind::Variable: + p = CompletionProperty::Variable; + break; + case LSPCompletionItemKind::Class: + case LSPCompletionItemKind::Interface: + p = CompletionProperty::Class; + break; + case LSPCompletionItemKind::Struct: + p = CompletionProperty::Class; + break; + case LSPCompletionItemKind::Module: + p =CompletionProperty::Namespace; + break; + case LSPCompletionItemKind::Enum: + case LSPCompletionItemKind::EnumMember: + p = CompletionProperty::Enum; + break; + default: + break; + } + return p; +} + +struct LSPClientCompletionItem : public LSPCompletionItem +{ + int argumentHintDepth = 0; + QString prefix; + QString postfix; + + LSPClientCompletionItem(const LSPCompletionItem & item) + : LSPCompletionItem(item) + { + // transform for later display + // sigh, remove (leading) whitespace (looking at clangd here) + // could skip the [] if empty detail, but it is a handy watermark anyway ;-) + label = QString(label.simplified() + QStringLiteral(" [") + + detail.simplified() + QStringLiteral("]")); + } + + LSPClientCompletionItem(const LSPSignatureInformation & sig, + int activeParameter, const QString & _sortText) + { + argumentHintDepth = 1; + documentation = sig.documentation; + label = sig.label; + sortText = _sortText; + // transform into prefix, name, suffix if active + if (activeParameter >= 0 && activeParameter < sig.parameters.length()) { + const auto& param = sig.parameters.at(activeParameter); + if (param.start >= 0 && param.start < label.length() && + param.end >= 0 && param.end < label.length() && + param.start < param.end) { + prefix = label.mid(0, param.start); + postfix = label.mid(param.end); + label = label.mid(param.start, param.end - param.start); + } + } + } +}; + + +static bool compare_match (const LSPCompletionItem & a, const LSPCompletionItem b) +{ return a.sortText < b.sortText; } + + +class LSPClientCompletionImpl : public LSPClientCompletion +{ + Q_OBJECT + + typedef LSPClientCompletionImpl self_type; + + QSharedPointer m_manager; + QSharedPointer m_server; + bool m_selectedDocumentation = false; + + QVector m_triggersCompletion; + QVector m_triggersSignature; + bool m_triggerSignature = false; + + QList m_matches; + LSPClientServer::RequestHandle m_handle, m_handleSig; + +public: + LSPClientCompletionImpl(QSharedPointer manager) + : LSPClientCompletion(nullptr), m_manager(manager), m_server(nullptr) + { + } + + void setServer(QSharedPointer server) override + { + m_server = server; + if (m_server) { + const auto& caps = m_server->capabilities(); + m_triggersCompletion = caps.completionProvider.triggerCharacters; + m_triggersSignature = caps.signatureHelpProvider.triggerCharacters; + } else { + m_triggersCompletion.clear(); + m_triggersSignature.clear(); + } + } + + virtual void setSelectedDocumentation(bool s) override + { m_selectedDocumentation = s; } + + QVariant data(const QModelIndex &index, int role) const override + { + if (!index.isValid() || index.row() >= m_matches.size()) { + return QVariant(); + } + + const auto &match = m_matches.at(index.row()); + + if (role == Qt::DisplayRole) { + if (index.column() == KTextEditor::CodeCompletionModel::Name) { + return match.label; + } else if (index.column() == KTextEditor::CodeCompletionModel::Prefix) { + return match.prefix; + } else if (index.column() == KTextEditor::CodeCompletionModel::Postfix) { + return match.postfix; + } + } else if (role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Icon) { + return kind_icon(match.kind); + } else if (role == KTextEditor::CodeCompletionModel::CompletionRole) { + return kind_property(match.kind); + } else if (role == KTextEditor::CodeCompletionModel::ArgumentHintDepth) { + return match.argumentHintDepth; + } else if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) { + // (ab)use depth to indicate sort order + return index.row(); + } else if (role == KTextEditor::CodeCompletionModel::IsExpandable) { + return !match.documentation.value.isEmpty(); + } else if (role == KTextEditor::CodeCompletionModel::ExpandingWidget && + !match.documentation.value.isEmpty()) { + // probably plaintext, but let's show markdown as-is for now + // FIXME better presentation of markdown + return match.documentation.value; + } else if (role == KTextEditor::CodeCompletionModel::ItemSelected && + !match.argumentHintDepth && !match.documentation.value.isEmpty() && + m_selectedDocumentation) { + return match.documentation.value; + } + + return QVariant(); + } + + bool shouldStartCompletion(KTextEditor::View *view, const QString &insertedText, + bool userInsertion, const KTextEditor::Cursor &position) override + { + qCInfo(LSPCLIENT) << "should start " << userInsertion << insertedText; + + if (!userInsertion || !m_server || insertedText.isEmpty()) { + return false; + } + + // covers most already ... + bool complete = CodeCompletionModelControllerInterface::shouldStartCompletion(view, + insertedText, userInsertion, position); + QChar lastChar = insertedText.at(insertedText.count() - 1); + + m_triggerSignature = false; + complete = complete || m_triggersCompletion.contains(lastChar); + if (m_triggersSignature.contains(lastChar)) { + complete = true; + m_triggerSignature = true; + } + + return complete; + } + + void completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType it) override + { + Q_UNUSED(it) + + qCInfo(LSPCLIENT) << "completion invoked" << m_server; + + // maybe use WaitForReset ?? + // but more complex and already looks good anyway + auto handler = [this] (const QList & compl) { + beginResetModel(); + qCInfo(LSPCLIENT) << "adding completions " << compl.size(); + for (const auto & item : compl) + m_matches.push_back(item); + std::stable_sort(m_matches.begin(), m_matches.end(), compare_match); + setRowCount(m_matches.size()); + endResetModel(); + }; + + auto sigHandler = [this] (const LSPSignatureHelp & sig) { + beginResetModel(); + qCInfo(LSPCLIENT) << "adding signatures " << sig.signatures.size(); + int index = 0; + for (const auto & item : sig.signatures) { + int sortIndex = 10 + index; + int active = -1; + if (index == sig.activeSignature) { + sortIndex = 0; + active = sig.activeParameter; + } + // trick active first, others after that + m_matches.push_back({item, active, QString(QStringLiteral("%1").arg(sortIndex, 3, 10))}); + ++index; + } + std::stable_sort(m_matches.begin(), m_matches.end(), compare_match); + setRowCount(m_matches.size()); + endResetModel(); + }; + + beginResetModel(); + m_matches.clear(); + auto document = view->document(); + if (m_server && document) { + // the default range is determined based on a reasonable identifier (word) + // which is generally fine and nice, but let's pass actual cursor position + // (which may be within this typical range) + auto position = view->cursorPosition(); + auto cursor = qMax(range.start(), qMin(range.end(), position)); + m_manager->update(document, false); + if (!m_triggerSignature) { + m_handle = m_server->documentCompletion(document->url(), + {cursor.line(), cursor.column()}, this, handler); + } + m_handleSig = m_server->signatureHelp(document->url(), + {cursor.line(), cursor.column()}, this, sigHandler); + } + setRowCount(m_matches.size()); + endResetModel(); + } + + void executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const override + { + if (index.row() < m_matches.size()) + view->document()->replaceText(word, m_matches.at(index.row()).insertText); + } + + void aborted(KTextEditor::View *view) override + { + Q_UNUSED(view); + beginResetModel(); + m_matches.clear(); + m_handle.cancel(); + m_handleSig.cancel(); + m_triggerSignature = false; + endResetModel(); + } +}; + +LSPClientCompletion* +LSPClientCompletion::new_(QSharedPointer manager) +{ + return new LSPClientCompletionImpl(manager); +} + +#include "lspclientcompletion.moc" diff --git a/addons/lspclient/lspclientcompletion.h b/addons/lspclient/lspclientcompletion.h new file mode 100644 index 000000000..da3d62a29 --- /dev/null +++ b/addons/lspclient/lspclientcompletion.h @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef LSPCLIENTCOMPLETION_H +#define LSPCLIENTCOMPLETION_H + +#include "lspclientserver.h" +#include "lspclientservermanager.h" + +#include +#include + +class LSPClientCompletion : public KTextEditor::CodeCompletionModel, public KTextEditor::CodeCompletionModelControllerInterface +{ + Q_OBJECT + + Q_INTERFACES(KTextEditor::CodeCompletionModelControllerInterface) + +public: + + // implementation factory method + static LSPClientCompletion* new_(QSharedPointer manager); + + LSPClientCompletion(QObject * parent) + : KTextEditor::CodeCompletionModel(parent) + {} + + virtual void setServer(QSharedPointer server) = 0; + virtual void setSelectedDocumentation(bool) = 0; +}; + +#endif diff --git a/addons/lspclient/lspclientconfigpage.cpp b/addons/lspclient/lspclientconfigpage.cpp new file mode 100644 index 000000000..63f5903f6 --- /dev/null +++ b/addons/lspclient/lspclientconfigpage.cpp @@ -0,0 +1,162 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "lspclientconfigpage.h" +#include "lspclientplugin.h" + +#include +#include +#include +#include + +#include +#include + +LSPClientConfigPage::LSPClientConfigPage(QWidget *parent, LSPClientPlugin *plugin) + : KTextEditor::ConfigPage(parent), m_plugin(plugin) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + + QGroupBox *outlineBox = new QGroupBox(i18n("Symbol Outline Options"), this); + QVBoxLayout *top = new QVBoxLayout(outlineBox); + m_symbolDetails = new QCheckBox(i18n("Display symbol details")); + m_symbolTree = new QCheckBox(i18n("Tree mode outline")); + m_symbolExpand = new QCheckBox(i18n("Automatically expand nodes in tree mode")); + m_symbolSort = new QCheckBox(i18n("Sort symbols alphabetically")); + top->addWidget(m_symbolDetails); + top->addWidget(m_symbolTree); + top->addWidget(m_symbolExpand); + top->addWidget(m_symbolSort); + layout->addWidget(outlineBox); + + outlineBox = new QGroupBox(i18n("General Options"), this); + top = new QVBoxLayout(outlineBox); + m_complDoc = new QCheckBox(i18n("Show selected completion documentation")); + m_refDeclaration = new QCheckBox(i18n("Include declaration in references")); + m_onTypeFormatting = new QCheckBox(i18n("Format on typing")); + m_incrementalSync = new QCheckBox(i18n("Incremental document synchronization")); + QHBoxLayout *diagLayout = new QHBoxLayout(); + m_diagnostics = new QCheckBox(i18n("Show diagnostics notifications")); + m_diagnosticsHighlight = new QCheckBox(i18n("Add highlights")); + m_diagnosticsMark = new QCheckBox(i18n("Add markers")); + top->addWidget(m_complDoc); + top->addWidget(m_refDeclaration); + top->addWidget(m_onTypeFormatting); + top->addWidget(m_incrementalSync); + diagLayout->addWidget(m_diagnostics); + diagLayout->addStretch(1); + diagLayout->addWidget(m_diagnosticsHighlight); + diagLayout->addStretch(1); + diagLayout->addWidget(m_diagnosticsMark); + top->addLayout(diagLayout); + layout->addWidget(outlineBox); + + outlineBox = new QGroupBox(i18n("Server Configuration"), this); + top = new QVBoxLayout(outlineBox); + m_configPath = new KUrlRequester(this); + top->addWidget(m_configPath); + layout->addWidget(outlineBox); + + layout->addStretch(1); + + reset(); + + for (const auto & cb : {m_symbolDetails, m_symbolExpand, m_symbolSort, m_symbolTree, + m_complDoc, m_refDeclaration, m_diagnostics, m_diagnosticsMark, + m_onTypeFormatting, m_incrementalSync}) + connect(cb, &QCheckBox::toggled, this, &LSPClientConfigPage::changed); + connect(m_configPath, &KUrlRequester::textChanged, this, &LSPClientConfigPage::changed); + connect(m_configPath, &KUrlRequester::urlSelected, this, &LSPClientConfigPage::changed); + + // custom control logic + auto h = [this] () + { + bool enabled = m_diagnostics->isChecked(); + m_diagnosticsHighlight->setEnabled(enabled); + m_diagnosticsMark->setEnabled(enabled); + }; + connect(this, &LSPClientConfigPage::changed, this, h); +} + +QString LSPClientConfigPage::name() const +{ + return QString(i18n("LSP Client")); +} + +QString LSPClientConfigPage::fullName() const +{ + return QString(i18n("LSP Client")); +} + +QIcon LSPClientConfigPage::icon() const +{ + return QIcon::fromTheme(QLatin1String("code-context")); +} + +void LSPClientConfigPage::apply() +{ + m_plugin->m_symbolDetails = m_symbolDetails->isChecked(); + m_plugin->m_symbolTree = m_symbolTree->isChecked(); + m_plugin->m_symbolExpand = m_symbolExpand->isChecked(); + m_plugin->m_symbolSort = m_symbolSort->isChecked(); + + m_plugin->m_complDoc = m_complDoc->isChecked(); + m_plugin->m_refDeclaration = m_refDeclaration->isChecked(); + + m_plugin->m_diagnostics = m_diagnostics->isChecked(); + m_plugin->m_diagnosticsHighlight = m_diagnosticsHighlight->isChecked(); + m_plugin->m_diagnosticsMark = m_diagnosticsMark->isChecked(); + + m_plugin->m_onTypeFormatting = m_onTypeFormatting->isChecked(); + m_plugin->m_incrementalSync = m_incrementalSync->isChecked(); + + m_plugin->m_configPath = m_configPath->url(); + + m_plugin->writeConfig(); +} + +void LSPClientConfigPage::reset() +{ + m_symbolDetails->setChecked(m_plugin->m_symbolDetails); + m_symbolTree->setChecked(m_plugin->m_symbolTree); + m_symbolExpand->setChecked(m_plugin->m_symbolExpand); + m_symbolSort->setChecked(m_plugin->m_symbolSort); + + m_complDoc->setChecked(m_plugin->m_complDoc); + m_refDeclaration->setChecked(m_plugin->m_refDeclaration); + + m_diagnostics->setChecked(m_plugin->m_diagnostics); + m_diagnosticsHighlight->setChecked(m_plugin->m_diagnosticsHighlight); + m_diagnosticsMark->setChecked(m_plugin->m_diagnosticsMark); + + m_onTypeFormatting->setChecked(m_plugin->m_onTypeFormatting); + m_incrementalSync->setChecked(m_plugin->m_incrementalSync); + + m_configPath->setUrl(m_plugin->m_configPath); +} + +void LSPClientConfigPage::defaults() +{ + reset(); +} diff --git a/addons/lspclient/lspclientconfigpage.h b/addons/lspclient/lspclientconfigpage.h new file mode 100644 index 000000000..b623e5fb1 --- /dev/null +++ b/addons/lspclient/lspclientconfigpage.h @@ -0,0 +1,71 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef LSPCLIENTCONFIGPAGE_H +#define LSPCLIENTCONFIGPAGE_H + +#include + +class LSPClientPlugin; + +class QCheckBox; +class QLineEdit; + +class KUrlRequester; + +class LSPClientConfigPage : public KTextEditor::ConfigPage +{ + Q_OBJECT + + public: + explicit LSPClientConfigPage(QWidget *parent = nullptr, LSPClientPlugin *plugin = nullptr); + ~LSPClientConfigPage() override {}; + + QString name() const override; + QString fullName() const override; + QIcon icon() const override; + + public Q_SLOTS: + void apply() override; + void defaults() override; + void reset() override; + + private: + QCheckBox* m_symbolDetails; + QCheckBox* m_symbolExpand; + QCheckBox* m_symbolTree; + QCheckBox* m_symbolSort; + QCheckBox* m_complDoc; + QCheckBox* m_refDeclaration; + QCheckBox* m_diagnostics; + QCheckBox* m_diagnosticsHighlight; + QCheckBox* m_diagnosticsMark; + QCheckBox* m_onTypeFormatting; + QCheckBox* m_incrementalSync; + KUrlRequester *m_configPath; + + LSPClientPlugin *m_plugin; +}; + +#endif diff --git a/addons/lspclient/lspclienthover.cpp b/addons/lspclient/lspclienthover.cpp new file mode 100644 index 000000000..20e15dac8 --- /dev/null +++ b/addons/lspclient/lspclienthover.cpp @@ -0,0 +1,118 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + Copyright (C) 2019 Christoph Cullmann + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "lspclienthover.h" +#include "lspclientplugin.h" + +#include "lspclient_debug.h" + +#include +#include +#include + +#include + +class LSPClientHoverImpl : public LSPClientHover +{ + Q_OBJECT + + typedef LSPClientHoverImpl self_type; + + QSharedPointer m_manager; + QSharedPointer m_server; + + LSPClientServer::RequestHandle m_handle; + +public: + LSPClientHoverImpl(QSharedPointer manager) + : LSPClientHover(), m_manager(manager), m_server(nullptr) + { + } + + void setServer(QSharedPointer server) override + { + m_server = server; + } + + /** + * This function is called whenever the users hovers over text such + * that the text hint delay passes. Then, textHint() is called + * for each registered TextHintProvider. + * + * Return the text hint (possibly Qt richtext) for @p view at @p position. + * + * If you do not have any contents to show, just return an empty QString(). + * + * \param view the view that requests the text hint + * \param position text cursor under the mouse position + * \return text tool tip to be displayed, may be Qt richtext + */ + QString textHint(KTextEditor::View *view, + const KTextEditor::Cursor &position) override + { + // hack: delayed handling of tooltip on our own, the API is too dumb for a-sync feedback ;=) + if (m_server) { + QPointer v(view); + auto h = [this,v,position] (const LSPHover & info) + { + if (!v || info.contents.isEmpty()) { + return; + } + + // combine contents elements to one string + QString finalTooltip; + for (auto &element : info.contents) { + if (!finalTooltip.isEmpty()) { + finalTooltip.append(QLatin1Char('\n')); + } + finalTooltip.append(element.value); + } + + // we need to cut this a bit if too long until we have + // something more sophisticated than a tool tip for it + if (finalTooltip.size() > 512) { + finalTooltip.resize(512); + finalTooltip.append(QStringLiteral("...")); + } + + // show tool tip: think about a better way for "large" stuff + QToolTip::showText(v->mapToGlobal(v->cursorToCoordinate(position)), finalTooltip); + }; + + m_handle.cancel() = m_server->documentHover(view->document()->url(), position, this, h); + } + + return QString(); + } + +}; + +LSPClientHover* +LSPClientHover::new_(QSharedPointer manager) +{ + return new LSPClientHoverImpl(manager); +} + +#include "lspclienthover.moc" diff --git a/addons/lspclient/lspclienthover.h b/addons/lspclient/lspclienthover.h new file mode 100644 index 000000000..c79e3830f --- /dev/null +++ b/addons/lspclient/lspclienthover.h @@ -0,0 +1,50 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + Copyright (C) 2019 Christoph Cullmann + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef LSPCLIENTHOVER_H +#define LSPCLIENTHOVER_H + +#include "lspclientserver.h" +#include "lspclientservermanager.h" + +#include + +class LSPClientHover : public QObject, public KTextEditor::TextHintProvider +{ + Q_OBJECT + +public: + + // implementation factory method + static LSPClientHover* new_(QSharedPointer manager); + + LSPClientHover() + : KTextEditor::TextHintProvider() + {} + + virtual void setServer(QSharedPointer server) = 0; +}; + +#endif diff --git a/addons/lspclient/lspclientplugin.cpp b/addons/lspclient/lspclientplugin.cpp new file mode 100644 index 000000000..b572afd4b --- /dev/null +++ b/addons/lspclient/lspclientplugin.cpp @@ -0,0 +1,133 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "lspclientplugin.h" +#include "lspclientpluginview.h" +#include "lspclientconfigpage.h" + +#include "lspclient_debug.h" + +#include +#include +#include +#include + +#include + +static const QString CONFIG_LSPCLIENT { QStringLiteral("lspclient") }; +static const QString CONFIG_SYMBOL_DETAILS { QStringLiteral("SymbolDetails") }; +static const QString CONFIG_SYMBOL_TREE { QStringLiteral("SymbolTree") }; +static const QString CONFIG_SYMBOL_EXPAND { QStringLiteral("SymbolExpand") }; +static const QString CONFIG_SYMBOL_SORT { QStringLiteral("SymbolSort") }; +static const QString CONFIG_COMPLETION_DOC { QStringLiteral("CompletionDocumentation") }; +static const QString CONFIG_REFERENCES_DECLARATION { QStringLiteral("ReferencesDeclaration") }; +static const QString CONFIG_TYPE_FORMATTING { QStringLiteral("TypeFormatting") }; +static const QString CONFIG_INCREMENTAL_SYNC { QStringLiteral("IncrementalSync") }; +static const QString CONFIG_DIAGNOSTICS { QStringLiteral("Diagnostics") }; +static const QString CONFIG_DIAGNOSTICS_HIGHLIGHT { QStringLiteral("DiagnosticsHighlight") }; +static const QString CONFIG_DIAGNOSTICS_MARK { QStringLiteral("DiagnosticsMark") }; +static const QString CONFIG_SERVER_CONFIG { QStringLiteral("ServerConfiguration") }; + + +K_PLUGIN_FACTORY_WITH_JSON(LSPClientPluginFactory, "lspclientplugin.json", registerPlugin();) + +LSPClientPlugin::LSPClientPlugin(QObject *parent, const QList &) + : KTextEditor::Plugin(parent) +{ + /** + * handle plugin verbosity + * the m_debugMode will be used to e.g. set debug level for started clangd, too + */ + m_debugMode = (qgetenv("LSPCLIENT_DEBUG") == QByteArray("1")); + if (!m_debugMode) { + QLoggingCategory::setFilterRules(QStringLiteral("katelspclientplugin.debug=false\nkatelspclientplugin.info=false")); + } else { + QLoggingCategory::setFilterRules(QStringLiteral("katelspclientplugin.debug=true\nkatelspclientplugin.info=true")); + } + + readConfig(); +} + +LSPClientPlugin::~LSPClientPlugin() +{ +} + +QObject *LSPClientPlugin::createView(KTextEditor::MainWindow *mainWindow) +{ + return LSPClientPluginView::new_(this, mainWindow); +} + +int LSPClientPlugin::configPages() const +{ + return 1; +} + +KTextEditor::ConfigPage *LSPClientPlugin::configPage(int number, QWidget *parent) +{ + if (number != 0) { + return nullptr; + } + + return new LSPClientConfigPage(parent, this); +} + +void LSPClientPlugin::readConfig() +{ + KConfigGroup config(KSharedConfig::openConfig(), CONFIG_LSPCLIENT); + m_symbolDetails = config.readEntry(CONFIG_SYMBOL_DETAILS, false); + m_symbolTree = config.readEntry(CONFIG_SYMBOL_TREE, true); + m_symbolExpand = config.readEntry(CONFIG_SYMBOL_EXPAND, true); + m_symbolSort = config.readEntry(CONFIG_SYMBOL_SORT, false); + m_complDoc = config.readEntry(CONFIG_COMPLETION_DOC, true); + m_refDeclaration = config.readEntry(CONFIG_REFERENCES_DECLARATION, true); + m_onTypeFormatting = config.readEntry(CONFIG_TYPE_FORMATTING, false); + m_incrementalSync = config.readEntry(CONFIG_INCREMENTAL_SYNC, false); + m_diagnostics = config.readEntry(CONFIG_DIAGNOSTICS, true); + m_diagnosticsHighlight = config.readEntry(CONFIG_DIAGNOSTICS_HIGHLIGHT, true); + m_diagnosticsMark = config.readEntry(CONFIG_DIAGNOSTICS_MARK, true); + m_configPath = config.readEntry(CONFIG_SERVER_CONFIG, QUrl()); + + emit update(); +} + +void LSPClientPlugin::writeConfig() const +{ + KConfigGroup config(KSharedConfig::openConfig(), CONFIG_LSPCLIENT); + config.writeEntry(CONFIG_SYMBOL_DETAILS, m_symbolDetails); + config.writeEntry(CONFIG_SYMBOL_TREE, m_symbolTree); + config.writeEntry(CONFIG_SYMBOL_EXPAND, m_symbolExpand); + config.writeEntry(CONFIG_SYMBOL_SORT, m_symbolSort); + config.writeEntry(CONFIG_COMPLETION_DOC, m_complDoc); + config.writeEntry(CONFIG_REFERENCES_DECLARATION, m_refDeclaration); + config.writeEntry(CONFIG_TYPE_FORMATTING, m_onTypeFormatting); + config.writeEntry(CONFIG_INCREMENTAL_SYNC, m_incrementalSync); + config.writeEntry(CONFIG_DIAGNOSTICS, m_diagnostics); + config.writeEntry(CONFIG_DIAGNOSTICS_HIGHLIGHT, m_diagnosticsHighlight); + config.writeEntry(CONFIG_DIAGNOSTICS_MARK, m_diagnosticsMark); + config.writeEntry(CONFIG_SERVER_CONFIG, m_configPath); + + emit update(); +} + +#include "lspclientplugin.moc" diff --git a/addons/lspclient/lspclientplugin.desktop b/addons/lspclient/lspclientplugin.desktop new file mode 100644 index 000000000..fc625411c --- /dev/null +++ b/addons/lspclient/lspclientplugin.desktop @@ -0,0 +1,48 @@ +[Desktop Entry] +Type=Service +ServiceTypes=KTextEditor/Plugin +X-KDE-Library=lspclientplugin +Name=LSP Client +Name[ca]=Client LSP +Name[de]=LSP-CLient +Name[el]=Πελάτης LSP +Name[en_GB]=LSP Client +Name[es]=Cliente LSP +Name[eu]=LSP bezeroa +Name[fr]=Client LSP +Name[gl]=Cliente de LSP +Name[it]=Client LSP +Name[ko]=LSP 클라이언트 +Name[nl]=LSP-client +Name[nn]=LSP-klient +Name[pl]=Klient LSP +Name[pt]=Cliente de LSP +Name[pt_BR]=Cliente LSP +Name[sk]=LSP Client +Name[sv]=LSP-klient +Name[uk]=Клієнт LSP +Name[x-test]=xxLSP Clientxx +Name[zh_CN]=LSP 客户端 +Name[zh_TW]=LSP 客戶端 +Comment=Language Server Protocol Client +Comment[ca]=Client del protocol de servidor de llenguatge +Comment[de]=Sprachserverprotokoll-Client +Comment[el]=Πελάτης Πρωτοκόλλου γλωσσικού εξυπηρετητή +Comment[en_GB]=Language Server Protocol Client +Comment[es]=Cliente del protocolo de servidor de lenguaje +Comment[eu]=Lengoaia Zerbitzari Protokolo bezeroa +Comment[fr]=Client Language Server Protocol +Comment[gl]=Cliente do protocolo de servidor de linguaxes +Comment[it]=Client Language Server Protocol +Comment[ko]=언어 서버 프로토콜 클라이언트 +Comment[nl]=Language Server Protocol Client +Comment[nn]=Klient for Language Server Protocol +Comment[pl]=Klient protokołu języka serwera +Comment[pt]=Cliente de Language Server Protocol (LSP) +Comment[pt_BR]=Cliente de Language Server Protocol (LSP) +Comment[sk]=Language Server Protocol Client +Comment[sv]=Klient för språkserverprotokollet (Language Server Protocol) +Comment[uk]=Клієнт протоколу сервера мов (LSP) +Comment[x-test]=xxLanguage Server Protocol Clientxx +Comment[zh_CN]=语言服务器协议客户端 +Comment[zh_TW]=「語言伺服器協定」客戶端 diff --git a/addons/lspclient/lspclientplugin.h b/addons/lspclient/lspclientplugin.h new file mode 100644 index 000000000..977b030bb --- /dev/null +++ b/addons/lspclient/lspclientplugin.h @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef LSPCLIENTPLUGIN_H +#define LSPCLIENTPLUGIN_H + +#include +#include +#include + +#include + +class LSPClientPlugin : public KTextEditor::Plugin +{ + Q_OBJECT + + public: + explicit LSPClientPlugin(QObject *parent = nullptr, const QList & = QList()); + ~LSPClientPlugin() override; + + QObject *createView(KTextEditor::MainWindow *mainWindow) override; + + int configPages() const override; + KTextEditor::ConfigPage *configPage(int number = 0, QWidget *parent = nullptr) override; + + void readConfig(); + void writeConfig() const; + + // settings + bool m_symbolDetails; + bool m_symbolExpand; + bool m_symbolTree; + bool m_symbolSort; + bool m_complDoc; + bool m_refDeclaration; + bool m_diagnostics; + bool m_diagnosticsHighlight; + bool m_diagnosticsMark; + bool m_onTypeFormatting; + bool m_incrementalSync; + QUrl m_configPath; + + // debug mode? + bool m_debugMode = false; + +private: + Q_SIGNALS: + // signal settings update + void update() const; +}; + +#endif diff --git a/addons/lspclient/lspclientpluginview.cpp b/addons/lspclient/lspclientpluginview.cpp new file mode 100644 index 000000000..d9349264e --- /dev/null +++ b/addons/lspclient/lspclientpluginview.cpp @@ -0,0 +1,1660 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "lspclientpluginview.h" +#include "lspclientsymbolview.h" +#include "lspclientplugin.h" +#include "lspclientservermanager.h" +#include "lspclientcompletion.h" +#include "lspclienthover.h" + +#include "lspclient_debug.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RangeData +{ + +enum { + // preserve UserRole for generic use where needed + FileUrlRole = Qt::UserRole + 1, + RangeRole, + KindRole, +}; + +class KindEnum +{ +public: + enum _kind { + Text = (int) LSPDocumentHighlightKind::Text, + Read = (int) LSPDocumentHighlightKind::Read, + Write = (int) LSPDocumentHighlightKind::Write, + Error = 10 + (int) LSPDiagnosticSeverity::Error, + Warning = 10 + (int) LSPDiagnosticSeverity::Warning, + Information = 10 + (int) LSPDiagnosticSeverity::Information, + Hint = 10 + (int) LSPDiagnosticSeverity::Hint, + Related + }; + + KindEnum(int v) + { m_value = (_kind) v; } + + KindEnum(LSPDocumentHighlightKind hl) + : KindEnum((_kind) (hl)) + {} + + KindEnum(LSPDiagnosticSeverity sev) + : KindEnum(_kind(10 + (int) sev)) + {} + + operator _kind() + { return m_value; } + +private: + _kind m_value; +}; + +static constexpr KTextEditor::MarkInterface::MarkTypes markType = KTextEditor::MarkInterface::markType31; +static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagError = KTextEditor::MarkInterface::Error; +static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagWarning = KTextEditor::MarkInterface::Warning; +static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagOther = KTextEditor::MarkInterface::markType30; +static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagAll = + KTextEditor::MarkInterface::MarkTypes (markTypeDiagError | markTypeDiagWarning | markTypeDiagOther); + +} + + +static QIcon +diagnosticsIcon(LSPDiagnosticSeverity severity) +{ +#define RETURN_CACHED_ICON(name) \ +{ \ +static QIcon icon(QIcon::fromTheme(QStringLiteral(name))); \ +return icon; \ +} + switch (severity) + { + case LSPDiagnosticSeverity::Error: + RETURN_CACHED_ICON("dialog-error") + case LSPDiagnosticSeverity::Warning: + RETURN_CACHED_ICON("dialog-warning") + case LSPDiagnosticSeverity::Information: + case LSPDiagnosticSeverity::Hint: + RETURN_CACHED_ICON("dialog-information") + default: + break; + } + return QIcon(); +} + +static QIcon +codeActionIcon() +{ + static QIcon icon(QIcon::fromTheme(QStringLiteral("insert-text"))); + return icon; +} + +KTextEditor::Document* +findDocument(KTextEditor::MainWindow *mainWindow, const QUrl & url) +{ + auto views = mainWindow->views(); + for (const auto v: views) { + auto doc = v->document(); + if (doc && doc->url() == url) + return doc; + } + return nullptr; +} + +// helper to read lines from unopened documents +// lightweight and does not require additional symbols +class FileLineReader +{ + QFile file; + int lastLineNo = -1; + QString lastLine; + +public: + FileLineReader(const QUrl & url) + : file(url.path()) + { + file.open(QIODevice::ReadOnly); + } + + // called with non-descending lineno + QString line(int lineno) + { + if (lineno == lastLineNo) { + return lastLine; + } + while (file.isOpen() && !file.atEnd()) { + auto line = file.readLine(); + if (++lastLineNo == lineno) { + QTextCodec::ConverterState state; + QTextCodec *codec = QTextCodec::codecForName("UTF-8"); + QString text = codec->toUnicode(line.constData(), line.size(), &state); + if (state.invalidChars > 0) { + text = QString::fromLatin1(line); + } + while (text.size() && text.at(text.size() -1).isSpace()) + text.chop(1); + lastLine = text; + return text; + } + } + return QString(); + } +}; + + +class LSPClientActionView : public QObject +{ + Q_OBJECT + + typedef LSPClientActionView self_type; + + LSPClientPlugin *m_plugin; + KTextEditor::MainWindow *m_mainWindow; + KXMLGUIClient *m_client; + QSharedPointer m_serverManager; + QScopedPointer m_viewTracker; + QScopedPointer m_completion; + QScopedPointer m_hover; + QScopedPointer m_symbolView; + + QPointer m_findDef; + QPointer m_findDecl; + QPointer m_findRef; + QPointer m_triggerHighlight; + QPointer m_triggerHover; + QPointer m_triggerFormat; + QPointer m_triggerRename; + QPointer m_complDocOn; + QPointer m_refDeclaration; + QPointer m_onTypeFormatting; + QPointer m_incrementalSync; + QPointer m_diagnostics; + QPointer m_diagnosticsHighlight; + QPointer m_diagnosticsMark; + QPointer m_diagnosticsSwitch; + QPointer m_diagnosticsCloseNon; + QPointer m_restartServer; + QPointer m_restartAll; + + // toolview + QScopedPointer m_toolView; + QPointer m_tabWidget; + // applied search ranges + typedef QMultiHash RangeCollection; + RangeCollection m_ranges; + // applied marks + typedef QSet DocumentCollection; + DocumentCollection m_marks; + // modelis either owned by tree added to tabwidget or owned here + QScopedPointer m_ownedModel; + // in either case, the model that directs applying marks/ranges + QPointer m_markModel; + // goto definition and declaration jump list is more a menu than a + // search result, so let's not keep adding new tabs for those + // previous tree for definition result + QPointer m_defTree; + // ... and for declaration + QPointer m_declTree; + + // diagnostics tab + QPointer m_diagnosticsTree; + // tree widget is either owned here or by tab + QScopedPointer m_diagnosticsTreeOwn; + QScopedPointer m_diagnosticsModel; + // diagnostics ranges + RangeCollection m_diagnosticsRanges; + // and marks + DocumentCollection m_diagnosticsMarks; + + // views on which completions have been registered + QSet m_completionViews; + + // views on which hovers have been registered + QSet m_hoverViews; + + // outstanding request + LSPClientServer::RequestHandle m_handle; + // timeout on request + bool m_req_timeout = false; + + // accept incoming applyEdit + bool m_accept_edit = false; + // characters to trigger format request + QVector m_onTypeFormattingTriggers; + + KActionCollection *actionCollection() const + { return m_client->actionCollection(); } + +public: + LSPClientActionView(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, + KXMLGUIClient *client, QSharedPointer serverManager) + : QObject(mainWin), m_plugin(plugin), m_mainWindow(mainWin), m_client(client), + m_serverManager(serverManager), + m_completion(LSPClientCompletion::new_(m_serverManager)), + m_hover(LSPClientHover::new_(m_serverManager)), + m_symbolView(LSPClientSymbolView::new_(plugin, mainWin, m_serverManager)) + { + connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &self_type::updateState); + connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &self_type::handleEsc); + connect(m_serverManager.data(), &LSPClientServerManager::serverChanged, this, &self_type::updateState); + + m_findDef = actionCollection()->addAction(QStringLiteral("lspclient_find_definition"), this, &self_type::goToDefinition); + m_findDef->setText(i18n("Go to Definition")); + m_findDecl = actionCollection()->addAction(QStringLiteral("lspclient_find_declaration"), this, &self_type::goToDeclaration); + m_findDecl->setText(i18n("Go to Declaration")); + m_findRef = actionCollection()->addAction(QStringLiteral("lspclient_find_references"), this, &self_type::findReferences); + m_findRef->setText(i18n("Find References")); + m_triggerHighlight = actionCollection()->addAction(QStringLiteral("lspclient_highlight"), this, &self_type::highlight); + m_triggerHighlight->setText(i18n("Highlight")); + // perhaps hover suggests to do so on mouse-over, + // but let's just use a (convenient) action/shortcut for it + m_triggerHover = actionCollection()->addAction(QStringLiteral("lspclient_hover"), this, &self_type::hover); + m_triggerHover->setText(i18n("Hover")); + m_triggerFormat = actionCollection()->addAction(QStringLiteral("lspclient_format"), this, &self_type::format); + m_triggerFormat->setText(i18n("Format")); + m_triggerRename = actionCollection()->addAction(QStringLiteral("lspclient_rename"), this, &self_type::rename); + m_triggerRename->setText(i18n("Rename")); + + // general options + m_complDocOn = actionCollection()->addAction(QStringLiteral("lspclient_completion_doc"), this, &self_type::displayOptionChanged); + m_complDocOn->setText(i18n("Show selected completion documentation")); + m_complDocOn->setCheckable(true); + m_refDeclaration = actionCollection()->addAction(QStringLiteral("lspclient_references_declaration"), this, &self_type::displayOptionChanged); + m_refDeclaration->setText(i18n("Include declaration in references")); + m_refDeclaration->setCheckable(true); + m_onTypeFormatting = actionCollection()->addAction(QStringLiteral("lspclient_type_formatting"), this, &self_type::displayOptionChanged); + m_onTypeFormatting->setText(i18n("Format on typing")); + m_onTypeFormatting->setCheckable(true); + m_incrementalSync = actionCollection()->addAction(QStringLiteral("lspclient_incremental_sync"), this, &self_type::displayOptionChanged); + m_incrementalSync->setText(i18n("Incremental document synchronization")); + m_incrementalSync->setCheckable(true); + + // diagnostics + m_diagnostics = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics"), this, &self_type::displayOptionChanged); + m_diagnostics->setText(i18n("Show diagnostics notifications")); + m_diagnostics->setCheckable(true); + m_diagnosticsHighlight = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_highlight"), this, &self_type::displayOptionChanged); + m_diagnosticsHighlight->setText(i18n("Show diagnostics highlights")); + m_diagnosticsHighlight->setCheckable(true); + m_diagnosticsMark = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_mark"), this, &self_type::displayOptionChanged); + m_diagnosticsMark->setText(i18n("Show diagnostics marks")); + m_diagnosticsMark->setCheckable(true); + m_diagnosticsSwitch = actionCollection()->addAction(QStringLiteral("lspclient_diagnostic_switch"), this, &self_type::switchToDiagnostics); + m_diagnosticsSwitch->setText(i18n("Switch to diagnostics tab")); + m_diagnosticsCloseNon = actionCollection()->addAction(QStringLiteral("lspclient_diagnostic_close_non"), this, &self_type::closeNonDiagnostics); + m_diagnosticsCloseNon->setText(i18n("Close all non-diagnostics tabs")); + + // server control + m_restartServer = actionCollection()->addAction(QStringLiteral("lspclient_restart_server"), this, &self_type::restartCurrent); + m_restartServer->setText(i18n("Restart LSP Server")); + m_restartAll = actionCollection()->addAction(QStringLiteral("lspclient_restart_all"), this, &self_type::restartAll); + m_restartAll->setText(i18n("Restart All LSP Servers")); + + // popup menu + auto menu = new KActionMenu(i18n("LSP Client"), this); + actionCollection()->addAction(QStringLiteral("popup_lspclient"), menu); + menu->addAction(m_findDef); + menu->addAction(m_findDecl); + menu->addAction(m_findRef); + menu->addAction(m_triggerHighlight); + menu->addAction(m_triggerHover); + menu->addAction(m_triggerFormat); + menu->addAction(m_triggerRename); + menu->addSeparator(); + menu->addAction(m_complDocOn); + menu->addAction(m_refDeclaration); + menu->addAction(m_onTypeFormatting); + menu->addAction(m_incrementalSync); + menu->addSeparator(); + menu->addAction(m_diagnostics); + menu->addAction(m_diagnosticsHighlight); + menu->addAction(m_diagnosticsMark); + menu->addAction(m_diagnosticsSwitch); + menu->addAction(m_diagnosticsCloseNon); + menu->addSeparator(); + menu->addAction(m_restartServer); + menu->addAction(m_restartAll); + + // sync with plugin settings if updated + connect(m_plugin, &LSPClientPlugin::update, this, &self_type::configUpdated); + + // toolview + m_toolView.reset(mainWin->createToolView(plugin, QStringLiteral("kate_lspclient"), + KTextEditor::MainWindow::Bottom, + QIcon::fromTheme(QStringLiteral("application-x-ms-dos-executable")), + i18n("LSP Client"))); + m_tabWidget = new QTabWidget(m_toolView.data()); + m_toolView->layout()->addWidget(m_tabWidget); + m_tabWidget->setFocusPolicy(Qt::NoFocus); + m_tabWidget->setTabsClosable(true); + KAcceleratorManager::setNoAccel(m_tabWidget); + connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &self_type::tabCloseRequested); + + // diagnostics tab + m_diagnosticsTree = new QTreeView(); + configureTreeView(m_diagnosticsTree); + m_diagnosticsTree->setAlternatingRowColors(true); + m_diagnosticsTreeOwn.reset(m_diagnosticsTree); + m_diagnosticsModel.reset(new QStandardItemModel()); + m_diagnosticsModel->setColumnCount(2); + m_diagnosticsTree->setModel(m_diagnosticsModel.data()); + connect(m_diagnosticsTree, &QTreeView::clicked, this, &self_type::goToItemLocation); + connect(m_diagnosticsTree, &QTreeView::doubleClicked, this, &self_type::triggerCodeAction); + + // track position in view to sync diagnostics list + m_viewTracker.reset(LSPClientViewTracker::new_(plugin, mainWin, 0, 500)); + connect(m_viewTracker.data(), &LSPClientViewTracker::newState, this, &self_type::onViewState); + + configUpdated(); + updateState(); + } + + ~LSPClientActionView() + { + // unregister all code-completion providers, else we might crash + for (auto view : qAsConst(m_completionViews)) { + qobject_cast(view)->unregisterCompletionModel(m_completion.data()); + } + + // unregister all text-hint providers, else we might crash + for (auto view : qAsConst(m_hoverViews)) { + qobject_cast(view)->unregisterTextHintProvider(m_hover.data()); + } + + clearAllLocationMarks(); + clearAllDiagnosticsMarks(); + } + + void configureTreeView(QTreeView *treeView) + { + treeView->setHeaderHidden(true); + treeView->setFocusPolicy(Qt::NoFocus); + treeView->setLayoutDirection(Qt::LeftToRight); + treeView->setSortingEnabled(false); + treeView->setEditTriggers(QAbstractItemView::NoEditTriggers); + } + + void displayOptionChanged() + { + m_diagnosticsHighlight->setEnabled(m_diagnostics->isChecked()); + m_diagnosticsMark->setEnabled(m_diagnostics->isChecked()); + auto index = m_tabWidget->indexOf(m_diagnosticsTree); + // setTabEnabled may still show it ... so let's be more forceful + if (m_diagnostics->isChecked() && m_diagnosticsTreeOwn) { + m_diagnosticsTreeOwn.take(); + m_tabWidget->insertTab(0, m_diagnosticsTree, i18nc("@title:tab", "Diagnostics")); + } else if (!m_diagnostics->isChecked() && !m_diagnosticsTreeOwn) { + m_diagnosticsTreeOwn.reset(m_diagnosticsTree); + m_tabWidget->removeTab(index); + } + m_diagnosticsSwitch->setEnabled(m_diagnostics->isChecked()); + m_serverManager->setIncrementalSync(m_incrementalSync->isChecked()); + updateState(); + } + + void configUpdated() + { + if (m_complDocOn) + m_complDocOn->setChecked(m_plugin->m_complDoc); + if (m_refDeclaration) + m_refDeclaration->setChecked(m_plugin->m_refDeclaration); + if (m_onTypeFormatting) + m_onTypeFormatting->setChecked(m_plugin->m_onTypeFormatting); + if (m_incrementalSync) + m_incrementalSync->setChecked(m_plugin->m_incrementalSync); + if (m_diagnostics) + m_diagnostics->setChecked(m_plugin->m_diagnostics); + if (m_diagnosticsHighlight) + m_diagnosticsHighlight->setChecked(m_plugin->m_diagnosticsHighlight); + if (m_diagnosticsMark) + m_diagnosticsMark->setChecked(m_plugin->m_diagnosticsMark); + displayOptionChanged(); + } + + void restartCurrent() + { + KTextEditor::View *activeView = m_mainWindow->activeView(); + auto server = m_serverManager->findServer(activeView); + if (server) + m_serverManager->restart(server.data()); + } + + void restartAll() + { + m_serverManager->restart(nullptr); + } + + static + void clearMarks(KTextEditor::Document *doc, RangeCollection & ranges, DocumentCollection & docs, uint markType) + { + KTextEditor::MarkInterface* iface = + docs.contains(doc) ? qobject_cast(doc) : nullptr; + if (iface) { + const QHash marks = iface->marks(); + QHashIterator i(marks); + while (i.hasNext()) { + i.next(); + if (i.value()->type & markType) { + iface->removeMark(i.value()->line, markType); + } + } + docs.remove(doc); + } + + for (auto it = ranges.find(doc); it != ranges.end() && it.key() == doc;) { + delete it.value(); + it = ranges.erase(it); + } + } + + static + void clearMarks(RangeCollection & ranges, DocumentCollection & docs, uint markType) + { + while (!ranges.empty()) { + clearMarks(ranges.begin().key(), ranges, docs, markType); + } + } + + Q_SLOT void clearAllMarks(KTextEditor::Document *doc) + { + clearMarks(doc, m_ranges, m_marks, RangeData::markType); + clearMarks(doc, m_diagnosticsRanges, m_diagnosticsMarks, RangeData::markTypeDiagAll); + } + + void clearAllLocationMarks() + { + clearMarks(m_ranges, m_marks, RangeData::markType); + // no longer add any again + m_ownedModel.reset(); + m_markModel.clear(); + } + + void clearAllDiagnosticsMarks() + { + clearMarks(m_diagnosticsRanges, m_diagnosticsMarks, RangeData::markTypeDiagAll); + } + + void addMarks(KTextEditor::Document *doc, QStandardItem *item, RangeCollection * ranges, DocumentCollection * docs) + { + Q_ASSERT(item); + KTextEditor::MovingInterface* miface = qobject_cast(doc); + KTextEditor::MarkInterface* iface = qobject_cast(doc); + KTextEditor::View* activeView = m_mainWindow->activeView(); + KTextEditor::ConfigInterface* ciface = qobject_cast(activeView); + + if (!miface || !iface) + return; + + auto url = item->data(RangeData::FileUrlRole).toUrl(); + if (url != doc->url()) + return; + + KTextEditor::Range range = item->data(RangeData::RangeRole).value(); + auto line = range.start().line(); + RangeData::KindEnum kind = (RangeData::KindEnum) item->data(RangeData::KindRole).toInt(); + + KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); + + bool enabled = m_diagnostics && m_diagnostics->isChecked() + && m_diagnosticsHighlight && m_diagnosticsHighlight->isChecked(); + KTextEditor::MarkInterface::MarkTypes markType = RangeData::markType; + switch (kind) { + case RangeData::KindEnum::Text: + { + // well, it's a bit like searching for something, so re-use that color + QColor rangeColor = Qt::yellow; + if (ciface) { + rangeColor = ciface->configValue(QStringLiteral("search-highlight-color")).value(); + } + attr->setBackground(rangeColor); + enabled = true; + break; + } + // FIXME are there any symbolic/configurable ways to pick these colors? + case RangeData::KindEnum::Read: + attr->setBackground(Qt::green); + enabled = true; + break; + case RangeData::KindEnum::Write: + attr->setBackground(Qt::red); + enabled = true; + break; + // use underlining for diagnostics to avoid lots of fancy flickering + case RangeData::KindEnum::Error: + markType = RangeData::markTypeDiagError; + attr->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); + attr->setUnderlineColor(Qt::red); + break; + case RangeData::KindEnum::Warning: + markType = RangeData::markTypeDiagWarning; + attr->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); + attr->setUnderlineColor(QColor(255, 128, 0)); + break; + case RangeData::KindEnum::Information: + case RangeData::KindEnum::Hint: + case RangeData::KindEnum::Related: + markType = RangeData::markTypeDiagOther; + attr->setUnderlineStyle(QTextCharFormat::DashUnderline); + attr->setUnderlineColor(Qt::blue); + break; + } + if (activeView) { + attr->setForeground(activeView->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color()); + } + + // highlight the range + if (enabled && ranges) { + KTextEditor::MovingRange* mr = miface->newMovingRange(range); + mr->setAttribute(attr); + mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection + mr->setAttributeOnlyForViews(true); + ranges->insert(doc, mr); + } + + // add match mark for range + const int ps = 32; + bool handleClick = true; + enabled = m_diagnostics && m_diagnostics->isChecked() + && m_diagnosticsMark && m_diagnosticsMark->isChecked(); + switch (markType) { + case RangeData::markType: + iface->setMarkDescription(markType, i18n("RangeHighLight")); + iface->setMarkPixmap(markType, QIcon().pixmap(0, 0)); + handleClick = false; + enabled = true; + break; + case RangeData::markTypeDiagError: + iface->setMarkDescription(markType, i18n("Error")); + iface->setMarkPixmap(markType, diagnosticsIcon(LSPDiagnosticSeverity::Error).pixmap(ps, ps)); + break; + case RangeData::markTypeDiagWarning: + iface->setMarkDescription(markType, i18n("Warning")); + iface->setMarkPixmap(markType, diagnosticsIcon(LSPDiagnosticSeverity::Warning).pixmap(ps, ps)); + break; + case RangeData::markTypeDiagOther: + iface->setMarkDescription(markType, i18n("Information")); + iface->setMarkPixmap(markType, diagnosticsIcon(LSPDiagnosticSeverity::Information).pixmap(ps, ps)); + break; + default: + Q_ASSERT(false); + break; + } + if (enabled && docs) { + iface->addMark(line, markType); + docs->insert(doc); + } + + // ensure runtime match + connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), + this, SLOT(clearAllMarks(KTextEditor::Document*)), Qt::UniqueConnection); + connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*)), + this, SLOT(clearAllMarks(KTextEditor::Document*)), Qt::UniqueConnection); + + if (handleClick) { + connect(doc, SIGNAL(markClicked(KTextEditor::Document*, KTextEditor::Mark, bool&)), + this, SLOT(onMarkClicked(KTextEditor::Document*,KTextEditor::Mark, bool&)), Qt::UniqueConnection); + } + } + + void addMarksRec(KTextEditor::Document *doc, QStandardItem *item, RangeCollection * ranges, DocumentCollection * docs) + { + Q_ASSERT(item); + addMarks(doc, item, ranges, docs); + for (int i = 0; i < item->rowCount(); ++i) { + addMarksRec(doc, item->child(i), ranges, docs); + } + } + + void addMarks(KTextEditor::Document *doc, QStandardItemModel *treeModel, RangeCollection & ranges, DocumentCollection & docs) + { + // check if already added + auto oranges = ranges.contains(doc) ? nullptr : &ranges; + auto odocs = docs.contains(doc) ? nullptr : &docs; + + if (!oranges && !odocs) + return; + + Q_ASSERT(treeModel); + addMarksRec(doc, treeModel->invisibleRootItem(), oranges, odocs); + } + + void goToDocumentLocation(const QUrl & uri, int line, int column) + { + KTextEditor::View *activeView = m_mainWindow->activeView(); + if (!activeView || uri.isEmpty() || line < 0 || column < 0) + return; + + KTextEditor::Document *document = activeView->document(); + KTextEditor::Cursor cdef(line, column); + + if (document && uri == document->url()) { + activeView->setCursorPosition(cdef); + } else { + KTextEditor::View *view = m_mainWindow->openUrl(uri); + if (view) { + view->setCursorPosition(cdef); + } + } + } + + void goToItemLocation(const QModelIndex & index) + { + auto url = index.data(RangeData::FileUrlRole).toUrl(); + auto start = index.data(RangeData::RangeRole).value().start(); + goToDocumentLocation(url, start.line(), start.column()); + } + + // custom item subclass that captures additional attributes; + // a bit more convenient than the variant/role way + struct DiagnosticItem : public QStandardItem + { + LSPDiagnostic m_diagnostic; + LSPCodeAction m_codeAction; + QSharedPointer m_snapshot; + + DiagnosticItem(const LSPDiagnostic & d) : + m_diagnostic(d) + {} + + DiagnosticItem(const LSPCodeAction & c, QSharedPointer s) : + m_codeAction(c), m_snapshot(s) + { m_diagnostic.range = LSPRange::invalid(); } + + bool isCodeAction() + { return !m_diagnostic.range.isValid() && m_codeAction.title.size(); } + }; + + + // double click on: + // diagnostic item -> request and add actions (below item) + // code action -> perform action (literal edit and/or execute command) + // (execution of command may lead to an applyEdit request from server) + void triggerCodeAction(const QModelIndex & index) + { + KTextEditor::View *activeView = m_mainWindow->activeView(); + QPointer document = activeView->document(); + auto server = m_serverManager->findServer(activeView); + auto it = dynamic_cast(m_diagnosticsModel->itemFromIndex(index)); + if (!server || !document || !it) + return; + + // click on an action ? + if (it->isCodeAction()) { + auto& action = it->m_codeAction; + // apply edit before command + applyWorkspaceEdit(action.edit, it->m_snapshot.data()); + const auto &command = action.command; + if (command.command.size()) { + // accept edit requests that may be sent to execute command + m_accept_edit = true; + // but only for a short time + QTimer::singleShot(2000, this, [this] { m_accept_edit = false; }); + server->executeCommand(command.command, command.arguments); + } + return; + } + + // only engage action if + // * active document matches diagnostic document + // * if really clicked a diagnostic item + // (which is the case as it != nullptr and not a code action) + // * if no code action invoked and added already + // (note; related items are also children) + auto url = it->data(RangeData::FileUrlRole).toUrl(); + if (url != document->url() || it->data(Qt::UserRole).toBool()) + return; + + // store some things to find item safely later on + QPersistentModelIndex pindex(index); + QSharedPointer snapshot(m_serverManager->snapshot(server.data())); + auto h = [this, url, snapshot, pindex] (const QList actions) + { + if (!pindex.isValid()) + return; + auto child = m_diagnosticsModel->itemFromIndex(pindex); + if (!child) + return; + // add actions below diagnostic item + for (const auto &action: actions) { + auto item = new DiagnosticItem(action, snapshot); + child->appendRow(item); + auto text = action.kind.size() ? + QStringLiteral("[%1] %2").arg(action.kind).arg(action.title) : + action.title; + item->setData(text, Qt::DisplayRole); + item->setData(codeActionIcon(), Qt::DecorationRole); + } + m_diagnosticsTree->setExpanded(child->index(), true); + // mark actions added + child->setData(true, Qt::UserRole); + }; + + auto range = activeView->selectionRange(); + if (!range.isValid()) { + range = document->documentRange(); + } + server->documentCodeAction(url, range, {}, {it->m_diagnostic}, this, h); + } + + void tabCloseRequested(int index) + { + auto widget = m_tabWidget->widget(index); + if (widget != m_diagnosticsTree) { + if (m_markModel && widget == m_markModel->parent()) { + clearAllLocationMarks(); + } + delete widget; + } + } + + void switchToDiagnostics() + { + m_tabWidget->setCurrentWidget(m_diagnosticsTree); + m_mainWindow->showToolView(m_toolView.data()); + } + + void closeNonDiagnostics() + { + for (int i = 0; i < m_tabWidget->count(); ) { + if (m_tabWidget->widget(i) != m_diagnosticsTree) { + tabCloseRequested(i); + } else { + ++i; + } + } + } + + // local helper to overcome some differences in LSP types + struct RangeItem + { + QUrl uri; + LSPRange range; + LSPDocumentHighlightKind kind; + }; + + static + bool compareRangeItem(const RangeItem & a, const RangeItem & b) + { return (a.uri < b.uri) || ((a.uri == b.uri) && a.range < b.range); } + + // provide Qt::DisplayRole (text) line lazily; + // only find line's text content when so requested + // This may then involve opening reading some file, at which time + // all items for that file will be resolved in one go. + struct LineItem : public QStandardItem + { + KTextEditor::MainWindow *m_mainWindow; + + LineItem(KTextEditor::MainWindow *mainWindow) : + m_mainWindow(mainWindow) + {} + + QVariant data(int role = Qt::UserRole + 1) const override + { + auto rootItem = this->parent(); + if (role != Qt::DisplayRole || !rootItem) { + return QStandardItem::data(role); + } + + auto line = data(Qt::UserRole); + // either of these mean we tried to obtain line already + if (line.isValid() || rootItem->data(RangeData::KindRole).toBool()) { + return QStandardItem::data(role).toString().append(line.toString()); + } + + KTextEditor::Document *doc = nullptr; + QScopedPointer fr; + for (int i = 0; i < rootItem->rowCount(); i++) { + auto child = rootItem->child(i); + if (i == 0) { + auto url = child->data(RangeData::FileUrlRole).toUrl(); + doc = findDocument(m_mainWindow, url); + if (!doc) { + fr.reset(new FileLineReader(url)); + } + } + auto lineno = child->data(RangeData::RangeRole).value().start().line(); + auto line = doc ? doc->line(lineno) : fr->line(lineno); + child->setData(line, Qt::UserRole); + } + + // mark as processed + rootItem->setData(RangeData::KindRole, true); + + // should work ok + return data(role); + } + + }; + + LSPRange transformRange(const QUrl & url, const LSPClientRevisionSnapshot & snapshot, const LSPRange & range) + { + KTextEditor::MovingInterface *miface; + qint64 revision; + + auto result = range; + snapshot.find(url, miface, revision); + if (miface) { + miface->transformRange(result, KTextEditor::MovingRange::DoNotExpand, + KTextEditor::MovingRange::AllowEmpty, revision); + } + return result; + } + + void fillItemRoles(QStandardItem * item, const QUrl & url, const LSPRange _range, + RangeData::KindEnum kind, const LSPClientRevisionSnapshot * snapshot = nullptr) + { + auto range = snapshot ? transformRange(url, *snapshot, _range) : _range; + item->setData(QVariant(url), RangeData::FileUrlRole); + QVariant vrange; + vrange.setValue(range); + item->setData(vrange, RangeData::RangeRole); + item->setData((int) kind, RangeData::KindRole); + } + + void makeTree(const QVector & locations, const LSPClientRevisionSnapshot * snapshot) + { + // group by url, assuming input is suitably sorted that way + auto treeModel = new QStandardItemModel(); + treeModel->setColumnCount(1); + + QUrl lastUrl; + QStandardItem *parent = nullptr; + for (const auto & loc: locations) { + if (loc.uri != lastUrl) { + if (parent) { + parent->setText(QStringLiteral("%1: %2").arg(lastUrl.path()).arg(parent->rowCount())); + } + lastUrl = loc.uri; + parent = new QStandardItem(); + treeModel->appendRow(parent); + } + auto item = new LineItem(m_mainWindow); + parent->appendRow(item); + // add partial display data; line will be added by item later on + item->setText(i18n("Line: %1: ", loc.range.start().line() + 1)); + fillItemRoles(item, loc.uri, loc.range, loc.kind, snapshot); + } + if (parent) + parent->setText(QStringLiteral("%1: %2").arg(lastUrl.path()).arg(parent->rowCount())); + + // plain heuristic; mark for auto-expand all when safe and/or useful to do so + if (treeModel->rowCount() <= 2 || locations.size() <= 20) { + treeModel->invisibleRootItem()->setData(true, RangeData::KindRole); + } + + m_ownedModel.reset(treeModel); + m_markModel = treeModel; + } + + void showTree(const QString & title, QPointer *targetTree) + { + // clean up previous target if any + if (targetTree && *targetTree) { + int index = m_tabWidget->indexOf(*targetTree); + if (index >= 0) + tabCloseRequested(index); + } + + // setup view + auto treeView = new QTreeView(); + configureTreeView(treeView); + + // transfer model from owned to tree and that in turn to tabwidget + auto treeModel = m_ownedModel.take(); + treeView->setModel(treeModel); + treeModel->setParent(treeView); + int index = m_tabWidget->addTab(treeView, title); + connect(treeView, &QTreeView::clicked, this, &self_type::goToItemLocation); + + if (treeModel->invisibleRootItem()->data(RangeData::KindRole).toBool()) { + treeView->expandAll(); + } + + // track for later cleanup + if (targetTree) + *targetTree = treeView; + + // activate the resulting tab + m_tabWidget->setCurrentIndex(index); + m_mainWindow->showToolView(m_toolView.data()); + } + + void showMessage(const QString & text, KTextEditor::Message::MessageType level) + { + KTextEditor::View *view = m_mainWindow->activeView(); + if (!view || !view->document()) return; + + auto kmsg = new KTextEditor::Message(text, level); + kmsg->setPosition(KTextEditor::Message::BottomInView); + kmsg->setAutoHide(500); + kmsg->setView(view); + view->document()->postMessage(kmsg); + } + + void handleEsc(QEvent *e) + { + if (!m_mainWindow) return; + + QKeyEvent *k = static_cast(e); + if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) { + if (!m_ranges.empty()) { + clearAllLocationMarks(); + } else if (m_toolView->isVisible()) { + m_mainWindow->hideToolView(m_toolView.data()); + } + } + } + + template + using LocationRequest = std::function; + + template + void positionRequest(const LocationRequest & req, const Handler & h, + QScopedPointer * snapshot = nullptr) + { + KTextEditor::View *activeView = m_mainWindow->activeView(); + auto server = m_serverManager->findServer(activeView); + if (!server) + return; + + // track revision if requested + if (snapshot) { + snapshot->reset(m_serverManager->snapshot(server.data())); + } + + KTextEditor::Cursor cursor = activeView->cursorPosition(); + + clearAllLocationMarks(); + m_req_timeout = false; + QTimer::singleShot(1000, this, [this] { m_req_timeout = true; }); + m_handle.cancel() = req(*server, activeView->document()->url(), + {cursor.line(), cursor.column()}, this, h); + } + + QString currentWord() + { + KTextEditor::View *activeView = m_mainWindow->activeView(); + if (activeView) { + KTextEditor::Cursor cursor = activeView->cursorPosition(); + return activeView->document()->wordAt(cursor); + } else { + return QString(); + } + } + + // some template and function type trickery here, but at least that buck stops here then ... + template>> + void processLocations(const QString & title, + const typename utils::identity>::type & req, bool onlyshow, + const std::function & itemConverter, + QPointer *targetTree = nullptr) + { + // no capture for move only using initializers available (yet), so shared outer type + // the additional level of indirection is so it can be 'filled-in' after lambda creation + QSharedPointer> s(new QScopedPointer); + auto h = [this, title, onlyshow, itemConverter, targetTree, s] (const QList & defs) + { + if (defs.count() == 0) { + showMessage(i18n("No results"), KTextEditor::Message::Information); + } else { + // convert to helper type + QVector ranges; + ranges.reserve(defs.size()); + for (const auto & def: defs) { + ranges.push_back(itemConverter(def)); + } + // ... so we can sort it also + std::stable_sort(ranges.begin(), ranges.end(), compareRangeItem); + makeTree(ranges, s.data()->data()); + + // assuming that reply ranges refer to revision when submitted + // (not specified anyway in protocol/reply) + if (defs.count() > 1 || onlyshow) { + showTree(title, targetTree); + } + // it's not nice to jump to some location if we are too late + if (!m_req_timeout && !onlyshow) { + // assuming here that the first location is the best one + const auto &item = itemConverter(defs.at(0)); + const auto &pos = item.range.start(); + goToDocumentLocation(item.uri, pos.line(), pos.column()); + } + // update marks + updateState(); + } + }; + + positionRequest(req, h, s.data()); + } + + static RangeItem + locationToRangeItem(const LSPLocation & loc) + { return {loc.uri, loc.range, LSPDocumentHighlightKind::Text}; } + + void goToDefinition() + { + auto title = i18nc("@title:tab", "Definition: %1", currentWord()); + processLocations(title, &LSPClientServer::documentDefinition, + false, &self_type::locationToRangeItem, &m_defTree); + } + + void goToDeclaration() + { + auto title = i18nc("@title:tab", "Declaration: %1", currentWord()); + processLocations(title, &LSPClientServer::documentDeclaration, + false, &self_type::locationToRangeItem, &m_declTree); + } + + void findReferences() + { + auto title = i18nc("@title:tab", "References: %1", currentWord()); + bool decl = m_refDeclaration->isChecked(); + auto req = [decl] (LSPClientServer & server, const QUrl & document, const LSPPosition & pos, + const QObject *context, const DocumentDefinitionReplyHandler & h) + { return server.documentReferences(document, pos, decl, context, h); }; + + processLocations(title, req, true, &self_type::locationToRangeItem); + } + + void highlight() + { + // determine current url to capture and use later on + QUrl url; + const KTextEditor::View* viewForRequest(m_mainWindow->activeView()); + if (viewForRequest && viewForRequest->document()) { + url = viewForRequest->document()->url(); + } + + auto title = i18nc("@title:tab", "Highlight: %1", currentWord()); + auto converter = [url] (const LSPDocumentHighlight & hl) + { return RangeItem {url, hl.range, hl.kind}; }; + + processLocations(title, &LSPClientServer::documentHighlight, true, converter); + } + + void hover() + { + // trigger manually the normally automagic hover + if (auto activeView = m_mainWindow->activeView()) { + m_hover->textHint(activeView, activeView->cursorPosition()); + } + } + + void applyEdits(KTextEditor::Document * doc, const LSPClientRevisionSnapshot * snapshot, + const QList & edits) + { + KTextEditor::MovingInterface* miface = qobject_cast(doc); + if (!miface) + return; + + // NOTE: + // server might be pretty sloppy wrt edits (e.g. python-language-server) + // e.g. send one edit for the whole document rather than 'surgical edits' + // and that even when requesting format for a limited selection + // ... but then we are but a client and do as we are told + // all-in-all a low priority feature + + // all coordinates in edits are wrt original document, + // so create moving ranges that will adjust to preceding edits as they are applied + QVector ranges; + for (const auto &edit: edits) { + auto range = snapshot ? transformRange(doc->url(), *snapshot, edit.range) : edit.range; + KTextEditor::MovingRange *mr = miface->newMovingRange(range); + ranges.append(mr); + } + + // now make one transaction (a.o. for one undo) and apply in sequence + { + KTextEditor::Document::EditingTransaction transaction(doc); + for (int i = 0; i < ranges.length(); ++i) { + doc->replaceText(ranges.at(i)->toRange(), edits.at(i).newText); + } + } + + qDeleteAll(ranges); + } + + void applyWorkspaceEdit(const LSPWorkspaceEdit & edit, const LSPClientRevisionSnapshot * snapshot) + { + auto currentView = m_mainWindow->activeView(); + for (auto it = edit.changes.begin(); it != edit.changes.end(); ++it) { + auto document = findDocument(m_mainWindow, it.key()); + if (!document) { + KTextEditor::View *view = m_mainWindow->openUrl(it.key()); + if (view) { + document = view->document(); + } + } + applyEdits(document, snapshot, it.value()); + } + if (currentView) { + m_mainWindow->activateView(currentView->document()); + } + } + + void onApplyEdit(const LSPApplyWorkspaceEditParams & edit, const ApplyEditReplyHandler & h, bool &handled) + { + if (handled) + return; + handled = true; + + if (m_accept_edit) { + qCInfo(LSPCLIENT) << "applying edit" << edit.label; + applyWorkspaceEdit(edit.edit, nullptr); + } else { + qCInfo(LSPCLIENT) << "ignoring edit"; + } + h({m_accept_edit, QString()}); + } + + template + void checkEditResult(const Collection & c) + { + if (c.size() == 0) { + showMessage(i18n("No edits"), KTextEditor::Message::Information); + } + } + + void delayCancelRequest(LSPClientServer::RequestHandle && h, int timeout_ms = 4000) + { + QTimer::singleShot(timeout_ms, this, [h] () mutable { h.cancel(); }); + } + + void format(QChar lastChar = QChar()) + { + KTextEditor::View *activeView = m_mainWindow->activeView(); + QPointer document = activeView->document(); + auto server = m_serverManager->findServer(activeView); + if (!server || !document) + return; + + int tabSize = 4; + bool insertSpaces = true; + auto cfgiface = qobject_cast(document); + if (cfgiface) { + tabSize = cfgiface->configValue(QStringLiteral("tab-width")).toInt(); + insertSpaces = cfgiface->configValue(QStringLiteral("replace-tabs")).toBool(); + } + + // sigh, no move initialization capture ... + // (again) assuming reply ranges wrt revisions submitted at this time + QSharedPointer snapshot(m_serverManager->snapshot(server.data())); + auto h = [this, document, snapshot, lastChar] (const QList & edits) + { + if (lastChar.isNull()) { + checkEditResult(edits); + } + if (document) { + applyEdits(document, snapshot.data(), edits); + } + }; + + auto options = LSPFormattingOptions {tabSize, insertSpaces, QJsonObject()}; + auto handle = !lastChar.isNull() ? + server->documentOnTypeFormatting(document->url(), activeView->cursorPosition(), + lastChar, options, this, h) : + (activeView->selection() ? + server->documentRangeFormatting(document->url(), activeView->selectionRange(), + options, this, h) : + server->documentFormatting(document->url(), + options, this, h)); + delayCancelRequest(std::move(handle)); + } + + void rename() + { + KTextEditor::View *activeView = m_mainWindow->activeView(); + QPointer document = activeView->document(); + auto server = m_serverManager->findServer(activeView); + if (!server || !document) + return; + + bool ok = false; + // results are typically (too) limited + // due to server implementation or limited view/scope + // so let's add a disclaimer that it's not our fault + QString newName = QInputDialog::getText(activeView, + i18nc("@title:window", "Rename"), + i18nc("@label:textbox", "New name (caution: not all references may be replaced)"), + QLineEdit::Normal, QString(), &ok); + if (!ok) { + return; + } + + QSharedPointer snapshot(m_serverManager->snapshot(server.data())); + auto h = [this, snapshot] (const LSPWorkspaceEdit & edit) + { + checkEditResult(edit.changes); + applyWorkspaceEdit(edit, snapshot.data()); + }; + auto handle = server->documentRename(document->url(), + activeView->cursorPosition(), newName, this, h); + delayCancelRequest(std::move(handle)); + } + + static QStandardItem* + getItem(const QStandardItemModel &model, const QUrl & url) + { + auto l = model.findItems(url.path()); + if (l.length()) { + return l.at(0); + } + return nullptr; + } + + // select/scroll to diagnostics item for document and (optionally) line + bool syncDiagnostics(KTextEditor::Document *document, int line, bool allowTop, bool doShow) + { + if (!m_diagnosticsTree) + return false; + + auto hint = QAbstractItemView::PositionAtTop; + QStandardItem *targetItem = nullptr; + QStandardItem *topItem = getItem(*m_diagnosticsModel, document->url()); + if (topItem) { + int count = topItem->rowCount(); + // let's not run wild on a linear search in a flood of diagnostics + // user is already in enough trouble as it is ;-) + if (count > 50) + count = 0; + for (int i = 0; i < count; ++i) { + auto item = topItem->child(i); + int itemline = item->data(RangeData::RangeRole).value().start().line(); + if (line == itemline && m_diagnosticsTree) { + targetItem = item; + hint = QAbstractItemView::PositionAtCenter; + break; + } + } + } + if (!targetItem && allowTop) { + targetItem = topItem; + } + if (targetItem) { + m_diagnosticsTree->blockSignals(true); + m_diagnosticsTree->scrollTo(targetItem->index(), hint); + m_diagnosticsTree->setCurrentIndex(targetItem->index()); + m_diagnosticsTree->blockSignals(false); + if (doShow) { + m_tabWidget->setCurrentWidget(m_diagnosticsTree); + m_mainWindow->showToolView(m_toolView.data()); + } + } + return targetItem != nullptr; + } + + void onViewState(KTextEditor::View *view, LSPClientViewTracker::State newState) + { + if (!view || !view->document()) + return; + + // select top item on view change, + // but otherwise leave selection unchanged if no match + switch(newState) { + case LSPClientViewTracker::ViewChanged: + syncDiagnostics(view->document(), view->cursorPosition().line(), true, false); + break; + case LSPClientViewTracker::LineChanged: + syncDiagnostics(view->document(), view->cursorPosition().line(), false, false); + break; + default: + // should not happen + break; + } + } + + Q_SLOT void onMarkClicked(KTextEditor::Document *document, KTextEditor::Mark mark, bool &handled) + { + // no action if no mark was sprinkled here + if (m_diagnosticsMarks.contains(document) && syncDiagnostics(document, mark.line, false, true)) { + handled = true; + } + } + + void onDiagnostics(const LSPPublishDiagnosticsParams & diagnostics) + { + if (!m_diagnosticsTree) + return; + + QStandardItemModel *model = m_diagnosticsModel.data(); + QStandardItem *topItem = getItem(*m_diagnosticsModel, diagnostics.uri); + + if (!topItem) { + // no need to create an empty one + if (diagnostics.diagnostics.size() == 0) { + return; + } + topItem = new QStandardItem(); + model->appendRow(topItem); + topItem->setText(diagnostics.uri.path()); + } else { + topItem->setRowCount(0); + } + + for (const auto & diag : diagnostics.diagnostics) { + auto item = new DiagnosticItem(diag); + topItem->appendRow(item); + QString source; + if (diag.source.length()) { + source = QStringLiteral("[%1] ").arg(diag.source); + } + item->setData(diagnosticsIcon(diag.severity), Qt::DecorationRole); + item->setText(source + diag.message); + fillItemRoles(item, diagnostics.uri, diag.range, diag.severity); + const auto &related = diag.relatedInformation; + if (!related.location.uri.isEmpty()) { + auto relatedItemMessage = new QStandardItem(); + relatedItemMessage->setText(related.message); + fillItemRoles(relatedItemMessage, related.location.uri, related.location.range, RangeData::KindEnum::Related); + auto relatedItemPath = new QStandardItem(); + auto basename = QFileInfo(related.location.uri.path()).fileName(); + relatedItemPath->setText(QStringLiteral("%1:%2").arg(basename).arg(related.location.range.start().line())); + item->appendRow({relatedItemMessage, relatedItemPath}); + m_diagnosticsTree->setExpanded(item->index(), true); + } + } + + // TODO perhaps add some custom delegate that only shows 1 line + // and only the whole text when item selected ?? + m_diagnosticsTree->setExpanded(topItem->index(), true); + m_diagnosticsTree->setRowHidden(topItem->row(), QModelIndex(), topItem->rowCount() == 0); + m_diagnosticsTree->scrollTo(topItem->index(), QAbstractItemView::PositionAtTop); + + auto header = m_diagnosticsTree->header(); + header->setStretchLastSection(false); + header->setMinimumSectionSize(0); + header->setSectionResizeMode(0, QHeaderView::Stretch); + header->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + updateState(); + } + + void onDocumentUrlChanged(KTextEditor::Document *doc) + { + // url already changed by this time and new url not useufl + (void)doc; + // note; url also changes when closed + // spec says; + // if a language has a project system, diagnostics are not cleared by *server* + // but in either case (url change or close); remove lingering diagnostics + // collect active urls + QSet fpaths; + for (const auto &view: m_mainWindow->views()) { + if (auto doc = view->document()) { + fpaths.insert(doc->url().path()); + } + } + // check and clear defunct entries + const auto &model = *m_diagnosticsModel; + for (int i = 0; i < model.rowCount(); ++i) { + auto item = model.item(i); + if (item && !fpaths.contains(item->text())) { + item->setRowCount(0); + if (m_diagnosticsTree) { + m_diagnosticsTree->setRowHidden(item->row(), QModelIndex(), true); + } + } + } + } + + void onTextChanged(KTextEditor::Document *doc) + { + if (m_onTypeFormattingTriggers.size() == 0) + return; + + KTextEditor::View *activeView = m_mainWindow->activeView(); + if (!activeView || activeView->document() != doc) + return; + + // NOTE the intendation mode should probably be set to None, + // so as not to experience unpleasant interference + auto cursor = activeView->cursorPosition(); + QChar lastChar = cursor.column() == 0 ? QChar::fromLatin1('\n') : + doc->characterAt({cursor.line(), cursor.column() - 1}); + if (m_onTypeFormattingTriggers.contains(lastChar)) { + format(lastChar); + } + } + + void updateState() + { + KTextEditor::View *activeView = m_mainWindow->activeView(); + auto doc = activeView ? activeView->document() : nullptr; + auto server = m_serverManager->findServer(activeView); + bool defEnabled = false, declEnabled = false, refEnabled = false; + bool hoverEnabled = false, highlightEnabled = false; + bool formatEnabled = false; + bool renameEnabled = false; + + if (server) { + const auto& caps = server->capabilities(); + defEnabled = caps.definitionProvider; + // FIXME no real official protocol way to detect, so enable anyway + declEnabled = caps.declarationProvider || true; + refEnabled = caps.referencesProvider; + hoverEnabled = caps.hoverProvider; + highlightEnabled = caps.documentHighlightProvider; + formatEnabled = caps.documentFormattingProvider || caps.documentRangeFormattingProvider; + renameEnabled = caps.renameProvider; + + connect(server.data(), &LSPClientServer::publishDiagnostics, + this, &self_type::onDiagnostics, Qt::UniqueConnection); + connect(server.data(), &LSPClientServer::applyEdit, + this, &self_type::onApplyEdit, Qt::UniqueConnection); + + // update format trigger characters + const auto & fmt = caps.documentOnTypeFormattingProvider; + if (fmt.provider && m_onTypeFormatting->isChecked()) { + m_onTypeFormattingTriggers = fmt.triggerCharacters; + } else { + m_onTypeFormattingTriggers.clear(); + } + // and monitor for such + if (doc) { + connect(doc, &KTextEditor::Document::textChanged, + this, &self_type::onTextChanged, Qt::UniqueConnection); + connect(doc, &KTextEditor::Document::documentUrlChanged, + this, &self_type::onDocumentUrlChanged, Qt::UniqueConnection); + } + } + + if (m_findDef) + m_findDef->setEnabled(defEnabled); + if (m_findDecl) + m_findDecl->setEnabled(declEnabled); + if (m_findRef) + m_findRef->setEnabled(refEnabled); + if (m_triggerHighlight) + m_triggerHighlight->setEnabled(highlightEnabled); + if (m_triggerHover) + m_triggerHover->setEnabled(hoverEnabled); + if (m_triggerFormat) + m_triggerFormat->setEnabled(formatEnabled); + if (m_triggerRename) + m_triggerRename->setEnabled(renameEnabled); + if (m_complDocOn) + m_complDocOn->setEnabled(server); + if (m_restartServer) + m_restartServer->setEnabled(server); + + // update completion with relevant server + m_completion->setServer(server); + if (m_complDocOn) + m_completion->setSelectedDocumentation(m_complDocOn->isChecked()); + updateCompletion(activeView, server.data()); + + // update hover with relevant server + m_hover->setServer(server); + updateHover(activeView, server.data()); + + // update marks if applicable + if (m_markModel && doc) + addMarks(doc, m_markModel, m_ranges, m_marks); + if (m_diagnosticsModel && doc) { + clearMarks(doc, m_diagnosticsRanges, m_diagnosticsMarks, RangeData::markTypeDiagAll); + addMarks(doc, m_diagnosticsModel.data(), m_diagnosticsRanges, m_diagnosticsMarks); + } + + // connect for cleanup stuff + if (activeView) + connect(activeView, &KTextEditor::View::destroyed, this, + &self_type::viewDestroyed, + Qt::UniqueConnection); + } + + void viewDestroyed(QObject *view) + { + m_completionViews.remove(static_cast(view)); + m_hoverViews.remove(static_cast(view)); + } + + void updateCompletion(KTextEditor::View *view, LSPClientServer * server) + { + bool registered = m_completionViews.contains(view); + + KTextEditor::CodeCompletionInterface *cci = qobject_cast(view); + + if (!cci) { + return; + } + + if (!registered && server && server->capabilities().completionProvider.provider) { + qCInfo(LSPCLIENT) << "registering cci"; + cci->registerCompletionModel(m_completion.data()); + m_completionViews.insert(view); + } + + if (registered && !server) { + qCInfo(LSPCLIENT) << "unregistering cci"; + cci->unregisterCompletionModel(m_completion.data()); + m_completionViews.remove(view); + } + } + + void updateHover(KTextEditor::View *view, LSPClientServer * server) + { + bool registered = m_hoverViews.contains(view); + + KTextEditor::TextHintInterface *cci = qobject_cast(view); + + if (!cci) { + return; + } + + if (!registered && server && server->capabilities().hoverProvider) { + qCInfo(LSPCLIENT) << "registering cci"; + cci->registerTextHintProvider(m_hover.data()); + m_hoverViews.insert(view); + } + + if (registered && !server) { + qCInfo(LSPCLIENT) << "unregistering cci"; + cci->unregisterTextHintProvider(m_hover.data()); + m_hoverViews.remove(view); + } + } +}; + + +class LSPClientPluginViewImpl : public QObject, public KXMLGUIClient +{ + Q_OBJECT + + typedef LSPClientPluginViewImpl self_type; + + KTextEditor::MainWindow *m_mainWindow; + QSharedPointer m_serverManager; + QScopedPointer m_actionView; + +public: + LSPClientPluginViewImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) + : QObject(mainWin), m_mainWindow(mainWin), + m_serverManager(LSPClientServerManager::new_(plugin, mainWin)), + m_actionView(new LSPClientActionView(plugin, mainWin, this, m_serverManager)) + { + KXMLGUIClient::setComponentName(QStringLiteral("lspclient"), i18n("LSP Client")); + setXMLFile(QStringLiteral("ui.rc")); + + m_mainWindow->guiFactory()->addClient(this); + } + + ~LSPClientPluginViewImpl() + { + // minimize/avoid some surprises; + // safe construction/destruction by separate (helper) objects; + // signals are auto-disconnected when high-level "view" objects are broken down + // so it only remains to clean up lowest level here then prior to removal + m_actionView.reset(); + m_serverManager.reset(); + m_mainWindow->guiFactory()->removeClient(this); + } +}; + + +QObject* +LSPClientPluginView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) +{ + return new LSPClientPluginViewImpl(plugin, mainWin); +} + +#include "lspclientpluginview.moc" diff --git a/addons/lspclient/lspclientpluginview.h b/addons/lspclient/lspclientpluginview.h new file mode 100644 index 000000000..47255a3ac --- /dev/null +++ b/addons/lspclient/lspclientpluginview.h @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef LSPCLIENTPLUGINVIEW_H +#define LSPCLIENTPLUGINVIEW_H + +#include + +class LSPClientPlugin; + +namespace KTextEditor { + class MainWindow; +} + +class LSPClientPluginView +{ +public: + // only needs a factory; no other public interface + static QObject* + new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin); +}; + +#endif + diff --git a/addons/lspclient/lspclientprotocol.h b/addons/lspclient/lspclientprotocol.h new file mode 100644 index 000000000..9730fdcfa --- /dev/null +++ b/addons/lspclient/lspclientprotocol.h @@ -0,0 +1,341 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef LSPCLIENTPROTOCOL_H +#define LSPCLIENTPROTOCOL_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// Following types roughly follow the types/interfaces as defined in LSP protocol spec +// although some deviation may arise where it has been deemed useful +// Moreover, to avoid introducing a custom 'optional' type, absence of an optional +// part/member is usually signalled by some 'invalid' marker (empty, negative). + +enum class LSPErrorCode +{ + // Defined by JSON RPC + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + serverErrorStart = -32099, + serverErrorEnd = -32000, + ServerNotInitialized = -32002, + UnknownErrorCode = -32001, + + // Defined by the protocol. + RequestCancelled = -32800, + ContentModified = -32801 +}; + +enum class LSPDocumentSyncKind +{ + None = 0, + Full = 1, + Incremental = 2 +}; + +struct LSPCompletionOptions +{ + bool provider = false; + bool resolveProvider = false; + QVector triggerCharacters; +}; + +struct LSPSignatureHelpOptions +{ + bool provider = false; + QVector triggerCharacters; +}; + +// ensure distinct type +struct LSPDocumentOnTypeFormattingOptions : public LSPSignatureHelpOptions {}; + +struct LSPServerCapabilities +{ + LSPDocumentSyncKind textDocumentSync = LSPDocumentSyncKind::None; + bool hoverProvider = false; + LSPCompletionOptions completionProvider; + LSPSignatureHelpOptions signatureHelpProvider; + bool definitionProvider = false; + // FIXME ? clangd unofficial extension + bool declarationProvider = false; + bool referencesProvider = false; + bool documentSymbolProvider = false; + bool documentHighlightProvider = false; + bool documentFormattingProvider = false; + bool documentRangeFormattingProvider = false; + LSPDocumentOnTypeFormattingOptions documentOnTypeFormattingProvider; + bool renameProvider = false; + // CodeActionOptions not useful/considered at present + bool codeActionProvider = false; +}; + +enum class LSPMarkupKind +{ + None = 0, + PlainText = 1, + MarkDown = 2 +}; + +struct LSPMarkupContent +{ + LSPMarkupKind kind = LSPMarkupKind::None; + QString value; +}; + +/** + * Language Server Protocol Position + * line + column, 0 based, negative for invalid + * maps 1:1 to KTextEditor::Cursor + */ +using LSPPosition = KTextEditor::Cursor; + +/** + * Language Server Protocol Range + * start + end tuple of LSPPosition + * maps 1:1 to KTextEditor::Range + */ +using LSPRange = KTextEditor::Range; + +struct LSPLocation +{ + QUrl uri; + LSPRange range; +}; + +struct LSPTextDocumentContentChangeEvent +{ + LSPRange range; + QString text; +}; + +enum class LSPDocumentHighlightKind +{ + Text = 1, + Read = 2, + Write = 3 +}; + +struct LSPDocumentHighlight +{ + LSPRange range; + LSPDocumentHighlightKind kind; +}; + +struct LSPHover +{ + // vector for contents to support all three variants: MarkedString | MarkedString[] | MarkupContent + // vector variant is still in use e.g. by Rust rls + QVector contents; + LSPRange range; +}; + +enum class LSPSymbolKind { + File = 1, + Module = 2, + Namespace = 3, + Package = 4, + Class = 5, + Method = 6, + Property = 7, + Field = 8, + Constructor = 9, + Enum = 10, + Interface = 11, + Function = 12, + Variable = 13, + Constant = 14, + String = 15, + Number = 16, + Boolean = 17, + Array = 18, +}; + +struct LSPSymbolInformation +{ + LSPSymbolInformation(const QString & _name, LSPSymbolKind _kind, + LSPRange _range, const QString & _detail) + : name(_name), detail(_detail), kind(_kind), range(_range) + {} + QString name; + QString detail; + LSPSymbolKind kind; + LSPRange range; + QList children; +}; + +enum class LSPCompletionItemKind +{ + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +}; + +struct LSPCompletionItem +{ + QString label; + LSPCompletionItemKind kind; + QString detail; + LSPMarkupContent documentation; + QString sortText; + QString insertText; +}; + +struct LSPParameterInformation +{ + // offsets into overall signature label + // (-1 if invalid) + int start; + int end; +}; + +struct LSPSignatureInformation +{ + QString label; + LSPMarkupContent documentation; + QList parameters; +}; + +struct LSPSignatureHelp +{ + QList signatures; + int activeSignature; + int activeParameter; +}; + +struct LSPFormattingOptions +{ + int tabSize; + bool insertSpaces; + // additional fields + QJsonObject extra; +}; + +struct LSPTextEdit +{ + LSPRange range; + QString newText; +}; + +enum class LSPDiagnosticSeverity +{ + Unknown = 0, + Error = 1, + Warning = 2, + Information = 3, + Hint = 4, +}; + +struct LSPDiagnosticRelatedInformation +{ + // empty url / invalid range when absent + LSPLocation location; + QString message; +}; + +struct LSPDiagnostic +{ + LSPRange range; + LSPDiagnosticSeverity severity; + QString code; + QString source; + QString message; + LSPDiagnosticRelatedInformation relatedInformation; +}; + +struct LSPPublishDiagnosticsParams +{ + QUrl uri; + QList diagnostics; +}; + +struct LSPCommand +{ + QString title; + QString command; + // pretty opaque + QJsonArray arguments; +}; + +struct LSPWorkspaceEdit +{ + // supported part for now + QHash> changes; +}; + +struct LSPCodeAction +{ + QString title; + QString kind; + QList diagnostics; + LSPWorkspaceEdit edit; + LSPCommand command; +}; + +struct LSPApplyWorkspaceEditParams +{ + QString label; + LSPWorkspaceEdit edit; +}; + +struct LSPApplyWorkspaceEditResponse +{ + bool applied; + QString failureReason; +}; + +#endif diff --git a/addons/lspclient/lspclientserver.cpp b/addons/lspclient/lspclientserver.cpp new file mode 100644 index 000000000..61fd35c12 --- /dev/null +++ b/addons/lspclient/lspclientserver.cpp @@ -0,0 +1,1384 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "lspclientserver.h" + +#include "lspclient_debug.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +// good/bad old school; allows easier concatenate +#define CONTENT_LENGTH "Content-Length" + +static const QString MEMBER_ID = QStringLiteral("id"); +static const QString MEMBER_METHOD = QStringLiteral("method"); +static const QString MEMBER_ERROR = QStringLiteral("error"); +static const QString MEMBER_CODE = QStringLiteral("code"); +static const QString MEMBER_MESSAGE = QStringLiteral("message"); +static const QString MEMBER_PARAMS = QStringLiteral("params"); +static const QString MEMBER_RESULT = QStringLiteral("result"); +static const QString MEMBER_URI = QStringLiteral("uri"); +static const QString MEMBER_VERSION = QStringLiteral("version"); +static const QString MEMBER_START = QStringLiteral("start"); +static const QString MEMBER_END = QStringLiteral("end"); +static const QString MEMBER_POSITION = QStringLiteral("position"); +static const QString MEMBER_LOCATION = QStringLiteral("location"); +static const QString MEMBER_RANGE = QStringLiteral("range"); +static const QString MEMBER_LINE = QStringLiteral("line"); +static const QString MEMBER_CHARACTER = QStringLiteral("character"); +static const QString MEMBER_KIND = QStringLiteral("kind"); +static const QString MEMBER_TEXT = QStringLiteral("text"); +static const QString MEMBER_LANGID = QStringLiteral("languageId"); +static const QString MEMBER_LABEL = QStringLiteral("label"); +static const QString MEMBER_DOCUMENTATION = QStringLiteral("documentation"); +static const QString MEMBER_DETAIL = QStringLiteral("detail"); +static const QString MEMBER_COMMAND = QStringLiteral("command"); +static const QString MEMBER_EDIT = QStringLiteral("edit"); +static const QString MEMBER_TITLE = QStringLiteral("title"); +static const QString MEMBER_ARGUMENTS = QStringLiteral("arguments"); +static const QString MEMBER_DIAGNOSTICS = QStringLiteral("diagnostics"); + +// message construction helpers +static QJsonObject +to_json(const LSPPosition & pos) +{ + return QJsonObject { + { MEMBER_LINE, pos.line() }, + { MEMBER_CHARACTER, pos.column() } + }; +} + +static QJsonObject +to_json(const LSPRange & range) +{ + return QJsonObject { + { MEMBER_START, to_json(range.start()) }, + { MEMBER_END, to_json(range.end()) } + }; +} + +static QJsonValue +to_json(const LSPLocation & location) +{ + if (location.uri.isValid()) { + return QJsonObject { + { MEMBER_URI, location.uri.toString() }, + { MEMBER_RANGE, to_json(location.range) } + }; + } + return QJsonValue(); +} + +static QJsonValue +to_json(const LSPDiagnosticRelatedInformation & related) +{ + auto loc = to_json(related.location); + if (loc.isObject()) { + return QJsonObject { + { MEMBER_LOCATION, to_json(related.location) }, + { MEMBER_MESSAGE, related.message } + }; + } + return QJsonValue(); +} + +static QJsonObject +to_json(const LSPDiagnostic & diagnostic) +{ + // required + auto result = QJsonObject(); + result[MEMBER_RANGE] = to_json(diagnostic.range); + result[MEMBER_MESSAGE] = diagnostic.message; + // optional + if (!diagnostic.code.isEmpty()) + result[QStringLiteral("code")] = diagnostic.code; + if (diagnostic.severity != LSPDiagnosticSeverity::Unknown) + result[QStringLiteral("severity")] = (int) diagnostic.severity; + if (!diagnostic.source.isEmpty()) + result[QStringLiteral("source")] = diagnostic.source; + auto related = to_json(diagnostic.relatedInformation); + if (related.isObject()) { + result[QStringLiteral("relatedInformation")] = related; + } + return result; +} + +static QJsonArray +to_json(const QList & changes) +{ + QJsonArray result; + for (const auto &change: changes) { + result.push_back(QJsonObject { + {MEMBER_RANGE, to_json(change.range)}, + {MEMBER_TEXT, change.text} + }); + } + return result; +} + +static QJsonObject +versionedTextDocumentIdentifier(const QUrl & document, int version = -1) +{ + QJsonObject map { { MEMBER_URI, document.toString() } }; + if (version >= 0) + map[MEMBER_VERSION] = version; + return map; +} + +static QJsonObject +textDocumentItem(const QUrl & document, const QString & lang, + const QString & text, int version) +{ + auto map = versionedTextDocumentIdentifier(document, version); + map[MEMBER_TEXT] = text; + map[MEMBER_LANGID] = lang; + return map; +} + +static QJsonObject +textDocumentParams(const QJsonObject & m) +{ + return QJsonObject { + { QStringLiteral("textDocument"), m} + }; +} + +static QJsonObject +textDocumentParams(const QUrl & document, int version = -1) +{ return textDocumentParams(versionedTextDocumentIdentifier(document, version)); } + +static QJsonObject +textDocumentPositionParams(const QUrl & document, LSPPosition pos) +{ + auto params = textDocumentParams(document); + params[MEMBER_POSITION] = to_json(pos); + return params; +} + +static QJsonObject +referenceParams(const QUrl & document, LSPPosition pos, bool decl) +{ + auto params = textDocumentPositionParams(document, pos); + params[QStringLiteral("context")] = QJsonObject { + { QStringLiteral("includeDeclaration"), decl } + }; + return params; +} + +static QJsonObject +formattingOptions(const LSPFormattingOptions & _options) +{ + auto options = _options.extra; + options[QStringLiteral("tabSize")] = _options.tabSize; + options[QStringLiteral("insertSpaces")] = _options.insertSpaces; + return options; +} + +static QJsonObject +documentRangeFormattingParams(const QUrl & document, const LSPRange *range, + const LSPFormattingOptions & _options) +{ + auto params = textDocumentParams(document); + if (range) { + params[MEMBER_RANGE] = to_json(*range); + } + params[QStringLiteral("options")] = formattingOptions(_options); + return params; +} + +static QJsonObject +documentOnTypeFormattingParams(const QUrl & document, const LSPPosition & pos, + const QChar & lastChar, const LSPFormattingOptions & _options) +{ + auto params = textDocumentPositionParams(document, pos); + params[QStringLiteral("ch")] = QString(lastChar); + params[QStringLiteral("options")] = formattingOptions(_options); + return params; +} + +static QJsonObject +renameParams(const QUrl & document, const LSPPosition & pos, const QString & newName) +{ + auto params = textDocumentPositionParams(document, pos); + params[QStringLiteral("newName")] = newName; + return params; +} + +static QJsonObject +codeActionParams(const QUrl & document, const LSPRange & range, + QList kinds, QList diagnostics) +{ + auto params = textDocumentParams(document); + params[MEMBER_RANGE] = to_json(range); + QJsonObject context; + QJsonArray diags; + for (const auto& diagnostic: diagnostics) { + diags.push_back(to_json(diagnostic)); + } + context[MEMBER_DIAGNOSTICS] = diags; + if (kinds.length()) + context[QStringLiteral("only")] = QJsonArray::fromStringList(kinds); + params[QStringLiteral("context")] = context; + return params; +} + +static QJsonObject +executeCommandParams(const QString & command, const QJsonValue & args) +{ + return QJsonObject { + { MEMBER_COMMAND, command }, + { MEMBER_ARGUMENTS, args } + }; +} + +static QJsonObject +applyWorkspaceEditResponse(const LSPApplyWorkspaceEditResponse & response) +{ + return QJsonObject { + { QStringLiteral("applied"), response.applied }, + { QStringLiteral("failureReason"), response.failureReason } + }; +} + +static void +from_json(QVector & trigger, const QJsonValue & json) +{ + for (const auto & t : json.toArray()) { + auto st = t.toString(); + if (st.length()) + trigger.push_back(st.at(0)); + } +} + +static void +from_json(LSPCompletionOptions & options, const QJsonValue & json) +{ + if (json.isObject()) { + auto ob = json.toObject(); + options.provider = true; + options.resolveProvider = ob.value(QStringLiteral("resolveProvider")).toBool(); + from_json(options.triggerCharacters, ob.value(QStringLiteral("triggerCharacters"))); + } +} + +static void +from_json(LSPSignatureHelpOptions & options, const QJsonValue & json) +{ + if (json.isObject()) { + auto ob = json.toObject(); + options.provider = true; + from_json(options.triggerCharacters, ob.value(QStringLiteral("triggerCharacters"))); + } +} + +static void +from_json(LSPDocumentOnTypeFormattingOptions & options, const QJsonValue & json) +{ + if (json.isObject()) { + auto ob = json.toObject(); + options.provider = true; + from_json(options.triggerCharacters, ob.value(QStringLiteral("moreTriggerCharacter"))); + auto trigger = ob.value(QStringLiteral("firstTriggerCharacter")).toString(); + if (trigger.size()) { + options.triggerCharacters.insert(0, trigger.at(0)); + } + } +} + +static void +from_json(LSPServerCapabilities & caps, const QJsonObject & json) +{ + auto sync = json.value(QStringLiteral("textDocumentSync")); + caps.textDocumentSync = (LSPDocumentSyncKind) + (sync.isObject() ? sync.toObject().value(QStringLiteral("change")) : sync).toInt((int)LSPDocumentSyncKind::None); + caps.hoverProvider = json.value(QStringLiteral("hoverProvider")).toBool(); + from_json(caps.completionProvider, json.value(QStringLiteral("completionProvider"))); + from_json(caps.signatureHelpProvider, json.value(QStringLiteral("signatureHelpProvider"))); + caps.definitionProvider = json.value(QStringLiteral("definitionProvider")).toBool(); + caps.declarationProvider = json.value(QStringLiteral("declarationProvider")).toBool(); + caps.referencesProvider = json.value(QStringLiteral("referencesProvider")).toBool(); + caps.documentSymbolProvider = json.value(QStringLiteral("documentSymbolProvider")).toBool(); + caps.documentHighlightProvider = json.value(QStringLiteral("documentHighlightProvider")).toBool(); + caps.documentFormattingProvider = json.value(QStringLiteral("documentFormattingProvider")).toBool(); + caps.documentRangeFormattingProvider = json.value(QStringLiteral("documentRangeFormattingProvider")).toBool(); + from_json(caps.documentOnTypeFormattingProvider, json.value(QStringLiteral("documentOnTypeFormattingProvider"))); + caps.renameProvider = json.value(QStringLiteral("renameProvider")).toBool(); + auto codeActionProvider = json.value(QStringLiteral("codeActionProvider")); + caps.codeActionProvider = codeActionProvider.toBool() || codeActionProvider.isObject(); +} + +// follow suit; as performed in kate docmanager +// normalize at this stage/layer to avoid surprises elsewhere +// sadly this is not a single QUrl method as one might hope ... +static QUrl +normalizeUrl(const QUrl & url) +{ + // Resolve symbolic links for local files (done anyway in KTextEditor) + if (url.isLocalFile()) { + QString normalizedUrl = QFileInfo(url.toLocalFile()).canonicalFilePath(); + if (!normalizedUrl.isEmpty()) { + return QUrl::fromLocalFile(normalizedUrl); + } + } + + // else: cleanup only the .. stuff + return url.adjusted(QUrl::NormalizePathSegments); +} + +static LSPMarkupContent +parseMarkupContent(const QJsonValue & v) +{ + LSPMarkupContent ret; + if (v.isObject()) { + const auto& vm = v.toObject(); + ret.value = vm.value(QStringLiteral("value")).toString(); + auto kind = vm.value(MEMBER_KIND).toString(); + if (kind == QStringLiteral("plaintext")) { + ret.kind = LSPMarkupKind::PlainText; + } else if (kind == QStringLiteral("markdown")) { + ret.kind = LSPMarkupKind::MarkDown; + } + } else if (v.isString()) { + ret.kind = LSPMarkupKind::PlainText; + ret.value = v.toString(); + } + return ret; +} + +static LSPPosition +parsePosition(const QJsonObject & m) +{ + auto line = m.value(MEMBER_LINE).toInt(-1); + auto column = m.value(MEMBER_CHARACTER).toInt(-1); + return {line, column}; +} + +static bool +isPositionValid(const LSPPosition & pos) +{ return pos.isValid(); } + +static LSPRange +parseRange(const QJsonObject & range) +{ + auto startpos = parsePosition(range.value(MEMBER_START).toObject()); + auto endpos = parsePosition(range.value(MEMBER_END).toObject()); + return {startpos, endpos}; +} + +static LSPLocation +parseLocation(const QJsonObject & loc) +{ + auto uri = normalizeUrl(QUrl(loc.value(MEMBER_URI).toString())); + auto range = parseRange(loc.value(MEMBER_RANGE).toObject()); + return {QUrl(uri), range}; +} + +static LSPDocumentHighlight +parseDocumentHighlight(const QJsonValue & result) +{ + auto hover = result.toObject(); + auto range = parseRange(hover.value(MEMBER_RANGE).toObject()); + auto kind = (LSPDocumentHighlightKind)hover.value(MEMBER_KIND).toInt((int)LSPDocumentHighlightKind::Text); // default is DocumentHighlightKind.Text + return {range, kind}; +} + +static QList +parseDocumentHighlightList(const QJsonValue & result) +{ + QList ret; + // could be array + if (result.isArray()) { + for (const auto & def : result.toArray()) { + ret.push_back(parseDocumentHighlight(def)); + } + } else if (result.isObject()) { + // or a single value + ret.push_back(parseDocumentHighlight(result)); + } + return ret; +} + +static LSPMarkupContent +parseHoverContentElement(const QJsonValue & contents) +{ + LSPMarkupContent result; + if (contents.isString()) { + result.value = contents.toString(); + } else { + // should be object, pretend so + auto cont = contents.toObject(); + auto text = cont.value(QStringLiteral("value")).toString(); + if (text.isEmpty()) { + // nothing to lose, try markdown + result = parseMarkupContent(contents); + } else { + result.value = text; + } + } + if (result.value.length()) + result.kind = LSPMarkupKind::PlainText; + return result; +} + +static LSPHover +parseHover(const QJsonValue & result) +{ + LSPHover ret; + auto hover = result.toObject(); + // normalize content which can be of many forms + ret.range = parseRange(hover.value(MEMBER_RANGE).toObject()); + auto contents = hover.value(QStringLiteral("contents")); + + // support the deprecated MarkedString[] variant, used by e.g. Rust rls + if (contents.isArray()) { + for (const auto & c : contents.toArray()) { + ret.contents.push_back(parseHoverContentElement(c)); + } + } else { + ret.contents.push_back(parseHoverContentElement(contents)); + } + + return ret; +} + +static QList +parseDocumentSymbols(const QJsonValue & result) +{ + // the reply could be old SymbolInformation[] or new (hierarchical) DocumentSymbol[] + // try to parse it adaptively in any case + // if new style, hierarchy is specified clearly in reply + // if old style, it is assumed the values enter linearly, that is; + // * a parent/container is listed before its children + // * if a name is defined/declared several times and then used as a parent, + // then it is the last instance that is used as a parent + + QList ret; + QMap index; + + std::function parseSymbol = + [&] (const QJsonObject & symbol, LSPSymbolInformation *parent) { + // if flat list, try to find parent by name + if (!parent) { + auto container = symbol.value(QStringLiteral("containerName")).toString(); + parent = index.value(container, nullptr); + } + auto list = parent ? &parent->children : &ret; + const auto& location = symbol.value(MEMBER_LOCATION).toObject(); + const auto& mrange = symbol.contains(MEMBER_RANGE) ? + symbol.value(MEMBER_RANGE) : location.value(MEMBER_RANGE); + auto range = parseRange(mrange.toObject()); + if (isPositionValid(range.start()) && isPositionValid(range.end())) { + auto name = symbol.value(QStringLiteral("name")).toString(); + auto kind = (LSPSymbolKind) symbol.value(MEMBER_KIND).toInt(); + auto detail = symbol.value(MEMBER_DETAIL).toString(); + list->push_back({name, kind, range, detail}); + index[name] = &list->back(); + // proceed recursively + for (const auto &child : symbol.value(QStringLiteral("children")).toArray()) + parseSymbol(child.toObject(), &list->back()); + } + }; + + for (const auto& info : result.toArray()) { + parseSymbol(info.toObject(), nullptr); + } + return ret; +} + +static QList +parseDocumentLocation(const QJsonValue & result) +{ + QList ret; + // could be array + if (result.isArray()) { + for (const auto & def : result.toArray()) { + ret.push_back(parseLocation(def.toObject())); + } + } else if (result.isObject()) { + // or a single value + ret.push_back(parseLocation(result.toObject())); + } + return ret; +} + +static QList +parseDocumentCompletion(const QJsonValue & result) +{ + QList ret; + QJsonArray items = result.toArray(); + // might be CompletionList + if (items.size() == 0) { + items = result.toObject().value(QStringLiteral("items")).toArray(); + } + for (const auto & vitem : items) { + const auto & item = vitem.toObject(); + auto label = item.value(MEMBER_LABEL).toString(); + auto detail = item.value(MEMBER_DETAIL).toString(); + auto doc = parseMarkupContent(item.value(MEMBER_DOCUMENTATION)); + auto sortText = item.value(QStringLiteral("sortText")).toString(); + if (sortText.isEmpty()) + sortText = label; + auto insertText = item.value(QStringLiteral("insertText")).toString(); + if (insertText.isEmpty()) + insertText = label; + auto kind = (LSPCompletionItemKind) item.value(MEMBER_KIND).toInt(); + ret.push_back({label, kind, detail, doc, sortText, insertText}); + } + return ret; +} + +static LSPSignatureInformation +parseSignatureInformation(const QJsonObject & json) +{ + LSPSignatureInformation info; + + info.label = json.value(MEMBER_LABEL).toString(); + info.documentation = parseMarkupContent(json.value(MEMBER_DOCUMENTATION)); + for (const auto & rpar : json.value(QStringLiteral("parameters")).toArray()) { + auto par = rpar.toObject(); + auto label = par.value(MEMBER_LABEL); + int begin = -1, end = -1; + if (label.isArray()) { + auto range = label.toArray(); + if (range.size() == 2) { + begin = range.at(0).toInt(-1); + end = range.at(1).toInt(-1); + if (begin > info.label.length()) + begin = -1; + if (end > info.label.length()) + end = -1; + } + } else { + auto sub = label.toString(); + if (sub.length()) { + begin = info.label.indexOf(sub); + if (begin >= 0) { + end = begin + sub.length(); + } + } + } + info.parameters.push_back({begin, end}); + } + return info; +} + +static LSPSignatureHelp +parseSignatureHelp(const QJsonValue & result) +{ + LSPSignatureHelp ret; + QJsonObject sig = result.toObject(); + + for (const auto & info: sig.value(QStringLiteral("signatures")).toArray()) { + ret.signatures.push_back(parseSignatureInformation(info.toObject())); + } + ret.activeSignature = sig.value(QStringLiteral("activeSignature")).toInt(0); + ret.activeParameter = sig.value(QStringLiteral("activeParameter")).toInt(0); + ret.activeSignature = qMin(qMax(ret.activeSignature, 0), ret.signatures.size()); + ret.activeParameter = qMin(qMax(ret.activeParameter, 0), ret.signatures.size()); + return ret; +} + +static QList +parseTextEdit(const QJsonValue & result) +{ + QList ret; + for (const auto &redit: result.toArray()) { + auto edit = redit.toObject(); + auto text = edit.value(QStringLiteral("newText")).toString(); + auto range = parseRange(edit.value(MEMBER_RANGE).toObject()); + ret.push_back({range, text}); + } + return ret; +} + +static LSPWorkspaceEdit +parseWorkSpaceEdit(const QJsonValue & result) +{ + QHash> ret; + auto changes = result.toObject().value(QStringLiteral("changes")).toObject(); + for (auto it = changes.begin(); it != changes.end(); ++it) { + ret.insert(normalizeUrl(QUrl(it.key())), parseTextEdit(it.value())); + } + return {ret}; +} + +static LSPCommand +parseCommand(const QJsonObject & result) +{ + auto title = result.value(MEMBER_TITLE).toString(); + auto command = result.value(MEMBER_COMMAND).toString(); + auto args = result.value(MEMBER_ARGUMENTS).toArray(); + return { title, command, args }; +} + +static QList +parseDiagnostics(const QJsonArray &result) +{ + QList ret; + for (const auto & vdiag : result) { + auto diag = vdiag.toObject(); + auto range = parseRange(diag.value(MEMBER_RANGE).toObject()); + auto severity = (LSPDiagnosticSeverity) diag.value(QStringLiteral("severity")).toInt(); + auto code = diag.value(QStringLiteral("code")).toString(); + auto source = diag.value(QStringLiteral("source")).toString(); + auto message = diag.value(MEMBER_MESSAGE).toString(); + auto related = diag.value(QStringLiteral("relatedInformation")).toObject(); + auto relLocation = parseLocation(related.value(MEMBER_LOCATION).toObject()); + auto relMessage = related.value(MEMBER_MESSAGE).toString(); + ret.push_back({range, severity, code, source, message, relLocation, relMessage}); + } + return ret; +} + +static QList +parseCodeAction(const QJsonValue & result) +{ + QList ret; + for (const auto &vaction: result.toArray()) { + auto action = vaction.toObject(); + // entry could be Command or CodeAction + if (!action.value(MEMBER_COMMAND).isString()) { + // CodeAction + auto title = action.value(MEMBER_TITLE).toString(); + auto kind = action.value(MEMBER_KIND).toString(); + auto command = parseCommand(action.value(MEMBER_COMMAND).toObject()); + auto edit = parseWorkSpaceEdit(action.value(MEMBER_EDIT)); + auto diagnostics = parseDiagnostics(action.value(MEMBER_DIAGNOSTICS).toArray()); + ret.push_back({title, kind, diagnostics, edit, command}); + } else { + // Command + auto command = parseCommand(action); + ret.push_back({command.title, QString(), {}, {}, command}); + } + } + return ret; +} + +static LSPPublishDiagnosticsParams +parseDiagnostics(const QJsonObject & result) +{ + LSPPublishDiagnosticsParams ret; + + ret.uri = normalizeUrl(QUrl(result.value(MEMBER_URI).toString())); + ret.diagnostics = parseDiagnostics(result.value(MEMBER_DIAGNOSTICS).toArray()); + return ret; +} + +static LSPApplyWorkspaceEditParams +parseApplyWorkspaceEditParams(const QJsonObject & result) +{ + LSPApplyWorkspaceEditParams ret; + + ret.label = result.value(MEMBER_LABEL).toString(); + ret.edit = parseWorkSpaceEdit(result.value(MEMBER_EDIT)); + return ret; +} + + +using GenericReplyType = QJsonValue; +using GenericReplyHandler = ReplyHandler; + +class LSPClientServer::LSPClientServerPrivate +{ + typedef LSPClientServerPrivate self_type; + + LSPClientServer *q; + // server cmd line + QStringList m_server; + // workspace root to pass along + QUrl m_root; + // user provided init + QJsonValue m_init; + // server process + QProcess m_sproc; + // server declared capabilites + LSPServerCapabilities m_capabilities; + // server state + State m_state = State::None; + // last msg id + int m_id = 0; + // receive buffer + QByteArray m_receive; + // registered reply handlers + QHash m_handlers; + // pending request responses + static constexpr int MAX_REQUESTS = 5; + QVector m_requests{MAX_REQUESTS + 1}; + +public: + LSPClientServerPrivate(LSPClientServer * _q, const QStringList & server, + const QUrl & root, const QJsonValue & init) + : q(_q), m_server(server), m_root(root), m_init(init) + { + // setup async reading + QObject::connect(&m_sproc, &QProcess::readyRead, utils::mem_fun(&self_type::read, this)); + QObject::connect(&m_sproc, &QProcess::stateChanged, utils::mem_fun(&self_type::onStateChanged, this)); + } + + ~LSPClientServerPrivate() + { + stop(TIMEOUT_SHUTDOWN, TIMEOUT_SHUTDOWN); + } + + const QStringList& cmdline() const + { + return m_server; + } + + State state() + { + return m_state; + } + + const LSPServerCapabilities& + capabilities() + { + return m_capabilities; + } + + int cancel(int reqid) + { + if (m_handlers.remove(reqid) > 0) { + auto params = QJsonObject { { MEMBER_ID, reqid } }; + write(init_request(QStringLiteral("$/cancelRequest"), params)); + } + return -1; + } + +private: + void setState(State s) + { + if (m_state != s) { + m_state = s; + emit q->stateChanged(q); + } + } + + RequestHandle write(const QJsonObject & msg, const GenericReplyHandler & h = nullptr, const int * id = nullptr) + { + RequestHandle ret; + ret.m_server = q; + + if (!running()) + return ret; + + auto ob = msg; + ob.insert(QStringLiteral("jsonrpc"), QStringLiteral("2.0")); + // notification == no handler + if (h) { + ob.insert(MEMBER_ID, ++m_id); + ret.m_id = m_id; + m_handlers[m_id] = h; + } else if (id) { + ob.insert(MEMBER_ID, *id); + } + + QJsonDocument json(ob); + auto sjson = json.toJson(); + + qCInfo(LSPCLIENT) << "calling" << msg[MEMBER_METHOD].toString(); + qCDebug(LSPCLIENT) << "sending message:\n" << QString::fromUtf8(sjson); + // some simple parsers expect length header first + auto hdr = QStringLiteral(CONTENT_LENGTH ": %1\r\n").arg(sjson.length()); + // write is async, so no blocking wait occurs here + m_sproc.write(hdr.toLatin1()); + m_sproc.write("\r\n"); + m_sproc.write(sjson); + + return ret; + } + + RequestHandle + send(const QJsonObject & msg, const GenericReplyHandler & h = nullptr) + { + if (m_state == State::Running) + return write(msg, h); + else + qCWarning(LSPCLIENT) << "send for non-running server"; + return RequestHandle(); + } + + void read() + { + // accumulate in buffer + m_receive.append(m_sproc.readAllStandardOutput()); + + // try to get one (or more) message + QByteArray &buffer = m_receive; + + while (true) { + qCDebug(LSPCLIENT) << "buffer size" << buffer.length(); + auto header = QByteArray(CONTENT_LENGTH ":"); + int index = buffer.indexOf(header); + if (index < 0) { + // avoid collecting junk + if (buffer.length() > 1 << 20) + buffer.clear(); + break; + } + index += header.length(); + int endindex = buffer.indexOf("\r\n", index); + auto msgstart = buffer.indexOf("\r\n\r\n", index); + if (endindex < 0 || msgstart < 0) + break; + msgstart += 4; + bool ok = false; + auto length = buffer.mid(index, endindex - index).toInt(&ok, 10); + // FIXME perhaps detect if no reply for some time + // then again possibly better left to user to restart in such case + if (!ok) { + qCWarning(LSPCLIENT) << "invalid " CONTENT_LENGTH; + // flush and try to carry on to some next header + buffer.remove(0, msgstart); + continue; + } + // sanity check to avoid extensive buffering + if (length > 1 << 29) { + qCWarning(LSPCLIENT) << "excessive size"; + buffer.clear(); + continue; + } + if (msgstart + length > buffer.length()) + break; + // now onto payload + auto payload = buffer.mid(msgstart, length); + buffer.remove(0, msgstart + length); + qCInfo(LSPCLIENT) << "got message payload size " << length; + qCDebug(LSPCLIENT) << "message payload:\n" << payload; + QJsonParseError error{}; + auto msg = QJsonDocument::fromJson(payload, &error); + if (error.error != QJsonParseError::NoError || !msg.isObject()) { + qCWarning(LSPCLIENT) << "invalid response payload"; + continue; + } + auto result = msg.object(); + // check if it is the expected result + int msgid = -1; + if (result.contains(MEMBER_ID)) { + msgid = result[MEMBER_ID].toInt(); + } else { + processNotification(result); + continue; + } + // could be request + if (result.contains(MEMBER_METHOD)) { + processRequest(result); + continue; + } + + // a valid reply; what to do with it now + auto it = m_handlers.find(msgid); + if (it != m_handlers.end()) { + // copy handler to local storage + const auto handler = *it; + + // remove handler from our set, do this pre handler execution to avoid races + m_handlers.erase(it); + + // run handler, might e.g. trigger some new LSP actions for this server + handler(result.value(MEMBER_RESULT)); + } else { + // could have been canceled + qCDebug(LSPCLIENT) << "unexpected reply id"; + } + } + } + + static QJsonObject + init_error(const LSPErrorCode code, const QString & msg) + { + return QJsonObject { + { MEMBER_ERROR, QJsonObject { + { MEMBER_CODE, (int) code }, + { MEMBER_MESSAGE, msg } + } + } + }; + } + + static QJsonObject + init_request(const QString & method, const QJsonObject & params = QJsonObject()) + { + return QJsonObject { + { MEMBER_METHOD, method }, + { MEMBER_PARAMS, params } + }; + } + + static QJsonObject + init_response(const QJsonValue & result = QJsonValue()) + { + return QJsonObject { + { MEMBER_RESULT, result } + }; + } + + bool running() + { + return m_sproc.state() == QProcess::Running; + } + + void onStateChanged(QProcess::ProcessState nstate) + { + if (nstate == QProcess::NotRunning) { + setState(State::None); + } + } + + void shutdown() + { + if (m_state == State::Running) { + qCInfo(LSPCLIENT) << "shutting down" << m_server; + // cancel all pending + m_handlers.clear(); + // shutdown sequence + send(init_request(QStringLiteral("shutdown"))); + // maybe we will get/see reply on the above, maybe not + // but not important or useful either way + send(init_request(QStringLiteral("exit"))); + // no longer fit for regular use + setState(State::Shutdown); + } + } + + void onInitializeReply(const QJsonValue & value) + { + // only parse parts that we use later on + from_json(m_capabilities, value.toObject().value(QStringLiteral("capabilities")).toObject()); + // finish init + initialized(); + } + + void initialize() + { + QJsonObject codeAction { + { QStringLiteral("codeActionLiteralSupport"), QJsonObject { + { QStringLiteral("codeActionKind"), QJsonObject { + { QStringLiteral("valueSet"), QJsonArray() } + } + } + } + } + }; + QJsonObject capabilities { + { QStringLiteral("textDocument"), + QJsonObject { + { QStringLiteral("documentSymbol"), + QJsonObject { { QStringLiteral("hierarchicalDocumentSymbolSupport"), true } }, + }, + { QStringLiteral("publishDiagnostics"), + QJsonObject { { QStringLiteral("relatedInformation"), true } } + }, + { QStringLiteral("codeAction"), codeAction } + } + } + }; + // NOTE a typical server does not use root all that much, + // other than for some corner case (in) requests + QJsonObject params { + { QStringLiteral("processId"), QCoreApplication::applicationPid() }, + { QStringLiteral("rootPath"), m_root.path() }, + { QStringLiteral("rootUri"), m_root.toString() }, + { QStringLiteral("capabilities"), capabilities }, + { QStringLiteral("initializationOptions"), m_init } + }; + // + write(init_request(QStringLiteral("initialize"), params), + utils::mem_fun(&self_type::onInitializeReply, this)); + } + + void initialized() + { + write(init_request(QStringLiteral("initialized"))); + setState(State::Running); + } + +public: + bool start() + { + if (m_state != State::None) + return true; + + auto program = m_server.front(); + auto args = m_server; + args.pop_front(); + qCInfo(LSPCLIENT) << "starting" << m_server << "with root" << m_root; + + // start LSP server in project root + m_sproc.setWorkingDirectory(m_root.path()); + + // at least we see some errors somewhere then + m_sproc.setProcessChannelMode(QProcess::ForwardedErrorChannel); + m_sproc.setReadChannel(QProcess::QProcess::StandardOutput); + m_sproc.start(program, args); + bool result = m_sproc.waitForStarted(); + if (!result) { + qCWarning(LSPCLIENT) << m_sproc.error(); + } else { + setState(State::Started); + // perform initial handshake + initialize(); + } + return result; + } + + void stop(int to_term, int to_kill) + { + if (running()) { + shutdown(); + if ((to_term >= 0) && !m_sproc.waitForFinished(to_term)) + m_sproc.terminate(); + if ((to_kill >= 0) && !m_sproc.waitForFinished(to_kill)) + m_sproc.kill(); + } + } + + RequestHandle documentSymbols(const QUrl & document, const GenericReplyHandler & h) + { + auto params = textDocumentParams(document); + return send(init_request(QStringLiteral("textDocument/documentSymbol"), params), h); + } + + RequestHandle documentDefinition(const QUrl & document, const LSPPosition & pos, + const GenericReplyHandler & h) + { + auto params = textDocumentPositionParams(document, pos); + return send(init_request(QStringLiteral("textDocument/definition"), params), h); + } + + RequestHandle documentDeclaration(const QUrl & document, const LSPPosition & pos, + const GenericReplyHandler & h) + { + auto params = textDocumentPositionParams(document, pos); + return send(init_request(QStringLiteral("textDocument/declaration"), params), h); + } + + RequestHandle documentHover(const QUrl & document, const LSPPosition & pos, + const GenericReplyHandler & h) + { + auto params = textDocumentPositionParams(document, pos); + return send(init_request(QStringLiteral("textDocument/hover"), params), h); + } + + RequestHandle documentHighlight(const QUrl & document, const LSPPosition & pos, + const GenericReplyHandler & h) + { + auto params = textDocumentPositionParams(document, pos); + return send(init_request(QStringLiteral("textDocument/documentHighlight"), params), h); + } + + RequestHandle documentReferences(const QUrl & document, const LSPPosition & pos, bool decl, + const GenericReplyHandler & h) + { + auto params = referenceParams(document, pos, decl); + return send(init_request(QStringLiteral("textDocument/references"), params), h); + } + + RequestHandle documentCompletion(const QUrl & document, const LSPPosition & pos, + const GenericReplyHandler & h) + { + auto params = textDocumentPositionParams(document, pos); + return send(init_request(QStringLiteral("textDocument/completion"), params), h); + } + + RequestHandle signatureHelp(const QUrl & document, const LSPPosition & pos, + const GenericReplyHandler & h) + { + auto params = textDocumentPositionParams(document, pos); + return send(init_request(QStringLiteral("textDocument/signatureHelp"), params), h); + } + + RequestHandle documentFormatting(const QUrl & document, const LSPFormattingOptions & options, + const GenericReplyHandler & h) + { + auto params = documentRangeFormattingParams(document, nullptr, options); + return send(init_request(QStringLiteral("textDocument/formatting"), params), h); + } + + RequestHandle documentRangeFormatting(const QUrl & document, const LSPRange & range, + const LSPFormattingOptions & options, const GenericReplyHandler & h) + { + auto params = documentRangeFormattingParams(document, &range, options); + return send(init_request(QStringLiteral("textDocument/rangeFormatting"), params), h); + } + + RequestHandle documentOnTypeFormatting(const QUrl & document, const LSPPosition & pos, + QChar lastChar, const LSPFormattingOptions & options, const GenericReplyHandler & h) + { + auto params = documentOnTypeFormattingParams(document, pos, lastChar, options); + return send(init_request(QStringLiteral("textDocument/onTypeFormatting"), params), h); + } + + RequestHandle documentRename(const QUrl & document, const LSPPosition & pos, + const QString newName, const GenericReplyHandler & h) + { + auto params = renameParams(document, pos, newName); + return send(init_request(QStringLiteral("textDocument/rename"), params), h); + } + + RequestHandle documentCodeAction(const QUrl & document, const LSPRange & range, + const QList & kinds, QList diagnostics, + const GenericReplyHandler & h) + { + auto params = codeActionParams(document, range, kinds, diagnostics); + return send(init_request(QStringLiteral("textDocument/codeAction"), params), h); + } + + void executeCommand(const QString & command, const QJsonValue & args) + { + auto params = executeCommandParams(command, args); + send(init_request(QStringLiteral("workspace/executeCommand"), params)); + } + + void didOpen(const QUrl & document, int version, const QString & langId, const QString & text) + { + auto params = textDocumentParams(textDocumentItem(document, langId, text, version)); + send(init_request(QStringLiteral("textDocument/didOpen"), params)); + } + + void didChange(const QUrl & document, int version, const QString & text, + const QList & changes) + { + Q_ASSERT(text.size() == 0 || changes.size() == 0); + auto params = textDocumentParams(document, version); + params[QStringLiteral("contentChanges")] = text.size() + ? QJsonArray { QJsonObject {{MEMBER_TEXT, text}} } + : to_json(changes); + send(init_request(QStringLiteral("textDocument/didChange"), params)); + } + + void didSave(const QUrl & document, const QString & text) + { + auto params = textDocumentParams(document); + params[QStringLiteral("text")] = text; + send(init_request(QStringLiteral("textDocument/didSave"), params)); + } + + void didClose(const QUrl & document) + { + auto params = textDocumentParams(document); + send(init_request(QStringLiteral("textDocument/didClose"), params)); + } + + void processNotification(const QJsonObject & msg) + { + auto method = msg[MEMBER_METHOD].toString(); + if (method == QStringLiteral("textDocument/publishDiagnostics")) { + emit q->publishDiagnostics(parseDiagnostics(msg[MEMBER_PARAMS].toObject())); + } else { + qCWarning(LSPCLIENT) << "discarding notification" << method; + } + } + + template + static GenericReplyHandler make_handler(const ReplyHandler & h, + const QObject *context, + typename utils::identity>::type c) + { + QPointer ctx(context); + return [ctx, h, c] (const GenericReplyType & m) { if (ctx) h(c(m)); }; + } + + GenericReplyHandler + prepareResponse(int msgid) + { + // allow limited number of outstanding requests + auto ctx = QPointer(q); + m_requests.push_back(msgid); + if (m_requests.size() > MAX_REQUESTS) { + m_requests.pop_front(); + } + auto h = [ctx, this, msgid] (const GenericReplyType & response) + { + if (!ctx) { + return; + } + auto index = m_requests.indexOf(msgid); + if (index >= 0) { + m_requests.remove(index); + write(init_response(response), nullptr, &msgid); + } else { + qCWarning(LSPCLIENT) << "discarding response" << msgid; + } + }; + return h; + } + + template + static ReplyHandler responseHandler(const GenericReplyHandler & h, + typename utils::identity>::type c) + { return [h, c] (const ReplyType & m) { h(c(m)); }; } + + // pretty rare and limited use, but anyway + void processRequest(const QJsonObject & msg) + { + auto method = msg[MEMBER_METHOD].toString(); + auto msgid = msg[MEMBER_ID].toInt(); + auto params = msg[MEMBER_PARAMS]; + bool handled = false; + if (method == QStringLiteral("workspace/applyEdit")) { + auto h = responseHandler(prepareResponse(msgid), applyWorkspaceEditResponse); + emit q->applyEdit(parseApplyWorkspaceEditParams(params.toObject()), h, handled); + } else { + write(init_error(LSPErrorCode::MethodNotFound, method), nullptr, &msgid); + qCWarning(LSPCLIENT) << "discarding request" << method; + } + } +}; + + +// generic convert handler +// sprinkle some connection-like context safety +// not so likely relevant/needed due to typical sequence of events, +// but in case the latter would be changed in surprising ways ... +template +static GenericReplyHandler make_handler(const ReplyHandler & h, + const QObject *context, + typename utils::identity>::type c) +{ + QPointer ctx(context); + return [ctx, h, c] (const GenericReplyType & m) { if (ctx) h(c(m)); }; +} + + +LSPClientServer::LSPClientServer(const QStringList & server, const QUrl & root, + const QJsonValue & init) + : d(new LSPClientServerPrivate(this, server, root, init)) +{} + +LSPClientServer::~LSPClientServer() +{ delete d; } + +const QStringList& LSPClientServer::cmdline() const +{ return d->cmdline(); } + +LSPClientServer::State LSPClientServer::state() const +{ return d->state(); } + +const LSPServerCapabilities& +LSPClientServer::capabilities() const +{ return d->capabilities(); } + +bool LSPClientServer::start() +{ return d->start(); } + +void LSPClientServer::stop(int to_t, int to_k) +{ return d->stop(to_t, to_k); } + +int LSPClientServer::cancel(int reqid) +{ return d->cancel(reqid); } + +LSPClientServer::RequestHandle +LSPClientServer::documentSymbols(const QUrl & document, const QObject *context, + const DocumentSymbolsReplyHandler & h) +{ return d->documentSymbols(document, make_handler(h, context, parseDocumentSymbols)); } + +LSPClientServer::RequestHandle +LSPClientServer::documentDefinition(const QUrl & document, const LSPPosition & pos, + const QObject *context, const DocumentDefinitionReplyHandler & h) +{ return d->documentDefinition(document, pos, make_handler(h, context, parseDocumentLocation)); } + +LSPClientServer::RequestHandle +LSPClientServer::documentDeclaration(const QUrl & document, const LSPPosition & pos, + const QObject *context, const DocumentDefinitionReplyHandler & h) +{ return d->documentDeclaration(document, pos, make_handler(h, context, parseDocumentLocation)); } + +LSPClientServer::RequestHandle +LSPClientServer::documentHover(const QUrl & document, const LSPPosition & pos, + const QObject *context, const DocumentHoverReplyHandler & h) +{ return d->documentHover(document, pos, make_handler(h, context, parseHover)); } + +LSPClientServer::RequestHandle +LSPClientServer::documentHighlight(const QUrl & document, const LSPPosition & pos, + const QObject *context, const DocumentHighlightReplyHandler & h) +{ return d->documentHighlight(document, pos, make_handler(h, context, parseDocumentHighlightList)); } + +LSPClientServer::RequestHandle +LSPClientServer::documentReferences(const QUrl & document, const LSPPosition & pos, bool decl, + const QObject *context, const DocumentDefinitionReplyHandler & h) +{ return d->documentReferences(document, pos, decl, make_handler(h, context, parseDocumentLocation)); } + +LSPClientServer::RequestHandle +LSPClientServer::documentCompletion(const QUrl & document, const LSPPosition & pos, + const QObject *context, const DocumentCompletionReplyHandler & h) +{ return d->documentCompletion(document, pos, make_handler(h, context, parseDocumentCompletion)); } + +LSPClientServer::RequestHandle +LSPClientServer::signatureHelp(const QUrl & document, const LSPPosition & pos, + const QObject *context, const SignatureHelpReplyHandler & h) +{ return d->signatureHelp(document, pos, make_handler(h, context, parseSignatureHelp)); } + +LSPClientServer::RequestHandle +LSPClientServer::documentFormatting(const QUrl & document, const LSPFormattingOptions & options, + const QObject *context, const FormattingReplyHandler & h) +{ return d->documentFormatting(document, options, make_handler(h, context, parseTextEdit)); } + +LSPClientServer::RequestHandle +LSPClientServer::documentRangeFormatting(const QUrl & document, const LSPRange & range, + const LSPFormattingOptions & options, + const QObject *context, const FormattingReplyHandler & h) +{ return d->documentRangeFormatting(document, range, options, make_handler(h, context, parseTextEdit)); } + +LSPClientServer::RequestHandle +LSPClientServer::documentOnTypeFormatting(const QUrl & document, const LSPPosition & pos, + const QChar lastChar, const LSPFormattingOptions & options, + const QObject *context, const FormattingReplyHandler & h) +{ return d->documentOnTypeFormatting(document, pos, lastChar, options, make_handler(h, context, parseTextEdit)); } + +LSPClientServer::RequestHandle +LSPClientServer::documentRename(const QUrl & document, const LSPPosition & pos, + const QString newName, + const QObject *context, const WorkspaceEditReplyHandler & h) +{ return d->documentRename(document, pos, newName, make_handler(h, context, parseWorkSpaceEdit)); } + +LSPClientServer::RequestHandle +LSPClientServer::documentCodeAction(const QUrl & document, const LSPRange & range, + const QList & kinds, QList diagnostics, + const QObject *context, const CodeActionReplyHandler & h) +{ return d->documentCodeAction(document, range, kinds, diagnostics, make_handler(h, context, parseCodeAction)); } + +void LSPClientServer::executeCommand(const QString & command, const QJsonValue & args) +{ return d->executeCommand(command, args); } + +void LSPClientServer::didOpen(const QUrl & document, int version, const QString & langId, const QString & text) +{ return d->didOpen(document, version, langId, text); } + +void LSPClientServer::didChange(const QUrl & document, int version, const QString & text, + const QList & changes) +{ return d->didChange(document, version, text, changes); } + +void LSPClientServer::didSave(const QUrl & document, const QString & text) +{ return d->didSave(document, text); } + +void LSPClientServer::didClose(const QUrl & document) +{ return d->didClose(document); } diff --git a/addons/lspclient/lspclientserver.h b/addons/lspclient/lspclientserver.h new file mode 100644 index 000000000..d2dccd1d3 --- /dev/null +++ b/addons/lspclient/lspclientserver.h @@ -0,0 +1,187 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef LSPCLIENTSERVER_H +#define LSPCLIENTSERVER_H + +#include "lspclientprotocol.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace utils +{ + +// template helper +// function bind helpers +template +inline std::function +mem_fun(R (T::*pm)(Args ...), Tp object) +{ + return [object, pm](Args... args) { + return (object->*pm)(std::forward(args)...); + }; +} + +template +inline std::function +mem_fun(R (T::*pm)(Args ...) const, Tp object) +{ + return [object, pm](Args... args) { + return (object->*pm)(std::forward(args)...); + }; +} + +// prevent argument deduction +template struct identity { typedef T type; }; + +} // namespace utils + + +static const int TIMEOUT_SHUTDOWN = 200; + +template +using ReplyHandler = std::function; + +using DocumentSymbolsReplyHandler = ReplyHandler>; +using DocumentDefinitionReplyHandler = ReplyHandler>; +using DocumentHighlightReplyHandler = ReplyHandler>; +using DocumentHoverReplyHandler = ReplyHandler; +using DocumentCompletionReplyHandler = ReplyHandler>; +using SignatureHelpReplyHandler = ReplyHandler; +using FormattingReplyHandler = ReplyHandler>; +using CodeActionReplyHandler = ReplyHandler>; +using WorkspaceEditReplyHandler = ReplyHandler; +using ApplyEditReplyHandler = ReplyHandler; + +class LSPClientServer : public QObject +{ + Q_OBJECT + +public: + enum class State + { + None, + Started, + Running, + Shutdown + }; + + class LSPClientServerPrivate; + class RequestHandle + { + friend class LSPClientServerPrivate; + QPointer m_server; + int m_id = -1; + public: + RequestHandle& cancel() + { + if (m_server) + m_server->cancel(m_id); + return *this; + } + }; + + LSPClientServer(const QStringList & server, const QUrl & root, + const QJsonValue & init = QJsonValue()); + ~LSPClientServer(); + + // server management + // request start + bool start(); + // request shutdown/stop + // if to_xxx >= 0 -> send signal if not exit'ed after timeout + void stop(int to_term_ms, int to_kill_ms); + int cancel(int id); + + // properties + const QStringList& cmdline() const; + State state() const; + Q_SIGNAL void stateChanged(LSPClientServer * server); + + const LSPServerCapabilities& capabilities() const; + + // language + RequestHandle documentSymbols(const QUrl & document, const QObject *context, + const DocumentSymbolsReplyHandler & h); + RequestHandle documentDefinition(const QUrl & document, const LSPPosition & pos, + const QObject *context, const DocumentDefinitionReplyHandler & h); + RequestHandle documentDeclaration(const QUrl & document, const LSPPosition & pos, + const QObject *context, const DocumentDefinitionReplyHandler & h); + RequestHandle documentHighlight(const QUrl & document, const LSPPosition & pos, + const QObject *context, const DocumentHighlightReplyHandler & h); + RequestHandle documentHover(const QUrl & document, const LSPPosition & pos, + const QObject *context, const DocumentHoverReplyHandler & h); + RequestHandle documentReferences(const QUrl & document, const LSPPosition & pos, bool decl, + const QObject *context, const DocumentDefinitionReplyHandler & h); + RequestHandle documentCompletion(const QUrl & document, const LSPPosition & pos, + const QObject *context, const DocumentCompletionReplyHandler & h); + RequestHandle signatureHelp(const QUrl & document, const LSPPosition & pos, + const QObject *context, const SignatureHelpReplyHandler & h); + + RequestHandle documentFormatting(const QUrl & document, const LSPFormattingOptions & options, + const QObject *context, const FormattingReplyHandler & h); + RequestHandle documentRangeFormatting(const QUrl & document, const LSPRange & range, + const LSPFormattingOptions & options, + const QObject *context, const FormattingReplyHandler & h); + RequestHandle documentOnTypeFormatting(const QUrl & document, const LSPPosition & pos, + QChar lastChar, const LSPFormattingOptions & options, + const QObject *context, const FormattingReplyHandler & h); + RequestHandle documentRename(const QUrl & document, const LSPPosition & pos, + const QString newName, + const QObject *context, const WorkspaceEditReplyHandler & h); + + RequestHandle documentCodeAction(const QUrl & document, const LSPRange & range, + const QList & kinds, QList diagnostics, + const QObject *context, const CodeActionReplyHandler & h); + void executeCommand(const QString & command, const QJsonValue & args); + + // sync + void didOpen(const QUrl & document, int version, const QString & langId, const QString & text); + // only 1 of text or changes should be non-empty and is considered + void didChange(const QUrl & document, int version, const QString & text, + const QList & changes = {}); + void didSave(const QUrl & document, const QString & text); + void didClose(const QUrl & document); + + // notifcation = signal +Q_SIGNALS: + void publishDiagnostics(const LSPPublishDiagnosticsParams & ); + + // request = signal + void applyEdit(const LSPApplyWorkspaceEditParams &req, const ApplyEditReplyHandler &h, bool &handled); + +private: + // pimpl data holder + LSPClientServerPrivate * const d; +}; + +#endif diff --git a/addons/lspclient/lspclientservermanager.cpp b/addons/lspclient/lspclientservermanager.cpp new file mode 100644 index 000000000..5769126b2 --- /dev/null +++ b/addons/lspclient/lspclientservermanager.cpp @@ -0,0 +1,784 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + * Some explanation here on server configuration JSON, pending such ending up + * in real user level documentation ... + * + * The default configuration in JSON format is roughly as follows; +{ + "global": + { + "root": null, + }, + "servers": + { + "Python": + { + "command": "python3 -m pyls --check-parent-process" + }, + "C": + { + "command": "clangd -log=verbose --background-index" + }, + "C++": + { + "use": "C++" + } + } +} + * (the "command" can be an array or a string, which is then split into array) + * + * From the above, the gist is presumably clear. In addition, each server + * entry object may also have an "initializationOptions" entry, which is passed + * along to the server as part of the 'initialize' method. A clangd-specific + * HACK^Hfeature uses this to add "compilationDatabasePath". + * + * Various stages of override/merge are applied; + * + user configuration (loaded from file) overrides (internal) default configuration + * + "lspclient" entry in projectMap overrides the above + * + the resulting "global" entry is used to supplement (not override) any server entry + * + * One server instance is used per (root, servertype) combination. + * If "root" is not specified, it default to the $HOME directory. If it is + * specified as an absolute path, then it used as-is, otherwise it is relative + * to the projectBase. For any document, the resulting "root" then determines + * whether or not a separate instance is needed. If so, the "root" is passed + * as rootUri/rootPath. + * + * In general, it is recommended to leave root unspecified, as it is not that + * important for a server (your mileage may vary though). Fewer instances + * are obviously more efficient, and they also have a 'wider' view than + * the view of many separate instances. + */ + +#include "lspclientservermanager.h" + +#include "lspclient_debug.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +// helper to find a proper root dir for the given document & file name that indicate the root dir +static QString rootForDocumentAndRootIndicationFileName(KTextEditor::Document *document, const QString &rootIndicationFileName) +{ + // search only feasible if document is local file + if (!document->url().isLocalFile()) { + return QString(); + } + + // search root upwards + QDir dir(QFileInfo(document->url().toLocalFile()).absolutePath()); + QSet seenDirectories; + while (!seenDirectories.contains(dir.absolutePath())) { + // update guard + seenDirectories.insert(dir.absolutePath()); + + // the file that indicates the root dir is there => all fine + if (dir.exists(rootIndicationFileName)) { + return dir.absolutePath(); + } + + // else: cd up, if possible or abort + if (!dir.cdUp()) { + break; + } + } + + // no root found, bad luck + return QString(); +} + +#include + +// local helper; +// recursively merge top json top onto bottom json +static QJsonObject +merge(const QJsonObject & bottom, const QJsonObject & top) +{ + QJsonObject result; + for (auto item = top.begin(); item != top.end(); item++) { + const auto & key = item.key(); + if (item.value().isObject()) { + result.insert(key, merge(bottom.value(key).toObject(), item.value().toObject())); + } else { + result.insert(key, item.value()); + } + } + // parts only in bottom + for (auto item = bottom.begin(); item != bottom.end(); item++) { + if (!result.contains(item.key())) { + result.insert(item.key(), item.value()); + } + } + return result; +} + +// map (highlight)mode to lsp languageId +static QString +languageId(const QString & mode) +{ + // special cases + static QHash m; + auto it = m.find(mode); + if (it != m.end()) { + return *it; + } + // assume sane naming + QString result = mode.toLower(); + result = result.replace(QStringLiteral("++"), QStringLiteral("pp")); + return result.replace(QStringLiteral("#"), QStringLiteral("sharp")); +} + +// helper guard to handle revision (un)lock +struct RevisionGuard +{ + QPointer m_doc; + KTextEditor::MovingInterface *m_movingInterface = nullptr; + qint64 m_revision = -1; + + RevisionGuard(KTextEditor::Document *doc = nullptr) : + m_doc(doc), + m_movingInterface(qobject_cast(doc)), + m_revision(-1) + { + if (m_movingInterface) { + m_revision = m_movingInterface->revision(); + m_movingInterface->lockRevision(m_revision); + } + } + + // really only need/allow this one (out of 5) + RevisionGuard(RevisionGuard && other) : RevisionGuard(nullptr) + { + std::swap(m_doc, other.m_doc); + std::swap(m_movingInterface, other.m_movingInterface); + std::swap(m_revision, other.m_revision); + } + + void release() + { + m_movingInterface = nullptr; + m_revision = -1; + } + + ~RevisionGuard() + { + // NOTE: hopefully the revision is still valid at this time + if (m_doc && m_movingInterface && m_revision >= 0) { + m_movingInterface->unlockRevision(m_revision); + } + } +}; + + +class LSPClientRevisionSnapshotImpl : public LSPClientRevisionSnapshot +{ + Q_OBJECT + + typedef LSPClientRevisionSnapshotImpl self_type; + + // std::map has more relaxed constraints on value_type + std::map m_guards; + + Q_SLOT + void clearRevisions(KTextEditor::Document *doc) + { + for (auto &item: m_guards) { + if (item.second.m_doc == doc) { + item.second.release(); + } + } + } + +public: + void add(KTextEditor::Document *doc) + { + Q_ASSERT(doc); + + // make sure revision is cleared when needed and no longer used (to unlock or otherwise) + // see e.g. implementation in katetexthistory.cpp and assert's in place there + auto conn = connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), + this, SLOT(clearRevisions(KTextEditor::Document*))); + Q_ASSERT(conn); + conn = connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*)), + this, SLOT(clearRevisions(KTextEditor::Document*))); + Q_ASSERT(conn); + m_guards.emplace(doc->url(), doc); + } + + void find(const QUrl & url, KTextEditor::MovingInterface* & miface, qint64 & revision) const override + { + auto it = m_guards.find(url); + if (it != m_guards.end()) { + miface = it->second.m_movingInterface; + revision = it->second.m_revision; + } else { + miface = nullptr; + revision = -1; + } + } +}; + +// helper class to sync document changes to LSP server +class LSPClientServerManagerImpl : public LSPClientServerManager +{ + Q_OBJECT + + typedef LSPClientServerManagerImpl self_type; + + struct DocumentInfo + { + QSharedPointer server; + KTextEditor::MovingInterface *movingInterface; + QUrl url; + qint64 version; + bool open:1; + bool modified:1; + // used for incremental update (if non-empty) + QList changes; + }; + + LSPClientPlugin *m_plugin; + KTextEditor::MainWindow *m_mainWindow; + // merged default and user config + QJsonObject m_serverConfig; + // root -> (mode -> server) + QMap>> m_servers; + QHash m_docs; + bool m_incrementalSync = false; + + typedef QVector> ServerList; + +public: + LSPClientServerManagerImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) + : m_plugin(plugin) , m_mainWindow(mainWin) + { + connect(plugin, &LSPClientPlugin::update, this, &self_type::updateServerConfig); + QTimer::singleShot(100, this, &self_type::updateServerConfig); + } + + ~LSPClientServerManagerImpl() + { + // stop everything as we go down + // several stages; + // stage 1; request shutdown of all servers (in parallel) + // (give that some time) + // stage 2; send TERM + // stage 3; send KILL + + // stage 1 + QEventLoop q; + QTimer t; + connect(&t, &QTimer::timeout, &q, &QEventLoop::quit); + + auto run = [&q, &t] (int ms) { + t.setSingleShot(true); + t.start(ms); + q.exec(); + }; + + int count = 0; + for (const auto & el: m_servers) { + for (const auto & s: el) { + disconnect(s.data(), nullptr, this, nullptr); + if (s->state() != LSPClientServer::State::None) { + auto handler = [&q, &count, s] () { + if (s->state() != LSPClientServer::State::None) { + if (--count == 0) { + q.quit(); + } + } + }; + connect(s.data(), &LSPClientServer::stateChanged, this, handler); + ++count; + s->stop(-1, -1); + } + } + } + run(500); + + // stage 2 and 3 + count = 0; + for (count = 0; count < 2; ++count) { + for (const auto & el: m_servers) { + for (const auto & s: el) { + s->stop(count == 0 ? 1 : -1, count == 0 ? -1 : 1); + } + } + run(100); + } + } + + void setIncrementalSync(bool inc) override + { m_incrementalSync = inc; } + + QSharedPointer + findServer(KTextEditor::Document *document, bool updatedoc = true) override + { + if (!document || document->url().isEmpty()) + return nullptr; + + auto it = m_docs.find(document); + auto server = it != m_docs.end() ? it->server : nullptr; + if (!server) { + if ((server = _findServer(document))) + trackDocument(document, server); + } + + if (server && updatedoc) + update(server.data(), false); + return server; + } + + QSharedPointer + findServer(KTextEditor::View *view, bool updatedoc = true) override + { return view ? findServer(view->document(), updatedoc) : nullptr; } + + // restart a specific server or all servers if server == nullptr + void restart(LSPClientServer * server) override + { + ServerList servers; + // find entry for server(s) and move out + for (auto & m : m_servers) { + for (auto it = m.begin() ; it != m.end(); ) { + if (!server || it->data() == server) { + servers.push_back(*it); + it = m.erase(it); + } else { + ++it; + } + } + } + restart(servers); + } + + virtual qint64 revision(KTextEditor::Document *doc) override + { + auto it = m_docs.find(doc); + return it != m_docs.end() ? it->version : -1; + } + + virtual LSPClientRevisionSnapshot* snapshot(LSPClientServer *server) override + { + auto result = new LSPClientRevisionSnapshotImpl; + for (auto it = m_docs.begin(); it != m_docs.end(); ++it) { + if (it->server == server) { + // sync server to latest revision that will be recorded + update(it.key(), false); + result->add(it.key()); + } + } + return result; + } + +private: + void showMessage(const QString &msg, KTextEditor::Message::MessageType level) + { + KTextEditor::View *view = m_mainWindow->activeView(); + if (!view || !view->document()) return; + + auto kmsg = new KTextEditor::Message(xi18nc("@info", "LSP Client: %1", msg), level); + kmsg->setPosition(KTextEditor::Message::AboveView); + kmsg->setAutoHide(5000); + kmsg->setAutoHideMode(KTextEditor::Message::Immediate); + kmsg->setView(view); + view->document()->postMessage(kmsg); + } + + // caller ensures that servers are no longer present in m_servers + void restart(const ServerList & servers) + { + // close docs + for (const auto & server : servers) { + // controlling server here, so disable usual state tracking response + disconnect(server.data(), nullptr, this, nullptr); + for (auto it = m_docs.begin(); it != m_docs.end(); ) { + auto &item = it.value(); + if (item.server == server) { + // no need to close if server not in proper state + if (server->state() != LSPClientServer::State::Running) { + item.open = false; + } + it = _close(it, true); + } else { + ++it; + } + } + } + + // helper captures servers + auto stopservers = [servers] (int t, int k) { + for (const auto & server : servers) { + server->stop(t, k); + } + }; + + // trigger server shutdown now + stopservers(-1, -1); + + // initiate delayed stages (TERM and KILL) + // async, so give a bit more time + QTimer::singleShot(2 * TIMEOUT_SHUTDOWN, this, [stopservers] () { stopservers(1, -1); }); + QTimer::singleShot(4 * TIMEOUT_SHUTDOWN, this, [stopservers] () { stopservers(-1, 1); }); + + // as for the start part + // trigger interested parties, which will again request a server as needed + // let's delay this; less chance for server instances to trip over each other + QTimer::singleShot(6 * TIMEOUT_SHUTDOWN, this, [this] () { emit serverChanged(); }); + } + + void onStateChanged(LSPClientServer *server) + { + if (server->state() == LSPClientServer::State::Running) { + // clear for normal operation + emit serverChanged(); + } else if (server->state() == LSPClientServer::State::None) { + // went down + showMessage(i18n("Server terminated unexpectedly: %1", server->cmdline().join(QLatin1Char(' '))), + KTextEditor::Message::Warning); + restart(server); + } + } + + QSharedPointer + _findServer(KTextEditor::Document *document) + { + QObject *projectView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin")); + const auto projectBase = QDir(projectView ? projectView->property("projectBaseDir").toString() : QString()); + const auto& projectMap = projectView ? projectView->property("projectMap").toMap() : QVariantMap(); + + // compute the LSP standardized language id + auto langId = languageId(document->highlightingMode()); + + // merge with project specific + auto projectConfig = QJsonDocument::fromVariant(projectMap).object().value(QStringLiteral("lspclient")).toObject(); + auto serverConfig = merge(m_serverConfig, projectConfig); + + // locate server config + QJsonValue config; + QSet used; + while (true) { + qCInfo(LSPCLIENT) << "language id " << langId; + used << langId; + config = serverConfig.value(QStringLiteral("servers")).toObject().value(langId); + if (config.isObject()) { + const auto & base = config.toObject().value(QStringLiteral("use")).toString(); + // basic cycle detection + if (!base.isEmpty() && !used.contains(base)) { + langId = base; + continue; + } + } + break; + } + + if (!config.isObject()) + return nullptr; + + // merge global settings + serverConfig = merge(serverConfig.value(QStringLiteral("global")).toObject(), config.toObject()); + + QString rootpath; + auto rootv = serverConfig.value(QStringLiteral("root")); + if (rootv.isString()) { + auto sroot = rootv.toString(); + if (QDir::isAbsolutePath(sroot)) { + rootpath = sroot; + } else if (!projectBase.isEmpty()) { + rootpath = QDir(projectBase).absoluteFilePath(sroot); + } + } + + /** + * no explicit set root dir? search for a matching root based on some name filters + * this is required for some LSP servers like rls that don't handle that on their own like clangd does + */ + if (rootpath.isEmpty()) { + const auto fileNamesForDetection = serverConfig.value(QStringLiteral("rootIndicationFileNames")); + if (fileNamesForDetection.isArray()) { + // we try each file name alternative in the listed order + // this allows to have preferences + for (auto name : fileNamesForDetection.toArray()) { + if (name.isString()) { + rootpath = rootForDocumentAndRootIndicationFileName(document, name.toString()); + if (!rootpath.isEmpty()) { + break; + } + } + } + } + } + + // last fallback: home directory + if (rootpath.isEmpty()) { + rootpath = QDir::homePath(); + } + + auto root = QUrl::fromLocalFile(rootpath); + auto server = m_servers.value(root).value(langId); + if (!server) { + QStringList cmdline; + + // choose debug command line for debug mode, fallback to command + auto vcmdline = serverConfig.value(m_plugin->m_debugMode ? QStringLiteral("commandDebug") : QStringLiteral("command")); + if (vcmdline.isUndefined()) { + vcmdline = serverConfig.value(QStringLiteral("command")); + } + + auto scmdline = vcmdline.toString(); + if (!scmdline.isEmpty()) { + cmdline = scmdline.split(QLatin1Char(' ')); + } else { + for (const auto& c : vcmdline.toArray()) { + cmdline.push_back(c.toString()); + } + } + if (cmdline.length() > 0) { + server.reset(new LSPClientServer(cmdline, root, serverConfig.value(QStringLiteral("initializationOptions")))); + m_servers[root][langId] = server; + connect(server.data(), &LSPClientServer::stateChanged, + this, &self_type::onStateChanged, Qt::UniqueConnection); + if (!server->start()) { + showMessage(i18n("Failed to start server: %1", cmdline.join(QLatin1Char(' '))), + KTextEditor::Message::Error); + } + } + } + return (server && server->state() == LSPClientServer::State::Running) ? server : nullptr; + } + + void updateServerConfig() + { + // default configuration, compiled into plugin resource, reading can't fail + QFile defaultConfigFile(QStringLiteral(":/lspclient/settings.json")); + defaultConfigFile.open(QIODevice::ReadOnly); + Q_ASSERT(defaultConfigFile.isOpen()); + m_serverConfig = QJsonDocument::fromJson(defaultConfigFile.readAll()).object(); + + // consider specified configuration + const auto& configPath = m_plugin->m_configPath.path(); + if (!configPath.isEmpty()) { + QFile f(configPath); + if (f.open(QIODevice::ReadOnly)) { + auto data = f.readAll(); + auto json = QJsonDocument::fromJson(data); + if (json.isObject()) { + m_serverConfig = merge(m_serverConfig, json.object()); + } else { + showMessage(i18n("Failed to parse server configuration: %1", configPath), + KTextEditor::Message::Error); + } + } else { + showMessage(i18n("Failed to read server configuration: %1", configPath), + KTextEditor::Message::Error); + } + } + + // we could (but do not) perform restartAll here; + // for now let's leave that up to user + // but maybe we do have a server now where not before, so let's signal + emit serverChanged(); + } + + void trackDocument(KTextEditor::Document *doc, QSharedPointer server) + { + auto it = m_docs.find(doc); + if (it == m_docs.end()) { + KTextEditor::MovingInterface* miface = qobject_cast(doc); + it = m_docs.insert(doc, {server, miface, doc->url(), 0, false, false, {}}); + // track document + connect(doc, &KTextEditor::Document::documentUrlChanged, this, &self_type::untrack, Qt::UniqueConnection); + connect(doc, &KTextEditor::Document::highlightingModeChanged, this, &self_type::untrack, Qt::UniqueConnection); + connect(doc, &KTextEditor::Document::aboutToClose, this, &self_type::untrack, Qt::UniqueConnection); + connect(doc, &KTextEditor::Document::destroyed, this, &self_type::untrack, Qt::UniqueConnection); + connect(doc, &KTextEditor::Document::textChanged, this, &self_type::onTextChanged, Qt::UniqueConnection); + // in case of incremental change + connect(doc, &KTextEditor::Document::textInserted, this, &self_type::onTextInserted, Qt::UniqueConnection); + connect(doc, &KTextEditor::Document::textRemoved, this, &self_type::onTextRemoved, Qt::UniqueConnection); + connect(doc, &KTextEditor::Document::lineWrapped, this, &self_type::onLineWrapped, Qt::UniqueConnection); + connect(doc, &KTextEditor::Document::lineUnwrapped, this, &self_type::onLineUnwrapped, Qt::UniqueConnection); + } else { + it->server = server; + } + } + + decltype(m_docs)::iterator + _close(decltype(m_docs)::iterator it, bool remove) + { + if (it != m_docs.end()) { + if (it->open) { + // release server side (use url as registered with) + (it->server)->didClose(it->url); + it->open = false; + } + if (remove) { + disconnect(it.key(), nullptr, this, nullptr); + it = m_docs.erase(it); + } + } + return it; + } + + void _close(KTextEditor::Document *doc, bool remove) + { + auto it = m_docs.find(doc); + if (it != m_docs.end()) { + _close(it, remove); + } + } + + void untrack(QObject *doc) + { + _close(qobject_cast(doc), true); + emit serverChanged(); + } + + void close(KTextEditor::Document *doc) + { _close(doc, false); } + + void update(const decltype(m_docs)::iterator & it, bool force) + { + auto doc = it.key(); + if (it != m_docs.end() && it->server) { + if (it->movingInterface) { + it->version = it->movingInterface->revision(); + } else if (it->modified) { + ++it->version; + } + if (!m_incrementalSync) { + it->changes.clear(); + } + if (it->open) { + if (it->modified || force) { + (it->server)->didChange(it->url, it->version, + (it->changes.size() == 0) ? doc->text() : QString(), + it->changes); + } + } else { + (it->server)->didOpen(it->url, it->version, languageId(doc->highlightingMode()), doc->text()); + it->open = true; + } + it->modified = false; + it->changes.clear(); + } + } + + void update(KTextEditor::Document *doc, bool force) override + { + update(m_docs.find(doc), force); + } + + void update(LSPClientServer * server, bool force) + { + for (auto it = m_docs.begin(); it != m_docs.end(); ++it) { + if (it->server == server) { + update(it, force); + } + } + } + + void onTextChanged(KTextEditor::Document *doc) + { + auto it = m_docs.find(doc); + if (it != m_docs.end()) { + it->modified = true; + } + } + + DocumentInfo* + getDocumentInfo(KTextEditor::Document *doc) + { + if (!m_incrementalSync) + return nullptr; + + auto it = m_docs.find(doc); + if (it != m_docs.end() && it->server) { + const auto& caps = it->server->capabilities(); + if (caps.textDocumentSync == LSPDocumentSyncKind::Incremental) { + return &(*it); + } + } + return nullptr; + } + + void onTextInserted(KTextEditor::Document *doc, const KTextEditor::Cursor &position, const QString &text) + { + auto info = getDocumentInfo(doc); + if (info) { + info->changes.push_back({LSPRange{position, position}, text}); + } + } + + void onTextRemoved(KTextEditor::Document *doc, const KTextEditor::Range &range, const QString &text) + { + (void)text; + auto info = getDocumentInfo(doc); + if (info) { + info->changes.push_back({range, QString()}); + } + } + + void onLineWrapped(KTextEditor::Document *doc, const KTextEditor::Cursor &position) + { + // so a 'newline' has been inserted at position + // could have been UNIX style or other kind, let's ask the document + auto text = doc->text({position, {position.line() + 1, 0}}); + onTextInserted(doc, position, text); + } + + void onLineUnwrapped(KTextEditor::Document *doc, int line) + { + // lines line-1 and line got replaced by current content of line-1 + Q_ASSERT(line > 0); + auto info = getDocumentInfo(doc); + if (info) { + LSPRange oldrange {{line - 1, 0}, {line + 1, 0}}; + LSPRange newrange {{line - 1, 0}, {line, 0}}; + auto text = doc->text(newrange); + info->changes.push_back({oldrange, text}); + } + } + +}; + +QSharedPointer +LSPClientServerManager::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) +{ + return QSharedPointer(new LSPClientServerManagerImpl(plugin, mainWin)); +} + +#include "lspclientservermanager.moc" diff --git a/addons/lspclient/lspclientservermanager.h b/addons/lspclient/lspclientservermanager.h new file mode 100644 index 000000000..5925c8157 --- /dev/null +++ b/addons/lspclient/lspclientservermanager.h @@ -0,0 +1,93 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef LSPCLIENTSERVERMANAGER_H +#define LSPCLIENTSERVERMANAGER_H + +#include "lspclientserver.h" +#include "lspclientplugin.h" + +#include + +namespace KTextEditor { + class MainWindow; + class Document; + class View; + class MovingInterface; +} + +class LSPClientRevisionSnapshot; + +/* + * A helper class that manages LSP servers in relation to a KTextDocument. + * That is, spin up a server for a document when so requested, and then + * monitor when the server is up (or goes down) and the document (for changes). + * Those changes may then be synced to the server when so requested (e.g. prior + * to another component performing an LSP request for a document). + * So, other than managing servers, it also manages the document-server + * relationship (and document), what's in a name ... + */ +class LSPClientServerManager : public QObject +{ + Q_OBJECT + +public: + // factory method; private implementation by interface + static QSharedPointer + new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin); + + virtual QSharedPointer + findServer(KTextEditor::Document *document, bool updatedoc = true) = 0; + + virtual QSharedPointer + findServer(KTextEditor::View *view, bool updatedoc = true) = 0; + + virtual void update(KTextEditor::Document *doc, bool force) = 0; + + virtual void restart(LSPClientServer *server) = 0; + + virtual void setIncrementalSync(bool inc) = 0; + + // latest sync'ed revision of doc (-1 if N/A) + virtual qint64 revision(KTextEditor::Document *doc) = 0; + + // lock all relevant documents' current revision and sync that to server + // locks are released when returned snapshot is delete'd + virtual LSPClientRevisionSnapshot* snapshot(LSPClientServer *server) = 0; + +public: +Q_SIGNALS: + void serverChanged(); +}; + +class LSPClientRevisionSnapshot : public QObject +{ + Q_OBJECT + +public: + // find a locked revision for url in snapshot + virtual void find(const QUrl & url, KTextEditor::MovingInterface* & miface, qint64 & revision) const = 0; +}; + +#endif diff --git a/addons/lspclient/lspclientsymbolview.cpp b/addons/lspclient/lspclientsymbolview.cpp new file mode 100644 index 000000000..9fe236d32 --- /dev/null +++ b/addons/lspclient/lspclientsymbolview.cpp @@ -0,0 +1,544 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + Copyright (C) 2019 Christoph Cullmann + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "lspclientsymbolview.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +class LSPClientViewTrackerImpl : public LSPClientViewTracker +{ + Q_OBJECT + + typedef LSPClientViewTrackerImpl self_type; + + LSPClientPlugin *m_plugin; + KTextEditor::MainWindow *m_mainWindow; + // timers to delay some todo's + QTimer m_changeTimer; + int m_change; + QTimer m_motionTimer; + int m_motion; + int m_oldCursorLine = -1; + +public: + LSPClientViewTrackerImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, int change_ms, int motion_ms) + : m_plugin(plugin), m_mainWindow(mainWin), m_change(change_ms), m_motion(motion_ms) + { + // get updated + m_changeTimer.setSingleShot(true); + auto ch = [this] () { emit newState(m_mainWindow->activeView(), TextChanged); }; + connect(&m_changeTimer, &QTimer::timeout, this, ch); + + m_motionTimer.setSingleShot(true); + auto mh = [this] () { emit newState(m_mainWindow->activeView(), LineChanged); }; + connect(&m_motionTimer, &QTimer::timeout, this, mh); + + // track views + connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &self_type::viewChanged); + } + + void viewChanged(KTextEditor::View *view) + { + m_motionTimer.stop(); + m_changeTimer.stop(); + + if (view) { + if (m_motion) { + connect(view, &KTextEditor::View::cursorPositionChanged, this, &self_type::cursorPositionChanged, Qt::UniqueConnection); + } + if (m_change > 0 && view->document()) { + connect(view->document(), &KTextEditor::Document::textChanged, this, &self_type::textChanged, Qt::UniqueConnection); + } + emit newState(view, ViewChanged); + m_oldCursorLine = view->cursorPosition().line(); + } + } + + void textChanged() + { + m_motionTimer.stop(); + m_changeTimer.start(m_change); + } + + void cursorPositionChanged(KTextEditor::View *view, const KTextEditor::Cursor &newPosition) + { + if (m_changeTimer.isActive()) { + // change trumps motion + return; + } + + if (view && newPosition.line() != m_oldCursorLine) { + m_oldCursorLine = newPosition.line(); + m_motionTimer.start(m_motion); + } + } +}; + +LSPClientViewTracker* +LSPClientViewTracker::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, + int change_ms, int motion_ms) +{ + return new LSPClientViewTrackerImpl(plugin, mainWin, change_ms, motion_ms); +} + +/* + * Instantiates and manages the symbol outline toolview. + */ +class LSPClientSymbolViewImpl : public QObject, public LSPClientSymbolView +{ + Q_OBJECT + + typedef LSPClientSymbolViewImpl self_type; + + LSPClientPlugin *m_plugin; + KTextEditor::MainWindow *m_mainWindow; + QSharedPointer m_serverManager; + QScopedPointer m_toolview; + // parent ownership + QPointer m_symbols; + QPointer m_filter; + QScopedPointer m_popup; + // initialized/updated from plugin settings + // managed by context menu later on + // parent ownership + QAction *m_detailsOn; + QAction *m_expandOn; + QAction *m_treeOn; + QAction *m_sortOn; + // view tracking + QScopedPointer m_viewTracker; + // outstanding request + LSPClientServer::RequestHandle m_handle; + // cached outline models + struct ModelData + { + KTextEditor::Document *document; + qint64 revision; + std::shared_ptr model; + }; + QList m_models; + // max number to cache + static constexpr int MAX_MODELS = 10; + // last outline model we constructed + std::shared_ptr m_outline; + // filter model, setup once + KRecursiveFilterProxyModel m_filterModel; + + // cached icons for model + const QIcon m_icon_pkg = QIcon::fromTheme(QStringLiteral("code-block")); + const QIcon m_icon_class = QIcon::fromTheme(QStringLiteral("code-class")); + const QIcon m_icon_typedef = QIcon::fromTheme(QStringLiteral("code-typedef")); + const QIcon m_icon_function = QIcon::fromTheme(QStringLiteral("code-function")); + const QIcon m_icon_var = QIcon::fromTheme(QStringLiteral("code-variable")); + +public: + LSPClientSymbolViewImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, + QSharedPointer manager) + : m_plugin(plugin), m_mainWindow(mainWin), m_serverManager(manager), m_outline(new QStandardItemModel()) + { + m_toolview.reset(m_mainWindow->createToolView(plugin, QStringLiteral("lspclient_symbol_outline"), + KTextEditor::MainWindow::Right, + QIcon::fromTheme(QStringLiteral("code-context")), + i18n("LSP Client Symbol Outline"))); + + m_symbols = new QTreeView(m_toolview.data()); + m_symbols->setFocusPolicy(Qt::NoFocus); + m_symbols->setLayoutDirection(Qt::LeftToRight); + m_toolview->layout()->setContentsMargins(0, 0, 0, 0); + m_toolview->layout()->addWidget(m_symbols); + m_toolview->layout()->setSpacing(0); + + // setup filter line edit + m_filter = new KLineEdit(m_toolview.data()); + m_toolview->layout()->addWidget(m_filter); + m_filter->setPlaceholderText(i18n("Filter...")); + m_filter->setClearButtonEnabled(true); + connect(m_filter, &KLineEdit::textChanged, this, &self_type::filterTextChanged); + + m_symbols->setContextMenuPolicy(Qt::CustomContextMenu); + m_symbols->setIndentation(10); + m_symbols->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_symbols->setAllColumnsShowFocus(true); + + // init filter model once, later we only swap the source model! + QItemSelectionModel *m = m_symbols->selectionModel(); + m_filterModel.setFilterCaseSensitivity(Qt::CaseInsensitive); + m_filterModel.setSortCaseSensitivity(Qt::CaseInsensitive); + m_filterModel.setSourceModel(m_outline.get()); + m_symbols->setModel(&m_filterModel); + delete m; + + connect(m_symbols, &QTreeView::customContextMenuRequested, this, &self_type::showContextMenu); + connect(m_symbols, &QTreeView::activated, this, &self_type::goToSymbol); + connect(m_symbols, &QTreeView::clicked, this, &self_type::goToSymbol); + + // context menu + m_popup.reset(new QMenu(m_symbols)); + m_treeOn = m_popup->addAction(i18n("Tree Mode"), this, &self_type::displayOptionChanged); + m_treeOn->setCheckable(true); + m_expandOn = m_popup->addAction(i18n("Automatically Expand Tree"), this, &self_type::displayOptionChanged); + m_expandOn->setCheckable(true); + m_sortOn = m_popup->addAction(i18n("Sort Alphabetically"), this, &self_type::displayOptionChanged); + m_sortOn->setCheckable(true); + m_detailsOn = m_popup->addAction(i18n("Show Details"), this, &self_type::displayOptionChanged); + m_detailsOn->setCheckable(true); + m_popup->addSeparator(); + m_popup->addAction(i18n("Expand All"), m_symbols.data(), &QTreeView::expandAll); + m_popup->addAction(i18n("Collapse All"), m_symbols.data(), &QTreeView::collapseAll); + + // sync with plugin settings if updated + connect(m_plugin, &LSPClientPlugin::update, this, &self_type::configUpdated); + + // get updated + m_viewTracker.reset(LSPClientViewTracker::new_(plugin, mainWin, 500, 100)); + connect(m_viewTracker.data(), &LSPClientViewTracker::newState, this, &self_type::onViewState); + connect(m_serverManager.data(), &LSPClientServerManager::serverChanged, + this, [this] () { refresh(false); }); + + // limit cached models; will not go beyond capacity set here + m_models.reserve(MAX_MODELS + 1); + + // initial trigger of symbols view update + configUpdated(); + } + + void displayOptionChanged() + { + m_expandOn->setEnabled(m_treeOn->isChecked()); + refresh(false); + } + + void configUpdated() + { + m_treeOn->setChecked(m_plugin->m_symbolTree); + m_detailsOn->setChecked(m_plugin->m_symbolDetails); + m_expandOn->setChecked(m_plugin->m_symbolExpand); + m_sortOn->setChecked(m_plugin->m_symbolSort); + displayOptionChanged(); + } + + void showContextMenu(const QPoint&) + { + m_popup->popup(QCursor::pos(), m_treeOn); + } + + void onViewState(KTextEditor::View *, LSPClientViewTracker::State newState) + { + switch(newState) { + case LSPClientViewTracker::ViewChanged: + refresh(true); + break; + case LSPClientViewTracker::TextChanged: + refresh(false); + break; + case LSPClientViewTracker::LineChanged: + updateCurrentTreeItem(); + break; + } + } + + void makeNodes(const QList & symbols, bool tree, + bool show_detail, QStandardItemModel * model, QStandardItem * parent, + bool &details) + { + const QIcon *icon = nullptr; + for (const auto& symbol: symbols) { + switch (symbol.kind) { + case LSPSymbolKind::File: + case LSPSymbolKind::Module: + case LSPSymbolKind::Namespace: + case LSPSymbolKind::Package: + if (symbol.children.count() == 0) + continue; + icon = &m_icon_pkg; + break; + case LSPSymbolKind::Class: + case LSPSymbolKind::Interface: + icon = &m_icon_class; + break; + case LSPSymbolKind::Enum: + icon = &m_icon_typedef; + break; + case LSPSymbolKind::Method: + case LSPSymbolKind::Function: + case LSPSymbolKind::Constructor: + icon = &m_icon_function; + break; + // all others considered/assumed Variable + case LSPSymbolKind::Variable: + case LSPSymbolKind::Constant: + case LSPSymbolKind::String: + case LSPSymbolKind::Number: + case LSPSymbolKind::Property: + case LSPSymbolKind::Field: + default: + // skip local variable + // property, field, etc unlikely in such case anyway + if (parent && parent->icon().cacheKey() == m_icon_function.cacheKey()) + continue; + icon = &m_icon_var; + } + + + auto node = new QStandardItem(); + if (parent && tree) + parent->appendRow(node); + else + model->appendRow(node); + + if (!symbol.detail.isEmpty()) + details = true; + auto detail = show_detail ? symbol.detail : QString(); + node->setText(symbol.name + detail); + node->setIcon(*icon); + node->setData(QVariant::fromValue(symbol.range), Qt::UserRole); + // recurse children + makeNodes(symbol.children, tree, show_detail, model, node, details); + } + } + + void onDocumentSymbols(const QList &outline) + { + onDocumentSymbolsOrProblem(outline, QString(), true); + } + + void onDocumentSymbolsOrProblem(const QList &outline, const QString &problem = QString(), bool cache = false) + { + if (!m_symbols) + return; + + // construct new model for data + auto newModel = std::make_shared(); + + // if we have some problem, just report that, else construct model + bool details = false; + if (problem.isEmpty()) { + makeNodes(outline, m_treeOn->isChecked(), m_detailsOn->isChecked(), newModel.get(), nullptr, details); + if (cache) { + // last request has been placed at head of model list + Q_ASSERT(!m_models.isEmpty()); + m_models[0].model = newModel; + } + } else { + newModel->appendRow(new QStandardItem(problem)); + } + + // cache detail info with model + newModel->invisibleRootItem()->setData(details); + + // fixup headers + QStringList headers{i18n("Symbols")}; + newModel->setHorizontalHeaderLabels(headers); + + setModel(newModel); + } + + void setModel(std::shared_ptr newModel) + { + Q_ASSERT(newModel); + + // update filter model, do this before the assignment below deletes the old model! + m_filterModel.setSourceModel(newModel.get()); + + // delete old outline if there, keep our new one alive + m_outline = newModel; + + // fixup sorting + if (m_sortOn->isChecked()) { + m_symbols->setSortingEnabled(true); + m_symbols->sortByColumn(0); + } else { + m_symbols->sortByColumn(-1); + } + + // handle auto-expansion + if (m_expandOn->isChecked()) { + m_symbols->expandAll(); + } + + // recover detail info from model data + bool details = newModel->invisibleRootItem()->data().toBool(); + + // disable detail setting if no such info available + // (as an indication there is nothing to show anyway) + m_detailsOn->setEnabled(details); + + // hide detail column if not needed/wanted + bool showDetails = m_detailsOn->isChecked() && details; + m_symbols->setColumnHidden(1, !showDetails); + + // current item tracking + updateCurrentTreeItem(); + } + + void refresh(bool clear) + { + // cancel old request! + m_handle.cancel(); + + // check if we have some server for the current view => trigger request + auto view = m_mainWindow->activeView(); + if (auto server = m_serverManager->findServer(view)) { + // clear current model in any case + // this avoids that we show stuff not matching the current view + // but let's only do it if needed, e.g. when changing view + // so as to avoid unhealthy flickering in other cases + if (clear) { + onDocumentSymbolsOrProblem(QList(), QString(), false); + } + + // check (valid) cache + auto doc = view->document(); + auto revision = m_serverManager->revision(doc); + auto it = m_models.begin(); + for (; it != m_models.end(); ++it) { + if (it->document == doc) { + break; + } + } + if (it != m_models.end()) { + // move to most recently used head + m_models.move(it - m_models.begin(), 0); + auto& model = m_models.front(); + // re-use if possible + if (revision == model.revision && model.model) { + setModel(model.model); + return; + } + it->revision = revision; + } else { + m_models.insert(0, {doc, revision, nullptr}); + if (m_models.size() > MAX_MODELS) { + m_models.pop_back(); + } + } + + server->documentSymbols(view->document()->url(), this, + utils::mem_fun(&self_type::onDocumentSymbols, this)); + + return; + } + + // else: inform that no server is there + onDocumentSymbolsOrProblem(QList(), i18n("No LSP server for this document.")); + } + + QStandardItem* getCurrentItem(QStandardItem * item, int line) + { + // first traverse the child items to have deepest match! + // only do this if our stuff is expanded + if (item == m_outline->invisibleRootItem() || m_symbols->isExpanded(m_filterModel.mapFromSource(m_outline->indexFromItem(item)))) { + for (int i = 0; i < item->rowCount(); i++) { + if (auto citem = getCurrentItem(item->child(i), line)) { + return citem; + } + } + } + + // does the line match our item? + return item->data(Qt::UserRole).value().overlapsLine(line) ? item : nullptr; + } + + void updateCurrentTreeItem() + { + KTextEditor::View* editView = m_mainWindow->activeView(); + if (!editView || !m_symbols) { + return; + } + + /** + * get item if any + */ + QStandardItem *item = getCurrentItem(m_outline->invisibleRootItem(), editView->cursorPositionVirtual().line()); + if (!item) { + return; + } + + /** + * select it + */ + QModelIndex index = m_filterModel.mapFromSource(m_outline->indexFromItem(item)); + m_symbols->scrollTo(index); + m_symbols->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Clear | QItemSelectionModel::Select); + } + + void goToSymbol(const QModelIndex &index) + { + KTextEditor::View *kv = m_mainWindow->activeView(); + const auto range = index.data(Qt::UserRole).value(); + if (kv && range.isValid()) { + kv->setCursorPosition(range.start()); + } + } + +private Q_SLOTS: + /** + * React on filter change + * @param filterText new filter text + */ + void filterTextChanged(const QString &filterText) + { + if (!m_symbols) { + return; + } + + /** + * filter + */ + m_filterModel.setFilterFixedString(filterText); + + /** + * expand + */ + if (!filterText.isEmpty()) { + QTimer::singleShot(100, m_symbols, &QTreeView::expandAll); + } + } +}; + + +QObject* +LSPClientSymbolView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, + QSharedPointer manager) +{ + return new LSPClientSymbolViewImpl(plugin, mainWin, manager); +} + +#include "lspclientsymbolview.moc" diff --git a/addons/lspclient/lspclientsymbolview.h b/addons/lspclient/lspclientsymbolview.h new file mode 100644 index 000000000..515c801fa --- /dev/null +++ b/addons/lspclient/lspclientsymbolview.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + Copyright (C) 2019 Christoph Cullmann + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef LSPCLIENTSYMBOLVIEW_H +#define LSPCLIENTSYMBOLVIEW_H + +#include "lspclientplugin.h" +#include "lspclientservermanager.h" + +#include + +class LSPClientSymbolView +{ +public: + // only needs a factory; no other public interface + static QObject* + new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, + QSharedPointer manager); +}; + +class LSPClientViewTracker : public QObject +{ + Q_OBJECT +public: + // factory method; private implementation by interface + static LSPClientViewTracker* + new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, + int change_ms, int motion_ms); + + enum State { + ViewChanged, + TextChanged, + LineChanged, + }; + +Q_SIGNALS: + void newState(KTextEditor::View *, State); +}; + +#endif diff --git a/addons/lspclient/plugin.qrc b/addons/lspclient/plugin.qrc new file mode 100644 index 000000000..1a8747d23 --- /dev/null +++ b/addons/lspclient/plugin.qrc @@ -0,0 +1,9 @@ + + + + ui.rc + + + settings.json + + diff --git a/addons/lspclient/settings.json b/addons/lspclient/settings.json new file mode 100644 index 000000000..5d20e1ee6 --- /dev/null +++ b/addons/lspclient/settings.json @@ -0,0 +1,33 @@ +{ + "servers": { + "bibtex": { + "use": "latex" + }, + "c": { + "command": ["clangd", "-log=error", "--background-index"], + "commandDebug": ["clangd", "-log=verbose", "--background-index"], + "url": "https://clang.llvm.org/extra/clangd/" + }, + "cpp": { + "use": "c" + }, + "latex": { + "command": ["texlab"], + "url": "https://texlab.netlify.com/" + }, + "go": { + "command": ["go-langserver"], + "commandDebug": ["go-langserver", "-trace"], + "url": "https://github.com/sourcegraph/go-langserver" + }, + "python": { + "command": ["python3", "-m", "pyls", "--check-parent-process"], + "url": "https://github.com/palantir/python-language-server" + }, + "rust": { + "command": ["rls"], + "rootIndicationFileNames": ["Cargo.lock", "Cargo.toml"], + "url": "https://github.com/rust-lang/rls" + } + } +} diff --git a/addons/lspclient/tests/CMakeLists.txt b/addons/lspclient/tests/CMakeLists.txt new file mode 100644 index 000000000..c9a15883e --- /dev/null +++ b/addons/lspclient/tests/CMakeLists.txt @@ -0,0 +1,11 @@ +add_executable(lsptestapp "") +target_include_directories(lsptestapp PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/..) +target_link_libraries(lsptestapp PRIVATE KF5::TextEditor) + +target_sources( + lsptestapp + PRIVATE + lsptestapp.cpp + ../lspclientserver.cpp + ${DEBUG_SOURCES} +) diff --git a/addons/lspclient/tests/lsptestapp.cpp b/addons/lspclient/tests/lsptestapp.cpp new file mode 100644 index 000000000..19c16e22b --- /dev/null +++ b/addons/lspclient/tests/lsptestapp.cpp @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: MIT + + Copyright (C) 2019 Mark Nauwelaerts + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "../lspclientserver.h" +#include + +#include +#include +#include +#include +#include + +int main(int argc, char ** argv) +{ + if (argc < 5) + return -1; + + LSPClientServer lsp(QString::fromLatin1(argv[1]).split(QStringLiteral(" ")), + QUrl(QString::fromLatin1(argv[2]))); + + QCoreApplication app(argc, argv); + QEventLoop q; + + auto state_h = [&lsp, &q] () { + if (lsp.state() == LSPClientServer::State::Running) + q.quit(); + }; + auto conn = QObject::connect(&lsp, &LSPClientServer::stateChanged, state_h); + lsp.start(); + q.exec(); + QObject::disconnect(conn); + + auto diagnostics_h = [] (const LSPPublishDiagnosticsParams & diag) { + std::cout << "diagnostics " << diag.uri.path().toUtf8().toStdString() << " count: " << diag.diagnostics.length(); + }; + + QObject::connect(&lsp, &LSPClientServer::publishDiagnostics, diagnostics_h); + + auto document = QUrl(QString::fromLatin1(argv[3])); + + QFile file(document.path()); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return -1; + QTextStream in(&file); + QString content = in.readAll(); + lsp.didOpen(document, 0, QString(), content); + + auto ds_h = [&q] (const QList & syms) { + std::cout << "symbol count: " << syms.length() << std::endl; + q.quit(); + }; + lsp.documentSymbols(document, &app, ds_h); + q.exec(); + + auto position = QString::fromLatin1(argv[4]).split(QStringLiteral(" ")); + auto def_h = [&q] (const QList & defs) { + std::cout << "definition count: " << defs.length() << std::endl; + q.quit(); + }; + lsp.documentDefinition(document, {position[0].toInt(), position[1].toInt()}, &app, def_h); + q.exec(); + + auto comp_h = [&q] (const QList & completions) { + std::cout << "completion count: " << completions.length() << std::endl; + q.quit(); + }; + lsp.documentCompletion(document, {position[0].toInt(), position[1].toInt()}, &app, comp_h); + q.exec(); + + auto sig_h = [&q] (const LSPSignatureHelp & help) { + std::cout << "signature help count: " << help.signatures.length() << std::endl; + q.quit(); + }; + lsp.signatureHelp(document, {position[0].toInt(), position[1].toInt()}, &app, sig_h); + q.exec(); + + auto hover_h = [&q] (const LSPHover & hover) { + for (auto &element : hover.contents) { + std::cout << "hover: " << element.value.toStdString() << std::endl; + } + q.quit(); + }; + lsp.documentHover(document, {position[0].toInt(), position[1].toInt()}, &app, hover_h); + q.exec(); + + auto ref_h = [&q] (const QList & refs) { + std::cout << "refs: " << refs.length() << std::endl; + q.quit(); + }; + lsp.documentReferences(document, {position[0].toInt(), position[1].toInt()}, true, &app, ref_h); + q.exec(); + + auto hl_h = [&q] (const QList & hls) { + std::cout << "highlights: " << hls.length() << std::endl; + q.quit(); + }; + lsp.documentHighlight(document, {position[0].toInt(), position[1].toInt()}, &app, hl_h); + q.exec(); + + auto fmt_h = [&q] (const QList & edits) { + std::cout << "edits: " << edits.length() << std::endl; + q.quit(); + }; + lsp.documentFormatting(document, {2, true, QJsonObject()}, &app, fmt_h); + q.exec(); + + // lsp.didOpen(document, 0, QStringLiteral("blah")); + lsp.didChange(document, 1, QStringLiteral("foo")); + lsp.didClose(document); +} diff --git a/addons/lspclient/ui.rc b/addons/lspclient/ui.rc new file mode 100644 index 000000000..0ad403afd --- /dev/null +++ b/addons/lspclient/ui.rc @@ -0,0 +1,34 @@ + + + + + + LSP Client + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/lumen/CMakeLists.txt b/addons/lumen/CMakeLists.txt index 55d7845da..b6614c029 100644 --- a/addons/lumen/CMakeLists.txt +++ b/addons/lumen/CMakeLists.txt @@ -1,18 +1,14 @@ -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) - -add_definitions(-DTRANSLATION_DOMAIN=\"ktexteditor_lumen\") - -set(ktexteditor_lumen_SRCS - lumen.cpp - dcd.cpp - completion.cpp +add_library(ktexteditor_lumen MODULE "") +target_compile_definitions(ktexteditor_lumen PRIVATE TRANSLATION_DOMAIN="ktexteditor_lumen") +target_link_libraries(ktexteditor_lumen PRIVATE KF5::TextEditor) + +target_sources( + ktexteditor_lumen + PRIVATE + lumen.cpp + dcd.cpp + completion.cpp ) -add_library(ktexteditor_lumen MODULE ${ktexteditor_lumen_SRCS}) kcoreaddons_desktop_to_json(ktexteditor_lumen ktexteditor_lumen.desktop) -target_link_libraries(ktexteditor_lumen - KF5::TextEditor -) - install(TARGETS ktexteditor_lumen DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) - diff --git a/addons/openheader/CMakeLists.txt b/addons/openheader/CMakeLists.txt index b373c2d85..badfe43fc 100644 --- a/addons/openheader/CMakeLists.txt +++ b/addons/openheader/CMakeLists.txt @@ -1,17 +1,13 @@ -project (kateopenheader) -add_definitions(-DTRANSLATION_DOMAIN=\"kateopenheader\") - -########### next target ############### -set(kateopenheaderplugin_PART_SRCS plugin_kateopenheader.cpp ) - -# resource for ui file and stuff -qt5_add_resources(kateopenheaderplugin_PART_SRCS plugin.qrc) - -add_library(kateopenheaderplugin MODULE ${kateopenheaderplugin_PART_SRCS}) -kcoreaddons_desktop_to_json (kateopenheaderplugin kateopenheaderplugin.desktop) -target_link_libraries(kateopenheaderplugin - KF5::TextEditor - KF5::I18n - KF5::Parts) - -install(TARGETS kateopenheaderplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) +add_library(kateopenheaderplugin MODULE "") +target_compile_definitions(kateopenheaderplugin PRIVATE TRANSLATION_DOMAIN="kateopenheader") +target_link_libraries(kateopenheaderplugin PRIVATE KF5::TextEditor) + +target_sources( + kateopenheaderplugin + PRIVATE + plugin_kateopenheader.cpp + plugin.qrc +) + +kcoreaddons_desktop_to_json(kateopenheaderplugin kateopenheaderplugin.desktop) +install(TARGETS kateopenheaderplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/openheader/plugin_kateopenheader.cpp b/addons/openheader/plugin_kateopenheader.cpp index 0c3812a9a..165798428 100644 --- a/addons/openheader/plugin_kateopenheader.cpp +++ b/addons/openheader/plugin_kateopenheader.cpp @@ -1,234 +1,234 @@ /* This file is part of the KDE project Copyright (C) 2001 Joseph Wenninger Copyright (C) 2009 Erlend Hamberg This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "plugin_kateopenheader.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KateOpenHeaderFactory,"kateopenheaderplugin.json", registerPlugin();) //K_EXPORT_PLUGIN(KateOpenHeaderFactory(KAboutData("kateopenheader","kateopenheader",ki18n("Open Header"), "0.1", ki18n("Open header for a source file"), KAboutData::License_LGPL_V2)) ) PluginViewKateOpenHeader::PluginViewKateOpenHeader(PluginKateOpenHeader *plugin, KTextEditor::MainWindow *mainwindow) : KTextEditor::Command(QStringList() << QStringLiteral("toggle-header"), mainwindow) , KXMLGUIClient() , m_plugin(plugin) , m_mainWindow(mainwindow) { KXMLGUIClient::setComponentName (QStringLiteral("kateopenheaderplugin"), i18n ("Open Header")); setXMLFile( QStringLiteral("ui.rc") ); QAction *a = actionCollection()->addAction(QStringLiteral("file_openheader")); a->setText(i18n("Open .h/.cpp/.c")); actionCollection()->setDefaultShortcut(a, Qt::Key_F12 ); connect(a, &QAction::triggered, plugin, &PluginKateOpenHeader::slotOpenHeader); mainwindow->guiFactory()->addClient (this); } PluginViewKateOpenHeader::~PluginViewKateOpenHeader() { m_mainWindow->guiFactory()->removeClient (this); } PluginKateOpenHeader::PluginKateOpenHeader( QObject* parent, const QList& ) : KTextEditor::Plugin ( parent ) { } PluginKateOpenHeader::~PluginKateOpenHeader() { } QObject *PluginKateOpenHeader::createView (KTextEditor::MainWindow *mainWindow) { return new PluginViewKateOpenHeader(this,mainWindow); } void PluginKateOpenHeader::slotOpenHeader () { KTextEditor::Application *application=KTextEditor::Editor::instance()->application(); if (!application->activeMainWindow()) return; KTextEditor::View * kv (application->activeMainWindow()->activeView()); if (!kv) return; QUrl url=kv->document()->url(); if ((!url.isValid()) || (url.isEmpty())) return; qDebug() << "Trying to open opposite of " << url.toString(); qDebug() << "Trying to open opposite of toLocalFile:" << url.toLocalFile(); qDebug() << "Trying to open opposite of path:" << url.path(); QFileInfo info( url.path() ); QString extension = info.suffix().toLower(); QStringList headers( QStringList() << QStringLiteral("h") << QStringLiteral("H") << QStringLiteral("hh") << QStringLiteral("hpp") << QStringLiteral("cuh")); QStringList sources( QStringList() << QStringLiteral("c") << QStringLiteral("cpp") << QStringLiteral("cc") << QStringLiteral("cp") << QStringLiteral("cxx") << QStringLiteral("m")<< QStringLiteral("cu")); if( sources.contains( extension ) ) { if (tryOpenInternal(url, headers)) return; tryOpen( url, headers ); } else if ( headers.contains( extension ) ) { if (tryOpenInternal(url, sources)) return; tryOpen( url, sources ); } } bool PluginKateOpenHeader::tryOpenInternal( const QUrl& url, const QStringList& extensions ) { KTextEditor::Application *application=KTextEditor::Editor::instance()->application(); if (!application->activeMainWindow()) return false; qDebug() << "Trying to find already opened" << url.toString() << " with extensions " << extensions.join(QStringLiteral(" ")); QString basename = QFileInfo( url.path() ).baseName(); QUrl newURL( url ); - for( QStringList::ConstIterator it = extensions.begin(); it != extensions.end(); ++it ) { - setFileName( &newURL,basename + QStringLiteral(".") + *it ); + for(const auto& extension : extensions) { + setFileName( &newURL,basename + QStringLiteral(".") + extension ); KTextEditor::Document *doc= application->findUrl(newURL); if (doc) { application->activeMainWindow()->openUrl(newURL); return true; } - setFileName(&newURL, basename + QStringLiteral(".") + (*it).toUpper() ); + setFileName(&newURL, basename + QStringLiteral(".") + extension.toUpper() ); doc= application->findUrl(newURL); if (doc) { application->activeMainWindow()->openUrl(newURL); return true; } } return false; } void PluginKateOpenHeader::tryOpen( const QUrl& url, const QStringList& extensions ) { KTextEditor::Application *application=KTextEditor::Editor::instance()->application(); if (!application->activeMainWindow()) return; qDebug() << "Trying to open " << url.toString() << " with extensions " << extensions.join(QStringLiteral(" ")); QString basename = QFileInfo( url.path() ).baseName(); QUrl newURL( url ); - for( QStringList::ConstIterator it = extensions.begin(); it != extensions.end(); ++it ) { - setFileName( &newURL,basename + QStringLiteral(".") + *it ); + for(const auto& extension : extensions) { + setFileName( &newURL,basename + QStringLiteral(".") + extension ); if( fileExists( newURL) ) { application->activeMainWindow()->openUrl( newURL ); return; } - setFileName(&newURL, basename + QStringLiteral(".") + (*it).toUpper() ); + setFileName(&newURL, basename + QStringLiteral(".") + extension.toUpper() ); if( fileExists( newURL) ) { application->activeMainWindow()->openUrl( newURL ); return; } } } bool PluginKateOpenHeader::fileExists(const QUrl &url) { if (url.isLocalFile()) { return QFile::exists(url.toLocalFile()); } KIO::JobFlags flags = KIO::DefaultFlags; KIO::StatJob *job = KIO::stat(url, flags); KJobWidgets::setWindow(job, KTextEditor::Editor::instance()->application()->activeMainWindow()->window()); job->setSide(KIO::StatJob::DestinationSide/*SourceSide*/); job->exec(); return !job->error(); } void PluginKateOpenHeader::setFileName(QUrl *url,const QString &_txt) { url->setFragment(QString()); int i = 0; while (i < _txt.length() && _txt[i] == QLatin1Char('/')) { ++i; } QString tmp = i ? _txt.mid(i) : _txt; QString path = url->path(); if (path.isEmpty()) #ifdef Q_OS_WIN path = url->isLocalFile() ? QDir::rootPath() : QStringLiteral("/"); #else path = QDir::rootPath(); #endif else { int lastSlash = path.lastIndexOf(QLatin1Char('/')); if (lastSlash == -1) { path.clear(); // there's only the file name, remove it } else if (!path.endsWith(QLatin1Char('/'))) { path.truncate(lastSlash + 1); // keep the "/" } } path += tmp; url->setPath(path); } bool PluginViewKateOpenHeader::exec(KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &) { Q_UNUSED(view) Q_UNUSED(cmd) Q_UNUSED(msg) m_plugin->slotOpenHeader(); return true; } bool PluginViewKateOpenHeader::help(KTextEditor::View *view, const QString &cmd, QString &msg) { Q_UNUSED(view) Q_UNUSED(cmd) msg = i18n("

toggle-header — switch between header and corresponding c/cpp file

" "

usage: toggle-header

" "

When editing C or C++ code, this command will switch between a header file and " "its corresponding C/C++ file or vice versa.

" "

For example, if you are editing myclass.cpp, toggle-header will change " "to myclass.h if this file is available.

" "

Pairs of the following filename suffixes will work:
" " Header files: h, H, hh, hpp
" " Source files: c, cpp, cc, cp, cxx

"); return true; } #include "plugin_kateopenheader.moc" diff --git a/addons/preview/CMakeLists.txt b/addons/preview/CMakeLists.txt index 7d9fd888b..5734d2db9 100644 --- a/addons/preview/CMakeLists.txt +++ b/addons/preview/CMakeLists.txt @@ -1,25 +1,23 @@ -add_definitions(-DTRANSLATION_DOMAIN=\"ktexteditorpreviewplugin\") +add_library(ktexteditorpreviewplugin MODULE "") +target_compile_definitions(ktexteditorpreviewplugin PRIVATE TRANSLATION_DOMAIN="ktexteditorpreviewplugin") +target_link_libraries(ktexteditorpreviewplugin PRIVATE KF5::TextEditor) include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category( + DEBUG_SOURCES + HEADER ktexteditorpreview_debug.h + IDENTIFIER KTEPREVIEW + CATEGORY_NAME "ktexteditorpreviewplugin" +) +target_sources(ktexteditorpreviewplugin PRIVATE ${DEBUG_SOURCES}) -set(ktexteditorpreviewplugin_SRCS +target_sources( + ktexteditorpreviewplugin + PRIVATE ktexteditorpreviewplugin.cpp ktexteditorpreviewview.cpp previewwidget.cpp kpartview.cpp ) -ecm_qt_declare_logging_category(ktexteditorpreviewplugin_SRCS - HEADER ktexteditorpreview_debug.h - IDENTIFIER KTEPREVIEW - CATEGORY_NAME "ktexteditorpreviewplugin" -) - -add_library(ktexteditorpreviewplugin MODULE ${ktexteditorpreviewplugin_SRCS}) - -target_link_libraries(ktexteditorpreviewplugin - KF5::TextEditor - KF5::I18n -) - install(TARGETS ktexteditorpreviewplugin DESTINATION ${KDE_INSTALL_PLUGINDIR}/ktexteditor) diff --git a/addons/preview/ktexteditorpreview.json b/addons/preview/ktexteditorpreview.json index 7ed59657a..d5fa3abd3 100644 --- a/addons/preview/ktexteditorpreview.json +++ b/addons/preview/ktexteditorpreview.json @@ -1,118 +1,121 @@ { "KPlugin": { "Authors": [ { "Email": "kossebau@kde.org", "Name": "Friedrich W. H. Kossebau", "Name[ast]": "Friedrich W. H. Kossebau", "Name[ca@valencia]": "Friedrich W. H. Kossebau", "Name[ca]": "Friedrich W. H. Kossebau", "Name[cs]": "Friedrich W. H. Kossebau", "Name[da]": "Friedrich W. H. Kossebau", "Name[de]": "Friedrich W. H. Kossebau", "Name[el]": "Friedrich W. H. Kossebau", "Name[en_GB]": "Friedrich W. H. Kossebau", "Name[es]": "Friedrich W. H. Kossebau", "Name[eu]": "Friedrich W. H. Kossebau", "Name[fi]": "Friedrich W. H. Kossebau", "Name[fr]": "Friedrich W. H. Kossebau", "Name[gl]": "Friedrich W. H. Kossebau", "Name[ia]": "Friedrich W. H. Kossebau", + "Name[id]": "Friedrich W. H. Kossebau", "Name[it]": "Friedrich W. H. Kossebau", "Name[ko]": "Friedrich W. H. Kossebau", "Name[nl]": "Friedrich W. H. Kossebau", "Name[nn]": "Friedrich W.H. Kossebau", "Name[pl]": "Friedrich W. H. Kossebau", "Name[pt]": "Friedrich W. H. Kossebau", "Name[pt_BR]": "Friedrich W. H. Kossebau", "Name[ru]": "Friedrich W. H. Kossebau", "Name[sk]": "Friedrich W. H. Kossebau", "Name[sl]": "Friedrich W. H. Kossebau", "Name[sr@ijekavian]": "Фридрих В.Х. Косебау", "Name[sr@ijekavianlatin]": "Fridrih V.H. Kosebau", "Name[sr@latin]": "Fridrih V.H. Kosebau", "Name[sr]": "Фридрих В.Х. Косебау", "Name[sv]": "Friedrich W. H. Kossebau", "Name[uk]": "Friedrich W. H. Kossebau", "Name[x-test]": "xxFriedrich W. H. Kossebauxx", "Name[zh_CN]": "Friedrich W. H. Kossebau", "Name[zh_TW]": "Friedrich W. H. Kossebau" } ], "Description": "Preview the document in the target format", "Description[ast]": "Previsualiza los documentos nel formatu de destín", "Description[ca@valencia]": "Vista prèvia del document en el format de destí", "Description[ca]": "Vista prèvia del document en el format de destí", "Description[da]": "Forhåndsvis dokumentet i målformatet", "Description[de]": "Vorschau des Dokuments im Zielformat", "Description[el]": "Προεπισκόπηση του εγγράφου στον τύπο αποθήκευσης προορισμού", "Description[en_GB]": "Preview the document in the target format", "Description[es]": "Mostrar vista previa del documento en el formato de destino", "Description[eu]": "Aurreikusi dokumentua helburu formatuan", "Description[fi]": "Esikatsele asiakirjaa kohdemuodossa", "Description[fr]": "Aperçu du document dans le format de destination", "Description[gl]": "Previsualizar o documento no formato de destino.", "Description[ia]": "Vide preliminarmente le documento in le formato objectivo", + "Description[id]": "Pratinjaukan dokumen di dalam format sasaran", "Description[it]": "Anteprima del documento nel formato di destinazione", "Description[ko]": "문서를 대상 형식으로 미리 보기", "Description[ml]": "ടാർഗെറ്റ് ഫോർമാറ്റിൽ പ്രമാണം തിരനോട്ടം ചെയ്യുക", "Description[nl]": "Het document bekijken in het doelformaat", "Description[nn]": "Førehandsvis dokumentet i målformatet", "Description[pl]": "Podejrzyj dokument w formacie docelowym", "Description[pt]": "Mostra uma antevisão do documento no formato-alvo", "Description[pt_BR]": "Visualize o documento no formato de destino", "Description[ru]": "Предварительный просмотр документа в целевом формате", "Description[sk]": "Náhľad dokumentu v cieľovom formáte", "Description[sl]": "Predogled dokumenta v ciljni obliki", "Description[sr@ijekavian]": "Прегледајте документ у циљном формату", "Description[sr@ijekavianlatin]": "Pregledajte dokument u ciljnom formatu", "Description[sr@latin]": "Pregledajte dokument u ciljnom formatu", "Description[sr]": "Прегледајте документ у циљном формату", "Description[sv]": "Förhandsgranska dokumentet i målformatet", "Description[uk]": "Попередній перегляд документа у форматі призначення", "Description[x-test]": "xxPreview the document in the target formatxx", "Description[zh_CN]": "以目标格式预览文档", "Description[zh_TW]": "以目標格式顯示文件預覽", "Icon": "document-preview", "Id": "ktexteditorpreview", "Name": "Document Preview", "Name[ast]": "Previsualización de documentos", "Name[ca@valencia]": "Vista prèvia de document", "Name[ca]": "Vista prèvia de document", "Name[cs]": "Náhled na dokument", "Name[da]": "Forhåndsvisning af dokument", "Name[de]": "Dokumentvorschau", "Name[el]": "Προεπισκόπηση εγγράφου", "Name[en_GB]": "Document Preview", "Name[es]": "Vista previa del documento", "Name[eu]": "Dokumentua aurreikusi", "Name[fi]": "Asiakirjan esikatselu", "Name[fr]": "Aperçu de document", "Name[gl]": "Vista previa do documento", "Name[ia]": "Vista preliminar de documento", + "Name[id]": "Pratinjau Dokumen", "Name[it]": "Anteprima del documento", "Name[ko]": "문서 미리 보기", "Name[ml]": "പ്രമാണ പ്രിവ്യൂ", "Name[nl]": "Voorbeeld van document", "Name[nn]": "Førehandsvising av dokument", "Name[pl]": "Podgląd dokumentu", "Name[pt]": "Antevisão do Documento", "Name[pt_BR]": "Visualização do documento", "Name[ru]": "Предварительный просмотр документа", "Name[sk]": "Náhľad dokumentu", "Name[sl]": "Predogled dokumenta", "Name[sr@ijekavian]": "Преглед документа", "Name[sr@ijekavianlatin]": "Pregled dokumenta", "Name[sr@latin]": "Pregled dokumenta", "Name[sr]": "Преглед документа", "Name[sv]": "Dokumentförhandsgranskning", "Name[uk]": "Перегляд документа", "Name[x-test]": "xxDocument Previewxx", "Name[zh_CN]": "文档预览", "Name[zh_TW]": "文件預覽", "ServiceTypes": [ "KTextEditor/Plugin", "KDevelop/Plugin" ] } } diff --git a/addons/project/CMakeLists.txt b/addons/project/CMakeLists.txt index 6a37acd1d..cdc9f6946 100644 --- a/addons/project/CMakeLists.txt +++ b/addons/project/CMakeLists.txt @@ -1,53 +1,71 @@ -project(kateprojectplugin) - -find_package(KF5NewStuff ${KF5_DEP_VERSION} REQUIRED) # For KMoreTools - -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) - -add_definitions(-DTRANSLATION_DOMAIN=\"kateproject\") -set(kateprojectplugin_PART_SRCS - fileutil.cpp - kateprojectplugin.cpp - kateprojectpluginview.cpp - kateproject.cpp - kateprojectworker.cpp - kateprojectitem.cpp - kateprojectview.cpp - kateprojectviewtree.cpp - kateprojecttreeviewcontextmenu.cpp - kateprojectinfoview.cpp - kateprojectcompletion.cpp - kateprojectindex.cpp - kateprojectinfoviewindex.cpp - kateprojectinfoviewterminal.cpp - kateprojectinfoviewcodeanalysis.cpp - kateprojectinfoviewnotes.cpp - kateprojectconfigpage.cpp - kateprojectcodeanalysistool.cpp - tools/kateprojectcodeanalysistoolcppcheck.cpp - tools/kateprojectcodeanalysistoolflake8.cpp - tools/kateprojectcodeanalysistoolshellcheck.cpp - tools/kateprojectcodeanalysisselector.cpp +find_package( + KF5 + QUIET + COMPONENTS + GuiAddons + NewStuff + ThreadWeaver ) -# resource for ui file and stuff -qt5_add_resources(kateprojectplugin_PART_SRCS plugin.qrc) +set_package_properties(KFGuiAddons PROPERTIES PURPOSE "Required to build the project addon") +set_package_properties(KF5NewStuff PROPERTIES PURPOSE "Required to build the project addon") +set_package_properties(KF5ThreadWeaver PROPERTIES PURPOSE "Required to build the project addon") -add_library(kateprojectplugin MODULE ${kateprojectplugin_PART_SRCS}) -kcoreaddons_desktop_to_json (kateprojectplugin kateprojectplugin.desktop) -target_link_libraries(kateprojectplugin - KF5::TextEditor - KF5::Parts KF5::I18n +if(NOT KF5_FOUND) + return() +endif() + +add_library(kateprojectplugin MODULE "") +target_compile_definitions(kateprojectplugin PRIVATE TRANSLATION_DOMAIN="kateproject") + +target_link_libraries( + kateprojectplugin + PRIVATE KF5::GuiAddons - KF5::ItemViews KF5::ItemModels KF5::IconThemes KF5::ThreadWeaver - KF5::NewStuff # For KMoreTools + KF5::NewStuff + KF5::TextEditor + KF5::ThreadWeaver +) + +include(CheckFunctionExists) +check_function_exists(ctermid HAVE_CTERMID) + +if(HAVE_CTERMID) + target_compile_definitions(kateprojectplugin PRIVATE HAVE_CTERMID) +endif() + +target_sources( + kateprojectplugin + PRIVATE + fileutil.cpp + kateprojectplugin.cpp + kateprojectpluginview.cpp + kateproject.cpp + kateprojectworker.cpp + kateprojectitem.cpp + kateprojectview.cpp + kateprojectviewtree.cpp + kateprojecttreeviewcontextmenu.cpp + kateprojectinfoview.cpp + kateprojectcompletion.cpp + kateprojectindex.cpp + kateprojectinfoviewindex.cpp + kateprojectinfoviewterminal.cpp + kateprojectinfoviewcodeanalysis.cpp + kateprojectinfoviewnotes.cpp + kateprojectconfigpage.cpp + kateprojectcodeanalysistool.cpp + tools/kateprojectcodeanalysistoolcppcheck.cpp + tools/kateprojectcodeanalysistoolflake8.cpp + tools/kateprojectcodeanalysistoolshellcheck.cpp + tools/kateprojectcodeanalysisselector.cpp + plugin.qrc ) -########### install files ############### -install(TARGETS kateprojectplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) -install( FILES kateproject.example DESTINATION ${DATA_INSTALL_DIR}/kateproject ) +kcoreaddons_desktop_to_json(kateprojectplugin kateprojectplugin.desktop) +install(TARGETS kateprojectplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) +install(FILES kateproject.example DESTINATION ${DATA_INSTALL_DIR}/kateproject) -############# unit tests ################ -if (BUILD_TESTING) - add_subdirectory(autotests) +if(BUILD_TESTING) + add_subdirectory(autotests) endif() diff --git a/addons/project/autotests/CMakeLists.txt b/addons/project/autotests/CMakeLists.txt index e5402ae26..0ef06829e 100644 --- a/addons/project/autotests/CMakeLists.txt +++ b/addons/project/autotests/CMakeLists.txt @@ -1,17 +1,24 @@ include(ECMMarkAsTest) -include_directories( - ${CMAKE_CURRENT_SOURCE_DIR}/.. +add_executable(projectplugin_test "") +target_include_directories(projectplugin_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + +find_package(Qt5Test QUIET REQUIRED) +target_link_libraries( + projectplugin_test + PRIVATE + KF5::TextEditor + Qt5::Test ) -# Project Plugin -set(ProjectPluginSrc +target_sources( + projectplugin_test + PRIVATE test1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../fileutil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../kateprojectcodeanalysistool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../tools/kateprojectcodeanalysistoolshellcheck.cpp ) -add_executable(projectplugin_test ${ProjectPluginSrc}) + add_test(NAME plugin-project_test COMMAND projectplugin_test) -target_link_libraries(projectplugin_test kdeinit_kate Qt5::Test) ecm_mark_as_test(projectplugin_test) diff --git a/addons/project/kateprojectinfoviewterminal.cpp b/addons/project/kateprojectinfoviewterminal.cpp index 9fed26d55..b72229766 100644 --- a/addons/project/kateprojectinfoviewterminal.cpp +++ b/addons/project/kateprojectinfoviewterminal.cpp @@ -1,118 +1,119 @@ /* This file is part of the Kate project. * * Copyright (C) 2012 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kateprojectinfoviewterminal.h" #include "kateprojectpluginview.h" #include #include #include KPluginFactory *KateProjectInfoViewTerminal::s_pluginFactory = nullptr; KateProjectInfoViewTerminal::KateProjectInfoViewTerminal(KateProjectPluginView *pluginView, const QString &directory) : QWidget() , m_pluginView(pluginView) , m_directory(directory) , m_konsolePart(nullptr) { /** * layout widget */ m_layout = new QVBoxLayout(this); m_layout->setSpacing(0); m_layout->setContentsMargins(0, 0, 0, 0); /** * initial terminal creation */ loadTerminal(); } KateProjectInfoViewTerminal::~KateProjectInfoViewTerminal() { /** * avoid endless loop */ if (m_konsolePart) { disconnect(m_konsolePart, &KParts::ReadOnlyPart::destroyed, this, &KateProjectInfoViewTerminal::loadTerminal); } } KPluginFactory *KateProjectInfoViewTerminal::pluginFactory() { if (s_pluginFactory) { return s_pluginFactory; } return s_pluginFactory = KPluginLoader(QStringLiteral("konsolepart")).factory(); } void KateProjectInfoViewTerminal::loadTerminal() { /** * null in any case, if loadTerminal fails below and we are in the destroyed event */ m_konsolePart = nullptr; + setFocusProxy(nullptr); /** * we shall not arrive here without a factory, if it is not there, no terminal toolview shall be created */ Q_ASSERT(pluginFactory()); /** * create part */ m_konsolePart = pluginFactory()->create(this, this); if (!m_konsolePart) { return; } /** * init locale translation stuff */ // FIXME KF5 KGlobal::locale()->insertCatalog("konsole"); /** * switch to right directory */ qobject_cast(m_konsolePart)->showShellInDir(m_directory); /** * add to widget */ m_layout->addWidget(m_konsolePart->widget()); setFocusProxy(m_konsolePart->widget()); /** * guard destruction, create new terminal! */ connect(m_konsolePart, &KParts::ReadOnlyPart::destroyed, this, &KateProjectInfoViewTerminal::loadTerminal); connect(m_konsolePart, SIGNAL(overrideShortcut(QKeyEvent *, bool &)), this, SLOT(overrideShortcut(QKeyEvent *, bool &))); } void KateProjectInfoViewTerminal::overrideShortcut(QKeyEvent *, bool &override) { /** * let konsole handle all shortcuts */ override = true; } diff --git a/addons/project/kateprojectplugin.cpp b/addons/project/kateprojectplugin.cpp index baf373a60..6c6040799 100644 --- a/addons/project/kateprojectplugin.cpp +++ b/addons/project/kateprojectplugin.cpp @@ -1,353 +1,360 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kateprojectplugin.h" #include "kateproject.h" #include "kateprojectconfigpage.h" #include "kateprojectpluginview.h" #include #include #include #include #include #include #include #include -#include "config.h" +#include #ifdef HAVE_CTERMID #include #include #include #include #include #endif namespace { 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; } KateProjectPlugin::KateProjectPlugin(QObject *parent, const QList &) : KTextEditor::Plugin(parent) , m_completion(this) , m_autoGit(true) , m_autoSubversion(true) , m_autoMercurial(true) , m_weaver(new ThreadWeaver::Queue(this)) { qRegisterMetaType("KateProjectSharedQStandardItem"); qRegisterMetaType("KateProjectSharedQMapStringItem"); qRegisterMetaType("KateProjectSharedProjectIndex"); connect(KTextEditor::Editor::instance()->application(), &KTextEditor::Application::documentCreated, this, &KateProjectPlugin::slotDocumentCreated); connect(&m_fileWatcher, &QFileSystemWatcher::directoryChanged, this, &KateProjectPlugin::slotDirectoryChanged); #ifdef HAVE_CTERMID /** * open project for our current working directory, if this kate has a terminal * http://stackoverflow.com/questions/1312922/detect-if-stdin-is-a-terminal-or-pipe-in-c-c-qt */ char tty[L_ctermid + 1] = {0}; ctermid(tty); int fd = ::open(tty, O_RDONLY); if (fd >= 0) { projectForDir(QDir::current()); ::close(fd); } #endif readConfig(); for (auto document : KTextEditor::Editor::instance()->application()->documents()) { slotDocumentCreated(document); } } KateProjectPlugin::~KateProjectPlugin() { for (KateProject *project : m_projects) { m_fileWatcher.removePath(QFileInfo(project->fileName()).canonicalPath()); delete project; } m_projects.clear(); m_weaver->shutDown(); delete m_weaver; } QObject *KateProjectPlugin::createView(KTextEditor::MainWindow *mainWindow) { return new KateProjectPluginView(this, mainWindow); } int KateProjectPlugin::configPages() const { return 1; } KTextEditor::ConfigPage *KateProjectPlugin::configPage(int number, QWidget *parent) { if (number != 0) { return nullptr; } return new KateProjectConfigPage(parent, this); } KateProject *KateProjectPlugin::createProjectForFileName(const QString &fileName) { KateProject *project = new KateProject(m_weaver); - if (!project->loadFromFile(fileName)) { delete project; return nullptr; } m_projects.append(project); m_fileWatcher.addPath(QFileInfo(fileName).canonicalPath()); emit projectCreated(project); return project; } KateProject *KateProjectPlugin::projectForDir(QDir dir) { /** - * search projects upwards + * search project file upwards * with recursion guard + * 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 */ QSet seenDirectories; + std::vector directoryStack; while (!seenDirectories.contains(dir.absolutePath())) { - /** - * fill recursion guard - */ + // update guard seenDirectories.insert(dir.absolutePath()); - /** - * check for project and load it if found - */ - QString canonicalPath = dir.canonicalPath(); - QString canonicalFileName = dir.filePath(ProjectFileName); + // remember directory for later project creation based on other criteria + directoryStack.push_back(dir.absolutePath()); + // check for project and load it if found + const QString canonicalPath = dir.canonicalPath(); + const QString canonicalFileName = dir.filePath(ProjectFileName); for (KateProject *project : m_projects) { if (project->baseDir() == canonicalPath || project->fileName() == canonicalFileName) { return project; } } + // project file found => done if (dir.exists(ProjectFileName)) { return createProjectForFileName(canonicalFileName); } - KateProject *project; - if ((project = detectGit(dir)) || (project = detectSubversion(dir)) || (project = detectMercurial(dir))) { - return project; - } - - /** - * else: cd up, if possible or abort - */ + // else: cd up, if possible or abort if (!dir.cdUp()) { break; } } + /** + * 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 return nullptr; } KateProject *KateProjectPlugin::projectForUrl(const QUrl &url) { if (url.isEmpty() || !url.isLocalFile()) { return nullptr; } return projectForDir(QFileInfo(url.toLocalFile()).absoluteDir()); } void KateProjectPlugin::slotDocumentCreated(KTextEditor::Document *document) { connect(document, &KTextEditor::Document::documentUrlChanged, this, &KateProjectPlugin::slotDocumentUrlChanged); connect(document, &KTextEditor::Document::destroyed, this, &KateProjectPlugin::slotDocumentDestroyed); slotDocumentUrlChanged(document); } void KateProjectPlugin::slotDocumentDestroyed(QObject *document) { if (KateProject *project = m_document2Project.value(document)) { project->unregisterDocument(static_cast(document)); } m_document2Project.remove(document); } void KateProjectPlugin::slotDocumentUrlChanged(KTextEditor::Document *document) { 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); } } void KateProjectPlugin::slotDirectoryChanged(const QString &path) { QString fileName = QDir(path).filePath(ProjectFileName); for (KateProject * project : m_projects) { if (project->fileName() == fileName) { QDateTime lastModified = QFileInfo(fileName).lastModified(); if (project->fileLastModified().isNull() || (lastModified > project->fileLastModified())) { project->reload(); } break; } } } KateProject* KateProjectPlugin::detectGit(const QDir &dir) { // allow .git as dir and file (file for git worktree stuff, https://git-scm.com/docs/git-worktree) if (m_autoGit && dir.exists(GitFolderName)) { return createProjectForRepository(QStringLiteral("git"), dir); } return nullptr; } KateProject* KateProjectPlugin::detectSubversion(const QDir &dir) { if (m_autoSubversion && dir.exists(SubversionFolderName) && QFileInfo(dir, SubversionFolderName).isDir()) { return createProjectForRepository(QStringLiteral("svn"), dir); } return nullptr; } KateProject* KateProjectPlugin::detectMercurial(const QDir &dir) { 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; cnf[QStringLiteral("name")] = dir.dirName(); cnf[QStringLiteral("files")] = (QVariantList() << files); KateProject *project = new KateProject(m_weaver); project->loadFromData(cnf, dir.canonicalPath()); m_projects.append(project); emit projectCreated(project); 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; } void KateProjectPlugin::readConfig() { KConfigGroup config(KSharedConfig::openConfig(), "project"); QStringList autorepository = config.readEntry("autorepository", DefaultConfig); m_autoGit = m_autoSubversion = m_autoMercurial = false; if (autorepository.contains(GitConfig)) { m_autoGit = true; } if (autorepository.contains(SubversionConfig)) { m_autoSubversion = true; } if (autorepository.contains(MercurialConfig)) { m_autoMercurial = true; } } 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); } diff --git a/addons/project/kateprojectplugin.desktop b/addons/project/kateprojectplugin.desktop index 6139faffe..ec33991f0 100644 --- a/addons/project/kateprojectplugin.desktop +++ b/addons/project/kateprojectplugin.desktop @@ -1,84 +1,85 @@ [Desktop Entry] Type=Service ServiceTypes=KTextEditor/Plugin X-KDE-Library=kateprojectplugin Name=Project Plugin Name[ar]=ملحقة المشاريع Name[ast]=Complementu de proyeutos Name[bg]=Приставка за проекти Name[bs]=Priključak projekta Name[ca]=Connector de projecte Name[ca@valencia]=Connector de projecte Name[cs]=Modul projektu Name[da]=Projekt-plugin Name[de]=Projektmodul Name[el]=Project πρόσθετο Name[en_GB]=Project Plugin Name[es]=Complemento de proyecto Name[et]=Projektiplugin Name[eu]=Proiektu plugina Name[fi]=Projektiliitännäinen Name[fr]=Module externe de projet Name[ga]=Breiseán Tionscadail Name[gl]=Complemento de proxecto Name[he]=תוסף פרוייקט Name[hu]=Projekt bővítmény Name[ia]=Plug-in de Projecto Name[id]=Plugin Projek Name[is]=Verkefnaviðbót Name[it]=Estensione di progetto Name[kk]=Жоба плагині Name[km]=កម្មវិធី​ជំនួយ​គម្រោង Name[ko]=프로젝트 플러그인 Name[lt]=Projekto papildiniai Name[mr]=परियोजना प्लगइन Name[nb]=Tillegg for prosjekt Name[nds]=Projektmoduul Name[nl]=Projectplug-in Name[nn]=Prosjekt-tillegg Name[pa]=ਪਰੋਜੈਕਟ ਪਲੱਗਇਨ Name[pl]=Wtyczka projektu Name[pt]='Plugin' de Projecto Name[pt_BR]=Plugin de projeto Name[ro]=Modul de proiecte Name[ru]=Модуль проектов Name[sk]=Modul projektu Name[sl]=Projektni vstavek Name[sr]=Пројектни прикључак Name[sr@ijekavian]=Пројектни прикључак Name[sr@ijekavianlatin]=Projektni priključak Name[sr@latin]=Projektni priključak Name[sv]=Projektinsticksprogram Name[tg]=Плагини лоиҳа Name[tr]=Proje Eklentisi Name[uk]=Додаток проєктів Name[x-test]=xxProject Pluginxx Name[zh_CN]=工程插件 Name[zh_TW]=專案外掛程式 Comment=Integration with Git and other source control systems Comment[ast]=Integración con Git y otros sistemes de control de códigu Comment[ca]=Integració amb el Git i altres sistemes de control de codi font Comment[ca@valencia]=Integració amb el Git i altres sistemes de control de codi font Comment[de]=Integration von Git und anderen Versionsverwaltungen für Quelltexte Comment[el]=Ενσωμάτωση με το Git και με άλλα συστήματα ελέγχου πηγαίου κώδικα Comment[en_GB]=Integration with Git and other source control systems Comment[es]=Integración con Git y otros sistemas de control de código fuente Comment[eu]=Git eta beste sorburu kontrolerako sistemekin bateratzea Comment[fi]=Integrointi Gitiin ja muihin versionhallintajärjestelmiin Comment[fr]=Intégration avec Git et d'autres systèmes de contrôle de version Comment[gl]=Integración con Git e outros sistemas de control de código fonte +Comment[id]=Integrasi dengan Git dan sistem kendali sumber lainnya Comment[it]=Integrazione con Git e con altri sistemi di controllo del sorgente Comment[ko]=Git 및 다른 버전 관리 시스템 통합 Comment[nl]=Integratie met Git en andere controlesystemen voor broncode Comment[nn]=Integrering med Git og andre versjonskontrollsystem Comment[pl]=Współpraca z Git i innymi systemami do zarządzania wersjami Comment[pt]=Integração com o Git e outros sistemas de controlo de versões Comment[pt_BR]=Integração com o Git e outros sistemas de controle de versões Comment[ru]=Интеграция с Git, а также с другими системами управления версиями Comment[sk]=Integrácia s Git a ostatnými systémami ovládania zdrojov Comment[sv]=Integrering med Git och andra källkontrollsystem Comment[tr]=Git ve diğer sürüm takibi sistemleriyle bütünleşme Comment[uk]=Інтеграція із Git та іншими системами керування кодом Comment[x-test]=xxIntegration with Git and other source control systemsxx Comment[zh_CN]=与 Git 和其他源代码控制系统集成 Comment[zh_TW]=與 Git 或其他來源控制系統整合 diff --git a/addons/project/kateprojectpluginview.cpp b/addons/project/kateprojectpluginview.cpp index 3798fef2b..47c47f2ed 100644 --- a/addons/project/kateprojectpluginview.cpp +++ b/addons/project/kateprojectpluginview.cpp @@ -1,526 +1,472 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kateprojectpluginview.h" #include "kateprojectinfoviewindex.h" #include "fileutil.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KateProjectPluginFactory, "kateprojectplugin.json", registerPlugin();) KateProjectPluginView::KateProjectPluginView(KateProjectPlugin *plugin, KTextEditor::MainWindow *mainWin) : QObject(mainWin) , m_plugin(plugin) , m_mainWindow(mainWin) , m_toolView(nullptr) , m_toolInfoView(nullptr) , m_lookupAction(nullptr) { KXMLGUIClient::setComponentName(QStringLiteral("kateproject"), i18n("Kate Project Manager")); setXMLFile(QStringLiteral("ui.rc")); + /** + * create toolviews + */ + m_toolView = m_mainWindow->createToolView(m_plugin, QStringLiteral("kateproject"), KTextEditor::MainWindow::Left, QIcon::fromTheme(QStringLiteral("project-open")), i18n("Projects")); + m_toolInfoView = m_mainWindow->createToolView(m_plugin, QStringLiteral("kateprojectinfo"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("view-choose")), i18n("Current Project")); + + /** + * create the combo + buttons for the toolViews + stacked widgets + */ + m_projectsCombo = new QComboBox(m_toolView); + m_projectsCombo->setFrame(false); + m_reloadButton = new QToolButton(m_toolView); + m_reloadButton->setAutoRaise(true); + m_reloadButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); + QHBoxLayout *layout = new QHBoxLayout(); + layout->setSpacing(0); + layout->addWidget(m_projectsCombo); + layout->addWidget(m_reloadButton); + m_toolView->layout()->addItem(layout); + m_toolView->layout()->setSpacing(0); + + m_stackedProjectViews = new QStackedWidget(m_toolView); + m_stackedProjectInfoViews = new QStackedWidget(m_toolInfoView); + + connect(m_projectsCombo, static_cast(&QComboBox::currentIndexChanged), this, &KateProjectPluginView::slotCurrentChanged); + connect(m_reloadButton, &QToolButton::clicked, this, &KateProjectPluginView::slotProjectReload); + /** * create views for all already existing projects * will create toolviews on demand! */ foreach(KateProject * project, m_plugin->projects()) viewForProject(project); /** * connect to important signals, e.g. for auto project view creation */ connect(m_plugin, &KateProjectPlugin::projectCreated, this, &KateProjectPluginView::viewForProject); connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &KateProjectPluginView::slotViewChanged); connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, &KateProjectPluginView::slotViewCreated); /** * connect for all already existing views */ foreach(KTextEditor::View * view, m_mainWindow->views()) slotViewCreated(view); /** * trigger once view change, to highlight right document */ slotViewChanged(); /** * back + forward */ auto a = actionCollection()->addAction(KStandardAction::Back, QStringLiteral("projects_prev_project"), this, SLOT(slotProjectPrev())); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Left)); a = actionCollection()->addAction(KStandardAction::Forward, QStringLiteral("projects_next_project"), this, SLOT(slotProjectNext())); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Right)); a = actionCollection()->addAction(KStandardAction::Goto, QStringLiteral("projects_goto_index"), this, SLOT(slotProjectIndex())); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::ALT | Qt::Key_1)); // popup menu auto popup = new KActionMenu(i18n("Project"), this); actionCollection()->addAction(QStringLiteral("popup_project"), popup); m_lookupAction = popup->menu()->addAction(i18n("Lookup: %1", QString()), this, &KateProjectPluginView::slotProjectIndex); connect(popup->menu(), &QMenu::aboutToShow, this, &KateProjectPluginView::slotContextMenuAboutToShow); /** * add us to gui */ m_mainWindow->guiFactory()->addClient(this); } KateProjectPluginView::~KateProjectPluginView() { /** * cleanup for all views */ foreach(QObject * view, m_textViews) { KTextEditor::CodeCompletionInterface *cci = qobject_cast(view); if (cci) { cci->unregisterCompletionModel(m_plugin->completion()); } } /** * cu toolviews */ delete m_toolView; m_toolView = nullptr; delete m_toolInfoView; m_toolInfoView = nullptr; /** * cu gui client */ m_mainWindow->guiFactory()->removeClient(this); } QPair KateProjectPluginView::viewForProject(KateProject *project) { /** * needs valid project */ Q_ASSERT(project); - /** - * create toolviews on demand - */ - if (!m_toolView) { - /** - * create toolviews - */ - m_toolView = m_mainWindow->createToolView(m_plugin, QStringLiteral("kateproject"), KTextEditor::MainWindow::Left, QIcon::fromTheme(QStringLiteral("project-open")), i18n("Projects")); - m_toolInfoView = m_mainWindow->createToolView(m_plugin, QStringLiteral("kateprojectinfo"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("view-choose")), i18n("Current Project")); - - /** - * create the combo + buttons for the toolViews + stacked widgets - */ - m_projectsCombo = new QComboBox(m_toolView); - m_projectsCombo->setFrame(false); - m_reloadButton = new QToolButton(m_toolView); - m_reloadButton->setAutoRaise(true); - m_reloadButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); - QHBoxLayout *layout = new QHBoxLayout(); - layout->setSpacing(0); - layout->addWidget(m_projectsCombo); - layout->addWidget(m_reloadButton); - m_toolView->layout()->addItem(layout); - m_toolView->layout()->setSpacing(0); - - m_stackedProjectViews = new QStackedWidget(m_toolView); - m_stackedProjectInfoViews = new QStackedWidget(m_toolInfoView); - - connect(m_projectsCombo, static_cast(&QComboBox::currentIndexChanged), this, &KateProjectPluginView::slotCurrentChanged); - connect(m_reloadButton, &QToolButton::clicked, this, &KateProjectPluginView::slotProjectReload); - } - /** * existing view? */ if (m_project2View.contains(project)) { return m_project2View.value(project); } /** * create new views */ KateProjectView *view = new KateProjectView(this, project); KateProjectInfoView *infoView = new KateProjectInfoView(this, project); /** * attach to toolboxes * first the views, then the combo, that triggers signals */ m_stackedProjectViews->addWidget(view); m_stackedProjectInfoViews->addWidget(infoView); - m_stackedProjectInfoViews->setFocusProxy(infoView); m_projectsCombo->addItem(QIcon::fromTheme(QStringLiteral("project-open")), project->name(), project->fileName()); /** * remember and return it */ return (m_project2View[project] = QPair (view, infoView)); } QString KateProjectPluginView::projectFileName() const { - // nothing there, skip - if (!m_toolView) { - return QString(); - } - QWidget *active = m_stackedProjectViews->currentWidget(); if (!active) { return QString(); } return static_cast(active)->project()->fileName(); } QString KateProjectPluginView::projectName() const { - // nothing there, skip - if (!m_toolView) { - return QString(); - } - QWidget *active = m_stackedProjectViews->currentWidget(); if (!active) { return QString(); } return static_cast(active)->project()->name(); } QString KateProjectPluginView::projectBaseDir() const { - // nothing there, skip - if (!m_toolView) { - return QString(); - } - QWidget *active = m_stackedProjectViews->currentWidget(); if (!active) { return QString(); } return static_cast(active)->project()->baseDir(); } QVariantMap KateProjectPluginView::projectMap() const { - // nothing there, skip - if (!m_toolView) { - return QVariantMap(); - } - QWidget *active = m_stackedProjectViews->currentWidget(); if (!active) { return QVariantMap(); } return static_cast(active)->project()->projectMap(); } QStringList KateProjectPluginView::projectFiles() const { - // nothing there, skip - if (!m_toolView) { - return QStringList(); - } - KateProjectView *active = static_cast(m_stackedProjectViews->currentWidget()); if (!active) { return QStringList(); } return active->project()->files(); } QString KateProjectPluginView::allProjectsCommonBaseDir() const { auto projects = m_plugin->projects(); if (projects.empty()) { return QString(); } if (projects.size() == 1) { return projects[0]->baseDir(); } QString commonParent1 = FileUtil::commonParent(projects[0]->baseDir(), projects[1]->baseDir()); for (int i = 2; i < projects.size(); i++) { commonParent1 = FileUtil::commonParent(commonParent1, projects[i]->baseDir()); } return commonParent1; } QStringList KateProjectPluginView::allProjectsFiles() const { QStringList fileList; foreach (auto project, m_plugin->projects()) { fileList.append(project->files()); } return fileList; } void KateProjectPluginView::slotViewChanged() { /** * get active view */ KTextEditor::View *activeView = m_mainWindow->activeView(); /** * update pointer, maybe disconnect before */ if (m_activeTextEditorView) { m_activeTextEditorView->document()->disconnect(this); } m_activeTextEditorView = activeView; /** * no current active view, return */ if (!m_activeTextEditorView) { return; } /** * connect to url changed, for auto load */ connect(m_activeTextEditorView->document(), &KTextEditor::Document::documentUrlChanged, this, &KateProjectPluginView::slotDocumentUrlChanged); /** * trigger slot once */ slotDocumentUrlChanged(m_activeTextEditorView->document()); } void KateProjectPluginView::slotCurrentChanged(int index) { - // nothing there, skip - if (!m_toolView) { - return; - } - - /** - * trigger change of stacked widgets - */ + // trigger change of stacked widgets m_stackedProjectViews->setCurrentIndex(index); m_stackedProjectInfoViews->setCurrentIndex(index); - /** - * open currently selected document - */ + // update focus proxy + open currently selected document if (QWidget *current = m_stackedProjectViews->currentWidget()) { + m_stackedProjectViews->setFocusProxy(current); static_cast(current)->openSelectedDocument(); } - /** - * project file name might have changed - */ + // update focus proxy + if (QWidget *current = m_stackedProjectInfoViews->currentWidget()) { + m_stackedProjectInfoViews->setFocusProxy(current); + } + + // project file name might have changed emit projectFileNameChanged(); emit projectMapChanged(); } void KateProjectPluginView::slotDocumentUrlChanged(KTextEditor::Document *document) { /** * abort if empty url or no local path */ if (document->url().isEmpty() || !document->url().isLocalFile()) { return; } /** * search matching project */ KateProject *project = m_plugin->projectForUrl(document->url()); if (!project) { return; } /** * select the file FIRST */ m_project2View.value(project).first->selectFile(document->url().toLocalFile()); /** * get active project view and switch it, if it is for a different project * do this AFTER file selection */ KateProjectView *active = static_cast(m_stackedProjectViews->currentWidget()); if (active != m_project2View.value(project).first) { int index = m_projectsCombo->findData(project->fileName()); if (index >= 0) { m_projectsCombo->setCurrentIndex(index); } } } void KateProjectPluginView::slotViewCreated(KTextEditor::View *view) { /** * connect to destroyed */ connect(view, &KTextEditor::View::destroyed, this, &KateProjectPluginView::slotViewDestroyed); /** * add completion model if possible */ KTextEditor::CodeCompletionInterface *cci = qobject_cast(view); if (cci) { cci->registerCompletionModel(m_plugin->completion()); } /** * remember for this view we need to cleanup! */ m_textViews.insert(view); } void KateProjectPluginView::slotViewDestroyed(QObject *view) { /** * remove remembered views for which we need to cleanup on exit! */ m_textViews.remove(view); } void KateProjectPluginView::slotProjectPrev() { - // nothing there, skip - if (!m_toolView) { - return; - } - if (!m_projectsCombo->count()) { return; } if (m_projectsCombo->currentIndex() == 0) { m_projectsCombo->setCurrentIndex(m_projectsCombo->count() - 1); } else { m_projectsCombo->setCurrentIndex(m_projectsCombo->currentIndex() - 1); } } void KateProjectPluginView::slotProjectNext() { - // nothing there, skip - if (!m_toolView) { - return; - } - if (!m_projectsCombo->count()) { return; } if (m_projectsCombo->currentIndex() + 1 == m_projectsCombo->count()) { m_projectsCombo->setCurrentIndex(0); } else { m_projectsCombo->setCurrentIndex(m_projectsCombo->currentIndex() + 1); } } void KateProjectPluginView::slotProjectReload() { - // nothing there, skip - if (!m_toolView) { - return; - } - /** * force reload if any active project */ if (QWidget *current = m_stackedProjectViews->currentWidget()) { static_cast(current)->project()->reload(true); } } QString KateProjectPluginView::currentWord() const { KTextEditor::View *kv = m_activeTextEditorView; if (!kv) { return QString(); } if (kv->selection() && kv->selectionRange().onSingleLine()) { return kv->selectionText(); } return kv->document()->wordAt(kv->cursorPosition()); } void KateProjectPluginView::slotProjectIndex() { - if (!m_toolView) { - return; - } const QString word = currentWord(); if (!word.isEmpty()) { auto tabView = qobject_cast(m_stackedProjectInfoViews->currentWidget()); if (tabView) { if (auto codeIndex = tabView->findChild()) { tabView->setCurrentWidget(codeIndex); } } m_mainWindow->showToolView(m_toolInfoView); emit projectLookupWord(word); } } void KateProjectPluginView::slotContextMenuAboutToShow() { const QString word = currentWord(); if (word.isEmpty()) { return; } const QString squeezed = KStringHandler::csqueeze(word, 30); m_lookupAction->setText(i18n("Lookup: %1", squeezed)); } #include "kateprojectpluginview.moc" diff --git a/addons/project/kateprojecttreeviewcontextmenu.cpp b/addons/project/kateprojecttreeviewcontextmenu.cpp index 0c4fdef2f..6ee0eafd2 100644 --- a/addons/project/kateprojecttreeviewcontextmenu.cpp +++ b/addons/project/kateprojecttreeviewcontextmenu.cpp @@ -1,161 +1,160 @@ /* This file is part of the Kate project. * * Copyright (C) 2013 Dominik Haumann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kateprojecttreeviewcontextmenu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include KateProjectTreeViewContextMenu::KateProjectTreeViewContextMenu() { } KateProjectTreeViewContextMenu::~KateProjectTreeViewContextMenu() { } static bool isGit(const QString &filename) { QFileInfo fi(filename); QDir dir(fi.absoluteDir()); QProcess git; git.setWorkingDirectory(dir.absolutePath()); QStringList args; // git ls-files -z results a bytearray where each entry is \0-terminated. // NOTE: Without -z, Umlauts such as "Der Bäcker/Das Brötchen.txt" do not work (#389415, #402213) args << QStringLiteral("ls-files") << QStringLiteral("-z") << fi.fileName(); git.start(QStringLiteral("git"), args); bool isGit = false; if (git.waitForStarted() && git.waitForFinished(-1)) { const QList byteArrayList = git.readAllStandardOutput().split('\0'); const QString fn = fi.fileName(); for (const QByteArray & byteArray : byteArrayList) { if (fn == QString::fromUtf8(byteArray)) { isGit = true; break; } } } return isGit; } void KateProjectTreeViewContextMenu::exec(const QString &filename, const QPoint &pos, QWidget *parent) { /** * Create context menu */ QMenu menu; /** * Copy Path */ QAction *copyAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy File Path")); /** * Handle "open with", * find correct mimetype to query for possible applications */ QMenu *openWithMenu = menu.addMenu(i18n("Open With")); QMimeType mimeType = QMimeDatabase().mimeTypeForFile(filename); - KService::List offers = KMimeTypeTrader::self()->query(mimeType.name(), QStringLiteral("Application")); + const KService::List offers = KMimeTypeTrader::self()->query(mimeType.name(), QStringLiteral("Application")); // For each one, insert a menu item... - for (KService::List::Iterator it = offers.begin(); it != offers.end(); ++it) { - KService::Ptr service = *it; + for (const auto& service : offers) { if (service->name() == QStringLiteral("Kate")) { continue; // omit Kate } QAction *action = openWithMenu->addAction(QIcon::fromTheme(service->icon()), service->name()); action->setData(service->entryPath()); } // Perhaps disable menu, if no entries openWithMenu->setEnabled(!openWithMenu->isEmpty()); /** * Open Containing folder */ auto openContaingFolderAction = menu.addAction(QIcon::fromTheme(QStringLiteral("document-open-folder")), i18n("&Open Containing Folder")); /** * File Properties Dialog */ auto filePropertiesAction = menu.addAction(QIcon::fromTheme(QStringLiteral("dialog-object-properties")), i18n("Properties")); /** * Git menu */ KMoreToolsMenuFactory menuFactory(QStringLiteral("kate/addons/project/git-tools")); QMenu gitMenu; // must live as long as the maybe filled menu items should live if (isGit(filename)) { menuFactory.fillMenuFromGroupingNames(&gitMenu, { QLatin1String("git-clients-and-actions") }, QUrl::fromLocalFile(filename)); menu.addSection(i18n("Git:")); Q_FOREACH(auto action, gitMenu.actions()) { menu.addAction(action); } } /** * run menu and handle the triggered action */ if (QAction* const action = menu.exec(pos)) { if (action == copyAction) { QApplication::clipboard()->setText(filename); } else if (action->parentWidget() == openWithMenu) { // handle "open with" const QString openWith = action->data().toString(); if (KService::Ptr app = KService::serviceByDesktopPath(openWith)) { QList list; list << QUrl::fromLocalFile(filename); KRun::runService(*app, list, parent); } } else if (action == openContaingFolderAction) { KIO::highlightInFileManager({ QUrl::fromLocalFile(filename) }); } else if (action == filePropertiesAction) { // code copied and adapted from frameworks/kio/src/filewidgets/knewfilemenu.cpp KFileItem fileItem(QUrl::fromLocalFile(filename)); QDialog* dlg = new KPropertiesDialog(fileItem); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->show(); } else { // One of the git actions was triggered } } } diff --git a/addons/project/kateprojectview.cpp b/addons/project/kateprojectview.cpp index 764ce19d5..c77451dc7 100644 --- a/addons/project/kateprojectview.cpp +++ b/addons/project/kateprojectview.cpp @@ -1,87 +1,90 @@ /* This file is part of the Kate project. * * Copyright (C) 2012 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kateprojectview.h" #include "kateprojectpluginview.h" #include #include #include #include #include #include #include KateProjectView::KateProjectView(KateProjectPluginView *pluginView, KateProject *project) : QWidget() , m_pluginView(pluginView) , m_project(project) , m_treeView(new KateProjectViewTree(pluginView, project)) , m_filter(new KLineEdit()) { /** * layout tree view and co. */ QVBoxLayout *layout = new QVBoxLayout(); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(m_treeView); layout->addWidget(m_filter); setLayout(layout); + // let tree get focus for keyboard selection of file to open + setFocusProxy(m_treeView); + /** * setup filter line edit */ - m_filter->setPlaceholderText(i18n("Search")); + m_filter->setPlaceholderText(i18n("Filter...")); m_filter->setClearButtonEnabled(true); connect(m_filter, &KLineEdit::textChanged, this, &KateProjectView::filterTextChanged); } KateProjectView::~KateProjectView() { } void KateProjectView::selectFile(const QString &file) { m_treeView->selectFile(file); } void KateProjectView::openSelectedDocument() { m_treeView->openSelectedDocument(); } void KateProjectView::filterTextChanged(const QString &filterText) { /** * filter */ static_cast(m_treeView->model())->setFilterFixedString(filterText); /** * expand */ if (!filterText.isEmpty()) { QTimer::singleShot(100, m_treeView, &QTreeView::expandAll); } } diff --git a/addons/project/kateprojectviewtree.cpp b/addons/project/kateprojectviewtree.cpp index bdb025cba..ee0fa849a 100644 --- a/addons/project/kateprojectviewtree.cpp +++ b/addons/project/kateprojectviewtree.cpp @@ -1,165 +1,167 @@ /* This file is part of the Kate project. * * Copyright (C) 2012 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kateprojectviewtree.h" #include "kateprojectpluginview.h" #include "kateprojecttreeviewcontextmenu.h" #include #include #include #if (QT_VERSION < QT_VERSION_CHECK(5, 10, 0)) #include #else #include #endif KateProjectViewTree::KateProjectViewTree(KateProjectPluginView *pluginView, KateProject *project) : QTreeView() , m_pluginView(pluginView) , m_project(project) { /** * default style */ setHeaderHidden(true); setEditTriggers(QAbstractItemView::NoEditTriggers); + setAllColumnsShowFocus(true); + setFocusPolicy(Qt::NoFocus); /** * attach view => project * do this once, model is stable for whole project life time * kill selection model * create sort proxy model */ QItemSelectionModel *m = selectionModel(); #if (QT_VERSION < QT_VERSION_CHECK(5, 10, 0)) QSortFilterProxyModel *sortModel = new KRecursiveFilterProxyModel(this); #else QSortFilterProxyModel *sortModel = new QSortFilterProxyModel(this); sortModel->setRecursiveFilteringEnabled(true); #endif //sortModel->setFilterRole(SortFilterRole); //sortModel->setSortRole(SortFilterRole); sortModel->setFilterCaseSensitivity(Qt::CaseInsensitive); sortModel->setSortCaseSensitivity(Qt::CaseInsensitive); sortModel->setSourceModel(m_project->model()); setModel(sortModel); delete m; /** * connect needed signals * we use activated + clicked as we want "always" single click activation + keyboard focus / enter working */ connect(this, &KateProjectViewTree::activated, this, &KateProjectViewTree::slotClicked); connect(this, &KateProjectViewTree::clicked, this, &KateProjectViewTree::slotClicked); connect(m_project, &KateProject::modelChanged, this, &KateProjectViewTree::slotModelChanged); /** * trigger once some slots */ slotModelChanged(); } KateProjectViewTree::~KateProjectViewTree() { } void KateProjectViewTree::selectFile(const QString &file) { /** * get item if any */ QStandardItem *item = m_project->itemForFile(file); if (!item) { return; } /** * select it */ QModelIndex index = static_cast(model())->mapFromSource(m_project->model()->indexFromItem(item)); scrollTo(index, QAbstractItemView::EnsureVisible); selectionModel()->setCurrentIndex(index, QItemSelectionModel::Clear | QItemSelectionModel::Select); } void KateProjectViewTree::openSelectedDocument() { /** * anything selected? */ QModelIndexList selecteStuff = selectedIndexes(); if (selecteStuff.isEmpty()) { return; } /** * open document for first element, if possible */ QString filePath = selecteStuff[0].data(Qt::UserRole).toString(); if (!filePath.isEmpty()) { m_pluginView->mainWindow()->openUrl(QUrl::fromLocalFile(filePath)); } } void KateProjectViewTree::slotClicked(const QModelIndex &index) { /** * open document, if any usable user data */ QString filePath = index.data(Qt::UserRole).toString(); if (!filePath.isEmpty()) { m_pluginView->mainWindow()->openUrl(QUrl::fromLocalFile(filePath)); selectionModel()->setCurrentIndex(index, QItemSelectionModel::Clear | QItemSelectionModel::Select); } } void KateProjectViewTree::slotModelChanged() { /** * model was updated * perhaps we need to highlight again new file */ KTextEditor::View *activeView = m_pluginView->mainWindow()->activeView(); if (activeView && activeView->document()->url().isLocalFile()) { selectFile(activeView->document()->url().toLocalFile()); } } void KateProjectViewTree::contextMenuEvent(QContextMenuEvent *event) { /** * get path file path or don't do anything */ QModelIndex index = selectionModel()->currentIndex(); QString filePath = index.data(Qt::UserRole).toString(); if (filePath.isEmpty()) { QTreeView::contextMenuEvent(event); return; } KateProjectTreeViewContextMenu menu; menu.exec(filePath, viewport()->mapToGlobal(event->pos()), this); event->accept(); } diff --git a/addons/replicode/CMakeLists.txt b/addons/replicode/CMakeLists.txt index c0c85af75..be28ad72f 100644 --- a/addons/replicode/CMakeLists.txt +++ b/addons/replicode/CMakeLists.txt @@ -1,32 +1,33 @@ -project(katereplicodeplugin) +find_package(KF5IconThemes QUIET) +set_package_properties(KF5IconThemes PROPERTIES PURPOSE "Required to build the replicode addon") -add_definitions(-DTRANSLATION_DOMAIN=\"kate-replicode-plugin\") +if(NOT KF5IconThemes_FOUND) + return() +endif() -# Set source variables -set(katereplicode_SRCS +add_library(katereplicodeplugin MODULE "") +target_compile_definitions(katereplicodeplugin PRIVATE TRANSLATION_DOMAIN="kate-replicode-plugin") + +target_link_libraries( + katereplicodeplugin + PRIVATE + KF5::TextEditor + KF5::IconThemes +) + +ki18n_wrap_ui(UI_SOURCES config.ui) +target_sources(katereplicodeplugin PRIVATE ${UI_SOURCES}) + +target_sources( + katereplicodeplugin + PRIVATE replicodeplugin.cpp replicodeconfigpage.cpp replicodeconfig.cpp replicodesettings.cpp replicodeview.cpp + plugin.qrc ) -# Generate files -ki18n_wrap_ui(katereplicode_SRCS config.ui) - -# resource for ui file and stuff -qt5_add_resources(katereplicode_SRCS plugin.qrc) - -add_library(katereplicodeplugin MODULE ${katereplicode_SRCS}) - kcoreaddons_desktop_to_json(katereplicodeplugin katereplicodeplugin.desktop) - -# Link it all together -target_link_libraries(katereplicodeplugin - KF5::TextEditor - KF5::IconThemes - KF5::I18n -) - -# Install install(TARGETS katereplicodeplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/replicode/katereplicodeplugin.desktop b/addons/replicode/katereplicodeplugin.desktop index 96d70e5d8..f4d8e577c 100644 --- a/addons/replicode/katereplicodeplugin.desktop +++ b/addons/replicode/katereplicodeplugin.desktop @@ -1,119 +1,120 @@ [Desktop Entry] Type=Service ServiceTypes=KTextEditor/Plugin X-KDE-Library=katereplicodeplugin # More information about Replicode: https://github.com/sandsmark/replicode Name=Replicode Name[ar]=ريپلكود Name[ast]=Replicode Name[ca]=Replicode Name[ca@valencia]=Replicode Name[cs]=Replicode Name[da]=Replicode Name[de]=Replicode Name[el]=Replicode Name[en_GB]=Replicode Name[es]=Replicode Name[et]=Replicode Name[eu]=Replicode Name[fa]=جایگزینی Name[fi]=Replicode Name[fr]=Replicode Name[gl]=Replicode Name[hu]=Replicode Name[ia]=Replicode Name[id]=Replicode Name[it]=Replicode Name[ko]=Replicode Name[nb]=Replicode Name[nds]=Replicode Name[nl]=Replicode Name[nn]=Replicode Name[pl]=Replicode Name[pt]=Replicode Name[pt_BR]=Replicode Name[ro]=Replicode Name[ru]=Replicode Name[sk]=Replikovať Name[sl]=Replicode Name[sr]=Репликод Name[sr@ijekavian]=Репликод Name[sr@ijekavianlatin]=Replicode Name[sr@latin]=Replicode Name[sv]=Replicode Name[tr]=Replicode Name[uk]=Replicode Name[x-test]=xxReplicodexx Name[zh_CN]=Replicode Name[zh_TW]=Replicode Comment=Replicode (constructivist AI language and runtime) Comment[ca]=Replicode (llenguatge i executable constructivista d'IA) Comment[ca@valencia]=Replicode (llenguatge i executable constructivista d'IA) Comment[de]=Replicode, eine konstruktivistische KI-Programmiersprache und Laufzeitumgebung) Comment[el]=Replicode (κονστρουκτιβιστική ΑΙ γλώσσα και εκτελέσιμη εφαρμογή) Comment[en_GB]=Replicode (constructivist AI language and runtime) Comment[es]=Replicode (lenguaje y biblioteca en tiempo de ejecución de IA constructivista) Comment[eu]=Replicode (AI lengoaia eta exekuzio garaiko liburutegi eraikitzailea) Comment[fi]=Replicode (konstruktivistinen AI-kieli ja -ajonaikaisympäristö) Comment[fr]=Replicode (langage et exécutif IA constructive) Comment[gl]=Replicode (linguaxe de intelixencia artificial constructivista e motor) +Comment[id]=Replicode (runtime dan bahasa AI konstruktivis) Comment[it]=Replicode (linguaggio costruttivista per IA ed l'esecuzione) Comment[ko]=Replicode(건설적 AI 언어 및 런타임) Comment[nl]=Replicode (constructivistische AI taal en runtime) Comment[nn]=Replicode (konstruktivistisk AI-språk og køyremiljø) Comment[pl]=Replicode (konstruktywistyczny język AI i biblioteka uruchomieniowa) Comment[pt]=Replicode (linguagem e ambiente de execução de linguagens de IA construtivas) Comment[pt_BR]=Replicode (ambiente de execução e linguagens de IA construtivas) Comment[ru]=Replicode (конструктивистский язык программирования ИИ и исполняемые модули) Comment[sk]=Replicode (konštruktívny jazyk AI a runtime) Comment[sv]=Replicode (konstruktivistiskt AI-språk och körprogram) Comment[tr]=Replicode (yapılandırmacı YZ dili ve çalıştırma zamanı) Comment[uk]=Replicode (конструктивістська мова та середовище виконання ШІ) Comment[x-test]=xxReplicode (constructivist AI language and runtime)xx Comment[zh_CN]=Replicode (构建主义的 AI 语言和运行时) Comment[zh_TW]=Replicode(建構型 AI 語言和其執行庫) GenericName=Replicode GenericName[ar]=ريپلكود Replicode GenericName[ast]=Replicode GenericName[ca]=Replicode GenericName[ca@valencia]=Replicode GenericName[cs]=Replicode GenericName[da]=Replicode GenericName[de]=Replicode GenericName[el]=Replicode GenericName[en_GB]=Replicode GenericName[es]=Replicode GenericName[et]=Replicode GenericName[eu]=Replicode GenericName[fa]=جایگزینی GenericName[fi]=Replicode GenericName[fr]=Replicode GenericName[gl]=Replicode GenericName[hu]=Replicode GenericName[ia]=Replicode GenericName[id]=Replicode GenericName[it]=Replicode GenericName[ko]=Replicode GenericName[nb]=Replicode GenericName[nds]=Replicode GenericName[nl]=Replicode GenericName[nn]=Replicode GenericName[pl]=Replicode GenericName[pt]=Replicode GenericName[pt_BR]=Replicode GenericName[ro]=Replicode GenericName[ru]=Replicode GenericName[sk]=Replikovať GenericName[sl]=Replicode GenericName[sr]=Репликод GenericName[sr@ijekavian]=Репликод GenericName[sr@ijekavianlatin]=Replicode GenericName[sr@latin]=Replicode GenericName[sv]=Replicode GenericName[tr]=Replicode GenericName[uk]=Replicode GenericName[x-test]=xxReplicodexx GenericName[zh_CN]=Replicode GenericName[zh_TW]=Replicode diff --git a/addons/rustcompletion/CMakeLists.txt b/addons/rustcompletion/CMakeLists.txt index eeee73be1..ee8f205be 100644 --- a/addons/rustcompletion/CMakeLists.txt +++ b/addons/rustcompletion/CMakeLists.txt @@ -1,24 +1,16 @@ -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) +add_library(kterustcompletionplugin MODULE "") +target_compile_definitions(kterustcompletionplugin PRIVATE TRANSLATION_DOMAIN="kterustcompletion") +target_link_libraries(kterustcompletionplugin PRIVATE KF5::TextEditor) -add_definitions(-DTRANSLATION_DOMAIN=\"kterustcompletion\") - -set(kterustcompletion_SRCS - kterustcompletion.cpp - kterustcompletionconfigpage.cpp - kterustcompletionplugin.cpp - kterustcompletionpluginview.cpp +target_sources( + kterustcompletionplugin + PRIVATE + kterustcompletion.cpp + kterustcompletionconfigpage.cpp + kterustcompletionplugin.cpp + kterustcompletionpluginview.cpp + plugin.qrc ) -# resource for ui file and stuff -qt5_add_resources(kterustcompletion_SRCS plugin.qrc) - -add_library(kterustcompletionplugin MODULE ${kterustcompletion_SRCS}) - kcoreaddons_desktop_to_json(kterustcompletionplugin kterustcompletionplugin.desktop) - -target_link_libraries(kterustcompletionplugin - KF5::TextEditor - KF5::XmlGui -) - install(TARGETS kterustcompletionplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/rustcompletion/kterustcompletionplugin.desktop b/addons/rustcompletion/kterustcompletionplugin.desktop index 0ec2bc63e..0e3978d44 100644 --- a/addons/rustcompletion/kterustcompletionplugin.desktop +++ b/addons/rustcompletion/kterustcompletionplugin.desktop @@ -1,84 +1,84 @@ [Desktop Entry] Type=Service ServiceTypes=KTextEditor/Plugin X-KDE-Library=kterustcompletionplugin Name=Rust code completion Name[ar]=إكمال كود رَسْت Name[ast]=Completáu de códigu en Rust Name[ca]=Compleció de codi per al Rust Name[ca@valencia]=Compleció de codi per al Rust Name[cs]=Automatické doplňování pro Rust Name[da]=Rust-kodefuldførelse Name[de]=Rust-Quelltext-Vervollständigung Name[el]=Αυτόματη συμπλήρωση Rust Name[en_GB]=Rust code completion Name[es]=Terminación de código para Rust Name[et]=Rusti automaatne lõpetamine Name[eu]=Rust-erako kode osatzea Name[fi]=Rust-koodintäydennys Name[fr]=Auto-complètement Rust Name[gl]=Completación automática de Rust Name[hu]=Rust kódkiegészítés Name[ia]=Autocompletion de codice Rust -Name[id]=Rust code completion +Name[id]=Penyelesaian kode Rust Name[it]=Completamento del codice Rust Name[ko]=Rust 자동 완성 Name[nb]=Rust kodefullføring Name[nl]=Code-aanvulling van Rust Name[nn]=Kodefullføring for Rust Name[pl]=Uzupełnianie kodu Rust Name[pt]=Completação de código em Rust Name[pt_BR]=Completação de código em Rust Name[ru]=Автодополнение Rust Name[sk]=Ukončovanie kódu Rust Name[sl]=Samodejno dopolnjevanje za Rust Name[sr]=Допуњавање раст кода Name[sr@ijekavian]=Допуњавање раст кода Name[sr@ijekavianlatin]=Dopunjavanje Rust koda Name[sr@latin]=Dopunjavanje Rust koda Name[sv]=Rust kodkomplettering Name[tr]=Rust kod tamamlama Name[uk]=Автодоповнення Rust Name[x-test]=xxRust code completionxx Name[zh_CN]=Rust 代码补全 Name[zh_TW]=Rust 自動補完 Comment=Code completion for Rust source code Comment[ar]=إكمال نصوص ‍رَسْت البرمجيّة Comment[ast]=Completáu de códigu pal códigu fonte en Rust Comment[ca]=Compleció de codi pel codi font del Rust Comment[ca@valencia]=Compleció de codi pel codi font del Rust Comment[cs]=Doplňování kódu pro Rust Comment[da]=Kodefuldførelse til Rust-kildekode Comment[de]=Vervollständigung für Rust-Quelltext Comment[el]=Συμπλήρωση κώδικα για πηγαίο κώδικα σε Rust Comment[en_GB]=Code completion for Rust source code Comment[es]=Terminación de código para código fuente en Rust Comment[et]=Rusti lähtekoodi automaatne lõpetamine Comment[eu]=Rust-eren iturburu koderako kode osatzea Comment[fi]=Koodintäydennys Rust-lähdekoodille Comment[fr]=Auto-complètement pour le code source Rust Comment[gl]=Completación automática de código Rust. Comment[hu]=Kódkiegészítés Rust forráskódokhoz Comment[ia]=Completion de codice pro le codice fonte de Rust -Comment[id]=Penyempurnaan kode untuk kode sumber Rust +Comment[id]=Penyelesaian kode untuk kode sumber Rust Comment[it]=Completamento del codice sorgente Rust Comment[ko]=Rust 코드 자동 완성 Comment[nb]=Kodefullføring for Rust kildekode Comment[nl]=Code-aanvulling voor broncode in Rust Comment[nn]=Autofullføring for Rust-programkode Comment[pl]=Uzupełnianie kodu dla Rust Comment[pt]=Completação de código para o código em Rust Comment[pt_BR]=Completação de código para código-fonte em Rust Comment[ru]=Автодополнение исходного кода на языке Rust Comment[sk]=Ukončovanie kódu pre zdrojový kód Rust Comment[sl]=Dopolnjevanje kode za izvorno kodo Rust Comment[sr]=Допуњавање кода за раст изворни код Comment[sr@ijekavian]=Допуњавање кода за раст изворни код Comment[sr@ijekavianlatin]=Dopunjavanje koda za Rust izvorni kod Comment[sr@latin]=Dopunjavanje koda za Rust izvorni kod Comment[sv]=Kodkomplettering för Rust-källkod Comment[tr]=Rust kaynak kodu için kod tamamlama Comment[uk]=Доповнення коду для інструкцій мовою Rust Comment[x-test]=xxCode completion for Rust source codexx Comment[zh_CN]=Rust 源代码的代码补全 Comment[zh_TW]=Rust 程式源碼的自動補完 diff --git a/addons/search/CMakeLists.txt b/addons/search/CMakeLists.txt index 856221fb6..a8a7f57ca 100644 --- a/addons/search/CMakeLists.txt +++ b/addons/search/CMakeLists.txt @@ -1,24 +1,34 @@ -add_definitions(-DTRANSLATION_DOMAIN=\"katesearch\") +find_package(KF5ItemViews QUIET) +set_package_properties(KF5ItemViews PROPERTIES PURPOSE "Required to build the search addon") -set(katesearchplugin_PART_SRCS +if(NOT KF5ItemViews_FOUND) + return() +endif() + +add_library(katesearchplugin MODULE "") +target_compile_definitions(katesearchplugin PRIVATE TRANSLATION_DOMAIN="katesearch") + +target_link_libraries( + katesearchplugin + PRIVATE + KF5::ItemViews + KF5::TextEditor +) + +ki18n_wrap_ui(UI_SOURCES search.ui results.ui) +target_sources(katesearchplugin PRIVATE ${UI_SOURCES}) + +target_sources( + katesearchplugin + PRIVATE plugin_search.cpp search_open_files.cpp SearchDiskFiles.cpp FolderFilesList.cpp replace_matches.cpp htmldelegate.cpp + plugin.qrc ) -ki18n_wrap_ui (katesearchplugin_PART_SRCS search.ui results.ui) - -# resource for ui file and stuff -qt5_add_resources(katesearchplugin_PART_SRCS plugin.qrc) - -add_library(katesearchplugin MODULE ${katesearchplugin_PART_SRCS}) -kcoreaddons_desktop_to_json (katesearchplugin katesearch.desktop) -target_link_libraries(katesearchplugin - KF5::TextEditor - KF5::Parts KF5::I18n KF5::IconThemes - KF5::ItemViews) - +kcoreaddons_desktop_to_json(katesearchplugin katesearch.desktop) install(TARGETS katesearchplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/search/FolderFilesList.cpp b/addons/search/FolderFilesList.cpp index df519ad30..f17cf0d7e 100644 --- a/addons/search/FolderFilesList.cpp +++ b/addons/search/FolderFilesList.cpp @@ -1,147 +1,147 @@ /* Kate search plugin * * Copyright (C) 2013 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #include "FolderFilesList.h" #include #include #include #include #include #include FolderFilesList::FolderFilesList(QObject *parent) : QThread(parent) {} FolderFilesList::~FolderFilesList() { m_cancelSearch = true; wait(); } void FolderFilesList::run() { m_files.clear(); QFileInfo folderInfo(m_folder); checkNextItem(folderInfo); if (m_cancelSearch) m_files.clear(); } void FolderFilesList::generateList(const QString &folder, bool recursive, bool hidden, bool symlinks, bool binary, const QString &types, const QString &excludes) { m_cancelSearch = false; m_folder = folder; if (!m_folder.endsWith(QLatin1Char('/'))) { m_folder += QLatin1Char('/'); } m_recursive = recursive; m_hidden = hidden; m_symlinks = symlinks; m_binary = binary; m_types.clear(); foreach (QString type, types.split(QLatin1Char(','), QString::SkipEmptyParts)) { m_types << type.trimmed(); } if (m_types.isEmpty()) { m_types << QStringLiteral("*"); } QStringList tmpExcludes = excludes.split(QLatin1Char(',')); m_excludeList.clear(); for (int i=0; i 100) { m_time.restart(); emit searching(item.absoluteFilePath()); } if (item.isFile()) { if (!m_binary) { QMimeType mimeType = QMimeDatabase().mimeTypeForFile(item); if (!mimeType.inherits(QStringLiteral("text/plain"))) { return; } } m_files << item.canonicalFilePath(); } else { QDir currentDir(item.absoluteFilePath()); if (!currentDir.isReadable()) { qDebug() << currentDir.absolutePath() << "Not readable"; return; } QDir::Filters filter = QDir::Files | QDir::NoDotAndDotDot | QDir::Readable; if (m_hidden) filter |= QDir::Hidden; if (m_recursive) filter |= QDir::AllDirs; if (!m_symlinks) filter |= QDir::NoSymLinks; // sort the items to have an deterministic order! const QFileInfoList currentItems = currentDir.entryInfoList(m_types, filter, QDir::Name | QDir::LocaleAware); bool skip; - for (int i = 0; i * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #include "plugin_search.h" #include "htmldelegate.h" #include #include #include #include #include #include #include #include #include "kacceleratormanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static QUrl localFileDirUp (const QUrl &url) { if (!url.isLocalFile()) return url; // else go up return QUrl::fromLocalFile (QFileInfo (url.toLocalFile()).dir().absolutePath()); } static QAction *menuEntry(QMenu *menu, const QString &before, const QString &after, const QString &desc, QString menuBefore = QString(), QString menuAfter = QString()); static QAction *menuEntry(QMenu *menu, const QString &before, const QString &after, const QString &desc, QString menuBefore, QString menuAfter) { if (menuBefore.isEmpty()) menuBefore = before; if (menuAfter.isEmpty()) menuAfter = after; QAction *const action = menu->addAction(menuBefore + menuAfter + QLatin1Char('\t') + desc); if (!action) return nullptr; action->setData(QString(before + QLatin1Char(' ') + after)); return action; } class TreeWidgetItem : public QTreeWidgetItem { public: TreeWidgetItem(QTreeWidget* parent):QTreeWidgetItem(parent){} TreeWidgetItem(QTreeWidget* parent, const QStringList &list):QTreeWidgetItem(parent, list){} TreeWidgetItem(QTreeWidgetItem* parent, const QStringList &list):QTreeWidgetItem(parent, list){} private: bool operator<(const QTreeWidgetItem &other) const override { if (childCount() == 0) { int line = data(0, ReplaceMatches::StartLineRole).toInt(); int column = data(0, ReplaceMatches::StartColumnRole).toInt(); int oLine = other.data(0, ReplaceMatches::StartLineRole).toInt(); int oColumn = other.data(0, ReplaceMatches::StartColumnRole).toInt(); if (line < oLine) { return true; } if ((line == oLine) && (column < oColumn)) { return true; } return false; } int sepCount = data(0, ReplaceMatches::FileUrlRole).toString().count(QDir::separator()); int oSepCount = other.data(0, ReplaceMatches::FileUrlRole).toString().count(QDir::separator()); if (sepCount < oSepCount) return true; if (sepCount > oSepCount) return false; return data(0, ReplaceMatches::FileUrlRole).toString().toLower() < other.data(0, ReplaceMatches::FileUrlRole).toString().toLower(); } }; Results::Results(QWidget *parent): QWidget(parent), matches(0), useRegExp(false), searchPlaceIndex(0) { setupUi(this); tree->setItemDelegate(new SPHtmlDelegate(tree)); } K_PLUGIN_FACTORY_WITH_JSON (KatePluginSearchFactory, "katesearch.json", registerPlugin();) KatePluginSearch::KatePluginSearch(QObject* parent, const QList&) : KTextEditor::Plugin (parent), m_searchCommand(nullptr) { m_searchCommand = new KateSearchCommand(this); } KatePluginSearch::~KatePluginSearch() { delete m_searchCommand; } QObject *KatePluginSearch::createView(KTextEditor::MainWindow *mainWindow) { KatePluginSearchView *view = new KatePluginSearchView(this, mainWindow, KTextEditor::Editor::instance()->application()); connect(m_searchCommand, &KateSearchCommand::setSearchPlace, view, &KatePluginSearchView::setSearchPlace); connect(m_searchCommand, &KateSearchCommand::setCurrentFolder, view, &KatePluginSearchView::setCurrentFolder); connect(m_searchCommand, &KateSearchCommand::setSearchString, view, &KatePluginSearchView::setSearchString); connect(m_searchCommand, &KateSearchCommand::startSearch, view, &KatePluginSearchView::startSearch); connect(m_searchCommand, SIGNAL(newTab()), view, SLOT(addTab())); return view; } bool ContainerWidget::focusNextPrevChild (bool next) { QWidget* fw = focusWidget(); bool found = false; emit nextFocus(fw, &found, next); if (found) { return true; } return QWidget::focusNextPrevChild(next); } void KatePluginSearchView::nextFocus(QWidget *currentWidget, bool *found, bool next) { *found = false; if (!currentWidget) { return; } // we use the object names here because there can be multiple replaceButtons (on multiple result tabs) if (next) { if (currentWidget->objectName() == QStringLiteral("tree") || currentWidget == m_ui.binaryCheckBox) { m_ui.newTabButton->setFocus(); *found = true; return; } if (currentWidget == m_ui.displayOptions) { if (m_ui.displayOptions->isChecked()) { m_ui.folderRequester->setFocus(); *found = true; return; } else { Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { return; } res->tree->setFocus(); *found = true; return; } } } else { if (currentWidget == m_ui.newTabButton) { if (m_ui.displayOptions->isChecked()) { m_ui.binaryCheckBox->setFocus(); } else { Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { return; } res->tree->setFocus(); } *found = true; return; } else { if (currentWidget->objectName() == QStringLiteral("tree")) { m_ui.displayOptions->setFocus(); *found = true; return; } } } } KatePluginSearchView::KatePluginSearchView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mainWin, KTextEditor::Application* application) : QObject (mainWin), m_kateApp(application), m_curResults(nullptr), m_searchJustOpened(false), m_switchToProjectModeWhenAvailable(false), m_searchDiskFilesDone(true), m_searchOpenFilesDone(true), m_isSearchAsYouType(false), +m_isLeftRight(false), m_projectPluginView(nullptr), m_mainWindow (mainWin) { KXMLGUIClient::setComponentName (QStringLiteral("katesearch"), i18n ("Kate Search & Replace")); setXMLFile( QStringLiteral("ui.rc") ); m_toolView = mainWin->createToolView (plugin, QStringLiteral("kate_plugin_katesearch"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Search and Replace")); ContainerWidget *container = new ContainerWidget(m_toolView); m_ui.setupUi(container); container->setFocusProxy(m_ui.searchCombo); connect(container, &ContainerWidget::nextFocus, this, &KatePluginSearchView::nextFocus); QAction *a = actionCollection()->addAction(QStringLiteral("search_in_files")); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_F)); a->setText(i18n("Search in Files")); connect(a, &QAction::triggered, this, &KatePluginSearchView::openSearchView); a = actionCollection()->addAction(QStringLiteral("search_in_files_new_tab")); a->setText(i18n("Search in Files (in new tab)")); // first add tab, then open search view, since open search view switches to show the search options connect(a, &QAction::triggered, this, &KatePluginSearchView::addTab); connect(a, &QAction::triggered, this, &KatePluginSearchView::openSearchView); a = actionCollection()->addAction(QStringLiteral("go_to_next_match")); a->setText(i18n("Go to Next Match")); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::Key_F6)); connect(a, &QAction::triggered, this, &KatePluginSearchView::goToNextMatch); a = actionCollection()->addAction(QStringLiteral("go_to_prev_match")); a->setText(i18n("Go to Previous Match")); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_F6)); connect(a, &QAction::triggered, this, &KatePluginSearchView::goToPreviousMatch); m_ui.resultTabWidget->tabBar()->setSelectionBehaviorOnRemove(QTabBar::SelectLeftTab); KAcceleratorManager::setNoAccel(m_ui.resultTabWidget); // Gnome does not seem to have all icons we want, so we use fall-back icons for those that are missing. QIcon dispOptIcon = QIcon::fromTheme(QStringLiteral("games-config-options"), QIcon::fromTheme(QStringLiteral("preferences-system"))); QIcon matchCaseIcon = QIcon::fromTheme(QStringLiteral("format-text-superscript"), QIcon::fromTheme(QStringLiteral("format-text-bold"))); QIcon useRegExpIcon = QIcon::fromTheme(QStringLiteral("code-context"), QIcon::fromTheme(QStringLiteral("edit-find-replace"))); QIcon expandResultsIcon = QIcon::fromTheme(QStringLiteral("view-list-tree"), QIcon::fromTheme(QStringLiteral("format-indent-more"))); m_ui.displayOptions->setIcon(dispOptIcon); m_ui.searchButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-find"))); m_ui.nextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search"))); m_ui.stopButton->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); m_ui.matchCase->setIcon(matchCaseIcon); m_ui.useRegExp->setIcon(useRegExpIcon); m_ui.expandResults->setIcon(expandResultsIcon); m_ui.searchPlaceCombo->setItemIcon(CurrentFile, QIcon::fromTheme(QStringLiteral("text-plain"))); m_ui.searchPlaceCombo->setItemIcon(OpenFiles, QIcon::fromTheme(QStringLiteral("text-plain"))); m_ui.searchPlaceCombo->setItemIcon(Folder, QIcon::fromTheme(QStringLiteral("folder"))); m_ui.folderUpButton->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); m_ui.currentFolderButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); m_ui.newTabButton->setIcon(QIcon::fromTheme(QStringLiteral("tab-new"))); m_ui.filterCombo->setToolTip(i18n("Comma separated list of file types to search in. Example: \"*.cpp,*.h\"\n")); m_ui.excludeCombo->setToolTip(i18n("Comma separated list of files and directories to exclude from the search. Example: \"build*\"")); // the order here is important to get the tabBar hidden for only one tab addTab(); m_ui.resultTabWidget->tabBar()->hide(); // get url-requester's combo box and sanely initialize KComboBox* cmbUrl = m_ui.folderRequester->comboBox(); cmbUrl->setDuplicatesEnabled(false); cmbUrl->setEditable(true); m_ui.folderRequester->setMode(KFile::Directory | KFile::LocalOnly); KUrlCompletion* cmpl = new KUrlCompletion(KUrlCompletion::DirCompletion); cmbUrl->setCompletionObject(cmpl); cmbUrl->setAutoDeleteCompletionObject(true); connect(m_ui.newTabButton, &QToolButton::clicked, this, &KatePluginSearchView::addTab); connect(m_ui.resultTabWidget, &QTabWidget::tabCloseRequested, this, &KatePluginSearchView::tabCloseRequested); connect(m_ui.resultTabWidget, &QTabWidget::currentChanged, this, &KatePluginSearchView::resultTabChanged); connect(m_ui.folderUpButton, &QToolButton::clicked, this, &KatePluginSearchView::navigateFolderUp); connect(m_ui.currentFolderButton, &QToolButton::clicked, this, &KatePluginSearchView::setCurrentFolder); connect(m_ui.expandResults, &QToolButton::clicked, this, &KatePluginSearchView::expandResults); connect(m_ui.searchCombo, &QComboBox::editTextChanged, &m_changeTimer, static_cast(&QTimer::start)); connect(m_ui.matchCase, &QToolButton::toggled, &m_changeTimer, static_cast(&QTimer::start)); connect(m_ui.matchCase, &QToolButton::toggled, this, [=]{ Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (res) { res->matchCase = m_ui.matchCase->isChecked(); } }); connect(m_ui.useRegExp, &QToolButton::toggled, &m_changeTimer, static_cast(&QTimer::start)); connect(m_ui.useRegExp, &QToolButton::toggled, this, [=]{ Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (res) { res->useRegExp = m_ui.useRegExp->isChecked(); } }); m_changeTimer.setInterval(300); m_changeTimer.setSingleShot(true); connect(&m_changeTimer, &QTimer::timeout, this, &KatePluginSearchView::startSearchWhileTyping); connect(m_ui.searchCombo->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::startSearch); // connecting to returnPressed() of the folderRequester doesn't work, I haven't found out why yet. But connecting to the linedit works: connect(m_ui.folderRequester->comboBox()->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::startSearch); connect(m_ui.filterCombo, static_cast(&KComboBox::returnPressed), this, &KatePluginSearchView::startSearch); connect(m_ui.excludeCombo, static_cast(&KComboBox::returnPressed), this, &KatePluginSearchView::startSearch); connect(m_ui.searchButton, &QPushButton::clicked, this, &KatePluginSearchView::startSearch); connect(m_ui.displayOptions, &QToolButton::toggled, this, &KatePluginSearchView::toggleOptions); connect(m_ui.searchPlaceCombo, static_cast(&QComboBox::currentIndexChanged), this, &KatePluginSearchView::searchPlaceChanged); connect(m_ui.searchPlaceCombo, static_cast(&QComboBox::currentIndexChanged), this, [this](int) { if (m_ui.searchPlaceCombo->currentIndex() == Folder) { m_ui.displayOptions->setChecked(true); } }); connect(m_ui.stopButton, &QPushButton::clicked, &m_searchOpenFiles, &SearchOpenFiles::cancelSearch); connect(m_ui.stopButton, &QPushButton::clicked, &m_searchDiskFiles, &SearchDiskFiles::cancelSearch); connect(m_ui.stopButton, &QPushButton::clicked, &m_folderFilesList, &FolderFilesList::cancelSearch); connect(m_ui.stopButton, &QPushButton::clicked, &m_replacer, &ReplaceMatches::cancelReplace); connect(m_ui.nextButton, &QToolButton::clicked, this, &KatePluginSearchView::goToNextMatch); connect(m_ui.replaceButton, &QPushButton::clicked, this, &KatePluginSearchView::replaceSingleMatch); connect(m_ui.replaceCheckedBtn, &QPushButton::clicked, this, &KatePluginSearchView::replaceChecked); connect(m_ui.replaceCombo->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::replaceChecked); m_ui.displayOptions->setChecked(true); connect(&m_searchOpenFiles, &SearchOpenFiles::matchFound, this, &KatePluginSearchView::matchFound); connect(&m_searchOpenFiles, &SearchOpenFiles::searchDone, this, &KatePluginSearchView::searchDone); connect(&m_searchOpenFiles, static_cast(&SearchOpenFiles::searching), this, &KatePluginSearchView::searching); connect(&m_folderFilesList, &FolderFilesList::finished, this, &KatePluginSearchView::folderFileListChanged); connect(&m_folderFilesList, &FolderFilesList::searching, this, &KatePluginSearchView::searching); connect(&m_searchDiskFiles, &SearchDiskFiles::matchFound, this, &KatePluginSearchView::matchFound); connect(&m_searchDiskFiles, &SearchDiskFiles::searchDone, this, &KatePluginSearchView::searchDone); connect(&m_searchDiskFiles, static_cast(&SearchDiskFiles::searching), this, &KatePluginSearchView::searching); connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, &m_searchOpenFiles, &SearchOpenFiles::cancelSearch); connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, &m_replacer, &ReplaceMatches::cancelReplace); connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, this, &KatePluginSearchView::clearDocMarks); connect(&m_replacer, &ReplaceMatches::replaceStatus, this, &KatePluginSearchView::replaceStatus); // Hook into line edit context menus m_ui.searchCombo->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_ui.searchCombo, &QComboBox::customContextMenuRequested, this, &KatePluginSearchView::searchContextMenu); m_ui.searchCombo->completer()->setCompletionMode(QCompleter::PopupCompletion); m_ui.searchCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); m_ui.searchCombo->setInsertPolicy(QComboBox::NoInsert); m_ui.searchCombo->lineEdit()->setClearButtonEnabled(true); m_ui.searchCombo->setMaxCount(25); m_ui.replaceCombo->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_ui.replaceCombo, &QComboBox::customContextMenuRequested, this, &KatePluginSearchView::replaceContextMenu); m_ui.replaceCombo->completer()->setCompletionMode(QCompleter::PopupCompletion); m_ui.replaceCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); m_ui.replaceCombo->setInsertPolicy(QComboBox::NoInsert); m_ui.replaceCombo->lineEdit()->setClearButtonEnabled(true); m_ui.replaceCombo->setMaxCount(25); m_toolView->setMinimumHeight(container->sizeHint().height()); connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KatePluginSearchView::handleEsc); // watch for project plugin view creation/deletion connect(m_mainWindow, &KTextEditor::MainWindow::pluginViewCreated, this, &KatePluginSearchView::slotPluginViewCreated); connect(m_mainWindow, &KTextEditor::MainWindow::pluginViewDeleted, this, &KatePluginSearchView::slotPluginViewDeleted); connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &KatePluginSearchView::docViewChanged); // Connect signals from project plugin to our slots m_projectPluginView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin")); slotPluginViewCreated(QStringLiteral("kateprojectplugin"), m_projectPluginView); m_replacer.setDocumentManager(m_kateApp); connect(&m_replacer, &ReplaceMatches::replaceDone, this, &KatePluginSearchView::replaceDone); searchPlaceChanged(); m_toolView->installEventFilter(this); m_mainWindow->guiFactory()->addClient(this); m_updateSumaryTimer.setInterval(1); m_updateSumaryTimer.setSingleShot(true); connect(&m_updateSumaryTimer, &QTimer::timeout, this, &KatePluginSearchView::updateResultsRootItem); } KatePluginSearchView::~KatePluginSearchView() { clearMarks(); m_mainWindow->guiFactory()->removeClient(this); delete m_toolView; } void KatePluginSearchView::navigateFolderUp() { // navigate one folder up m_ui.folderRequester->setUrl(localFileDirUp(m_ui.folderRequester->url())); } void KatePluginSearchView::setCurrentFolder() { if (!m_mainWindow) { return; } KTextEditor::View* editView = m_mainWindow->activeView(); if (editView && editView->document()) { // upUrl as we want the folder not the file m_ui.folderRequester->setUrl(localFileDirUp(editView->document()->url())); } m_ui.displayOptions->setChecked(true); } void KatePluginSearchView::openSearchView() { if (!m_mainWindow) { return; } if (!m_toolView->isVisible()) { m_mainWindow->showToolView(m_toolView); } m_ui.searchCombo->setFocus(Qt::OtherFocusReason); if (m_ui.searchPlaceCombo->currentIndex() == Folder) { m_ui.displayOptions->setChecked(true); } KTextEditor::View* editView = m_mainWindow->activeView(); if (editView && editView->document()) { if (m_ui.folderRequester->text().isEmpty()) { // upUrl as we want the folder not the file m_ui.folderRequester->setUrl(localFileDirUp (editView->document()->url())); } QString selection; if (editView->selection()) { selection = editView->selectionText(); // remove possible trailing '\n' if (selection.endsWith(QLatin1Char('\n'))) { selection = selection.left(selection.size() -1); } } if (selection.isEmpty()) { selection = editView->document()->wordAt(editView->cursorPosition()); } if (!selection.isEmpty() && !selection.contains(QLatin1Char('\n'))) { m_ui.searchCombo->blockSignals(true); m_ui.searchCombo->lineEdit()->setText(selection); m_ui.searchCombo->blockSignals(false); } m_ui.searchCombo->lineEdit()->selectAll(); m_searchJustOpened = true; startSearchWhileTyping(); } } void KatePluginSearchView::handleEsc(QEvent *e) { if (!m_mainWindow) return; QKeyEvent *k = static_cast(e); if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) { static ulong lastTimeStamp; if (lastTimeStamp == k->timestamp()) { // Same as previous... This looks like a bug somewhere... return; } lastTimeStamp = k->timestamp(); if (!m_matchRanges.isEmpty()) { clearMarks(); } else if (m_toolView->isVisible()) { m_mainWindow->hideToolView(m_toolView); } // Remove check marks Results *curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!curResults) { qWarning() << "This is a bug"; return; } QTreeWidgetItemIterator it(curResults->tree); while (*it) { (*it)->setCheckState(0, Qt::Unchecked); ++it; } } } void KatePluginSearchView::setSearchString(const QString &pattern) { m_ui.searchCombo->lineEdit()->setText(pattern); } void KatePluginSearchView::toggleOptions(bool show) { m_ui.stackedWidget->setCurrentIndex((show) ? 1:0); } void KatePluginSearchView::setSearchPlace(int place) { m_ui.searchPlaceCombo->setCurrentIndex(place); } QStringList KatePluginSearchView::filterFiles(const QStringList& files) const { QString types = m_ui.filterCombo->currentText(); QString excludes = m_ui.excludeCombo->currentText(); if (((types.isEmpty() || types == QStringLiteral("*"))) && (excludes.isEmpty())) { // shortcut for use all files return files; } QStringList tmpTypes = types.split(QLatin1Char(',')); - QVector typeList; + QVector typeList(tmpTypes.size()); for (int i=0; i excludeList; + QVector excludeList(tmpExcludes.size()); for (int i=0; i openList; for (int i=0; idocuments().size(); i++) { int index = fileList.indexOf(m_kateApp->documents()[i]->url().toLocalFile()); if (index != -1) { openList << m_kateApp->documents()[i]; fileList.removeAt(index); } } // search order is important: Open files starts immediately and should finish // earliest after first event loop. // The DiskFile might finish immediately if (openList.size() > 0) { m_searchOpenFiles.startSearch(openList, m_curResults->regExp); } else { m_searchOpenFilesDone = true; } m_searchDiskFiles.startSearch(fileList, m_curResults->regExp); } void KatePluginSearchView::searchPlaceChanged() { int searchPlace = m_ui.searchPlaceCombo->currentIndex(); const bool inFolder = (searchPlace == Folder); m_ui.filterCombo->setEnabled(searchPlace >= Folder); m_ui.excludeCombo->setEnabled(searchPlace >= Folder); m_ui.folderRequester->setEnabled(inFolder); m_ui.folderUpButton->setEnabled(inFolder); m_ui.currentFolderButton->setEnabled(inFolder); m_ui.recursiveCheckBox->setEnabled(inFolder); m_ui.hiddenCheckBox->setEnabled(inFolder); m_ui.symLinkCheckBox->setEnabled(inFolder); m_ui.binaryCheckBox->setEnabled(inFolder); if (inFolder && sender() == m_ui.searchPlaceCombo) { setCurrentFolder(); } // ... and the labels: m_ui.folderLabel->setEnabled(m_ui.folderRequester->isEnabled()); m_ui.filterLabel->setEnabled(m_ui.filterCombo->isEnabled()); m_ui.excludeLabel->setEnabled(m_ui.excludeCombo->isEnabled()); Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (res) { res->searchPlaceIndex = searchPlace; } } void KatePluginSearchView::addHeaderItem() { QTreeWidgetItem *item = new QTreeWidgetItem(m_curResults->tree, QStringList()); item->setCheckState(0, Qt::Checked); item->setFlags(item->flags() | Qt::ItemIsTristate); m_curResults->tree->expandItem(item); } QTreeWidgetItem * KatePluginSearchView::rootFileItem(const QString &url, const QString &fName) { if (!m_curResults) { return nullptr; } QUrl fullUrl = QUrl::fromUserInput(url); QString path = fullUrl.isLocalFile() ? localFileDirUp(fullUrl).path() : fullUrl.url(); if (!path.isEmpty() && !path.endsWith(QLatin1Char('/'))) { path += QLatin1Char('/'); } path.replace(m_resultBaseDir, QString()); QString name = fullUrl.fileName(); if (url.isEmpty()) { name = fName; } // make sure we have a root item if (m_curResults->tree->topLevelItemCount() == 0) { addHeaderItem(); } QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (m_isSearchAsYouType) { return root; } for (int i=0; ichildCount(); i++) { //qDebug() << root->child(i)->data(0, ReplaceMatches::FileNameRole).toString() << fName; if ((root->child(i)->data(0, ReplaceMatches::FileUrlRole).toString() == url)&& (root->child(i)->data(0, ReplaceMatches::FileNameRole).toString() == fName)) { int matches = root->child(i)->data(0, ReplaceMatches::StartLineRole).toInt() + 1; QString tmpUrl = QStringLiteral("%1%2: %3").arg(path, name).arg(matches); root->child(i)->setData(0, Qt::DisplayRole, tmpUrl); root->child(i)->setData(0, ReplaceMatches::StartLineRole, matches); return root->child(i); } } // file item not found create a new one QString tmpUrl = QStringLiteral("%1%2: %3").arg(path, name).arg(1); TreeWidgetItem *item = new TreeWidgetItem(root, QStringList(tmpUrl)); item->setData(0, ReplaceMatches::FileUrlRole, url); item->setData(0, ReplaceMatches::FileNameRole, fName); item->setData(0, ReplaceMatches::StartLineRole, 1); item->setCheckState(0, Qt::Checked); item->setFlags(item->flags() | Qt::ItemIsTristate); return item; } void KatePluginSearchView::addMatchMark(KTextEditor::Document* doc, QTreeWidgetItem *item) { if (!doc || !item) { return; } KTextEditor::View* activeView = m_mainWindow->activeView(); KTextEditor::MovingInterface* miface = qobject_cast(doc); KTextEditor::ConfigInterface* ciface = qobject_cast(activeView); KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); int line = item->data(0, ReplaceMatches::StartLineRole).toInt(); int column = item->data(0, ReplaceMatches::StartColumnRole).toInt(); int endLine = item->data(0, ReplaceMatches::EndLineRole).toInt(); int endColumn = item->data(0, ReplaceMatches::EndColumnRole).toInt(); bool isReplaced = item->data(0, ReplaceMatches::ReplacedRole).toBool(); if (isReplaced) { QColor replaceColor(Qt::green); if (ciface) replaceColor = ciface->configValue(QStringLiteral("replace-highlight-color")).value(); attr->setBackground(replaceColor); if (activeView) { attr->setForeground(activeView->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color()); } } else { QColor searchColor(Qt::yellow); if (ciface) searchColor = ciface->configValue(QStringLiteral("search-highlight-color")).value(); attr->setBackground(searchColor); if (activeView) { attr->setForeground(activeView->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color()); } } KTextEditor::Range range(line, column, endLine, endColumn); // Check that the match still matches if (m_curResults) { if (!isReplaced) { // special handling for "(?=\\n)" in multi-line search QRegularExpression tmpReg = m_curResults->regExp; if (m_curResults->regExp.pattern().endsWith(QStringLiteral("(?=\\n)"))) { QString newPatern = tmpReg.pattern(); newPatern.replace(QStringLiteral("(?=\\n)"), QStringLiteral("$")); tmpReg.setPattern(newPatern); } // Check that the match still matches ;) if (tmpReg.match(doc->text(range)).capturedStart() != 0) { qDebug() << doc->text(range) << "Does not match" << m_curResults->regExp.pattern(); return; } } else { if (doc->text(range) != item->data(0, ReplaceMatches::ReplacedTextRole).toString()) { qDebug() << doc->text(range) << "Does not match" << item->data(0, ReplaceMatches::ReplacedTextRole).toString(); return; } } } // Highlight the match KTextEditor::MovingRange* mr = miface->newMovingRange(range); mr->setAttribute(attr); mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection mr->setAttributeOnlyForViews(true); m_matchRanges.append(mr); // Add a match mark KTextEditor::MarkInterface* iface = qobject_cast(doc); if (!iface) return; iface->setMarkDescription(KTextEditor::MarkInterface::markType32, i18n("SearchHighLight")); iface->setMarkPixmap(KTextEditor::MarkInterface::markType32, QIcon().pixmap(0,0)); iface->addMark(line, KTextEditor::MarkInterface::markType32); connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(clearMarks()), Qt::UniqueConnection); } static const int contextLen = 70; void KatePluginSearchView::matchFound(const QString &url, const QString &fName, const QString &lineContent, int matchLen, int startLine, int startColumn, int endLine, int endColumn) { if (!m_curResults) { return; } int preLen = contextLen; int preStart = startColumn - preLen; if (preStart < 0) { preLen += preStart; preStart = 0; } QString pre; if (preLen == contextLen) { pre = QStringLiteral("..."); } pre += lineContent.mid(preStart, preLen).toHtmlEscaped(); QString match = lineContent.mid(startColumn, matchLen).toHtmlEscaped(); match.replace(QLatin1Char('\n'), QStringLiteral("\\n")); QString post = lineContent.mid(startColumn + matchLen, contextLen); if (post.size() >= contextLen) { post += QStringLiteral("..."); } post = post.toHtmlEscaped(); QStringList row; row << i18n("Line: %1 Column: %2: %3", startLine+1, startColumn+1, pre+QStringLiteral("")+match+QStringLiteral("")+post); TreeWidgetItem *item = new TreeWidgetItem(rootFileItem(url, fName), row); item->setData(0, ReplaceMatches::FileUrlRole, url); item->setData(0, Qt::ToolTipRole, url); item->setData(0, ReplaceMatches::FileNameRole, fName); item->setData(0, ReplaceMatches::StartLineRole, startLine); item->setData(0, ReplaceMatches::StartColumnRole, startColumn); item->setData(0, ReplaceMatches::MatchLenRole, matchLen); item->setData(0, ReplaceMatches::PreMatchRole, pre); item->setData(0, ReplaceMatches::MatchRole, match); item->setData(0, ReplaceMatches::PostMatchRole, post); item->setData(0, ReplaceMatches::EndLineRole, endLine); item->setData(0, ReplaceMatches::EndColumnRole, endColumn); item->setCheckState (0, Qt::Checked); m_curResults->matches++; } void KatePluginSearchView::clearMarks() { foreach (KTextEditor::Document* doc, m_kateApp->documents()) { clearDocMarks(doc); } qDeleteAll(m_matchRanges); m_matchRanges.clear(); } void KatePluginSearchView::clearDocMarks(KTextEditor::Document* doc) { KTextEditor::MarkInterface* iface; iface = qobject_cast(doc); if (iface) { const QHash marks = iface->marks(); QHashIterator i(marks); while (i.hasNext()) { i.next(); if (i.value()->type & KTextEditor::MarkInterface::markType32) { iface->removeMark(i.value()->line, KTextEditor::MarkInterface::markType32); } } } int i = 0; while (idocument() == doc) { delete m_matchRanges.at(i); m_matchRanges.removeAt(i); } else { i++; } } m_curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { qWarning() << "This is a bug"; return; } } void KatePluginSearchView::startSearch() { m_changeTimer.stop(); // make sure not to start a "while you type" search now m_mainWindow->showToolView(m_toolView); // in case we are invoked from the command interface m_switchToProjectModeWhenAvailable = false; // now that we started, don't switch back automatically if (m_ui.searchCombo->currentText().isEmpty()) { // return pressed in the folder combo or filter combo return; } m_isSearchAsYouType = false; QString currentSearchText = m_ui.searchCombo->currentText(); m_ui.searchCombo->setItemText(0, QString()); // remove the text from index 0 on enter/search int index = m_ui.searchCombo->findText(currentSearchText); if (index > 0) { m_ui.searchCombo->removeItem(index); } m_ui.searchCombo->insertItem(1, currentSearchText); m_ui.searchCombo->setCurrentIndex(1); if (m_ui.filterCombo->findText(m_ui.filterCombo->currentText()) == -1) { m_ui.filterCombo->insertItem(0, m_ui.filterCombo->currentText()); m_ui.filterCombo->setCurrentIndex(0); } if (m_ui.excludeCombo->findText(m_ui.excludeCombo->currentText()) == -1) { m_ui.excludeCombo->insertItem(0, m_ui.excludeCombo->currentText()); m_ui.excludeCombo->setCurrentIndex(0); } if (m_ui.folderRequester->comboBox()->findText(m_ui.folderRequester->comboBox()->currentText()) == -1) { m_ui.folderRequester->comboBox()->insertItem(0, m_ui.folderRequester->comboBox()->currentText()); m_ui.folderRequester->comboBox()->setCurrentIndex(0); } m_curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { qWarning() << "This is a bug"; return; } QRegularExpression::PatternOptions patternOptions = (m_ui.matchCase->isChecked() ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption); QString pattern = (m_ui.useRegExp->isChecked() ? currentSearchText : QRegularExpression::escape(currentSearchText)); QRegularExpression reg(pattern, patternOptions); if (!reg.isValid()) { //qDebug() << "invalid regexp"; indicateMatch(false); return; } m_curResults->regExp = reg; m_curResults->useRegExp = m_ui.useRegExp->isChecked(); m_curResults->matchCase = m_ui.matchCase->isChecked(); m_curResults->searchPlaceIndex = m_ui.searchPlaceCombo->currentIndex(); m_ui.newTabButton->setDisabled(true); m_ui.searchCombo->setDisabled(true); m_ui.searchButton->setDisabled(true); m_ui.displayOptions->setChecked (false); m_ui.displayOptions->setDisabled(true); m_ui.replaceCheckedBtn->setDisabled(true); m_ui.replaceButton->setDisabled(true); - m_ui.stopAndNext->setCurrentIndex(1); + m_ui.stopAndNext->setCurrentWidget(m_ui.stopButton); m_ui.replaceCombo->setDisabled(true); m_ui.searchPlaceCombo->setDisabled(true); m_ui.useRegExp->setDisabled(true); m_ui.matchCase->setDisabled(true); m_ui.expandResults->setDisabled(true); m_ui.currentFolderButton->setDisabled(true); clearMarks(); m_curResults->tree->clear(); m_curResults->tree->setCurrentItem(nullptr); m_curResults->matches = 0; disconnect(m_curResults->tree, &QTreeWidget::itemChanged, &m_updateSumaryTimer, nullptr); m_ui.resultTabWidget->setTabText(m_ui.resultTabWidget->currentIndex(), m_ui.searchCombo->currentText()); m_toolView->setCursor(Qt::WaitCursor); m_searchDiskFilesDone = false; m_searchOpenFilesDone = false; const bool inCurrentProject = m_ui.searchPlaceCombo->currentIndex() == Project; const bool inAllOpenProjects = m_ui.searchPlaceCombo->currentIndex() == AllProjects; if (m_ui.searchPlaceCombo->currentIndex() == CurrentFile) { m_searchDiskFilesDone = true; m_resultBaseDir.clear(); QList documents; documents << m_mainWindow->activeView()->document(); addHeaderItem(); m_searchOpenFiles.startSearch(documents, reg); } else if (m_ui.searchPlaceCombo->currentIndex() == OpenFiles) { m_searchDiskFilesDone = true; m_resultBaseDir.clear(); const QList documents = m_kateApp->documents(); addHeaderItem(); m_searchOpenFiles.startSearch(documents, reg); } else if (m_ui.searchPlaceCombo->currentIndex() == Folder) { m_resultBaseDir = m_ui.folderRequester->url().path(); if (!m_resultBaseDir.isEmpty() && !m_resultBaseDir.endsWith(QLatin1Char('/'))) m_resultBaseDir += QLatin1Char('/'); addHeaderItem(); m_folderFilesList.generateList(m_ui.folderRequester->text(), m_ui.recursiveCheckBox->isChecked(), m_ui.hiddenCheckBox->isChecked(), m_ui.symLinkCheckBox->isChecked(), m_ui.binaryCheckBox->isChecked(), m_ui.filterCombo->currentText(), m_ui.excludeCombo->currentText()); // the file list will be ready when the thread returns (connected to folderFileListChanged) } else if (inCurrentProject || inAllOpenProjects) { /** * init search with file list from current project, if any */ m_resultBaseDir.clear(); QStringList files; if (m_projectPluginView) { if (inCurrentProject) { m_resultBaseDir = m_projectPluginView->property ("projectBaseDir").toString(); } else { m_resultBaseDir = m_projectPluginView->property ("allProjectsCommonBaseDir").toString(); } if (!m_resultBaseDir.endsWith(QLatin1Char('/'))) m_resultBaseDir += QLatin1Char('/'); QStringList projectFiles; if (inCurrentProject) { projectFiles = m_projectPluginView->property ("projectFiles").toStringList(); } else { projectFiles = m_projectPluginView->property ("allProjectsFiles").toStringList(); } files = filterFiles(projectFiles); } addHeaderItem(); QList openList; for (int i=0; idocuments().size(); i++) { int index = files.indexOf(m_kateApp->documents()[i]->url().toString()); if (index != -1) { openList << m_kateApp->documents()[i]; files.removeAt(index); } } // search order is important: Open files starts immediately and should finish // earliest after first event loop. // The DiskFile might finish immediately if (openList.size() > 0) { m_searchOpenFiles.startSearch(openList, m_curResults->regExp); } else { m_searchOpenFilesDone = true; } m_searchDiskFiles.startSearch(files, reg); } else { Q_ASSERT_X(false, "KatePluginSearchView::startSearch", "case not handled"); } } void KatePluginSearchView::startSearchWhileTyping() { if (!m_searchDiskFilesDone || !m_searchOpenFilesDone) { return; } m_isSearchAsYouType = true; QString currentSearchText = m_ui.searchCombo->currentText(); m_ui.searchButton->setDisabled(currentSearchText.isEmpty()); // Do not clear the search results if you press up by mistake if (currentSearchText.isEmpty()) return; if (!m_mainWindow->activeView()) return; KTextEditor::Document *doc = m_mainWindow->activeView()->document(); if (!doc) return; m_curResults =qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { qWarning() << "This is a bug"; return; } // check if we typed something or just changed combobox index // changing index should not trigger a search-as-you-type if (m_ui.searchCombo->currentIndex() > 0 && currentSearchText == m_ui.searchCombo->itemText(m_ui.searchCombo->currentIndex())) { return; } // Now we should have a true typed text change QRegularExpression::PatternOptions patternOptions = (m_ui.matchCase->isChecked() ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption); QString pattern = (m_ui.useRegExp->isChecked() ? currentSearchText : QRegularExpression::escape(currentSearchText)); QRegularExpression reg(pattern, patternOptions); if (!reg.isValid()) { //qDebug() << "invalid regexp"; indicateMatch(false); return; } disconnect(m_curResults->tree, &QTreeWidget::itemChanged, &m_updateSumaryTimer, nullptr); m_curResults->regExp = reg; m_curResults->useRegExp = m_ui.useRegExp->isChecked(); m_ui.replaceCheckedBtn->setDisabled(true); m_ui.replaceButton->setDisabled(true); m_ui.nextButton->setDisabled(true); int cursorPosition = m_ui.searchCombo->lineEdit()->cursorPosition(); bool hasSelected = m_ui.searchCombo->lineEdit()->hasSelectedText(); m_ui.searchCombo->blockSignals(true); m_ui.searchCombo->setItemText(0, currentSearchText); m_ui.searchCombo->setCurrentIndex(0); m_ui.searchCombo->lineEdit()->setCursorPosition(cursorPosition); if (hasSelected) { // This restores the select all from invoking openSearchView // This selects too much if we have a partial selection and toggle match-case/regexp m_ui.searchCombo->lineEdit()->selectAll(); } m_ui.searchCombo->blockSignals(false); // Prepare for the new search content clearMarks(); m_resultBaseDir.clear(); m_curResults->tree->clear(); m_curResults->tree->setCurrentItem(nullptr); m_curResults->matches = 0; // Add the search-as-you-type header item TreeWidgetItem *item = new TreeWidgetItem(m_curResults->tree, QStringList()); item->setData(0, ReplaceMatches::FileUrlRole, doc->url().toString()); item->setData(0, ReplaceMatches::FileNameRole, doc->documentName()); item->setData(0, ReplaceMatches::StartLineRole, 0); item->setCheckState(0, Qt::Checked); item->setFlags(item->flags() | Qt::ItemIsTristate); // Do the search int searchStoppedAt = m_searchOpenFiles.searchOpenFile(doc, reg, 0); searchWhileTypingDone(); if (searchStoppedAt != 0) { delete m_infoMessage; const QString msg = i18n("Searching while you type was interrupted. It would have taken too long."); m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Warning); m_infoMessage->setPosition(KTextEditor::Message::TopInView); m_infoMessage->setAutoHide(3000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(m_mainWindow->activeView()); m_mainWindow->activeView()->document()->postMessage(m_infoMessage); } } void KatePluginSearchView::searchDone() { m_changeTimer.stop(); // avoid "while you type" search directly after if (sender() == &m_searchDiskFiles) { m_searchDiskFilesDone = true; } if (sender() == &m_searchOpenFiles) { m_searchOpenFilesDone = true; } if (!m_searchDiskFilesDone || !m_searchOpenFilesDone) { return; } QWidget* fw = QApplication::focusWidget(); // NOTE: we take the focus widget here before the enabling/disabling // moves the focus around. m_ui.newTabButton->setDisabled(false); m_ui.searchCombo->setDisabled(false); m_ui.searchButton->setDisabled(false); - m_ui.stopAndNext->setCurrentIndex(0); + m_ui.stopAndNext->setCurrentWidget(m_ui.nextButton); m_ui.displayOptions->setDisabled(false); m_ui.replaceCombo->setDisabled(false); m_ui.searchPlaceCombo->setDisabled(false); m_ui.useRegExp->setDisabled(false); m_ui.matchCase->setDisabled(false); m_ui.expandResults->setDisabled(false); m_ui.currentFolderButton->setDisabled(false); if (!m_curResults) { return; } m_ui.replaceCheckedBtn->setDisabled(m_curResults->matches < 1); m_ui.replaceButton->setDisabled(m_curResults->matches < 1); m_ui.nextButton->setDisabled(m_curResults->matches < 1); m_curResults->tree->sortItems(0, Qt::AscendingOrder); m_curResults->tree->expandAll(); m_curResults->tree->resizeColumnToContents(0); if (m_curResults->tree->columnWidth(0) < m_curResults->tree->width()-30) { m_curResults->tree->setColumnWidth(0, m_curResults->tree->width()-30); } // expand the "header item " to display all files and all results if configured expandResults(); updateResultsRootItem(); connect(m_curResults->tree, &QTreeWidget::itemChanged, &m_updateSumaryTimer, static_cast(&QTimer::start)); indicateMatch(m_curResults->matches > 0); m_curResults = nullptr; m_toolView->unsetCursor(); if (fw == m_ui.stopButton) { m_ui.searchCombo->setFocus(); } m_searchJustOpened = false; } void KatePluginSearchView::searchWhileTypingDone() { if (!m_curResults) { return; } bool popupVisible = m_ui.searchCombo->lineEdit()->completer()->popup()->isVisible(); m_ui.replaceCheckedBtn->setDisabled(m_curResults->matches < 1); m_ui.replaceButton->setDisabled(m_curResults->matches < 1); m_ui.nextButton->setDisabled(m_curResults->matches < 1); m_curResults->tree->expandAll(); m_curResults->tree->resizeColumnToContents(0); if (m_curResults->tree->columnWidth(0) < m_curResults->tree->width()-30) { m_curResults->tree->setColumnWidth(0, m_curResults->tree->width()-30); } QWidget *focusObject = nullptr; QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (root) { QTreeWidgetItem *child = root->child(0); if (!m_searchJustOpened) { focusObject = qobject_cast(QGuiApplication::focusObject()); } indicateMatch(child); updateResultsRootItem(); connect(m_curResults->tree, &QTreeWidget::itemChanged, &m_updateSumaryTimer, static_cast(&QTimer::start)); } m_curResults = nullptr; if (focusObject) { focusObject->setFocus(); } if (popupVisible) { m_ui.searchCombo->lineEdit()->completer()->complete(); } m_searchJustOpened = false; } void KatePluginSearchView::searching(const QString &file) { if (!m_curResults) { return; } QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (root) { if (file.size() > 70) { root->setData(0, Qt::DisplayRole, i18n("Searching: ...%1", file.right(70))); } else { root->setData(0, Qt::DisplayRole, i18n("Searching: %1", file)); } } } void KatePluginSearchView::indicateMatch(bool hasMatch) { QLineEdit * const lineEdit = m_ui.searchCombo->lineEdit(); QPalette background(lineEdit->palette()); if (hasMatch) { // Green background for line edit KColorScheme::adjustBackground(background, KColorScheme::PositiveBackground); } else { // Reset background of line edit background = QPalette(); } // Red background for line edit //KColorScheme::adjustBackground(background, KColorScheme::NegativeBackground); // Neutral background //KColorScheme::adjustBackground(background, KColorScheme::NeutralBackground); lineEdit->setPalette(background); } void KatePluginSearchView::replaceSingleMatch() { // Save the search text if (m_ui.searchCombo->findText(m_ui.searchCombo->currentText()) == -1) { m_ui.searchCombo->insertItem(1, m_ui.searchCombo->currentText()); m_ui.searchCombo->setCurrentIndex(1); } // Save the replace text if (m_ui.replaceCombo->findText(m_ui.replaceCombo->currentText()) == -1) { m_ui.replaceCombo->insertItem(1, m_ui.replaceCombo->currentText()); m_ui.replaceCombo->setCurrentIndex(1); } // Check if the cursor is at the current item if not jump there Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { return; // Security measure } QTreeWidgetItem *item = res->tree->currentItem(); if (!item || !item->parent()) { // Nothing was selected goToNextMatch(); return; } if (!m_mainWindow->activeView() || !m_mainWindow->activeView()->cursorPosition().isValid()) { itemSelected(item); // Correct any bad cursor positions return; } int cursorLine = m_mainWindow->activeView()->cursorPosition().line(); int cursorColumn = m_mainWindow->activeView()->cursorPosition().column(); int startLine = item->data(0, ReplaceMatches::StartLineRole).toInt(); int startColumn = item->data(0, ReplaceMatches::StartColumnRole).toInt(); if ((cursorLine != startLine) || (cursorColumn != startColumn)) { itemSelected(item); return; } KTextEditor::Document *doc = m_mainWindow->activeView()->document(); // Find the corresponding range int i; for (i=0; idocument() != doc) continue; if (m_matchRanges[i]->start().line() != startLine) continue; if (m_matchRanges[i]->start().column() != startColumn) continue; break; } if (i >=m_matchRanges.size()) { goToNextMatch(); return; } m_replacer.replaceSingleMatch(doc, item, res->regExp, m_ui.replaceCombo->currentText()); goToNextMatch(); } void KatePluginSearchView::replaceChecked() { if (m_ui.searchCombo->findText(m_ui.searchCombo->currentText()) == -1) { m_ui.searchCombo->insertItem(1, m_ui.searchCombo->currentText()); m_ui.searchCombo->setCurrentIndex(1); } if (m_ui.replaceCombo->findText(m_ui.replaceCombo->currentText()) == -1) { m_ui.replaceCombo->insertItem(1, m_ui.replaceCombo->currentText()); m_ui.replaceCombo->setCurrentIndex(1); } m_curResults =qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { qWarning() << "Results not found"; return; } - m_ui.stopAndNext->setCurrentIndex(1); + m_ui.stopAndNext->setCurrentWidget(m_ui.stopButton); m_ui.displayOptions->setChecked(false); m_ui.displayOptions->setDisabled(true); m_ui.newTabButton->setDisabled(true); m_ui.searchCombo->setDisabled(true); m_ui.searchButton->setDisabled(true); m_ui.replaceCheckedBtn->setDisabled(true); m_ui.replaceButton->setDisabled(true); m_ui.replaceCombo->setDisabled(true); m_ui.searchPlaceCombo->setDisabled(true); m_ui.useRegExp->setDisabled(true); m_ui.matchCase->setDisabled(true); m_ui.expandResults->setDisabled(true); m_ui.currentFolderButton->setDisabled(true); m_curResults->replaceStr = m_ui.replaceCombo->currentText(); QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (root) { m_curResults->treeRootText = root->data(0, Qt::DisplayRole).toString(); } m_replacer.replaceChecked(m_curResults->tree, m_curResults->regExp, m_curResults->replaceStr); } void KatePluginSearchView::replaceStatus(const QUrl &url, int replacedInFile, int matchesInFile) { if (!m_curResults) { qDebug() << "m_curResults == nullptr"; return; } QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (root) { QString file = url.toString(QUrl::PreferLocalFile); if (file.size() > 70) { root->setData(0, Qt::DisplayRole, i18n("Processed %1 of %2 matches in: ...%3", replacedInFile, matchesInFile, file.right(70))); } else { root->setData(0, Qt::DisplayRole, i18n("Processed %1 of %2 matches in: %3", replacedInFile, matchesInFile, file)); } } } void KatePluginSearchView::replaceDone() { - m_ui.stopAndNext->setCurrentIndex(0); + m_ui.stopAndNext->setCurrentWidget(m_ui.nextButton); m_ui.replaceCombo->setDisabled(false); m_ui.newTabButton->setDisabled(false); m_ui.searchCombo->setDisabled(false); m_ui.searchButton->setDisabled(false); m_ui.replaceCheckedBtn->setDisabled(false); m_ui.replaceButton->setDisabled(false); m_ui.displayOptions->setDisabled(false); m_ui.searchPlaceCombo->setDisabled(false); m_ui.useRegExp->setDisabled(false); m_ui.matchCase->setDisabled(false); m_ui.expandResults->setDisabled(false); m_ui.currentFolderButton->setDisabled(false); if (!m_curResults) { qDebug() << "m_curResults == nullptr"; return; } QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (root) { root->setData(0, Qt::DisplayRole, m_curResults->treeRootText); } } void KatePluginSearchView::docViewChanged() { if (!m_mainWindow->activeView()) { return; } Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { qDebug() << "No res"; return; } m_curResults = res; // add the marks if it is not already open KTextEditor::Document *doc = m_mainWindow->activeView()->document(); if (doc && res->tree->topLevelItemCount() > 0) { // There is always one root item with match count // and X children with files or matches in case of search while typing QTreeWidgetItem *rootItem = res->tree->topLevelItem(0); QTreeWidgetItem *fileItem = nullptr; for (int i=0; ichildCount(); i++) { QString url = rootItem->child(i)->data(0, ReplaceMatches::FileUrlRole).toString(); QString fName = rootItem->child(i)->data(0, ReplaceMatches::FileNameRole).toString(); if (url == doc->url().toString() && fName == doc->documentName()) { fileItem = rootItem->child(i); break; } } if (fileItem) { clearDocMarks(doc); if (m_isSearchAsYouType) { fileItem = fileItem->parent(); } for (int i=0; ichildCount(); i++) { if (fileItem->child(i)->checkState(0) == Qt::Unchecked) { continue; } addMatchMark(doc, fileItem->child(i)); } } // Re-add the highlighting on document reload connect(doc, &KTextEditor::Document::reloaded, this, &KatePluginSearchView::docViewChanged, Qt::UniqueConnection); } } void KatePluginSearchView::expandResults() { m_curResults =qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { qWarning() << "Results not found"; return; } if (m_ui.expandResults->isChecked()) { m_curResults->tree->expandAll(); } else { QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); m_curResults->tree->expandItem(root); if (root && (root->childCount() > 1)) { for (int i=0; ichildCount(); i++) { m_curResults->tree->collapseItem(root->child(i)); } } } } void KatePluginSearchView::updateResultsRootItem() { m_curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { return; } QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (!root) { // nothing to update return; } int checkedItemCount = 0; if (m_curResults->matches > 1) { for (QTreeWidgetItemIterator it(m_curResults->tree, QTreeWidgetItemIterator::Checked|QTreeWidgetItemIterator::NoChildren); *it; ++it) { checkedItemCount++; } } QString checkedStr = i18np("One checked", "%1 checked", checkedItemCount); int searchPlace = m_ui.searchPlaceCombo->currentIndex(); if (m_isSearchAsYouType) { searchPlace = CurrentFile; } switch (searchPlace) { case CurrentFile: root->setData(0, Qt::DisplayRole, i18np("One match (%2) found in file", "%1 matches (%2) found in file", m_curResults->matches, checkedStr)); break; case OpenFiles: root->setData(0, Qt::DisplayRole, i18np("One match (%2) found in open files", "%1 matches (%2) found in open files", m_curResults->matches, checkedStr)); break; case Folder: root->setData(0, Qt::DisplayRole, i18np("One match (%3) found in folder %2", "%1 matches (%3) found in folder %2", m_curResults->matches, m_resultBaseDir, checkedStr)); break; case Project: { QString projectName; if (m_projectPluginView) { projectName = m_projectPluginView->property("projectName").toString(); } root->setData(0, Qt::DisplayRole, i18np("One match (%4) found in project %2 (%3)", "%1 matches (%4) found in project %2 (%3)", m_curResults->matches, projectName, m_resultBaseDir, checkedStr)); break; } case AllProjects: // "in Open Projects" root->setData(0, Qt::DisplayRole, i18np("One match (%3) found in all open projects (common parent: %2)", "%1 matches (%3) found in all open projects (common parent: %2)", m_curResults->matches, m_resultBaseDir, checkedStr)); break; } docViewChanged(); } void KatePluginSearchView::itemSelected(QTreeWidgetItem *item) { if (!item) return; m_curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { return; } while (item->data(0, ReplaceMatches::StartColumnRole).toString().isEmpty()) { item->treeWidget()->expandItem(item); item = item->child(0); if (!item) return; } item->treeWidget()->setCurrentItem(item); // get stuff int toLine = item->data(0, ReplaceMatches::StartLineRole).toInt(); int toColumn = item->data(0, ReplaceMatches::StartColumnRole).toInt(); KTextEditor::Document* doc; QString url = item->data(0, ReplaceMatches::FileUrlRole).toString(); if (!url.isEmpty()) { doc = m_kateApp->findUrl(QUrl::fromUserInput(url)); } else { doc = m_replacer.findNamed(item->data(0, ReplaceMatches::FileNameRole).toString()); } // add the marks to the document if it is not already open if (!doc) { doc = m_kateApp->openUrl(QUrl::fromUserInput(url)); } if (!doc) return; // open the right view... m_mainWindow->activateView(doc); // any view active? if (!m_mainWindow->activeView()) { return; } // set the cursor to the correct position m_mainWindow->activeView()->setCursorPosition(KTextEditor::Cursor(toLine, toColumn)); m_mainWindow->activeView()->setFocus(); } void KatePluginSearchView::goToNextMatch() { bool wrapFromFirst = false; bool startFromFirst = false; bool startFromCursor = false; Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { return; } QTreeWidgetItem *curr = res->tree->currentItem(); bool focusInView = m_mainWindow->activeView() && m_mainWindow->activeView()->hasFocus(); if (!curr && focusInView) { // no item has been visited && focus is not in searchCombo (probably in the view) -> // jump to the closest match after current cursor position // check if current file is in the file list curr = res->tree->topLevelItem(0); while (curr && curr->data(0, ReplaceMatches::FileUrlRole).toString() != m_mainWindow->activeView()->document()->url().toString()) { curr = res->tree->itemBelow(curr); } // now we are either in this file or !curr if (curr) { QTreeWidgetItem *fileBefore = curr; res->tree->expandItem(curr); int lineNr = 0; int columnNr = 0; if (m_mainWindow->activeView()->cursorPosition().isValid()) { lineNr = m_mainWindow->activeView()->cursorPosition().line(); columnNr = m_mainWindow->activeView()->cursorPosition().column(); } if (!curr->data(0, ReplaceMatches::StartColumnRole).isValid()) { curr = res->tree->itemBelow(curr); }; while (curr && curr->data(0, ReplaceMatches::StartLineRole).toInt() <= lineNr && curr->data(0, ReplaceMatches::FileUrlRole).toString() == m_mainWindow->activeView()->document()->url().toString()) { if (curr->data(0, ReplaceMatches::StartLineRole).toInt() == lineNr && curr->data(0, ReplaceMatches::StartColumnRole).toInt() >= columnNr - curr->data(0, ReplaceMatches::MatchLenRole).toInt()) { break; } fileBefore = curr; curr = res->tree->itemBelow(curr); } curr = fileBefore; startFromCursor = true; } } if (!curr) { curr = res->tree->topLevelItem(0); startFromFirst = true; } if (!curr) return; if (!curr->data(0, ReplaceMatches::StartColumnRole).toString().isEmpty()) { curr = res->tree->itemBelow(curr); if (!curr) { wrapFromFirst = true; curr = res->tree->topLevelItem(0); } } itemSelected(curr); if (startFromFirst) { delete m_infoMessage; const QString msg = i18n("Starting from first match"); m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information); m_infoMessage->setPosition(KTextEditor::Message::TopInView); m_infoMessage->setAutoHide(2000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(m_mainWindow->activeView()); m_mainWindow->activeView()->document()->postMessage(m_infoMessage); } else if (startFromCursor) { delete m_infoMessage; const QString msg = i18n("Next from cursor"); m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information); m_infoMessage->setPosition(KTextEditor::Message::BottomInView); m_infoMessage->setAutoHide(2000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(m_mainWindow->activeView()); m_mainWindow->activeView()->document()->postMessage(m_infoMessage); } else if (wrapFromFirst) { delete m_infoMessage; const QString msg = i18n("Continuing from first match"); m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information); m_infoMessage->setPosition(KTextEditor::Message::TopInView); m_infoMessage->setAutoHide(2000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(m_mainWindow->activeView()); m_mainWindow->activeView()->document()->postMessage(m_infoMessage); } } void KatePluginSearchView::goToPreviousMatch() { bool fromLast = false; Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { return; } if (res->tree->topLevelItemCount() == 0) { return; } QTreeWidgetItem *curr = res->tree->currentItem(); if (!curr) { // no item has been visited -> jump to the closest match before current cursor position // check if current file is in the file curr = res->tree->topLevelItem(0); while (curr && curr->data(0, ReplaceMatches::FileUrlRole).toString() != m_mainWindow->activeView()->document()->url().toString()) { curr = res->tree->itemBelow(curr); } // now we are either in this file or !curr if (curr) { res->tree->expandItem(curr); int lineNr = 0; int columnNr = 0; if (m_mainWindow->activeView()->cursorPosition().isValid()) { lineNr = m_mainWindow->activeView()->cursorPosition().line(); columnNr = m_mainWindow->activeView()->cursorPosition().column()-1; } if (!curr->data(0, ReplaceMatches::StartColumnRole).isValid()) { curr = res->tree->itemBelow(curr); }; while (curr && curr->data(0, ReplaceMatches::StartLineRole).toInt() <= lineNr && curr->data(0, ReplaceMatches::FileUrlRole).toString() == m_mainWindow->activeView()->document()->url().toString()) { if (curr->data(0, ReplaceMatches::StartLineRole).toInt() == lineNr && curr->data(0, ReplaceMatches::StartColumnRole).toInt() > columnNr) { break; } curr = res->tree->itemBelow(curr); } } } QTreeWidgetItem *startChild = curr; // go to the item above. (curr == null is not a problem) curr = res->tree->itemAbove(curr); // expand the items above if needed if (curr && curr->data(0, ReplaceMatches::StartColumnRole).toString().isEmpty()) { res->tree->expandItem(curr); // probably this file item curr = res->tree->itemAbove(curr); if (curr && curr->data(0, ReplaceMatches::StartColumnRole).toString().isEmpty()) { res->tree->expandItem(curr); // probably file above if this is reached } curr = res->tree->itemAbove(startChild); } // skip file name items and the root item while (curr && curr->data(0, ReplaceMatches::StartColumnRole).toString().isEmpty()) { curr = res->tree->itemAbove(curr); } if (!curr) { // select the last child of the last next-to-top-level item QTreeWidgetItem *root = res->tree->topLevelItem(0); // select the last "root item" if (!root || (root->childCount() < 1)) return; root = root->child(root->childCount()-1); // select the last match of the "root item" if (!root || (root->childCount() < 1)) return; curr = root->child(root->childCount()-1); fromLast = true; } itemSelected(curr); if (fromLast) { delete m_infoMessage; const QString msg = i18n("Continuing from last match"); m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information); m_infoMessage->setPosition(KTextEditor::Message::BottomInView); m_infoMessage->setAutoHide(2000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(m_mainWindow->activeView()); m_mainWindow->activeView()->document()->postMessage(m_infoMessage); } } void KatePluginSearchView::readSessionConfig(const KConfigGroup &cg) { m_ui.searchCombo->clear(); m_ui.searchCombo->addItem(QString()); // Add empty Item m_ui.searchCombo->addItems(cg.readEntry("Search", QStringList())); m_ui.replaceCombo->clear(); m_ui.replaceCombo->addItem(QString()); // Add empty Item m_ui.replaceCombo->addItems(cg.readEntry("Replaces", QStringList())); m_ui.matchCase->setChecked(cg.readEntry("MatchCase", false)); m_ui.useRegExp->setChecked(cg.readEntry("UseRegExp", false)); m_ui.expandResults->setChecked(cg.readEntry("ExpandSearchResults", false)); int searchPlaceIndex = cg.readEntry("Place", 1); if (searchPlaceIndex < 0) { searchPlaceIndex = Folder; // for the case we happen to read -1 as Place } if ((searchPlaceIndex == Project) && (searchPlaceIndex >= m_ui.searchPlaceCombo->count())) { // handle the case that project mode was selected, but not yet available m_switchToProjectModeWhenAvailable = true; searchPlaceIndex = Folder; } m_ui.searchPlaceCombo->setCurrentIndex(searchPlaceIndex); m_ui.recursiveCheckBox->setChecked(cg.readEntry("Recursive", true)); m_ui.hiddenCheckBox->setChecked(cg.readEntry("HiddenFiles", false)); m_ui.symLinkCheckBox->setChecked(cg.readEntry("FollowSymLink", false)); m_ui.binaryCheckBox->setChecked(cg.readEntry("BinaryFiles", false)); m_ui.folderRequester->comboBox()->clear(); m_ui.folderRequester->comboBox()->addItems(cg.readEntry("SearchDiskFiless", QStringList())); m_ui.folderRequester->setText(cg.readEntry("SearchDiskFiles", QString())); m_ui.filterCombo->clear(); m_ui.filterCombo->addItems(cg.readEntry("Filters", QStringList())); m_ui.filterCombo->setCurrentIndex(cg.readEntry("CurrentFilter", -1)); m_ui.excludeCombo->clear(); m_ui.excludeCombo->addItems(cg.readEntry("ExcludeFilters", QStringList())); m_ui.excludeCombo->setCurrentIndex(cg.readEntry("CurrentExcludeFilter", -1)); m_ui.displayOptions->setChecked(searchPlaceIndex == Folder); } void KatePluginSearchView::writeSessionConfig(KConfigGroup &cg) { QStringList searchHistoy; for (int i=1; icount(); i++) { searchHistoy << m_ui.searchCombo->itemText(i); } cg.writeEntry("Search", searchHistoy); QStringList replaceHistoy; for (int i=1; icount(); i++) { replaceHistoy << m_ui.replaceCombo->itemText(i); } cg.writeEntry("Replaces", replaceHistoy); cg.writeEntry("MatchCase", m_ui.matchCase->isChecked()); cg.writeEntry("UseRegExp", m_ui.useRegExp->isChecked()); cg.writeEntry("ExpandSearchResults", m_ui.expandResults->isChecked()); cg.writeEntry("Place", m_ui.searchPlaceCombo->currentIndex()); cg.writeEntry("Recursive", m_ui.recursiveCheckBox->isChecked()); cg.writeEntry("HiddenFiles", m_ui.hiddenCheckBox->isChecked()); cg.writeEntry("FollowSymLink", m_ui.symLinkCheckBox->isChecked()); cg.writeEntry("BinaryFiles", m_ui.binaryCheckBox->isChecked()); QStringList folders; for (int i=0; icomboBox()->count(), 10); i++) { folders << m_ui.folderRequester->comboBox()->itemText(i); } cg.writeEntry("SearchDiskFiless", folders); cg.writeEntry("SearchDiskFiles", m_ui.folderRequester->text()); QStringList filterItems; for (int i=0; icount(), 10); i++) { filterItems << m_ui.filterCombo->itemText(i); } cg.writeEntry("Filters", filterItems); cg.writeEntry("CurrentFilter", m_ui.filterCombo->findText(m_ui.filterCombo->currentText())); QStringList excludeFilterItems; for (int i=0; icount(), 10); i++) { excludeFilterItems << m_ui.excludeCombo->itemText(i); } cg.writeEntry("ExcludeFilters", excludeFilterItems); cg.writeEntry("CurrentExcludeFilter", m_ui.excludeCombo->findText(m_ui.excludeCombo->currentText())); } void KatePluginSearchView::addTab() { if ((sender() != m_ui.newTabButton) && (m_ui.resultTabWidget->count() > 0) && m_ui.resultTabWidget->tabText(m_ui.resultTabWidget->currentIndex()).isEmpty()) { return; } Results *res = new Results(); res->tree->setRootIsDecorated(false); connect(res->tree, &QTreeWidget::itemDoubleClicked, this, &KatePluginSearchView::itemSelected, Qt::UniqueConnection); res->searchPlaceIndex = m_ui.searchPlaceCombo->currentIndex(); res->useRegExp = m_ui.useRegExp->isChecked(); res->matchCase = m_ui.matchCase->isChecked(); m_ui.resultTabWidget->addTab(res, QString()); m_ui.resultTabWidget->setCurrentIndex(m_ui.resultTabWidget->count()-1); m_ui.stackedWidget->setCurrentIndex(0); m_ui.resultTabWidget->tabBar()->show(); m_ui.displayOptions->setChecked(false); res->tree->installEventFilter(this); } void KatePluginSearchView::tabCloseRequested(int index) { Results *tmp = qobject_cast(m_ui.resultTabWidget->widget(index)); if (m_curResults == tmp) { m_searchOpenFiles.cancelSearch(); m_searchDiskFiles.cancelSearch(); } if (m_ui.resultTabWidget->count() > 1) { delete tmp; // remove the tab m_curResults = nullptr; } if (m_ui.resultTabWidget->count() == 1) { m_ui.resultTabWidget->tabBar()->hide(); } } void KatePluginSearchView::resultTabChanged(int index) { if (index < 0) { return; } Results *res = qobject_cast(m_ui.resultTabWidget->widget(index)); if (!res) { qDebug() << "No res found"; return; } m_ui.searchCombo->blockSignals(true); m_ui.matchCase->blockSignals(true); m_ui.useRegExp->blockSignals(true); m_ui.searchPlaceCombo->blockSignals(true); m_ui.searchCombo->lineEdit()->setText(m_ui.resultTabWidget->tabText(index)); m_ui.useRegExp->setChecked(res->useRegExp); m_ui.matchCase->setChecked(res->matchCase); m_ui.searchPlaceCombo->setCurrentIndex(res->searchPlaceIndex); m_ui.searchCombo->blockSignals(false); m_ui.matchCase->blockSignals(false); m_ui.useRegExp->blockSignals(false); m_ui.searchPlaceCombo->blockSignals(false); searchPlaceChanged(); } +void KatePluginSearchView::onResize(const QSize& size) +{ + bool vertical = size.width() < size.height(); + + if(!m_isLeftRight && vertical) { + m_isLeftRight = true; + + m_ui.gridLayout->addWidget(m_ui.searchCombo, 0, 1, 1, 8); + m_ui.gridLayout->addWidget(m_ui.findLabel, 0, 0); + m_ui.gridLayout->addWidget(m_ui.searchButton, 1, 0, 1, 2); + m_ui.gridLayout->addWidget(m_ui.stopAndNext, 1, 2); + m_ui.gridLayout->addWidget(m_ui.searchPlaceCombo, 1, 3, 1, 3); + m_ui.gridLayout->addWidget(m_ui.displayOptions, 1, 6); + m_ui.gridLayout->addWidget(m_ui.matchCase, 1, 7); + m_ui.gridLayout->addWidget(m_ui.useRegExp, 1, 8); + + m_ui.gridLayout->addWidget(m_ui.replaceCombo, 2, 1, 1, 8); + m_ui.gridLayout->addWidget(m_ui.replaceLabel, 2, 0); + m_ui.gridLayout->addWidget(m_ui.replaceButton, 3, 0, 1, 2); + m_ui.gridLayout->addWidget(m_ui.replaceCheckedBtn, 3, 2); + m_ui.gridLayout->addWidget(m_ui.expandResults, 3, 7); + m_ui.gridLayout->addWidget(m_ui.newTabButton, 3, 8); + + m_ui.gridLayout->setColumnStretch(4, 2); + m_ui.gridLayout->setColumnStretch(2, 0); + } + else if(m_isLeftRight && !vertical) { + m_isLeftRight = false; + m_ui.gridLayout->addWidget(m_ui.searchCombo, 0, 2); + m_ui.gridLayout->addWidget(m_ui.findLabel, 0, 1); + m_ui.gridLayout->addWidget(m_ui.searchButton, 0, 3); + m_ui.gridLayout->addWidget(m_ui.stopAndNext, 0, 4); + m_ui.gridLayout->addWidget(m_ui.searchPlaceCombo, 0, 5, 1, 4); + m_ui.gridLayout->addWidget(m_ui.matchCase, 1, 5); + m_ui.gridLayout->addWidget(m_ui.useRegExp, 1, 6); + + m_ui.gridLayout->addWidget(m_ui.replaceCombo, 1, 2); + m_ui.gridLayout->addWidget(m_ui.replaceLabel, 1, 1); + m_ui.gridLayout->addWidget(m_ui.replaceButton, 1, 3); + m_ui.gridLayout->addWidget(m_ui.replaceCheckedBtn, 1, 4); + m_ui.gridLayout->addWidget(m_ui.expandResults, 1, 8); + m_ui.gridLayout->addWidget(m_ui.newTabButton, 0, 0); + m_ui.gridLayout->addWidget(m_ui.displayOptions, 1, 0); + + m_ui.gridLayout->setColumnStretch(4, 0); + m_ui.gridLayout->setColumnStretch(2, 2); + + m_ui.findLabel->setAlignment(Qt::AlignRight); + m_ui.replaceLabel->setAlignment(Qt::AlignRight); + } +} bool KatePluginSearchView::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *ke = static_cast(event); QTreeWidget *tree = qobject_cast(obj); if (tree) { if (ke->matches(QKeySequence::Copy)) { // user pressed ctrl+c -> copy full URL to the clipboard QVariant variant = tree->currentItem()->data(0, ReplaceMatches::FileUrlRole); QApplication::clipboard()->setText(variant.toString()); event->accept(); return true; } if (ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return) { if (tree->currentItem()) { itemSelected(tree->currentItem()); event->accept(); return true; } } } // NOTE: Qt::Key_Escape is handled by handleEsc } + if (event->type() == QEvent::Resize) { + QResizeEvent *re = static_cast(event); + if(obj == m_toolView) + { + onResize(re->size()); + } + } return QObject::eventFilter(obj, event); } void KatePluginSearchView::searchContextMenu(const QPoint& pos) { QSet actionPointers; QMenu* const contextMenu = m_ui.searchCombo->lineEdit()->createStandardContextMenu(); if (!contextMenu) return; if (m_ui.useRegExp->isChecked()) { QMenu* menu = contextMenu->addMenu(i18n("Add...")); if (!menu) return; menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); actionPointers << menuEntry(menu, QStringLiteral("^"), QStringLiteral(""), i18n("Beginning of line")); actionPointers << menuEntry(menu, QStringLiteral("$"), QStringLiteral(""), i18n("End of line")); menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("."), QStringLiteral(""), i18n("Any single character (excluding line breaks)")); menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("+"), QStringLiteral(""), i18n("One or more occurrences")); actionPointers << menuEntry(menu, QStringLiteral("*"), QStringLiteral(""), i18n("Zero or more occurrences")); actionPointers << menuEntry(menu, QStringLiteral("?"), QStringLiteral(""), i18n("Zero or one occurrences")); actionPointers << menuEntry(menu, QStringLiteral("{"), QStringLiteral(",}"), i18n(" through occurrences"), QStringLiteral("{a"), QStringLiteral(",b}")); menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("("), QStringLiteral(")"), i18n("Group, capturing")); actionPointers << menuEntry(menu, QStringLiteral("|"), QStringLiteral(""), i18n("Or")); actionPointers << menuEntry(menu, QStringLiteral("["), QStringLiteral("]"), i18n("Set of characters")); actionPointers << menuEntry(menu, QStringLiteral("[^"), QStringLiteral("]"), i18n("Negative set of characters")); actionPointers << menuEntry(menu, QStringLiteral("(?:"), QStringLiteral(")"), i18n("Group, non-capturing"), QStringLiteral("(?:E")); actionPointers << menuEntry(menu, QStringLiteral("(?="), QStringLiteral(")"), i18n("Lookahead"), QStringLiteral("(?=E")); actionPointers << menuEntry(menu, QStringLiteral("(?!"), QStringLiteral(")"), i18n("Negative lookahead"), QStringLiteral("(?!E")); menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("\\n"), QStringLiteral(""), i18n("Line break")); actionPointers << menuEntry(menu, QStringLiteral("\\t"), QStringLiteral(""), i18n("Tab")); actionPointers << menuEntry(menu, QStringLiteral("\\b"), QStringLiteral(""), i18n("Word boundary")); actionPointers << menuEntry(menu, QStringLiteral("\\B"), QStringLiteral(""), i18n("Not word boundary")); actionPointers << menuEntry(menu, QStringLiteral("\\d"), QStringLiteral(""), i18n("Digit")); actionPointers << menuEntry(menu, QStringLiteral("\\D"), QStringLiteral(""), i18n("Non-digit")); actionPointers << menuEntry(menu, QStringLiteral("\\s"), QStringLiteral(""), i18n("Whitespace (excluding line breaks)")); actionPointers << menuEntry(menu, QStringLiteral("\\S"), QStringLiteral(""), i18n("Non-whitespace (excluding line breaks)")); actionPointers << menuEntry(menu, QStringLiteral("\\w"), QStringLiteral(""), i18n("Word character (alphanumerics plus '_')")); actionPointers << menuEntry(menu, QStringLiteral("\\W"), QStringLiteral(""), i18n("Non-word character")); } // Show menu QAction * const result = contextMenu->exec(m_ui.searchCombo->mapToGlobal(pos)); // Act on action if (result && actionPointers.contains(result)) { QLineEdit * lineEdit = m_ui.searchCombo->lineEdit(); const int cursorPos = lineEdit->cursorPosition(); QStringList beforeAfter = result->data().toString().split(QLatin1Char(' ')); if (beforeAfter.size() != 2) return; lineEdit->insert(beforeAfter[0] + beforeAfter[1]); lineEdit->setCursorPosition(cursorPos + beforeAfter[0].count()); lineEdit->setFocus(); } } void KatePluginSearchView::replaceContextMenu(const QPoint& pos) { QMenu* const contextMenu = m_ui.replaceCombo->lineEdit()->createStandardContextMenu(); if (!contextMenu) return; QMenu* menu = contextMenu->addMenu(i18n("Add...")); if (!menu) return; menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); QSet actionPointers; actionPointers << menuEntry(menu, QStringLiteral("\\n"), QStringLiteral(""), i18n("Line break")); actionPointers << menuEntry(menu, QStringLiteral("\\t"), QStringLiteral(""), i18n("Tab")); if (m_ui.useRegExp->isChecked()) { menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("\\0"), QStringLiteral(""), i18n("Regular expression capture 0 (whole match)")); actionPointers << menuEntry(menu, QStringLiteral("\\"), QStringLiteral(""), i18n("Regular expression capture 1-9"), QStringLiteral("\\#")); actionPointers << menuEntry(menu, QStringLiteral("\\{"), QStringLiteral("}"), i18n("Regular expression capture 0-999"), QStringLiteral("\\{#")); menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("\\U\\"), QStringLiteral(""), i18n("Upper-cased capture 0-9"), QStringLiteral("\\U\\#")); actionPointers << menuEntry(menu, QStringLiteral("\\U\\{"), QStringLiteral("}"), i18n("Upper-cased capture 0-999"), QStringLiteral("\\U\\{###")); actionPointers << menuEntry(menu, QStringLiteral("\\L\\"), QStringLiteral(""), i18n("Lower-cased capture 0-9"), QStringLiteral("\\L\\#")); actionPointers << menuEntry(menu, QStringLiteral("\\L\\{"), QStringLiteral("}"), i18n("Lower-cased capture 0-999"), QStringLiteral("\\L\\{###")); } // Show menu QAction * const result = contextMenu->exec(m_ui.replaceCombo->mapToGlobal(pos)); // Act on action if (result && actionPointers.contains(result)) { QLineEdit * lineEdit = m_ui.replaceCombo->lineEdit(); const int cursorPos = lineEdit->cursorPosition(); QStringList beforeAfter = result->data().toString().split(QLatin1Char(' ')); if (beforeAfter.size() != 2) return; lineEdit->insert(beforeAfter[0] + beforeAfter[1]); lineEdit->setCursorPosition(cursorPos + beforeAfter[0].count()); lineEdit->setFocus(); } } void KatePluginSearchView::slotPluginViewCreated(const QString &name, QObject *pluginView) { // add view if (pluginView && name == QStringLiteral("kateprojectplugin")) { m_projectPluginView = pluginView; slotProjectFileNameChanged(); connect (pluginView, SIGNAL(projectFileNameChanged()), this, SLOT(slotProjectFileNameChanged())); } } void KatePluginSearchView::slotPluginViewDeleted(const QString &name, QObject *) { // remove view if (name == QStringLiteral("kateprojectplugin")) { m_projectPluginView = nullptr; slotProjectFileNameChanged(); } } void KatePluginSearchView::slotProjectFileNameChanged() { // query new project file name QString projectFileName; if (m_projectPluginView) { projectFileName = m_projectPluginView->property("projectFileName").toString(); } // have project, enable gui for it if (!projectFileName.isEmpty()) { if (m_ui.searchPlaceCombo->count() <= Project) { // add "in Project" m_ui.searchPlaceCombo->addItem (QIcon::fromTheme(QStringLiteral("project-open")), i18n("In Current Project")); if (m_switchToProjectModeWhenAvailable) { // switch to search "in Project" m_switchToProjectModeWhenAvailable = false; setSearchPlace(Project); } // add "in Open Projects" m_ui.searchPlaceCombo->addItem(QIcon::fromTheme(QStringLiteral("project-open")), i18n("In All Open Projects")); } } // else: disable gui for it else { if (m_ui.searchPlaceCombo->count() >= Project) { // switch to search "in Open files", if "in Project" is active if (m_ui.searchPlaceCombo->currentIndex() >= Project) { setSearchPlace(OpenFiles); } // remove "in Project" and "in all projects" while (m_ui.searchPlaceCombo->count() > Project) { m_ui.searchPlaceCombo->removeItem(m_ui.searchPlaceCombo->count()-1); } } } } KateSearchCommand::KateSearchCommand(QObject *parent) : KTextEditor::Command(QStringList() << QStringLiteral("grep") << QStringLiteral("newGrep") << QStringLiteral("search") << QStringLiteral("newSearch") << QStringLiteral("pgrep") << QStringLiteral("newPGrep"), parent) { } bool KateSearchCommand::exec(KTextEditor::View* /*view*/, const QString& cmd, QString& /*msg*/, const KTextEditor::Range &) { //create a list of args QStringList args(cmd.split(QLatin1Char(' '), QString::KeepEmptyParts)); QString command = args.takeFirst(); QString searchText = args.join(QLatin1Char(' ')); if (command == QStringLiteral("grep") || command == QStringLiteral("newGrep")) { emit setSearchPlace(KatePluginSearchView::Folder); emit setCurrentFolder(); if (command == QStringLiteral("newGrep")) emit newTab(); } else if (command == QStringLiteral("search") || command == QStringLiteral("newSearch")) { emit setSearchPlace(KatePluginSearchView::OpenFiles); if (command == QStringLiteral("newSearch")) emit newTab(); } else if (command == QStringLiteral("pgrep") || command == QStringLiteral("newPGrep")) { emit setSearchPlace(KatePluginSearchView::Project); if (command == QStringLiteral("newPGrep")) emit newTab(); } emit setSearchString(searchText); emit startSearch(); return true; } bool KateSearchCommand::help(KTextEditor::View */*view*/, const QString &cmd, QString & msg) { if (cmd.startsWith(QStringLiteral("grep"))) { msg = i18n("Usage: grep [pattern to search for in folder]"); } else if (cmd.startsWith(QStringLiteral("newGrep"))) { msg = i18n("Usage: newGrep [pattern to search for in folder]"); } else if (cmd.startsWith(QStringLiteral("search"))) { msg = i18n("Usage: search [pattern to search for in open files]"); } else if (cmd.startsWith(QStringLiteral("newSearch"))) { msg = i18n("Usage: search [pattern to search for in open files]"); } else if (cmd.startsWith(QStringLiteral("pgrep"))) { msg = i18n("Usage: pgrep [pattern to search for in current project]"); } else if (cmd.startsWith(QStringLiteral("newPGrep"))) { msg = i18n("Usage: newPGrep [pattern to search for in current project]"); } return true; } #include "plugin_search.moc" // kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/search/plugin_search.h b/addons/search/plugin_search.h index 79ae4eee5..15035bd22 100644 --- a/addons/search/plugin_search.h +++ b/addons/search/plugin_search.h @@ -1,244 +1,247 @@ /* Kate search plugin * * Copyright (C) 2011-2013 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #ifndef _PLUGIN_SEARCH_H_ #define _PLUGIN_SEARCH_H_ #include #include #include #include #include #include #include #include #include #include #include "ui_search.h" #include "ui_results.h" #include "search_open_files.h" #include "SearchDiskFiles.h" #include "FolderFilesList.h" #include "replace_matches.h" class KateSearchCommand; namespace KTextEditor{ class MovingRange; } class Results: public QWidget, public Ui::Results { Q_OBJECT public: Results(QWidget *parent = nullptr); int matches; QRegularExpression regExp; bool useRegExp; bool matchCase; QString replaceStr; int searchPlaceIndex; QString treeRootText; }; // This class keeps the focus inside the S&R plugin when pressing tab/shift+tab by overriding focusNextPrevChild() class ContainerWidget:public QWidget { Q_OBJECT public: ContainerWidget(QWidget *parent): QWidget(parent) {} Q_SIGNALS: void nextFocus(QWidget *currentWidget, bool *found, bool next); protected: bool focusNextPrevChild (bool next) override; }; class KatePluginSearch : public KTextEditor::Plugin { Q_OBJECT public: explicit KatePluginSearch(QObject* parent = nullptr, const QList& = QList()); ~KatePluginSearch() override; QObject *createView(KTextEditor::MainWindow *mainWindow) override; private: KateSearchCommand* m_searchCommand; }; class KatePluginSearchView : public QObject, public KXMLGUIClient, public KTextEditor::SessionConfigInterface { Q_OBJECT Q_INTERFACES(KTextEditor::SessionConfigInterface) public: enum SearchPlaces { CurrentFile, OpenFiles, Folder, Project, AllProjects }; KatePluginSearchView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mainWindow, KTextEditor::Application* application); ~KatePluginSearchView() override; void readSessionConfig (const KConfigGroup& config) override; void writeSessionConfig (KConfigGroup& config) override; public Q_SLOTS: void startSearch(); void setSearchString(const QString &pattern); void navigateFolderUp(); void setCurrentFolder(); void setSearchPlace(int place); void goToNextMatch(); void goToPreviousMatch(); private Q_SLOTS: void openSearchView(); void handleEsc(QEvent *e); void nextFocus(QWidget *currentWidget, bool *found, bool next); void addTab(); void tabCloseRequested(int index); void toggleOptions(bool show); void searchContextMenu(const QPoint& pos); void replaceContextMenu(const QPoint& pos); void searchPlaceChanged(); void startSearchWhileTyping(); void folderFileListChanged(); void matchFound(const QString &url, const QString &fileName, const QString &lineContent, int matchLen, int startLine, int startColumn, int endLine, int endColumn); void addMatchMark(KTextEditor::Document* doc, QTreeWidgetItem *item); void searchDone(); void searchWhileTypingDone(); void indicateMatch(bool hasMatch); void searching(const QString &file); void itemSelected(QTreeWidgetItem *item); void clearMarks(); void clearDocMarks(KTextEditor::Document* doc); void replaceSingleMatch(); void replaceChecked(); void replaceStatus(const QUrl &url, int replacedInFile, int matchesInFile); void replaceDone(); void docViewChanged(); void resultTabChanged(int index); void expandResults(); void updateResultsRootItem(); /** * keep track if the project plugin is alive and if the project file did change */ void slotPluginViewCreated (const QString &name, QObject *pluginView); void slotPluginViewDeleted (const QString &name, QObject *pluginView); void slotProjectFileNameChanged (); protected: bool eventFilter(QObject *obj, QEvent *ev) override; void addHeaderItem(); private: QTreeWidgetItem *rootFileItem(const QString &url, const QString &fName); QStringList filterFiles(const QStringList& files) const; + + void onResize(const QSize& size); Ui::SearchDialog m_ui; QWidget *m_toolView; KTextEditor::Application *m_kateApp; SearchOpenFiles m_searchOpenFiles; FolderFilesList m_folderFilesList; SearchDiskFiles m_searchDiskFiles; ReplaceMatches m_replacer; QAction *m_matchCase; QAction *m_useRegExp; Results *m_curResults; bool m_searchJustOpened; bool m_switchToProjectModeWhenAvailable; bool m_searchDiskFilesDone; bool m_searchOpenFilesDone; bool m_isSearchAsYouType; + bool m_isLeftRight; QString m_resultBaseDir; QList m_matchRanges; QTimer m_changeTimer; QTimer m_updateSumaryTimer; QPointer m_infoMessage; /** * current project plugin view, if any */ QObject *m_projectPluginView; - + /** * our main window */ KTextEditor::MainWindow *m_mainWindow; }; class KateSearchCommand : public KTextEditor::Command { Q_OBJECT public: KateSearchCommand(QObject *parent); Q_SIGNALS: void setSearchPlace(int place); void setCurrentFolder(); void setSearchString(const QString &pattern); void startSearch(); void newTab(); // // KTextEditor::Command // public: bool exec (KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &range = KTextEditor::Range::invalid()) override; bool help (KTextEditor::View *view, const QString &cmd, QString &msg) override; }; #endif // kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/search/search.ui b/addons/search/search.ui index 2af8f0ac9..d7fdfe3fc 100644 --- a/addons/search/search.ui +++ b/addons/search/search.ui @@ -1,553 +1,496 @@ SearchDialog 0 0 634 204 Add new search tab ... Find: searchCombo true false Search Search 0 0 - - - 0 - - - - - 0 - 0 - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - + + 0 + - - false - + + false + Next Next - - - - - - - 0 - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - 0 - + + Stop Stop - - - In Current File In Open Files In Folder Show search options ... true Replace: replaceCombo true false Replace false Replace Checked Match case true true Use regular expressions true true Qt::Horizontal 13 20 Expand results true true 0 0 1 0 0 0 0 true true 0 0 0 0 Qt::Vertical 1 1 0 0 0 0 0 0 Qt::Horizontal Folder: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter folderRequester Go one folder up. Use the current document's path. Filter: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter filterCombo true QComboBox::InsertAtTop * Exclude: excludeCombo true Recursive true Include hidden Follow symbolic links Include binary files Qt::Horizontal 40 20 KUrlRequester QWidget
kurlrequester.h
KUrlComboRequester KUrlRequester
kurlrequester.h
KComboBox QComboBox
kcombobox.h
newTabButton searchCombo replaceCombo searchButton nextButton stopButton replaceButton replaceCheckedBtn searchPlaceCombo matchCase useRegExp expandResults displayOptions folderRequester folderUpButton currentFolderButton filterCombo excludeCombo recursiveCheckBox hiddenCheckBox symLinkCheckBox binaryCheckBox resultTabWidget
diff --git a/addons/sessionapplet/CMakeLists.txt b/addons/sessionapplet/CMakeLists.txt index 477c4cd5b..053bbf688 100644 --- a/addons/sessionapplet/CMakeLists.txt +++ b/addons/sessionapplet/CMakeLists.txt @@ -1,6 +1,36 @@ -project(katesessionapplet) +find_package(KF5Plasma QUIET) +set_package_properties(KF5Plasma PROPERTIES PURPOSE "Required to build the sessionapplet addon") -add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.plasma.katesessions\") +find_package(Qt5Widgets QUIET) +set_package_properties(Qt5Widgets PROPERTIES PURPOSE "Required to build the sessionapplet addon") + +if(NOT KF5Plasma_FOUND OR NOT Qt5Widgets_FOUND) + return() +endif() + +add_library(plasma_engine_katesessions MODULE "") +target_compile_definitions(plasma_engine_katesessions PRIVATE TRANSLATION_DOMAIN="plasma_applet_org.kde.plasma.katesessions") + +target_link_libraries( + plasma_engine_katesessions + PRIVATE + KF5::I18n + KF5::Plasma + Qt5::Widgets +) + +target_sources( + plasma_engine_katesessions + PRIVATE + katesessionsengine.cpp + katesessionsmodel.cpp + katesessionsjob.cpp + katesessionsservice.cpp +) plasma_install_package(applet org.kde.plasma.katesessions) -add_subdirectory(engine) +kcoreaddons_desktop_to_json(plasma_engine_katesessions plasma-dataengine-katesessions.desktop) +install(TARGETS plasma_engine_katesessions DESTINATION ${PLUGIN_INSTALL_DIR}/plasma/dataengine) +install(FILES plasma-dataengine-katesessions.desktop DESTINATION ${SERVICES_INSTALL_DIR}) +install(FILES org.kde.plasma.katesessions.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) + diff --git a/addons/sessionapplet/engine/CMakeLists.txt b/addons/sessionapplet/engine/CMakeLists.txt deleted file mode 100644 index 9ecff36dd..000000000 --- a/addons/sessionapplet/engine/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Plasma Data Engine -set(plasma_engine_katesessions_SRCS katesessionsengine.cpp katesessionsmodel.cpp katesessionsjob.cpp katesessionsservice.cpp) -add_library(plasma_engine_katesessions MODULE ${plasma_engine_katesessions_SRCS}) -kcoreaddons_desktop_to_json(plasma_engine_katesessions plasma-dataengine-katesessions.desktop) -target_link_libraries(plasma_engine_katesessions - Qt5::Widgets # QAction - KF5::IconThemes - KF5::Plasma - KF5::Service - KF5::I18n -) - -install(TARGETS plasma_engine_katesessions DESTINATION ${PLUGIN_INSTALL_DIR}/plasma/dataengine) -install(FILES plasma-dataengine-katesessions.desktop DESTINATION ${SERVICES_INSTALL_DIR}) -install(FILES org.kde.plasma.katesessions.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) - diff --git a/addons/sessionapplet/engine/katesessionsengine.cpp b/addons/sessionapplet/katesessionsengine.cpp similarity index 100% rename from addons/sessionapplet/engine/katesessionsengine.cpp rename to addons/sessionapplet/katesessionsengine.cpp diff --git a/addons/sessionapplet/engine/katesessionsengine.h b/addons/sessionapplet/katesessionsengine.h similarity index 100% rename from addons/sessionapplet/engine/katesessionsengine.h rename to addons/sessionapplet/katesessionsengine.h diff --git a/addons/sessionapplet/engine/katesessionsjob.cpp b/addons/sessionapplet/katesessionsjob.cpp similarity index 100% rename from addons/sessionapplet/engine/katesessionsjob.cpp rename to addons/sessionapplet/katesessionsjob.cpp diff --git a/addons/sessionapplet/engine/katesessionsjob.h b/addons/sessionapplet/katesessionsjob.h similarity index 100% rename from addons/sessionapplet/engine/katesessionsjob.h rename to addons/sessionapplet/katesessionsjob.h diff --git a/addons/sessionapplet/engine/katesessionsmodel.cpp b/addons/sessionapplet/katesessionsmodel.cpp similarity index 100% rename from addons/sessionapplet/engine/katesessionsmodel.cpp rename to addons/sessionapplet/katesessionsmodel.cpp diff --git a/addons/sessionapplet/engine/katesessionsmodel.h b/addons/sessionapplet/katesessionsmodel.h similarity index 100% rename from addons/sessionapplet/engine/katesessionsmodel.h rename to addons/sessionapplet/katesessionsmodel.h diff --git a/addons/sessionapplet/engine/katesessionsservice.cpp b/addons/sessionapplet/katesessionsservice.cpp similarity index 100% rename from addons/sessionapplet/engine/katesessionsservice.cpp rename to addons/sessionapplet/katesessionsservice.cpp diff --git a/addons/sessionapplet/engine/katesessionsservice.h b/addons/sessionapplet/katesessionsservice.h similarity index 100% rename from addons/sessionapplet/engine/katesessionsservice.h rename to addons/sessionapplet/katesessionsservice.h diff --git a/addons/sessionapplet/engine/org.kde.plasma.katesessions.operations b/addons/sessionapplet/org.kde.plasma.katesessions.operations similarity index 100% rename from addons/sessionapplet/engine/org.kde.plasma.katesessions.operations rename to addons/sessionapplet/org.kde.plasma.katesessions.operations diff --git a/addons/sessionapplet/engine/plasma-dataengine-katesessions.desktop b/addons/sessionapplet/plasma-dataengine-katesessions.desktop similarity index 100% rename from addons/sessionapplet/engine/plasma-dataengine-katesessions.desktop rename to addons/sessionapplet/plasma-dataengine-katesessions.desktop diff --git a/addons/snippets/CMakeLists.txt b/addons/snippets/CMakeLists.txt index 41828660a..845d8d43d 100644 --- a/addons/snippets/CMakeLists.txt +++ b/addons/snippets/CMakeLists.txt @@ -1,34 +1,38 @@ -project (katesnippets) -add_definitions(-DTRANSLATION_DOMAIN=\"katesnippetsplugin\") +find_package(KF5NewStuff QUIET) +set_package_properties(KF5NewStuff PROPERTIES PURPOSE "Required to build the snippets addon") -########### next target ############### +if(NOT KF5NewStuff_FOUND) + return() +endif() -set(katesnippetsplugin_PART_SRCS katesnippets.cpp -katesnippetglobal.cpp -snippetview.cpp -snippetstore.cpp -snippetrepository.cpp -snippetcompletionmodel.cpp -snippetcompletionitem.cpp -snippet.cpp -editrepository.cpp -editsnippet.cpp ) +add_library(katesnippetsplugin MODULE "") +target_compile_definitions(katesnippetsplugin PRIVATE TRANSLATION_DOMAIN="katesnippetsplugin") -ki18n_wrap_ui(katesnippetsplugin_PART_SRCS -snippetview.ui -editrepository.ui -editsnippet.ui) - -# resource for ui file and stuff -qt5_add_resources(katesnippetsplugin_PART_SRCS plugin.qrc) - -add_library (katesnippetsplugin MODULE ${katesnippetsplugin_PART_SRCS}) - -kcoreaddons_desktop_to_json (katesnippetsplugin katesnippetsplugin.desktop) - -target_link_libraries(katesnippetsplugin +target_link_libraries( + katesnippetsplugin + PRIVATE + KF5::NewStuff KF5::TextEditor - KF5::Parts KF5::I18n - KF5::NewStuff KF5::ItemViews KF5::IconThemes) - -install(TARGETS katesnippetsplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) +) + +ki18n_wrap_ui(UI_SOURCES snippetview.ui editrepository.ui editsnippet.ui) +target_sources(katesnippetsplugin PRIVATE ${UI_SOURCES}) + +target_sources( + katesnippetsplugin + PRIVATE + katesnippets.cpp + katesnippetglobal.cpp + snippetview.cpp + snippetstore.cpp + snippetrepository.cpp + snippetcompletionmodel.cpp + snippetcompletionitem.cpp + snippet.cpp + editrepository.cpp + editsnippet.cpp + plugin.qrc +) + +kcoreaddons_desktop_to_json(katesnippetsplugin katesnippetsplugin.desktop) +install(TARGETS katesnippetsplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/snippets/editsnippet.cpp b/addons/snippets/editsnippet.cpp index 7aeef9cbe..b66dd3b27 100644 --- a/addons/snippets/editsnippet.cpp +++ b/addons/snippets/editsnippet.cpp @@ -1,209 +1,210 @@ /* This file is part of the Kate project. * Based on the snippet plugin from KDevelop 4. * * Copyright (C) 2007 Robert Gruber * Copyright (C) 2010 Milian Wolff * Copyright (C) 2012 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "editsnippet.h" #include "ui_editsnippet.h" #include "snippetrepository.h" #include "snippetstore.h" #include "snippet.h" #include #include #include #include #include #include #include #include #include #include #include #include KTextEditor::View* createView(QWidget* tabWidget) { auto document = KTextEditor::Editor::instance()->createDocument(tabWidget); auto view = document->createView(tabWidget); view->action("file_save")->setEnabled(false); tabWidget->layout()->addWidget(view); view->setStatusBarEnabled(false); return view; } EditSnippet::EditSnippet(SnippetRepository* repository, Snippet* snippet, QWidget* parent) : QDialog(parent), m_ui(new Ui::EditSnippetBase), m_repo(repository) , m_snippet(snippet), m_topBoxModified(false) { Q_ASSERT(m_repo); m_ui->setupUi(this); connect(this, &QDialog::accepted, this, &EditSnippet::save); m_okButton = m_ui->buttons->button(QDialogButtonBox::Ok); KGuiItem::assign(m_okButton, KStandardGuiItem::ok()); m_ui->buttons->addButton(m_okButton, QDialogButtonBox::AcceptRole); connect(m_okButton, &QPushButton::clicked, this, &QDialog::accept); auto cancelButton = m_ui->buttons->button(QDialogButtonBox::Cancel); KGuiItem::assign(cancelButton, KStandardGuiItem::cancel()); m_ui->buttons->addButton(cancelButton, QDialogButtonBox::RejectRole); connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); m_snippetView = createView(m_ui->snippetTab); if ( !m_repo->fileTypes().isEmpty() ) { m_snippetView->document()->setMode(m_repo->fileTypes().first()); } m_scriptsView = createView(m_ui->scriptTab); m_scriptsView->document()->setMode(QStringLiteral("JavaScript")); m_scriptsView->document()->setText(m_repo->script()); m_scriptsView->document()->setModified(false); // view for testing the snippet m_testView = createView(m_ui->testWidget); // splitter default size ratio m_ui->splitter->setSizes(QList() << 400 << 150); connect(m_ui->dotest_button, &QPushButton::clicked, this, &EditSnippet::test); // modified notification stuff connect(m_ui->snippetNameEdit, &QLineEdit::textEdited, this, &EditSnippet::topBoxModified); connect(m_ui->snippetNameEdit, &QLineEdit::textEdited, this, &EditSnippet::validate); connect(m_ui->snippetShortcut, &KKeySequenceWidget::keySequenceChanged, this, &EditSnippet::topBoxModified); connect(m_snippetView->document(), &KTextEditor::Document::textChanged, this, &EditSnippet::validate); auto showHelp = [](const QString& text) { QWhatsThis::showText(QCursor::pos(), text); }; connect(m_ui->snippetLabel, &QLabel::linkActivated, showHelp); connect(m_ui->scriptLabel, &QLabel::linkActivated, showHelp); // if we edit a snippet, add all existing data if ( m_snippet ) { setWindowTitle(i18n("Edit Snippet %1 in %2", m_snippet->text(), m_repo->text())); m_snippetView->document()->setText(m_snippet->snippet()); m_ui->snippetNameEdit->setText(m_snippet->text()); m_ui->snippetShortcut->setKeySequence(m_snippet->action()->shortcut()); // unset modified flags m_snippetView->document()->setModified(false); m_topBoxModified = false; } else { setWindowTitle(i18n("Create New Snippet in Repository %1", m_repo->text())); } m_ui->messageWidget->hide(); validate(); m_ui->snippetNameEdit->setFocus(); setTabOrder(m_ui->snippetNameEdit, m_snippetView); QSize initSize = sizeHint(); initSize.setHeight( initSize.height() + 200 ); } void EditSnippet::test() { m_testView->document()->clear(); m_testView->insertTemplate(KTextEditor::Cursor(0, 0), m_snippetView->document()->text(), m_scriptsView->document()->text()); m_testView->setFocus(); } EditSnippet::~EditSnippet() { delete m_ui; } void EditSnippet::setSnippetText( const QString& text ) { m_snippetView->document()->setText(text); validate(); } void EditSnippet::validate() { const QString& name = m_ui->snippetNameEdit->text(); bool valid = !name.isEmpty() && !m_snippetView->document()->isEmpty(); // make sure the snippetname includes no spaces if ( name.contains(QLatin1Char(' ')) || name.contains(QLatin1Char('\t')) ) { m_ui->messageWidget->setText(i18n("Snippet name cannot contain spaces")); m_ui->messageWidget->animatedShow(); valid = false; } else { // hide message widget if snippet does not include spaces m_ui->messageWidget->animatedHide(); } if ( valid ) { m_ui->messageWidget->hide(); } m_okButton->setEnabled(valid); } void EditSnippet::save() { Q_ASSERT(!m_ui->snippetNameEdit->text().isEmpty()); if ( !m_snippet ) { // save as new snippet m_snippet = new Snippet(); + m_snippet->action(); // ensure that the snippet's QAction is created before it is added to a widget by the rowsInserted() signal m_repo->appendRow(m_snippet); } m_snippet->setSnippet(m_snippetView->document()->text()); m_snippetView->document()->setModified(false); m_snippet->setText(m_ui->snippetNameEdit->text()); m_snippet->action()->setShortcut(m_ui->snippetShortcut->keySequence()); m_repo->setScript(m_scriptsView->document()->text()); m_scriptsView->document()->setModified(false); m_topBoxModified = false; m_repo->save(); setWindowTitle(i18n("Edit Snippet %1 in %2", m_snippet->text(), m_repo->text())); } void EditSnippet::reject() { if (m_topBoxModified || m_snippetView->document()->isModified() || m_scriptsView->document()->isModified()) { int ret = KMessageBox::warningContinueCancel(qApp->activeWindow(), i18n("The snippet contains unsaved changes. Do you want to continue and lose all changes?"), i18n("Warning - Unsaved Changes") ); if (ret == KMessageBox::Cancel) { return; } } QDialog::reject(); } void EditSnippet::topBoxModified() { m_topBoxModified = true; } diff --git a/addons/snippets/katesnippets.cpp b/addons/snippets/katesnippets.cpp index 55dc604e6..ecf57d746 100644 --- a/addons/snippets/katesnippets.cpp +++ b/addons/snippets/katesnippets.cpp @@ -1,131 +1,131 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katesnippets.h" #include "snippetcompletionmodel.h" #include "katesnippetglobal.h" #include "snippetview.h" #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KateSnippetsPluginFactory, "katesnippetsplugin.json", registerPlugin();) KateSnippetsPlugin::KateSnippetsPlugin(QObject *parent, const QList &) : KTextEditor::Plugin(parent) , m_snippetGlobal(new KateSnippetGlobal(this)) { } KateSnippetsPlugin::~KateSnippetsPlugin() { } QObject *KateSnippetsPlugin::createView(KTextEditor::MainWindow *mainWindow) { KateSnippetsPluginView *view = new KateSnippetsPluginView(this, mainWindow); return view; } KateSnippetsPluginView::KateSnippetsPluginView(KateSnippetsPlugin *plugin, KTextEditor::MainWindow *mainWindow) : QObject(mainWindow), m_plugin(plugin), m_mainWindow(mainWindow), m_toolView(nullptr), m_snippets(nullptr) { KXMLGUIClient::setComponentName(QStringLiteral("katesnippets"), i18n("Snippets tool view")); setXMLFile(QStringLiteral("ui.rc")); // Toolview for snippets m_toolView = mainWindow->createToolView(plugin, QStringLiteral("kate_private_plugin_katesnippetsplugin"), KTextEditor::MainWindow::Right, QIcon::fromTheme(QStringLiteral("document-new")), i18n("Snippets")); // add snippets widget - m_snippets = new SnippetView(KateSnippetGlobal::self(), m_toolView.data()); + m_snippets = new SnippetView(KateSnippetGlobal::self(), mainWindow, m_toolView.data()); m_toolView->layout()->addWidget(m_snippets); - m_snippets->setupActionsForWindow(m_toolView); + m_snippets->setupActionsForWindow(mainWindow->window()); m_toolView->addActions(m_snippets->actions()); // create actions QAction *a = actionCollection()->addAction(QStringLiteral("tools_create_snippet")); a->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); a->setText(i18n("Create Snippet")); connect(a, &QAction::triggered, this, &KateSnippetsPluginView::createSnippet); connect(mainWindow, &KTextEditor::MainWindow::viewCreated, this, &KateSnippetsPluginView::slotViewCreated); /** * connect for all already existing views */ foreach (KTextEditor::View *view, mainWindow->views()) { slotViewCreated(view); } // register if factory around if (auto factory = m_mainWindow->guiFactory()) { factory->addClient(this); } } KateSnippetsPluginView::~KateSnippetsPluginView() { // cleanup for all views Q_FOREACH (auto view, m_textViews) { if (! view) { continue; } auto iface = qobject_cast(view); iface->unregisterCompletionModel(KateSnippetGlobal::self()->completionModel()); } // unregister if factory around if (auto factory = m_mainWindow->guiFactory()) { factory->removeClient(this); } if (m_toolView) { delete m_toolView; } } void KateSnippetsPluginView::slotViewCreated(KTextEditor::View *view) { m_textViews.append(QPointer(view)); // add snippet completion auto model = KateSnippetGlobal::self()->completionModel(); auto iface = qobject_cast(view); iface->unregisterCompletionModel(model); iface->registerCompletionModel(model); } void KateSnippetsPluginView::createSnippet() { KateSnippetGlobal::self()->createSnippet(m_mainWindow->activeView()); } #include "katesnippets.moc" diff --git a/addons/snippets/katesnippetsplugin.desktop b/addons/snippets/katesnippetsplugin.desktop index 199e48a28..9121c2149 100644 --- a/addons/snippets/katesnippetsplugin.desktop +++ b/addons/snippets/katesnippetsplugin.desktop @@ -1,59 +1,61 @@ [Desktop Entry] Type=Service ServiceTypes=KTextEditor/Plugin,KDevelop/Plugin X-KDE-Library=katesnippetsplugin Name=Snippets Tool View Name[ast]=Vista de ferramientes de snippets Name[ca]=Vista d'eina pels retalls Name[ca@valencia]=Vista d'eina pels retalls Name[cs]=Nástrojový pohled ústřižků Name[de]=Werkzeugansicht für Textbausteine Name[el]=Προβολή εργαλείου τμημάτων κώδικα Name[en_GB]=Snippets Tool View Name[es]=Vista de la herramienta de fragmentos de código Name[eu]=Testu-zatien tresnaren ikuspegia Name[fi]=Leiketyökalunäkymä Name[fr]=Vue des outils de fragments de code Name[gl]=Vista da ferramenta de fragmentos +Name[id]=Tampilan Alat Snippet Name[it]=Vista dello strumento per i frammenti di testo Name[ko]=스니펫 도구 보기 Name[nl]=Weergave van hulpmiddel voor fragmenten Name[nn]=Tekstbitar-verktøy Name[pl]=Widok narzędzia wycinków Name[pt]=Área de Ferramentas de Excertos Name[pt_BR]=Exibição da ferramenta de trechos Name[ru]=Управление фрагментами Name[sk]=Pohľad nástroja úryvkov Name[sv]=Verktygsvy för kodsnuttar Name[tr]=Parçacık Araç Görünümü Name[uk]=Панель фрагментів Name[x-test]=xxSnippets Tool Viewxx Name[zh_CN]=代码片段工具视图 Name[zh_TW]=片段工具檢視 Comment=Manage your code snippets or download new ones Comment[ast]=Xestiona snippets de códigu o baxa otros nuevos Comment[ca]=Gestiona els retalls de codi o en baixa de nous Comment[ca@valencia]=Gestiona els retalls de codi o en baixa de nous Comment[de]=Verwaltung Ihrer Textbausteine oder Herunterladen neuer Textbausteine Comment[el]=Διαχειριστείτε τμήματα κώδικα ή κάετε λήψη νέων Comment[en_GB]=Manage your code snippets or download new ones Comment[es]=Gestione sus fragmentos de código o descargue otros nuevos Comment[eu]=Kudeatu zure kodearen testu-zatiak edo jeitsi berriak Comment[fi]=Hallitse koodileikkeitäsi ja lataa uusia Comment[fr]=Gérez vos fragments de code et téléchargez-en de nouveaux Comment[gl]=Xestione os seus fragmentos de código ou descargue novos +Comment[id]=Kelola kode snippet-mu atau unduh yang barunya Comment[it]=Gestisce i tuoi frammenti di codice o ne scarica dei nuovi Comment[ko]=내 코드 스니펫을 관리하고 새로운 스니펫 다운로드 Comment[nl]=Uw codefragmenten beheren of nieuwe downloaden Comment[nn]=Handsame tekst-/kodebitar og last ned nye Comment[pl]=Zarządzaj swoimi wycinkami kodu lub pobierz nowe Comment[pt]=Faça a gestão dos seus excertos de código ou transfira novos Comment[pt_BR]=Gerencie seus trechos de código ou baixe novos Comment[ru]=Управление фрагментами кода с возможностью их загрузки из интернета Comment[sk]=Spravujte svoje útržky kódu a sťahujte nové Comment[sv]=Hantera kodsnuttar eller ladda ner nya Comment[tr]=Kod parçacıklarını yönet veya yenilerini indir Comment[uk]=Керування фрагментами вашого коду та отримання фрагментів коду інших Comment[x-test]=xxManage your code snippets or download new onesxx Comment[zh_CN]=管理您的代码片段或者下载新的 Comment[zh_TW]=管理您的程式碼片段或下載新片段 diff --git a/addons/snippets/snippetcompletionmodel.cpp b/addons/snippets/snippetcompletionmodel.cpp index 024ff55fb..0851c1de5 100644 --- a/addons/snippets/snippetcompletionmodel.cpp +++ b/addons/snippets/snippetcompletionmodel.cpp @@ -1,188 +1,188 @@ /* This file is part of the Kate project. * Based on the snippet plugin from KDevelop 4. * * Copyright (C) 2008 Andreas Pakulat * Copyright (C) 2012 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "snippetcompletionmodel.h" #include #include #include "snippetstore.h" #include "snippetrepository.h" #include "snippet.h" #include "snippetcompletionitem.h" #include SnippetCompletionModel::SnippetCompletionModel() : KTextEditor::CodeCompletionModel(nullptr) { setHasGroups(false); } SnippetCompletionModel::~SnippetCompletionModel() { qDeleteAll(m_snippets); m_snippets.clear(); } QVariant SnippetCompletionModel::data( const QModelIndex& idx, int role ) const { if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) return 11000; // grouping of snippets if (!idx.parent().isValid()) { if (role == Qt::DisplayRole) { return i18n("Snippets"); } if (role == KTextEditor::CodeCompletionModel::GroupRole) { return Qt::DisplayRole; } return QVariant(); } // snippets if( !idx.isValid() || idx.row() < 0 || idx.row() >= m_snippets.count() ) { return QVariant(); } else { return m_snippets.at( idx.row() )->data(idx, role, nullptr); } } void SnippetCompletionModel::executeCompletionItem (KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const { if ( index.parent().isValid() ) { m_snippets[index.row()]->execute(view, word); } } void SnippetCompletionModel::completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType invocationType) { Q_UNUSED( range ); Q_UNUSED( invocationType ); initData(view); } void SnippetCompletionModel::initData(KTextEditor::View* view) { QString mode = view->document()->highlightingModeAt(view->cursorPosition()); if ( mode.isEmpty() ) { mode = view->document()->highlightingMode(); } beginResetModel(); qDeleteAll(m_snippets); m_snippets.clear(); SnippetStore* store = SnippetStore::self(); for(int i = 0; i < store->rowCount(); i++ ) { if ( store->item(i, 0)->checkState() != Qt::Checked ) { continue; } SnippetRepository* repo = dynamic_cast( store->item( i, 0 ) ); if( repo && (repo->fileTypes().isEmpty() || repo->fileTypes().contains(mode)) ) { for ( int j = 0; j < repo->rowCount(); ++j ) { if ( Snippet* snippet = dynamic_cast(repo->child(j)) ) { m_snippets << new SnippetCompletionItem(snippet, repo); } } } } endResetModel(); } QModelIndex SnippetCompletionModel::parent(const QModelIndex& index) const { if (index.internalId()) { return createIndex(0, 0, quintptr (0)); } else { return QModelIndex(); } } QModelIndex SnippetCompletionModel::index(int row, int column, const QModelIndex& parent) const { if (!parent.isValid()) { if (row == 0) { return createIndex(row, column, quintptr (0)); //header index } else { return QModelIndex(); } } else if (parent.parent().isValid()) { //we only have header and children, no subheaders return QModelIndex(); } if (row < 0 || row >= m_snippets.count() || column < 0 || column >= ColumnCount ) { return QModelIndex(); } return createIndex(row, column, 1); // normal item index } int SnippetCompletionModel::rowCount (const QModelIndex & parent) const { if (!parent.isValid() && !m_snippets.isEmpty()) { return 1; //one toplevel node (group header) } else if(parent.parent().isValid()) { return 0; //we don't have sub children } else { return m_snippets.count(); // only the children } } KTextEditor::Range SnippetCompletionModel::completionRange(KTextEditor::View* view, const KTextEditor::Cursor& position) { const QString& line = view->document()->line(position.line()); KTextEditor::Range range(position, position); // include everything non-space before for ( int i = position.column() - 1; i >= 0; --i ) { if ( line.at(i).isSpace() ) { break; } else { range.setStart(KTextEditor::Cursor (range.start().line(), i)); } } // include everything non-space after for ( int i = position.column() + 1; i < line.length(); ++i ) { if ( line.at(i).isSpace() ) { break; } else { range.setEnd(KTextEditor::Cursor (range.end().line(), i)); } } return range; } bool SnippetCompletionModel::shouldAbortCompletion(KTextEditor::View* view, const KTextEditor::Range& range, const QString& currentCompletion) { if(view->cursorPosition() < range.start() || view->cursorPosition() > range.end()) { return true; //Always abort when the completion-range has been left } - for ( int i = 0; i < currentCompletion.length(); ++i ) { - if ( currentCompletion.at(i).isSpace() ) { + for (const auto token : currentCompletion) { + if ( token.isSpace() ) { return true; } } // else it's valid return false; } diff --git a/addons/snippets/snippetview.cpp b/addons/snippets/snippetview.cpp index 545d46fb4..45ab48680 100644 --- a/addons/snippets/snippetview.cpp +++ b/addons/snippets/snippetview.cpp @@ -1,378 +1,378 @@ /* This file is part of the Kate project. * Based on the snippet plugin from KDevelop 4. * * Copyright (C) 2007 Robert Gruber * Copyright (C) 2010 Milian Wolff * Copyright (C) 2012 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "snippetview.h" #include "snippet.h" #include "katesnippetglobal.h" #include "snippetrepository.h" #include "snippetstore.h" #include "editrepository.h" #include "editsnippet.h" #include #include #include #include #include #include #include #include #include class SnippetFilterModel : public QSortFilterProxyModel { public: SnippetFilterModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) { }; bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override { auto index = sourceModel()->index(sourceRow, 0, sourceParent); auto item = SnippetStore::self()->itemFromIndex(index); if ( ! item ) { return false; } auto snippet = dynamic_cast(item); if ( ! snippet ) { return true; } return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); } }; void SnippetView::setupActionsForWindow(QWidget* widget) { const auto& model = SnippetStore::self(); for ( int i = 0; i < model->rowCount(); i++ ) { auto index = model->index(i, 0, QModelIndex()); auto item = model->itemFromIndex(index); auto repo = dynamic_cast(item); if ( ! repo ) { continue; } for ( int j = 0; j < model->rowCount(index); j++ ) { auto item = model->itemFromIndex(model->index(j, 0, index)); auto snippet = dynamic_cast(item); if ( ! snippet ) { continue; } snippet->registerActionForView(widget); } } } -SnippetView::SnippetView(KateSnippetGlobal* plugin, QWidget* parent) +SnippetView::SnippetView(KateSnippetGlobal* plugin, KTextEditor::MainWindow *mainWindow, QWidget* parent) : QWidget(parent), Ui::SnippetViewBase(), m_plugin(plugin) { Ui::SnippetViewBase::setupUi(this); setWindowTitle(i18n("Snippets")); setWindowIcon(QIcon::fromTheme(QStringLiteral("document-new"), windowIcon())); snippetTree->setContextMenuPolicy( Qt::CustomContextMenu ); snippetTree->viewport()->installEventFilter( this ); connect(snippetTree, &QTreeView::customContextMenuRequested, this, &SnippetView::contextMenu); m_proxy = new SnippetFilterModel(this); m_proxy->setFilterKeyColumn(0); m_proxy->setSourceModel(SnippetStore::self()); connect(filterText, &KLineEdit::textChanged, m_proxy, &QSortFilterProxyModel::setFilterFixedString); snippetTree->setModel(m_proxy); snippetTree->header()->hide(); m_addRepoAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-new")), i18n("Add Repository"), this); connect(m_addRepoAction, &QAction::triggered, this, &SnippetView::slotAddRepo); addAction(m_addRepoAction); m_editRepoAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-txt")), i18n("Edit Repository"), this); connect(m_editRepoAction, &QAction::triggered, this, &SnippetView::slotEditRepo); addAction(m_editRepoAction); m_removeRepoAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Remove Repository"), this); connect(m_removeRepoAction, &QAction::triggered, this, &SnippetView::slotRemoveRepo); addAction(m_removeRepoAction); const bool newStuffAllowed = KAuthorized::authorize(QStringLiteral("ghns")); m_putNewStuffAction = new QAction(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff")), i18n("Publish Repository"), this); m_putNewStuffAction->setVisible(newStuffAllowed); connect(m_putNewStuffAction, &QAction::triggered, this, &SnippetView::slotSnippetToGHNS); addAction(m_putNewStuffAction); QAction* separator = new QAction(this); separator->setSeparator(true); addAction(separator); m_addSnippetAction = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Add Snippet"), this); connect(m_addSnippetAction, &QAction::triggered, this, &SnippetView::slotAddSnippet); addAction(m_addSnippetAction); m_editSnippetAction = new QAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Snippet"), this); connect(m_editSnippetAction, &QAction::triggered, this, &SnippetView::slotEditSnippet); addAction(m_editSnippetAction); m_removeSnippetAction = new QAction(QIcon::fromTheme(QStringLiteral("document-close")), i18n("Remove Snippet"), this); connect(m_removeSnippetAction, &QAction::triggered, this, &SnippetView::slotRemoveSnippet); addAction(m_removeSnippetAction); addAction(separator); m_getNewStuffAction = new QAction(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff")), i18n("Get New Snippets"), this); m_getNewStuffAction->setVisible(newStuffAllowed); connect(m_getNewStuffAction, &QAction::triggered, this, &SnippetView::slotGHNS); addAction(m_getNewStuffAction); connect(snippetTree->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SnippetView::validateActions); validateActions(); - connect(snippetTree->model(), &QAbstractItemModel::rowsInserted, this, [this]() { setupActionsForWindow(this); }); + connect(snippetTree->model(), &QAbstractItemModel::rowsInserted, this, [this, mainWindow]() { setupActionsForWindow(mainWindow->window()); }); m_proxy->setDynamicSortFilter(true); m_proxy->sort(0, Qt::AscendingOrder); } void SnippetView::validateActions() { QStandardItem* item = currentItem(); Snippet* selectedSnippet = dynamic_cast( item ); SnippetRepository* selectedRepo = dynamic_cast( item ); m_addRepoAction->setEnabled(true); m_editRepoAction->setEnabled(selectedRepo); m_removeRepoAction->setEnabled(selectedRepo); m_putNewStuffAction->setEnabled(selectedRepo); m_addSnippetAction->setEnabled(selectedRepo || selectedSnippet); m_editSnippetAction->setEnabled(selectedSnippet); m_removeSnippetAction->setEnabled(selectedSnippet); } QStandardItem* SnippetView::currentItem() { ///TODO: support multiple selected items QModelIndex index = snippetTree->currentIndex(); index = m_proxy->mapToSource(index); return SnippetStore::self()->itemFromIndex( index ); } void SnippetView::slotSnippetClicked (const QModelIndex & index) { QStandardItem* item = SnippetStore::self()->itemFromIndex( m_proxy->mapToSource(index) ); if (!item) return; Snippet* snippet = dynamic_cast( item ); if (!snippet) return; m_plugin->insertSnippet( snippet ); } void SnippetView::contextMenu (const QPoint& pos) { QModelIndex index = snippetTree->indexAt( pos ); index = m_proxy->mapToSource(index); QStandardItem* item = SnippetStore::self()->itemFromIndex( index ); if (!item) { // User clicked into an empty place of the tree QMenu menu(this); menu.addSection(i18n("Snippets")); menu.addAction(m_addRepoAction); menu.addAction(m_getNewStuffAction); menu.exec(snippetTree->mapToGlobal(pos)); } else if (Snippet* snippet = dynamic_cast( item )) { QMenu menu(this); menu.addSection(i18n("Snippet: %1", snippet->text())); menu.addAction(m_editSnippetAction); menu.addAction(m_removeSnippetAction); menu.exec(snippetTree->mapToGlobal(pos)); } else if (SnippetRepository* repo = dynamic_cast( item )) { QMenu menu(this); menu.addSection(i18n("Repository: %1", repo->text())); menu.addAction(m_addSnippetAction); menu.addSeparator(); menu.addAction(m_editRepoAction); menu.addAction(m_removeRepoAction); menu.addAction(m_putNewStuffAction); menu.exec(snippetTree->mapToGlobal(pos)); } } void SnippetView::slotEditSnippet() { QStandardItem* item = currentItem(); if (!item) return; Snippet* snippet = dynamic_cast( item ); if (!snippet) return; SnippetRepository* repo = dynamic_cast( item->parent() ); if (!repo) return; EditSnippet dlg(repo, snippet, this); dlg.exec(); } void SnippetView::slotAddSnippet() { QStandardItem* item = currentItem(); if (!item) return; SnippetRepository* repo = dynamic_cast( item ); if (!repo) { repo = dynamic_cast( item->parent() ); if ( !repo ) return; } EditSnippet dlg(repo, nullptr, this); dlg.exec(); } void SnippetView::slotRemoveSnippet() { QStandardItem* item = currentItem(); if (!item) return; SnippetRepository* repo = dynamic_cast( item->parent() ); if (!repo) return; int ans = KMessageBox::warningContinueCancel( QApplication::activeWindow(), i18n("Do you really want to delete the snippet \"%1\"?", item->text()) ); if ( ans == KMessageBox::Continue ) { item->parent()->removeRow(item->row()); repo->save(); } } void SnippetView::slotAddRepo() { EditRepository dlg(nullptr, this); dlg.exec(); } void SnippetView::slotEditRepo() { QStandardItem* item = currentItem(); if (!item) return; SnippetRepository* repo = dynamic_cast( item ); if (!repo) return; EditRepository dlg(repo, this); dlg.exec(); } void SnippetView::slotRemoveRepo() { QStandardItem* item = currentItem(); if (!item) return; SnippetRepository* repo = dynamic_cast( item ); if (!repo) return; int ans = KMessageBox::warningContinueCancel( QApplication::activeWindow(), i18n("Do you really want to delete the repository \"%1\" with all its snippets?", repo->text()) ); if ( ans == KMessageBox::Continue ) { repo->remove(); } } void SnippetView::slotGHNS() { KNS3::DownloadDialog dialog(QStringLiteral(":/katesnippets/ktexteditor_codesnippets_core.knsrc"), this); dialog.exec(); foreach ( const KNS3::Entry& entry, dialog.changedEntries() ) { foreach ( const QString& path, entry.uninstalledFiles() ) { if ( path.endsWith(QLatin1String(".xml")) ) { if ( SnippetRepository* repo = SnippetStore::self()->repositoryForFile(path) ) { repo->remove(); } } } foreach ( const QString& path, entry.installedFiles() ) { if ( path.endsWith(QLatin1String(".xml")) ) { SnippetStore::self()->appendRow(new SnippetRepository(path)); } } } } void SnippetView::slotSnippetToGHNS() { QStandardItem* item = currentItem(); if ( !item) return; SnippetRepository* repo = dynamic_cast( item ); if ( !repo ) return; KNS3::UploadDialog dialog(QStringLiteral(":/katesnippets/ktexteditor_codesnippets_core.knsrc"), this); dialog.setUploadFile(QUrl::fromLocalFile(repo->file())); dialog.setUploadName(repo->text()); dialog.exec(); } bool SnippetView::eventFilter(QObject* obj, QEvent* e) { // no, listening to activated() is not enough since that would also trigger the edit mode which we _dont_ want here // users may still rename stuff via select + F2 though if (obj == snippetTree->viewport()) { const bool singleClick = style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this); if ( (!singleClick && e->type() == QEvent::MouseButtonDblClick) || (singleClick && e->type() == QEvent::MouseButtonRelease) ) { QMouseEvent* mouseEvent = dynamic_cast(e); Q_ASSERT(mouseEvent); QModelIndex clickedIndex = snippetTree->indexAt(mouseEvent->pos()); if (clickedIndex.isValid() && clickedIndex.parent().isValid()) { slotSnippetClicked(clickedIndex); e->accept(); return true; } } } return QObject::eventFilter(obj, e); } diff --git a/addons/snippets/snippetview.h b/addons/snippets/snippetview.h index 682c8a7bc..adab26c0d 100644 --- a/addons/snippets/snippetview.h +++ b/addons/snippets/snippetview.h @@ -1,122 +1,124 @@ /* This file is part of the Kate project. * Based on the snippet plugin from KDevelop 4. * * Copyright (C) 2007 Robert Gruber * Copyright (C) 2010 Milian Wolff * Copyright (C) 2012 Christoph Cullmann * Copyright (C) 2014 Sven Brauch * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef SNIPPETVIEW_H #define SNIPPETVIEW_H +#include + #include "ui_snippetview.h" class QStandardItem; class KateSnippetGlobal; class QAction; class QSortFilterProxyModel; namespace KTextEditor { } /** * This class gets embedded into the right tool view by the KateSnippetGlobal. * @author Robert Gruber * @author Milian Wolff */ class SnippetView : public QWidget, public Ui::SnippetViewBase { Q_OBJECT public: - explicit SnippetView(KateSnippetGlobal* plugin, QWidget* parent = nullptr); + explicit SnippetView(KateSnippetGlobal* plugin, KTextEditor::MainWindow *mainWindow, QWidget* parent = nullptr); public: void setupActionsForWindow(QWidget* widget); private Q_SLOTS: /** * Opens the "Add Repository" dialog. */ void slotAddRepo(); /** * Opens the "Edit repository" dialog. */ void slotEditRepo(); /** * Removes the selected repository from the disk. */ void slotRemoveRepo(); /** * Insert the selected snippet into the current file */ void slotSnippetClicked(const QModelIndex & index); /** * Open the edit dialog for the selected snippet */ void slotEditSnippet(); /** * Removes the selected snippet from the tree and the filesystem */ void slotRemoveSnippet(); /** * Creates a new snippet and open the edit dialog for it */ void slotAddSnippet(); /** * Slot to get hot new stuff. */ void slotGHNS(); /** * Slot to put the selected snippet to GHNS */ void slotSnippetToGHNS(); void contextMenu (const QPoint & pos); /// disables or enables available actions based on the currently selected item void validateActions(); /// insert snippet on double click bool eventFilter(QObject* , QEvent* ) override; private: QStandardItem* currentItem(); KateSnippetGlobal* m_plugin; QSortFilterProxyModel* m_proxy; QAction *m_addRepoAction; QAction *m_removeRepoAction; QAction *m_editRepoAction; QAction *m_addSnippetAction; QAction *m_removeSnippetAction; QAction *m_editSnippetAction; QAction *m_getNewStuffAction; QAction *m_putNewStuffAction; }; #endif diff --git a/addons/symbolviewer/CMakeLists.txt b/addons/symbolviewer/CMakeLists.txt index 5e874fc08..c6c016f0b 100644 --- a/addons/symbolviewer/CMakeLists.txt +++ b/addons/symbolviewer/CMakeLists.txt @@ -1,15 +1,24 @@ -add_definitions(-DTRANSLATION_DOMAIN=\"katesymbolviewer\") -########### next target ############### -set(katesymbolviewerplugin_PART_SRCS cpp_parser.cpp tcl_parser.cpp fortran_parser.cpp perl_parser.cpp -php_parser.cpp xslt_parser.cpp xml_parser.cpp ruby_parser.cpp python_parser.cpp bash_parser.cpp ecma_parser.cpp plugin_katesymbolviewer.cpp ) +add_library(katesymbolviewerplugin MODULE "") +target_compile_definitions(katesymbolviewerplugin PRIVATE TRANSLATION_DOMAIN="katesymbolviewer") +target_link_libraries(katesymbolviewerplugin PRIVATE KF5::TextEditor) -# resource for ui file and stuff -qt5_add_resources(katesymbolviewerplugin_PART_SRCS plugin.qrc) - -add_library(katesymbolviewerplugin MODULE ${katesymbolviewerplugin_PART_SRCS}) - -kcoreaddons_desktop_to_json (katesymbolviewerplugin katesymbolviewerplugin.desktop) - -target_link_libraries(katesymbolviewerplugin KF5::TextEditor KF5::I18n KF5::IconThemes) +target_sources( + katesymbolviewerplugin + PRIVATE + cpp_parser.cpp + tcl_parser.cpp + fortran_parser.cpp + perl_parser.cpp + php_parser.cpp + xslt_parser.cpp + xml_parser.cpp + ruby_parser.cpp + python_parser.cpp + bash_parser.cpp + ecma_parser.cpp + plugin_katesymbolviewer.cpp + plugin.qrc +) +kcoreaddons_desktop_to_json(katesymbolviewerplugin katesymbolviewerplugin.desktop) install(TARGETS katesymbolviewerplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/symbolviewer/plugin_katesymbolviewer.cpp b/addons/symbolviewer/plugin_katesymbolviewer.cpp index e4ec4634d..420e69eb0 100644 --- a/addons/symbolviewer/plugin_katesymbolviewer.cpp +++ b/addons/symbolviewer/plugin_katesymbolviewer.cpp @@ -1,480 +1,485 @@ /*************************************************************************** * plugin_katesymbolviewer.cpp - description * ------------------- * begin : Apr 2 2003 * author : 2003 Massimo Callegari * email : massimocallegari@yahoo.it * * Changes: * Nov 09 2004 v.1.3 - For changelog please refer to KDE CVS * Nov 05 2004 v.1.2 - Choose parser from the current highlight. Minor i18n changes. * Nov 28 2003 v.1.1 - Structured for multilanguage support * Added preliminary Tcl/Tk parser (thanks Rohit). To be improved. * Various bugfixing. * Jun 19 2003 v.1.0 - Removed QTimer (polling is Evil(tm)... ) * - Captured documentChanged() event to refresh symbol list * - Tooltips vanished into nowhere...sigh :( * May 04 2003 v 0.6 - Symbol List becomes a K3ListView object. Removed Tooltip class. * Added a QTimer that every 200ms checks: * * if the list width has changed * * if the document has changed * Added an entry in the m_popup menu to switch between List and Tree mode * Various bugfixing. * Apr 24 2003 v 0.5 - Added three check buttons in m_popup menu to show/hide symbols * Apr 23 2003 v 0.4 - "View Symbol" moved in Settings menu. "Refresh List" is no * longer in Kate menu. Moved into a m_popup menu activated by a * mouse right button click. + Bugfixing. * Apr 22 2003 v 0.3 - Added macro extraction + several bugfixing * Apr 19 2003 v 0.2 - Added to CVS. Extract functions and structures * Apr 07 2003 v 0.1 - First version. * * Copyright (C) 2014,2018 by Kåre Särs * ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "plugin_katesymbolviewer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON (KatePluginSymbolViewerFactory, "katesymbolviewerplugin.json", registerPlugin();) KatePluginSymbolViewerView::KatePluginSymbolViewerView(KatePluginSymbolViewer *plugin, KTextEditor::MainWindow *mw) :QObject(mw) ,m_mainWindow(mw) ,m_plugin(plugin) { // FIXME KF5 KGlobal::locale()->insertCatalog("katesymbolviewerplugin"); KXMLGUIClient::setComponentName (QStringLiteral("katesymbolviewer"), i18n ("SymbolViewer")); setXMLFile(QStringLiteral("ui.rc")); mw->guiFactory()->addClient (this); m_symbols = nullptr; // FIXME Let the parser decide which options are available and how they are named // because not all these options are supported by all parsers m_popup = new QMenu(m_symbols); m_treeOn = m_popup->addAction(i18n("Tree Mode"), this, &KatePluginSymbolViewerView::displayOptionChanged); m_treeOn->setCheckable(true); m_expandOn = m_popup->addAction(i18n("Expand Tree"), this, &KatePluginSymbolViewerView::displayOptionChanged); m_expandOn->setCheckable(true); m_sort = m_popup->addAction(i18n("Show Sorted"), this, &KatePluginSymbolViewerView::displayOptionChanged); m_sort->setCheckable(true); m_popup->addSeparator(); m_macro = m_popup->addAction(i18n("Show Macros"), this, &KatePluginSymbolViewerView::displayOptionChanged); m_macro->setCheckable(true); m_struct = m_popup->addAction(i18n("Show Structures"), this, &KatePluginSymbolViewerView::displayOptionChanged); m_struct->setCheckable(true); m_func = m_popup->addAction(i18n("Show Functions"), this, &KatePluginSymbolViewerView::displayOptionChanged); m_func->setCheckable(true); m_typesOn = m_popup->addAction(i18n("Show Parameters"), this, &KatePluginSymbolViewerView::displayOptionChanged); m_typesOn->setCheckable(true); KConfigGroup config(KSharedConfig::openConfig(), "PluginSymbolViewer"); m_typesOn->setChecked(config.readEntry(QStringLiteral("ViewTypes"), false)); m_expandOn->setChecked(config.readEntry(QStringLiteral("ExpandTree"), false)); m_treeOn->setChecked(config.readEntry(QStringLiteral("TreeView"), false)); m_sort->setChecked(config.readEntry(QStringLiteral("SortSymbols"), false)); m_macro->setChecked(true); m_struct->setChecked(true); m_func->setChecked(true); m_expandOn->setEnabled(m_treeOn->isChecked()); m_typesOn->setEnabled(m_func->isChecked()); m_updateTimer.setSingleShot(true); connect(&m_updateTimer, &QTimer::timeout, this, &KatePluginSymbolViewerView::parseSymbols); m_currItemTimer.setSingleShot(true); connect(&m_currItemTimer, &QTimer::timeout, this, &KatePluginSymbolViewerView::updateCurrTreeItem); QPixmap cls( ( const char** ) class_xpm ); m_toolview = m_mainWindow->createToolView(plugin, QStringLiteral("kate_plugin_symbolviewer"), KTextEditor::MainWindow::Left, cls, i18n("Symbol List")); QWidget *container = new QWidget(m_toolview); QHBoxLayout *layout = new QHBoxLayout(container); m_symbols = new QTreeWidget(); m_symbols->setFocusPolicy(Qt::NoFocus); m_symbols->setLayoutDirection( Qt::LeftToRight ); layout->addWidget(m_symbols, 10); layout->setContentsMargins(0,0,0,0); connect(m_symbols, &QTreeWidget::itemClicked, this, &KatePluginSymbolViewerView::goToSymbol); connect(m_symbols, &QTreeWidget::customContextMenuRequested, this, &KatePluginSymbolViewerView::slotShowContextMenu); connect(m_symbols, &QTreeWidget::itemExpanded, this, &KatePluginSymbolViewerView::updateCurrTreeItem); connect(m_symbols, &QTreeWidget::itemCollapsed, this, &KatePluginSymbolViewerView::updateCurrTreeItem); connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &KatePluginSymbolViewerView::slotDocChanged); QStringList titles; titles << i18nc("@title:column", "Symbols") << i18nc("@title:column", "Position"); m_symbols->setColumnCount(2); m_symbols->setHeaderLabels(titles); m_symbols->setColumnHidden(1, true); m_symbols->setSortingEnabled(m_sort->isChecked()); m_symbols->setRootIsDecorated(0); m_symbols->setContextMenuPolicy(Qt::CustomContextMenu); m_symbols->setIndentation(10); m_toolview->installEventFilter(this); + + // register view + m_plugin->m_views.insert(this); } KatePluginSymbolViewerView::~KatePluginSymbolViewerView() { + // un-register view + m_plugin->m_views.remove(this); + m_mainWindow->guiFactory()->removeClient (this); delete m_toolview; delete m_popup; } void KatePluginSymbolViewerView::slotDocChanged() { parseSymbols(); KTextEditor::View *view = m_mainWindow->activeView(); //qDebug()<<"Document changed !!!!" << view; if (view) { connect(view, &KTextEditor::View::cursorPositionChanged, this, &KatePluginSymbolViewerView::cursorPositionChanged, Qt::UniqueConnection); if (view->document()) { connect(view->document(), &KTextEditor::Document::textChanged, this, &KatePluginSymbolViewerView::slotDocEdited, Qt::UniqueConnection); } } } void KatePluginSymbolViewerView::slotDocEdited() { m_currItemTimer.stop(); // Avoid unneeded update m_updateTimer.start(500); } void KatePluginSymbolViewerView::cursorPositionChanged() { if (m_updateTimer.isActive()) { // No need for update, will come anyway return; } KTextEditor::View* editView = m_mainWindow->activeView(); if (!editView) { return; } int currLine = editView->cursorPositionVirtual().line(); if (currLine != m_oldCursorLine) { m_oldCursorLine = currLine; m_currItemTimer.start(100); } } void KatePluginSymbolViewerView::updateCurrTreeItem() { if (!m_mainWindow) { return; } KTextEditor::View* editView = m_mainWindow->activeView(); if (!editView) { return; } KTextEditor::Document* doc = editView->document(); if (!doc) { return; } int currLine = editView->cursorPositionVirtual().line(); int newItemLine = 0; QTreeWidgetItem *newItem = nullptr; QTreeWidgetItem *tmp = nullptr; for (int i=0; itopLevelItemCount(); i++) { tmp = newActveItem(newItemLine, currLine, m_symbols->topLevelItem(i)); if (tmp) newItem = tmp; } if (!newItem) { return; } // check if the item has a parent and if that parent is expanded. // if the parent is not expanded, set the parent as current item in stead of // expanding the tree. The tree was probably collapsed on purpose QTreeWidgetItem *parent = newItem->parent(); if (parent && !parent->isExpanded()) { newItem = parent; } m_symbols->blockSignals(true); m_symbols->setCurrentItem(newItem); m_symbols->blockSignals(false); } QTreeWidgetItem *KatePluginSymbolViewerView::newActveItem(int &newItemLine, int currLine, QTreeWidgetItem *item) { QTreeWidgetItem *newItem = nullptr; QTreeWidgetItem *tmp = nullptr; int itemLine = item->data(1, Qt::DisplayRole).toInt(); if ((itemLine <= currLine) && (itemLine >= newItemLine)) { newItemLine = itemLine; newItem = item; } for (int i=0; ichildCount(); i++) { tmp = newActveItem(newItemLine, currLine, item->child(i)); if (tmp) newItem = tmp; } return newItem; } bool KatePluginSymbolViewerView::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *ke = static_cast(event); if ((obj == m_toolview) && (ke->key() == Qt::Key_Escape)) { m_mainWindow->activeView()->setFocus(); event->accept(); return true; } } return QObject::eventFilter(obj, event); } void KatePluginSymbolViewerView::slotShowContextMenu(const QPoint&) { m_popup->popup(QCursor::pos(), m_treeOn); } /** * Each popup menu action is connected to this slot which offer the possibility * to modify the menu depended on current settings. */ void KatePluginSymbolViewerView::displayOptionChanged() { m_expandOn->setEnabled(m_treeOn->isChecked()); m_typesOn->setEnabled(m_func->isChecked()); parseSymbols(); } void KatePluginSymbolViewerView::parseSymbols() { if (!m_symbols) return; m_symbols->clear(); // Qt docu recommends to populate view with disabled sorting // https://doc.qt.io/qt-5/qtreeview.html#sortingEnabled-prop m_symbols->setSortingEnabled(false); Qt::SortOrder sortOrder = m_symbols->header()->sortIndicatorOrder(); if (!m_mainWindow->activeView()) return; KTextEditor::Document *doc = m_mainWindow->activeView()->document(); // be sure we have some document around ! if (!doc) return; /** Get the current highlighting mode */ QString hlModeName = doc->mode(); if (hlModeName.contains(QLatin1String("C++")) || hlModeName == QLatin1String("C") || hlModeName == QLatin1String("ANSI C89")) parseCppSymbols(); else if (hlModeName == QLatin1String("PHP (HTML)")) parsePhpSymbols(); else if (hlModeName == QLatin1String("Tcl/Tk")) parseTclSymbols(); else if (hlModeName == QLatin1String("Fortran")) parseFortranSymbols(); else if (hlModeName == QLatin1String("Perl")) parsePerlSymbols(); else if (hlModeName == QLatin1String("Python")) parsePythonSymbols(); else if (hlModeName == QLatin1String("Ruby")) parseRubySymbols(); else if (hlModeName == QLatin1String("Java")) parseCppSymbols(); else if (hlModeName == QLatin1String("xslt")) parseXsltSymbols(); else if (hlModeName == QLatin1String("XML") || hlModeName == QLatin1String("HTML")) parseXMLSymbols(); else if (hlModeName == QLatin1String("Bash")) parseBashSymbols(); else if (hlModeName == QLatin1String("ActionScript 2.0") || hlModeName == QLatin1String("JavaScript") || hlModeName == QLatin1String("QML")) parseEcmaSymbols(); else { QTreeWidgetItem *node = new QTreeWidgetItem(m_symbols); node->setText(0, i18n("Sorry, not supported yet!")); // Setting invalid line number avoid jump to top of document when clicked node->setText(1, QStringLiteral("-1")); node = new QTreeWidgetItem(m_symbols); node->setText(0, i18n("File type: %1", hlModeName)); node->setText(1, QStringLiteral("-1")); } m_oldCursorLine = -1; updateCurrTreeItem(); if (m_sort->isChecked()) { m_symbols->setSortingEnabled(true); m_symbols->sortItems(0, sortOrder); } } void KatePluginSymbolViewerView::goToSymbol(QTreeWidgetItem *it) { KTextEditor::View *kv = m_mainWindow->activeView(); // be sure we really have a view ! if (!kv) return; //qDebug()<<"Slot Activated at pos: "<indexOfTopLevelItem(it); if (!it || it->text(1).isEmpty()) { return; } kv->setCursorPosition (KTextEditor::Cursor (it->text(1).toInt(nullptr, 10), 0)); } KatePluginSymbolViewer::KatePluginSymbolViewer( QObject* parent, const QList& ) : KTextEditor::Plugin (parent) { //qDebug()<<"KatePluginSymbolViewer"; } KatePluginSymbolViewer::~KatePluginSymbolViewer() { //qDebug()<<"~KatePluginSymbolViewer"; } QObject *KatePluginSymbolViewer::createView (KTextEditor::MainWindow *mainWindow) { - m_view = new KatePluginSymbolViewerView (this, mainWindow); - return m_view; + return new KatePluginSymbolViewerView (this, mainWindow); } KTextEditor::ConfigPage* KatePluginSymbolViewer::configPage(int, QWidget *parent) { KatePluginSymbolViewerConfigPage* p = new KatePluginSymbolViewerConfigPage(this, parent); KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("PluginSymbolViewer")); p->viewReturns->setChecked(config.readEntry(QStringLiteral("ViewTypes"), false)); p->expandTree->setChecked(config.readEntry(QStringLiteral("ExpandTree"), false)); p->treeView->setChecked(config.readEntry(QStringLiteral("TreeView"), false)); p->sortSymbols->setChecked(config.readEntry(QStringLiteral("SortSymbols"), false)); connect(p, &KatePluginSymbolViewerConfigPage::configPageApplyRequest, this, &KatePluginSymbolViewer::applyConfig); return (KTextEditor::ConfigPage*)p; } void KatePluginSymbolViewer::applyConfig(KatePluginSymbolViewerConfigPage* p) { KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("PluginSymbolViewer")); config.writeEntry(QStringLiteral("ViewTypes"), p->viewReturns->isChecked()); config.writeEntry(QStringLiteral("ExpandTree"), p->expandTree->isChecked()); config.writeEntry(QStringLiteral("TreeView"), p->treeView->isChecked()); config.writeEntry(QStringLiteral("SortSymbols"), p->sortSymbols->isChecked()); - if (m_view) { - m_view->m_typesOn->setChecked(p->viewReturns->isChecked()); - m_view->m_expandOn->setChecked(p->expandTree->isChecked()); - m_view->m_treeOn->setChecked(p->treeView->isChecked()); - m_view->m_sort->setChecked(p->sortSymbols->isChecked()); + for (auto view : m_views) { + view->m_typesOn->setChecked(p->viewReturns->isChecked()); + view->m_expandOn->setChecked(p->expandTree->isChecked()); + view->m_treeOn->setChecked(p->treeView->isChecked()); + view->m_sort->setChecked(p->sortSymbols->isChecked()); - m_view->m_expandOn->setEnabled(m_view->m_treeOn->isChecked()); - m_view->m_typesOn->setEnabled(m_view->m_func->isChecked()); + view->m_expandOn->setEnabled(view->m_treeOn->isChecked()); + view->m_typesOn->setEnabled(view->m_func->isChecked()); } } // BEGIN KatePluginSymbolViewerConfigPage KatePluginSymbolViewerConfigPage::KatePluginSymbolViewerConfigPage( QObject* /*parent*/ /*= 0L*/, QWidget *parentWidget /*= 0L*/) : KTextEditor::ConfigPage( parentWidget ) { QVBoxLayout *lo = new QVBoxLayout( this ); //int spacing = KDialog::spacingHint(); //lo->setSpacing( spacing ); viewReturns = new QCheckBox(i18n("Display functions parameters")); expandTree = new QCheckBox(i18n("Automatically expand nodes in tree mode")); treeView = new QCheckBox(i18n("Always display symbols in tree mode")); sortSymbols = new QCheckBox(i18n("Always sort symbols")); - + QGroupBox* parserGBox = new QGroupBox( i18n("Parser Options"), this); QVBoxLayout* top = new QVBoxLayout(parserGBox); top->addWidget(viewReturns); top->addWidget(expandTree); top->addWidget(treeView); top->addWidget(sortSymbols); //QGroupBox* generalGBox = new QGroupBox( i18n("General Options"), this); //QVBoxLayout* genLay = new QVBoxLayout(generalGBox); //genLay->addWidget( ); lo->addWidget( parserGBox ); //lo->addWidget( generalGBox ); lo->addStretch( 1 ); // throw signal changed connect(viewReturns, &QCheckBox::toggled, this, &KatePluginSymbolViewerConfigPage::changed); connect(expandTree, &QCheckBox::toggled, this, &KatePluginSymbolViewerConfigPage::changed); connect(treeView, &QCheckBox::toggled, this, &KatePluginSymbolViewerConfigPage::changed); connect(sortSymbols, &QCheckBox::toggled, this, &KatePluginSymbolViewerConfigPage::changed); } KatePluginSymbolViewerConfigPage::~KatePluginSymbolViewerConfigPage() {} QString KatePluginSymbolViewerConfigPage::name() const { return i18n("Symbol Viewer"); } QString KatePluginSymbolViewerConfigPage::fullName() const { return i18n("Symbol Viewer Configuration Page"); } QIcon KatePluginSymbolViewerConfigPage::icon() const { return QPixmap(( const char** ) class_xpm ); } void KatePluginSymbolViewerConfigPage::apply() { emit configPageApplyRequest( this ); } // END KatePluginSymbolViewerConfigPage #include "plugin_katesymbolviewer.moc" // kate: space-indent on; indent-width 2; replace-tabs on; diff --git a/addons/symbolviewer/plugin_katesymbolviewer.h b/addons/symbolviewer/plugin_katesymbolviewer.h index c496f1f65..b8cd31054 100644 --- a/addons/symbolviewer/plugin_katesymbolviewer.h +++ b/addons/symbolviewer/plugin_katesymbolviewer.h @@ -1,312 +1,316 @@ /*************************************************************************** plugin_katesymbolviewer.h - description ------------------- begin : Apr 2 2003 author : 2003 Massimo Callegari email : massimocallegari@yahoo.it ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef _PLUGIN_KATE_SYMBOLVIEWER_H_ #define _PLUGIN_KATE_SYMBOLVIEWER_H_ #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include /** * Plugin's config page */ class KatePluginSymbolViewerConfigPage : public KTextEditor::ConfigPage { Q_OBJECT friend class KatePluginSymbolViewer; public: explicit KatePluginSymbolViewerConfigPage (QObject* parent = nullptr, QWidget *parentWidget = nullptr); ~KatePluginSymbolViewerConfigPage () override; /** * Reimplemented from KTextEditor::ConfigPage * just emits configPageApplyRequest( this ). */ QString name() const override; QString fullName() const override; QIcon icon() const override; void apply() override; void reset () override { ; } void defaults () override { ; } Q_SIGNALS: /** * Ask the plugin to set initial values */ void configPageApplyRequest( KatePluginSymbolViewerConfigPage* ); /** * Ask the plugin to apply changes */ void configPageInitRequest( KatePluginSymbolViewerConfigPage* ); private: QCheckBox* viewReturns; QCheckBox* expandTree; QCheckBox* treeView; QCheckBox* sortSymbols; }; class KatePluginSymbolViewer; class KatePluginSymbolViewerView : public QObject, public KXMLGUIClient { Q_OBJECT friend class KatePluginSymbolViewer; public: KatePluginSymbolViewerView (KatePluginSymbolViewer *plugin, KTextEditor::MainWindow *mw); ~KatePluginSymbolViewerView () override; public Q_SLOTS: void displayOptionChanged(); void parseSymbols(); void slotDocChanged(); void goToSymbol(QTreeWidgetItem *); void slotShowContextMenu(const QPoint&); void cursorPositionChanged(); QTreeWidgetItem *newActveItem(int &currMinLine, int currLine, QTreeWidgetItem *item); void updateCurrTreeItem(); void slotDocEdited(); protected: bool eventFilter(QObject *obj, QEvent *ev) override; private: KTextEditor::MainWindow *m_mainWindow; KatePluginSymbolViewer *m_plugin; QMenu *m_popup; QWidget *m_toolview; QTreeWidget *m_symbols; QAction *m_treeOn; // FIXME Rename other actions accordingly QAction *m_sort; // m_sortOn etc QAction *m_macro; QAction *m_struct; QAction *m_func; QAction *m_typesOn; QAction *m_expandOn; QTimer m_updateTimer; QTimer m_currItemTimer; int m_oldCursorLine; void updatePixmapScroll(); void parseCppSymbols(void); void parseTclSymbols(void); void parseFortranSymbols(void); void parsePerlSymbols(void); void parsePythonSymbols(void); void parseRubySymbols(void); void parseXsltSymbols(void); void parseXMLSymbols(void); void parsePhpSymbols(void); void parseBashSymbols(void); void parseEcmaSymbols(void); }; class KatePluginSymbolViewer : public KTextEditor::Plugin { + + friend class KatePluginSymbolViewerView; + Q_OBJECT public: explicit KatePluginSymbolViewer(QObject* parent = nullptr, const QList& = QList()); ~KatePluginSymbolViewer() override; QObject *createView (KTextEditor::MainWindow *mainWindow) override; int configPages () const override { return 1; } KTextEditor::ConfigPage *configPage (int number = 0, QWidget *parent = nullptr) override; public Q_SLOTS: void applyConfig( KatePluginSymbolViewerConfigPage* p ); private: - KatePluginSymbolViewerView* m_view = nullptr; + QSet m_views; }; /* XPM */ static const char* const class_xpm[] = { "16 16 10 1", " c None", ". c #000000", "+ c #A4E8FC", "@ c #24D0FC", "# c #001CD0", "$ c #0080E8", "% c #C0FFFF", "& c #00FFFF", "* c #008080", "= c #00C0C0", " .. ", " .++.. ", " .+++@@. ", " .@@@@@#... ", " .$$@@##.%%.. ", " .$$$##.%%%&&. ", " .$$$#.&&&&&*. ", " ...#.==&&**. ", " .++..===***. ", " .+++@@.==**. ", " .@@@@@#..=*. ", " .$$@@##. .. ", " .$$$###. ", " .$$$##. ", " ..$#. ", " .. "}; static const char * const class_int_xpm[] = { "16 16 10 1", " c None", ". c #000000", "+ c #B8B8B8", "@ c #8A8A8A", "# c #212121", "$ c #575757", "% c #CCCCCC", "& c #9A9A9A", "* c #4D4D4D", "= c #747474", " .. ", " .++.. ", " .+++@@. ", " .@@@@@#... ", " .$$@@##.%%.. ", " .$$$##.%%%&&. ", " .$$$#.&&&&&*. ", " ...#.==&&**. ", " .++..===***. ", " .+++@@.==**. ", " .@@@@@#..=*. ", " .$$@@##. .. ", " .$$$###. ", " .$$$##. ", " ..$#. ", " .. "}; static const char* const struct_xpm[] = { "16 16 14 1", " c None", ". c #000000", "+ c #C0FFC0", "@ c #00FF00", "# c #008000", "$ c #00C000", "% c #C0FFFF", "& c #00FFFF", "* c #008080", "= c #00C0C0", "- c #FFFFC0", "; c #FFFF00", "> c #808000", ", c #C0C000", " .. ", " .++.. ", " .+++@@. ", " .@@@@@#... ", " .$$@@##.%%.. ", " .$$$##.%%%&&. ", " .$$$#.&&&&&*. ", " ...#.==&&**. ", " .--..===***. ", " .---;;.==**. ", " .;;;;;>..=*. ", " .,,;;>>. .. ", " .,,,>>>. ", " .,,,>>. ", " ..,>. ", " .. "}; static const char* const macro_xpm[] = { "16 16 14 1", " c None", ". c #000000", "+ c #FF7FE5", "@ c #FF00C7", "# c #7F0066", "$ c #BC0096", "% c #C0FFFF", "& c #00FFFF", "* c #008080", "= c #00C0C0", "- c #D493FF", "; c #A100FF", "> c #470082", ", c #6B00B7", " .. ", " .++.. ", " .+++@@. ", " .@@@@@#... ", " .$$@@##.%%.. ", " .$$$##.%%%&&. ", " .$$$#.&&&&&*. ", " ...#.==&&**. ", " .--..===***. ", " .---;;.==**. ", " .;;;;;>..=*. ", " .,,;;>>. .. ", " .,,,>>>. ", " .,,,>>. ", " ..,>. ", " .. "}; static const char* const method_xpm[] = { "16 16 5 1", " c None", ". c #000000", "+ c #FCFC80", "@ c #E0BC38", "# c #F0DC5C", " ", " ", " ", " .. ", " .++.. ", " .+++++. ", " .+++++@. ", " .. .##++@@. ", " .++..###@@@. ", " .+++++.##@@. ", " .+++++@..#@. ", " .##++@@. .. ", " .###@@@. ", " .###@@. ", " ..#@. ", " .. " }; #endif diff --git a/addons/tabswitcher/CMakeLists.txt b/addons/tabswitcher/CMakeLists.txt index 65734ce81..aa7328b1b 100644 --- a/addons/tabswitcher/CMakeLists.txt +++ b/addons/tabswitcher/CMakeLists.txt @@ -1,32 +1,20 @@ -project(tabswitcherplugin) -add_definitions(-DTRANSLATION_DOMAIN=\"tabswitcherplugin\") +add_library (tabswitcherplugin MODULE "") +target_compile_definitions(tabswitcherplugin PRIVATE TRANSLATION_DOMAIN="tabswitcherplugin") +target_link_libraries(tabswitcherplugin PRIVATE KF5::TextEditor) -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) - -set(tabswitcherplugin_PART_SRCS +target_sources( + tabswitcherplugin + PRIVATE tabswitcher.cpp tabswitcherfilesmodel.cpp tabswitchertreeview.cpp + plugin.qrc ) -# resource for ui file and stuff -qt5_add_resources(tabswitcherplugin_PART_SRCS plugin.qrc) - -add_library (tabswitcherplugin MODULE ${tabswitcherplugin_PART_SRCS}) - -kcoreaddons_desktop_to_json (tabswitcherplugin tabswitcherplugin.desktop) - -target_link_libraries(tabswitcherplugin - KF5::TextEditor - KF5::IconThemes - KF5::I18n - KF5::Service -) - -install(TARGETS tabswitcherplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) +kcoreaddons_desktop_to_json(tabswitcherplugin tabswitcherplugin.desktop) +install(TARGETS tabswitcherplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) -############# unit tests ################ -if (BUILD_TESTING) - add_subdirectory(autotests) - add_subdirectory(tests) +if(BUILD_TESTING) + add_subdirectory(autotests) + add_subdirectory(tests) endif() diff --git a/addons/tabswitcher/autotests/CMakeLists.txt b/addons/tabswitcher/autotests/CMakeLists.txt index 5a406a1d1..e1a953670 100644 --- a/addons/tabswitcher/autotests/CMakeLists.txt +++ b/addons/tabswitcher/autotests/CMakeLists.txt @@ -1,11 +1,22 @@ include(ECMMarkAsTest) -include_directories( - ${CMAKE_CURRENT_SOURCE_DIR}/.. +add_executable(tabswitcher_test "") +target_include_directories(tabswitcher_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + +find_package(Qt5Test QUIET REQUIRED) +target_link_libraries( + tabswitcher_test + PRIVATE + KF5::TextEditor + Qt5::Test +) + +target_sources( + tabswitcher_test + PRIVATE + tabswitchertest.cpp + ../tabswitcherfilesmodel.cpp ) -set(Src tabswitchertest.cpp ../tabswitcherfilesmodel.cpp) -add_executable(tabswitcher_test ${Src}) add_test(NAME plugin-tabswitcher_test COMMAND tabswitcher_test) -target_link_libraries(tabswitcher_test kdeinit_kate Qt5::Test) ecm_mark_as_test(tabswitcher_test) diff --git a/addons/tabswitcher/tabswitcherplugin.desktop b/addons/tabswitcher/tabswitcherplugin.desktop index fab1d3b53..2e6a9badf 100644 --- a/addons/tabswitcher/tabswitcherplugin.desktop +++ b/addons/tabswitcher/tabswitcherplugin.desktop @@ -1,60 +1,62 @@ [Desktop Entry] Type=Service ServiceTypes=KTextEditor/Plugin X-KDE-Library=tabswitcherplugin Name=Document Switcher Name[ast]=Conmutador de documentos Name[ca]=Commutador de documents Name[ca@valencia]=Commutador de documents Name[cs]=Přepínač dokumentů Name[de]=Dokumentumschalter Name[el]=Εναλλαγή εγγράφου Name[en_GB]=Document Switcher Name[es]=Selector de documentos Name[eu]=Dokumentu-aldatzailea Name[fi]=Tiedostovaihtaja Name[fr]=Changeur de document Name[gl]=Alternador de documentos +Name[id]=Pengalih dokumen Name[it]=Scambiatore di documenti Name[ko]=문서 전환기 Name[nl]=Documentenwisselaar Name[nn]=Dokumentbytar Name[pl]=Przełącznik dokumentów Name[pt]=Selector de Documentos Name[pt_BR]=Seletor de documentos Name[ru]=Переключатель документов Name[sk]=Prepínač dokumentov Name[sv]=Dokumentbyte Name[tr]=Belge Değiştirici Name[uk]=Перемикач між документами Name[x-test]=xxDocument Switcherxx Name[zh_CN]=文档切换器 Name[zh_TW]=文件切換器 Comment=Quick document switching with Alt+Tab behavior Comment[ast]=Cambéu rápidu de documentos col comportamientu d'Alt+Tabulador Comment[ca]=Canvia ràpidament de document amb el comportament d'Alt+Tab Comment[ca@valencia]=Canvia ràpidament de document amb el comportament d'Alt+Tab Comment[cs]=Rychlé přepínání dokumentů s chováním jako ALT+Tab Comment[de]=Schnellwechsler für Dokumente mit Alt+Tabulator Comment[el]=Γρήγορη εναλλαγή εγγράφου με Alt+Tab Comment[en_GB]=Quick document switching with Alt+Tab behaviour Comment[es]=Cambio rápido de documento con el comportamiento de Alt+Tab Comment[eu]=Dokumentu arteko aldatze azkarra Alt+Tab jokabidearekin Comment[fi]=Tiedostojen pikavaihtaja Alt+Sarkain-toiminnalla Comment[fr]=Commutateur rapide de documents utilisant le comportement de la combinaison alt + tab Comment[gl]=Comportamento de alternancia rápida entre documentos mediante Alt+Tab. +Comment[id]=Pengalihan dokumen cepat pakai perilaku ALT+Tab Comment[it]=Cambio rapido dei documenti con Alt+Tab Comment[ko]=Alt+Tab과 비슷하게 작동하는 빠른 문서 전환기 Comment[nl]=Snel van document wisselen met Alt+Tab Comment[nn]=Snøgt byte av dokument med «Alt + Tab»-åtferd Comment[pl]=Szybkie przełączanie dokumentów przy użyciu klawiszy Alt+Tab Comment[pt]=Mudança rápida de documentos com o comportamento do Alt+Tab Comment[pt_BR]=Troca rápida de documentos com o comportamento do Alt+Tab Comment[ru]=Переключатель документов с поведением как у переключателя окон по Alt+Tab Comment[sk]=Rýchle prepínanie dokumentov so správaním Alt+Tab Comment[sv]=Snabbt dokumentbyte med Alt+Tabulator beteende Comment[tr]=ALT+Tab ile hızlı belge değitirme Comment[uk]=Поведінка швидкого перемикача між документами за Alt+Tab Comment[x-test]=xxQuick document switching with Alt+Tab behaviorxx Comment[zh_CN]=使用 Alt+Tab 快速切换文档 Comment[zh_TW]=用 Alt+Tab 快速切換文件的行為 diff --git a/addons/tabswitcher/tests/CMakeLists.txt b/addons/tabswitcher/tests/CMakeLists.txt index df88775c9..47d045f62 100644 --- a/addons/tabswitcher/tests/CMakeLists.txt +++ b/addons/tabswitcher/tests/CMakeLists.txt @@ -1,3 +1,13 @@ -# small test for the tabswitches -add_executable(tstestapp tstestapp.cpp ../tabswitcherfilesmodel.cpp) -target_link_libraries(tstestapp KF5::TextEditor) +add_executable(tstestapp "") + +target_link_libraries( + tstestapp + PRIVATE KF5::TextEditor +) + +target_sources( + tstestapp + PRIVATE + tstestapp.cpp + ../tabswitcherfilesmodel.cpp +) diff --git a/addons/textfilter/CMakeLists.txt b/addons/textfilter/CMakeLists.txt index 3d87ba636..ce27cfea3 100644 --- a/addons/textfilter/CMakeLists.txt +++ b/addons/textfilter/CMakeLists.txt @@ -1,26 +1,16 @@ -project(textfilterplugin) -add_definitions(-DTRANSLATION_DOMAIN=\"katetextfilter\") +add_library(textfilterplugin MODULE "") +target_compile_definitions(textfilterplugin PRIVATE TRANSLATION_DOMAIN="katetextfilter") +target_link_libraries(textfilterplugin PRIVATE KF5::TextEditor) -include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) +ki18n_wrap_ui(UI_SOURCES textfilterwidget.ui) +target_sources(textfilterplugin PRIVATE ${UI_SOURCES}) -set(textfilterplugin_PART_SRCS +target_sources( + textfilterplugin + PRIVATE plugin_katetextfilter.cpp + plugin.qrc ) -ki18n_wrap_ui(textfilterplugin_PART_SRCS textfilterwidget.ui) - -# resource for ui file and stuff -qt5_add_resources(textfilterplugin_PART_SRCS plugin.qrc) - -add_library (textfilterplugin MODULE ${textfilterplugin_PART_SRCS}) - -kcoreaddons_desktop_to_json (textfilterplugin textfilterplugin.desktop) - -target_link_libraries(textfilterplugin - KF5::TextEditor - KF5::IconThemes - KF5::I18n - KF5::Service -) - -install(TARGETS textfilterplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) +kcoreaddons_desktop_to_json(textfilterplugin textfilterplugin.desktop) +install(TARGETS textfilterplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/textfilter/textfilterplugin.desktop b/addons/textfilter/textfilterplugin.desktop index cf274fa19..582540230 100644 --- a/addons/textfilter/textfilterplugin.desktop +++ b/addons/textfilter/textfilterplugin.desktop @@ -1,91 +1,92 @@ [Desktop Entry] Type=Service ServiceTypes=KTextEditor/Plugin X-KDE-Library=textfilterplugin Name=Text Filter Name[ar]=مرشّح النّصوص Name[ast]=Peñera de testu Name[bg]=Текстов филтър Name[bs]=Filter teksta Name[ca]=Filtre de text Name[ca@valencia]=Filtre de text Name[cs]=Textový filtr Name[da]=Tekstfilter Name[de]=Textfilter Name[el]=Φίλτρο κειμένου Name[en_GB]=Text Filter Name[es]=Filtro de texto Name[et]=Tekstifilter Name[eu]=Testuen iragazkia Name[fa]=پالایه قرمز Name[fi]=Tekstisuodatin Name[fr]=Filtre de texte Name[ga]=Scagaire Téacs Name[gl]=Filtro de texto Name[he]=מסנן טקסט Name[hu]=Szövegszűrő Name[ia]=Filtro de texto Name[id]=Filter Teks Name[it]=Filtro di testo Name[ja]=テキストフィルタ Name[kk]=Мәтін сүзгісі Name[km]=តម្រង​អត្ថបទ Name[ko]=텍스트 필터 Name[lt]=Teksto filtras Name[lv]=Teksta filtrs Name[mr]=पाठ्य गाळणी Name[nb]=Tekstfilter Name[nds]=Textfilter Name[nl]=Tekstfilter Name[nn]=Tekstfilter Name[pa]=ਟੈਕਸਟ ਫਿਲਟਰ Name[pl]=Filtr tekstu Name[pt]=Filtro de Texto Name[pt_BR]=Filtro de texto Name[ro]=Filtru text Name[ru]=Текстовый фильтр Name[si]=පෙළ පෙරහන Name[sk]=Textový filter Name[sl]=Filter besedila Name[sr]=Филтер текста Name[sr@ijekavian]=Филтер текста Name[sr@ijekavianlatin]=Filter teksta Name[sr@latin]=Filter teksta Name[sv]=Textfilter Name[tg]=Филтри матн Name[tr]=Metin Süzgeci Name[ug]=تېكىست سۈزگۈچ Name[uk]=Фільтр тексту Name[vi]=Bộ lọc văn bản Name[x-test]=xxText Filterxx Name[zh_CN]=文本过滤器 Name[zh_TW]=文字過濾器 Comment=Process text using terminal commands Comment[ast]=Procesa testos usando comandos de la terminal Comment[ca]=Processa el text usant ordres de terminal Comment[ca@valencia]=Processa el text usant ordres de terminal Comment[cs]=Zpracujte text pomocí příkazů v terminálu Comment[de]=Bearbeitung von Text mit Terminal-Befehlen Comment[el]=Επεξεργασία κειμένου με εντολές του τερματικού Comment[en_GB]=Process text using terminal commands Comment[es]=Procesar texto usando órdenes de la terminal Comment[eu]=Prozesatu testua terminaleko komandoak erabiliz Comment[fi]=Käsittele tekstiä päätekomennoin Comment[fr]=Traiter du texte à l'aide des commandes du terminal Comment[gl]=Procesar texto usando ordes do terminal Comment[ia]=Tracta texto usante commandos de terminal +Comment[id]=Teks proses menggunakan perintah terminal Comment[it]=Elabora il testo usando i comandi da terminale Comment[ko]=터미널 명령으로 텍스트 처리 Comment[nl]=Tekst verwerken met commando's op de terminal Comment[nn]=Køyr tekst gjennom terminalkommandoar Comment[pl]=Przetwórz text przy użyciu poleceń terminala Comment[pt]=Processar o texto com comandos do terminal Comment[pt_BR]=Processar texto usando comandos do terminal Comment[ru]=Выполнение операций с текстом вызовами из командной строки Comment[sk]=Spracovať text pomocou príkazov terminálu Comment[sv]=Behandla text med terminalkommandon Comment[tr]=Uçbirim komutlarını kullanarak metni işle Comment[uk]=Обробити текст за допомогою команд термінала Comment[x-test]=xxProcess text using terminal commandsxx Comment[zh_CN]=使用终端命令处理文本 Comment[zh_TW]=使用終端器指令處理文字 diff --git a/addons/xmlcheck/CMakeLists.txt b/addons/xmlcheck/CMakeLists.txt index a193f3b63..04ccba157 100644 --- a/addons/xmlcheck/CMakeLists.txt +++ b/addons/xmlcheck/CMakeLists.txt @@ -1,31 +1,18 @@ -project(katexmlcheckplugin) # what is this? -add_definitions(-DTRANSLATION_DOMAIN=\"katexmlcheck\") - -# Maybe remove these later // I just copied this from xmltools example remove_definitions(-DQT_NO_CAST_TO_ASCII) remove_definitions(-DQT_NO_CAST_FROM_ASCII) remove_definitions(-DQT_NO_URL_CAST_FROM_STRING) remove_definitions(-DQT_NO_CAST_FROM_BYTEARRAY) +add_library(katexmlcheckplugin MODULE "") +target_compile_definitions(katexmlcheckplugin PRIVATE TRANSLATION_DOMAIN="katexmlcheck") +target_link_libraries(katexmlcheckplugin PRIVATE KF5::TextEditor) -set(katexmlcheckplugin_PART_SRCS +target_sources( + katexmlcheckplugin + PRIVATE plugin_katexmlcheck.cpp + plugin.qrc ) -# resource for ui file and stuff -qt5_add_resources(katexmlcheckplugin_PART_SRCS plugin.qrc) - -add_library(katexmlcheckplugin MODULE ${katexmlcheckplugin_PART_SRCS}) -target_link_libraries(katexmlcheckplugin - KF5::TextEditor - KF5::Parts - KF5::IconThemes - KF5::I18n - KF5::Service -) - -# this did not changed -install(TARGETS katexmlcheckplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) - -# It is to generate json -kcoreaddons_desktop_to_json (katexmlcheckplugin katexmlcheck.desktop) +kcoreaddons_desktop_to_json(katexmlcheckplugin katexmlcheck.desktop) +install(TARGETS katexmlcheckplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/xmltools/CMakeLists.txt b/addons/xmltools/CMakeLists.txt index b374b07ee..be4ee19fc 100644 --- a/addons/xmltools/CMakeLists.txt +++ b/addons/xmltools/CMakeLists.txt @@ -1,35 +1,36 @@ -project(katexmltoolsplugin) -add_definitions(-DTRANSLATION_DOMAIN=\"katexmltools\") - -# Maybe remove these later remove_definitions(-DQT_NO_CAST_TO_ASCII) remove_definitions(-DQT_NO_CAST_FROM_ASCII) remove_definitions(-DQT_NO_URL_CAST_FROM_STRING) remove_definitions(-DQT_NO_CAST_FROM_BYTEARRAY) -set(katexmltoolsplugin_PART_SRCS +add_library(katexmltoolsplugin MODULE "") +target_compile_definitions(katexmltoolsplugin PRIVATE TRANSLATION_DOMAIN="katexmltools") +target_link_libraries(katexmltoolsplugin PRIVATE KF5::TextEditor) + +target_sources( + katexmltoolsplugin + PRIVATE pseudo_dtd.cpp plugin_katexmltools.cpp + plugin.qrc ) -# resource for ui file and stuff -qt5_add_resources(katexmltoolsplugin_PART_SRCS plugin.qrc) - -add_library(katexmltoolsplugin MODULE ${katexmltoolsplugin_PART_SRCS}) +kcoreaddons_desktop_to_json(katexmltoolsplugin katexmltools.desktop) +install(TARGETS katexmltoolsplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) -target_link_libraries(katexmltoolsplugin - KF5::TextEditor - KF5::Parts - KF5::IconThemes - KF5::I18n - KF5::Service +install( + FILES + html4-loose.dtd.xml + html4-strict.dtd.xml + kde-docbook.dtd.xml + simplify_dtd.xsl + xhtml1-frameset.dtd.xml + xhtml1-strict.dtd.xml + xhtml1-transitional.dtd.xml + xslt-1.0.dtd.xml + testcases.xml + language.dtd.xml + kpartgui.dtd.xml + kcfg.dtd.xml + DESTINATION ${DATA_INSTALL_DIR}/katexmltools ) - -########### install files ############### -install(TARGETS katexmltoolsplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) -install( FILES html4-loose.dtd.xml html4-strict.dtd.xml kde-docbook.dtd.xml simplify_dtd.xsl - xhtml1-frameset.dtd.xml xhtml1-strict.dtd.xml xhtml1-transitional.dtd.xml xslt-1.0.dtd.xml - testcases.xml language.dtd.xml kpartgui.dtd.xml kcfg.dtd.xml - DESTINATION ${DATA_INSTALL_DIR}/katexmltools ) - -kcoreaddons_desktop_to_json (katexmltoolsplugin katexmltools.desktop) diff --git a/addons/xmltools/plugin_katexmltools.cpp b/addons/xmltools/plugin_katexmltools.cpp index 1cf116cdc..9d91864a1 100644 --- a/addons/xmltools/plugin_katexmltools.cpp +++ b/addons/xmltools/plugin_katexmltools.cpp @@ -1,1114 +1,1113 @@ /*************************************************************************** pluginKatexmltools.cpp List elements, attributes, attribute values and entities allowed by DTD. Needs a DTD in XML format ( as produced by dtdparse ) for most features. copyright : ( C ) 2001-2002 by Daniel Naber email : daniel.naber@t-online.de Copyright (C) 2005 by Anders Lund KDE SC 4 version (C) 2010 Tomas Trnka ***************************************************************************/ /*************************************************************************** This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or ( at your option ) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ***************************************************************************/ /* README: The basic idea is this: certain keyEvents(), namely [<&" ], trigger a completion box. This is intended as a help for editing. There are some cases where the XML spec is not followed, e.g. one can add the same attribute twice to an element. Also see the user documentation. If backspace is pressed after a completion popup was closed, the popup will re-open. This way typos can be corrected and the popup will reappear, which is quite comfortable. FIXME: -( docbook ) : insert space between the quotes, press "de" and return -> only "d" inserted -The "Insert Element" dialog isn't case insensitive, but it should be -See the "fixme"'s in the code TODO: -check for mem leaks -add "Go to opening/parent tag"? -check doctype to get top-level element -can undo behaviour be improved?, e.g. the plugins internal deletions of text don't have to be an extra step -don't offer entities if inside tag but outside attribute value -Support for more than one namespace at the same time ( e.g. XSLT + XSL-FO )? =>This could also be handled in the XSLT DTD fragment, as described in the XSLT 1.0 spec, but then at it will only show you HTML elements! =>So better "Assign meta DTD" and "Add meta DTD", the latter will expand the current meta DTD -Option to insert empty element in form -Show expanded entities with QChar::QChar( int rc ) + unicode font -Don't ignore entities defined in the document's prologue -Only offer 'valid' elements, i.e. don't take the elements as a set but check if the DTD is matched ( order, number of occurrences, ... ) -Maybe only read the meta DTD file once, then store the resulting QMap on disk ( using QDataStream )? We'll then have to compare timeOf_cacheFile <-> timeOf_metaDtd. -Try to use libxml */ #include "plugin_katexmltools.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(PluginKateXMLToolsFactory, "katexmltools.json", registerPlugin();) PluginKateXMLTools::PluginKateXMLTools(QObject *const parent, const QVariantList &) : KTextEditor::Plugin(parent) { } PluginKateXMLTools::~PluginKateXMLTools() { } QObject *PluginKateXMLTools::createView(KTextEditor::MainWindow *mainWindow) { return new PluginKateXMLToolsView(mainWindow); } PluginKateXMLToolsView::PluginKateXMLToolsView(KTextEditor::MainWindow *mainWin) : QObject(mainWin) , KXMLGUIClient() , m_mainWindow(mainWin) , m_model(this) { //qDebug() << "PluginKateXMLTools constructor called"; KXMLGUIClient::setComponentName(QStringLiteral("katexmltools"), i18n("Kate XML Tools")); setXMLFile(QStringLiteral("ui.rc")); QAction *actionInsert = new QAction(i18n("&Insert Element..."), this); connect(actionInsert, &QAction::triggered, &m_model, &PluginKateXMLToolsCompletionModel::slotInsertElement); actionCollection()->addAction(QStringLiteral("xml_tool_insert_element"), actionInsert); actionCollection()->setDefaultShortcut(actionInsert, Qt::CTRL + Qt::Key_Return); QAction *actionClose = new QAction(i18n("&Close Element"), this); connect(actionClose, &QAction::triggered, &m_model, &PluginKateXMLToolsCompletionModel::slotCloseElement); actionCollection()->addAction(QStringLiteral("xml_tool_close_element"), actionClose); actionCollection()->setDefaultShortcut(actionClose, Qt::CTRL + Qt::Key_Less); QAction *actionAssignDTD = new QAction(i18n("Assign Meta &DTD..."), this); connect(actionAssignDTD, &QAction::triggered, &m_model, &PluginKateXMLToolsCompletionModel::getDTD); actionCollection()->addAction(QStringLiteral("xml_tool_assign"), actionAssignDTD); mainWin->guiFactory()->addClient(this); connect(KTextEditor::Editor::instance()->application(), &KTextEditor::Application::documentDeleted, &m_model, &PluginKateXMLToolsCompletionModel::slotDocumentDeleted); } PluginKateXMLToolsView::~PluginKateXMLToolsView() { m_mainWindow->guiFactory()->removeClient(this); //qDebug() << "xml tools destructor 1..."; //TODO: unregister the model } PluginKateXMLToolsCompletionModel::PluginKateXMLToolsCompletionModel(QObject *const parent) : CodeCompletionModel(parent) , m_viewToAssignTo(nullptr) , m_mode(none) , m_correctPos(0) { } PluginKateXMLToolsCompletionModel::~PluginKateXMLToolsCompletionModel() { qDeleteAll(m_dtds); m_dtds.clear(); } void PluginKateXMLToolsCompletionModel::slotDocumentDeleted(KTextEditor::Document *doc) { // Remove the document from m_DTDs, and also delete the PseudoDTD // if it becomes unused. if (m_docDtds.contains(doc)) { qDebug() << "XMLTools:slotDocumentDeleted: documents: " << m_docDtds.count() << ", DTDs: " << m_dtds.count(); PseudoDTD *dtd = m_docDtds.take(doc); if (m_docDtds.key(dtd)) { return; } QHash::iterator it; for (it = m_dtds.begin() ; it != m_dtds.end() ; ++it) { if (it.value() == dtd) { m_dtds.erase(it); delete dtd; return; } } } } void PluginKateXMLToolsCompletionModel::completionInvoked(KTextEditor::View *kv, const KTextEditor::Range &range, const InvocationType invocationType) { Q_UNUSED(range) Q_UNUSED(invocationType) qDebug() << "xml tools completionInvoked"; KTextEditor::Document *doc = kv->document(); if (! m_docDtds[ doc ]) // no meta DTD assigned yet { return; } // debug to test speed: //QTime t; t.start(); beginResetModel(); m_allowed.clear(); // get char on the left of the cursor: KTextEditor::Cursor curpos = kv->cursorPosition(); uint line = curpos.line(), col = curpos.column(); QString lineStr = kv->document()->line(line); QString leftCh = lineStr.mid(col - 1, 1); QString secondLeftCh = lineStr.mid(col - 2, 1); if (leftCh == QLatin1String("&")) { qDebug() << "Getting entities"; m_allowed = m_docDtds[doc]->entities(QString()); m_mode = entities; } else if (leftCh == QLatin1String("<")) { qDebug() << "*outside tag -> get elements"; QString parentElement = getParentElement(*kv, 1); qDebug() << "parent: " << parentElement; m_allowed = m_docDtds[doc]->allowedElements(parentElement); m_mode = elements; } else if (leftCh == QLatin1String("/") && secondLeftCh == QLatin1String("<")) { qDebug() << "*close parent element"; QString parentElement = getParentElement(*kv, 2); if (! parentElement.isEmpty()) { m_mode = closingtag; m_allowed = QStringList(parentElement); } } else if (leftCh == QLatin1String(" ") || (isQuote(leftCh) && secondLeftCh == QLatin1String("="))) { // TODO: check secondLeftChar, too?! then you don't need to trigger // with space and we yet save CPU power QString currentElement = insideTag(*kv); QString currentAttribute; if (! currentElement.isEmpty()) { currentAttribute = insideAttribute(*kv); } qDebug() << "Tag: " << currentElement; qDebug() << "Attr: " << currentAttribute; if (! currentElement.isEmpty() && ! currentAttribute.isEmpty()) { qDebug() << "*inside attribute -> get attribute values"; m_allowed = m_docDtds[doc]->attributeValues(currentElement, currentAttribute); if (m_allowed.count() == 1 && (m_allowed[0] == QLatin1String("CDATA") || m_allowed[0] == QLatin1String("ID") || m_allowed[0] == QLatin1String("IDREF") || m_allowed[0] == QLatin1String("IDREFS") || m_allowed[0] == QLatin1String("ENTITY") || m_allowed[0] == QLatin1String("ENTITIES") || m_allowed[0] == QLatin1String("NMTOKEN") || m_allowed[0] == QLatin1String("NMTOKENS") || m_allowed[0] == QLatin1String("NAME"))) { // these must not be taken literally, e.g. don't insert the string "CDATA" m_allowed.clear(); } else { m_mode = attributevalues; } } else if (! currentElement.isEmpty()) { qDebug() << "*inside tag -> get attributes"; m_allowed = m_docDtds[doc]->allowedAttributes(currentElement); m_mode = attributes; } } //qDebug() << "time elapsed (ms): " << t.elapsed(); qDebug() << "Allowed strings: " << m_allowed.count(); if (m_allowed.count() >= 1 && m_allowed[0] != QLatin1String("__EMPTY")) { m_allowed = sortQStringList(m_allowed); } setRowCount(m_allowed.count()); endResetModel(); } int PluginKateXMLToolsCompletionModel::columnCount(const QModelIndex &) const { return 1; } int PluginKateXMLToolsCompletionModel::rowCount(const QModelIndex &parent) const { if (!m_allowed.isEmpty()) { // Is there smth to complete? if (!parent.isValid()) { // Return the only one group node for root return 1; } if (parent.internalId() == groupNode) { // Return available rows count for group level node return m_allowed.size(); } } return 0; } QModelIndex PluginKateXMLToolsCompletionModel::parent(const QModelIndex &index) const { if (!index.isValid()) { // Is root/invalid index? return QModelIndex(); // Nothing to return... } if (index.internalId() == groupNode) { // Return a root node for group return QModelIndex(); } // Otherwise, this is a leaf level, so return the only group as a parent return createIndex(0, 0, groupNode); } QModelIndex PluginKateXMLToolsCompletionModel::index(const int row, const int column, const QModelIndex &parent) const { if (!parent.isValid()) { // At 'top' level only 'header' present, so nothing else than row 0 can be here... return row == 0 ? createIndex(row, column, groupNode) : QModelIndex(); } if (parent.internalId() == groupNode) { // Is this a group node? if (0 <= row && row < m_allowed.size()) { // Make sure to return only valid indices return createIndex(row, column, (void *)nullptr); // Just return a leaf-level index } } // Leaf node has no children... nothing to return return QModelIndex(); } QVariant PluginKateXMLToolsCompletionModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { // Nothing to do w/ invalid index return QVariant(); } if (index.internalId() == groupNode) { // Return group level node data switch (role) { case KTextEditor::CodeCompletionModel::GroupRole: return QVariant(Qt::DisplayRole); case Qt::DisplayRole: return currentModeToString(); default: break; } return QVariant(); // Nothing to return for other roles } switch (role) { case Qt::DisplayRole: switch (index.column()) { case KTextEditor::CodeCompletionModel::Name: return m_allowed.at(index.row()); default: break; } default: break; } return QVariant(); } bool PluginKateXMLToolsCompletionModel::shouldStartCompletion(KTextEditor::View *view, const QString &insertedText, bool userInsertion, const KTextEditor::Cursor &position) { Q_UNUSED(view) Q_UNUSED(userInsertion) Q_UNUSED(position) const QString triggerChars = QStringLiteral("&application()->activeMainWindow()) { return; } KTextEditor::View *kv = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView(); if (! kv) { qDebug() << "Warning: no KTextEditor::View"; return; } // ### replace this with something more sane // Start where the supplied XML-DTDs are fed by default unless // user changed directory last time: QString defaultDir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("katexmltools")) + "/katexmltools/"; if (m_urlString.isNull()) { m_urlString = defaultDir; } // Guess the meta DTD by looking at the doctype's public identifier. // XML allows comments etc. before the doctype, so look further than // just the first line. // Example syntax: // uint checkMaxLines = 200; QString documentStart = kv->document()->text(KTextEditor::Range(0, 0, checkMaxLines + 1, 0)); QRegExp re(" */ filename = QStringLiteral("xslt-1.0.dtd.xml"); doctype = QStringLiteral("XSLT 1.0"); } else { qDebug() << "No doctype found"; } QUrl url; if (filename.isEmpty()) { // no meta dtd found for this file url = QFileDialog::getOpenFileUrl(KTextEditor::Editor::instance()->application()->activeMainWindow()->window(), i18n("Assign Meta DTD in XML Format"), QUrl::fromLocalFile(m_urlString), QStringLiteral("*.xml")); } else { url.setUrl(defaultDir + filename); KMessageBox::information(nullptr, i18n("The current file has been identified " "as a document of type \"%1\". The meta DTD for this document type " "will now be loaded.", doctype), i18n("Loading XML Meta DTD"), QStringLiteral("DTDAssigned")); } if (url.isEmpty()) { return; } m_urlString = url.url(); // remember directory for next time if (m_dtds[ m_urlString ]) { assignDTD(m_dtds[ m_urlString ], kv); } else { m_dtdString.clear(); m_viewToAssignTo = kv; QGuiApplication::setOverrideCursor(Qt::WaitCursor); KIO::TransferJob *job = KIO::get(url); connect(job, &KIO::TransferJob::result, this, &PluginKateXMLToolsCompletionModel::slotFinished); connect(job, &KIO::TransferJob::data, this, &PluginKateXMLToolsCompletionModel::slotData); } qDebug() << "XMLTools::getDTD: Documents: " << m_docDtds.count() << ", DTDs: " << m_dtds.count(); } void PluginKateXMLToolsCompletionModel::slotFinished(KJob *job) { if (job->error()) { //qDebug() << "XML Plugin error: DTD in XML format (" << filename << " ) could not be loaded"; static_cast(job)->uiDelegate()->showErrorMessage(); } else if (static_cast(job)->isErrorPage()) { // catch failed loading loading via http: KMessageBox::error(nullptr, i18n("The file '%1' could not be opened. " "The server returned an error.", m_urlString), i18n("XML Plugin Error")); } else { PseudoDTD *dtd = new PseudoDTD(); dtd->analyzeDTD(m_urlString, m_dtdString); m_dtds.insert(m_urlString, dtd); assignDTD(dtd, m_viewToAssignTo); // clean up a bit m_viewToAssignTo = nullptr; m_dtdString.clear(); } QGuiApplication::restoreOverrideCursor(); } void PluginKateXMLToolsCompletionModel::slotData(KIO::Job *, const QByteArray &data) { m_dtdString += QString(data); } void PluginKateXMLToolsCompletionModel::assignDTD(PseudoDTD *dtd, KTextEditor::View *view) { m_docDtds.insert(view->document(), dtd); //TODO:perhaps foreach views()? KTextEditor::CodeCompletionInterface *cci = qobject_cast(view); if (cci) { cci->registerCompletionModel(this); cci->setAutomaticInvocationEnabled(true); qDebug() << "PluginKateXMLToolsView: completion model registered"; } else { qWarning() << "PluginKateXMLToolsView: completion interface unavailable"; } } /** * Offer a line edit with completion for possible elements at cursor position and insert the * tag one chosen/entered by the user, plus its closing tag. If there's a text selection, * add the markup around it. */ void PluginKateXMLToolsCompletionModel::slotInsertElement() { if (!KTextEditor::Editor::instance()->application()->activeMainWindow()) { return; } KTextEditor::View *kv = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView(); if (! kv) { qDebug() << "Warning: no KTextEditor::View"; return; } KTextEditor::Document *doc = kv->document(); PseudoDTD *dtd = m_docDtds[doc]; QString parentElement = getParentElement(*kv, 0); QStringList allowed; if (dtd) { allowed = dtd->allowedElements(parentElement); } QString text; InsertElement dialog(allowed, kv); if (dialog.exec() == QDialog::Accepted) { text = dialog.text(); } if (!text.isEmpty()) { QStringList list = text.split(QChar(' ')); QString pre; QString post; // anders: use if the tag is required to be empty. // In that case maybe we should not remove the selection? or overwrite it? int adjust = 0; // how much to move cursor. // if we know that we have attributes, it goes // just after the tag name, otherwise between tags. if (dtd && dtd->allowedAttributes(list[0]).count()) { adjust++; // the ">" } if (dtd && dtd->allowedElements(list[0]).contains(QStringLiteral("__EMPTY"))) { pre = '<' + text + "/>"; if (adjust) { adjust++; // for the "/" } } else { pre = '<' + text + '>'; post = "'; } QString marked; if (! post.isEmpty()) { marked = kv->selectionText(); } KTextEditor::Document::EditingTransaction transaction(doc); if (! marked.isEmpty()) { kv->removeSelectionText(); } // with the old selection now removed, curPos points to the start of pre KTextEditor::Cursor curPos = kv->cursorPosition(); curPos.setColumn(curPos.column() + pre.length() - adjust); kv->insertText(pre + marked + post); kv->setCursorPosition(curPos); } } /** * Insert a closing tag for the nearest not-closed parent element. */ void PluginKateXMLToolsCompletionModel::slotCloseElement() { if (!KTextEditor::Editor::instance()->application()->activeMainWindow()) { return; } KTextEditor::View *kv = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView(); if (! kv) { qDebug() << "Warning: no KTextEditor::View"; return; } QString parentElement = getParentElement(*kv, 0); //qDebug() << "parentElement: '" << parentElement << "'"; QString closeTag = "'; if (! parentElement.isEmpty()) { kv->insertText(closeTag); } } // modify the completion string before it gets inserted void PluginKateXMLToolsCompletionModel::executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const { KTextEditor::Range toReplace = word; KTextEditor::Document *document = view->document(); QString text = data(index.sibling(index.row(), Name), Qt::DisplayRole).toString(); qDebug() << "executeCompletionItem text: " << text; int line, col; view->cursorPosition().position(line, col); QString lineStr = document->line(line); QString leftCh = lineStr.mid(col - 1, 1); QString rightCh = lineStr.mid(col, 1); int posCorrection = 0; // where to move the cursor after completion ( >0 = move right ) if (m_mode == entities) { text = text + ';'; } else if (m_mode == attributes) { text = text + "=\"\""; posCorrection = -1; if (!rightCh.isEmpty() && rightCh != QLatin1String(">") && rightCh != QLatin1String("/") && rightCh != QLatin1String(" ")) { // TODO: other whitespaces // add space in front of the next attribute text = text + ' '; posCorrection--; } } else if (m_mode == attributevalues) { // TODO: support more than one line uint startAttValue = 0; uint endAttValue = 0; // find left quote: for (startAttValue = col; startAttValue > 0; startAttValue--) { QString ch = lineStr.mid(startAttValue - 1, 1); if (isQuote(ch)) { break; } } // find right quote: for (endAttValue = col; endAttValue <= (uint) lineStr.length(); endAttValue++) { QString ch = lineStr.mid(endAttValue - 1, 1); if (isQuote(ch)) { break; } } // replace the current contents of the attribute if (startAttValue < endAttValue) { toReplace = KTextEditor::Range(line, startAttValue, line, endAttValue - 1); } } else if (m_mode == elements) { // anders: if the tag is marked EMPTY, insert in form QString str; bool isEmptyTag = m_docDtds[document]->allowedElements(text).contains(QStringLiteral("__EMPTY")); if (isEmptyTag) { str = text + "/>"; } else { str = text + ">'; } // Place the cursor where it is most likely wanted: // always inside the tag if the tag is empty AND the DTD indicates that there are attribs) // outside for open tags, UNLESS there are mandatory attributes if (m_docDtds[document]->requiredAttributes(text).count() || (isEmptyTag && m_docDtds[document]->allowedAttributes(text).count())) { posCorrection = text.length() - str.length(); } else if (! isEmptyTag) { posCorrection = text.length() - str.length() + 1; } text = str; } else if (m_mode == closingtag) { text += '>'; } document->replaceText(toReplace, text); // move the cursor to desired position KTextEditor::Cursor curPos = view->cursorPosition(); curPos.setColumn(curPos.column() + posCorrection); view->setCursorPosition(curPos); } // ======================================================================== // Pseudo-XML stuff: /** * Check if cursor is inside a tag, that is * if "<" occurs before ">" occurs ( on the left side of the cursor ). * Return the tag name, return "" if we cursor is outside a tag. */ QString PluginKateXMLToolsCompletionModel::insideTag(KTextEditor::View &kv) { int line, col; kv.cursorPosition().position(line, col); int y = line; // another variable because uint <-> int do { QString lineStr = kv.document()->line(y); for (uint x = col; x > 0; x--) { QString ch = lineStr.mid(x - 1, 1); if (ch == QLatin1String(">")) { // cursor is outside tag return QString(); } if (ch == QLatin1String("<")) { QString tag; // look for white space on the right to get the tag name for (int z = x; z <= lineStr.length() ; ++z) { ch = lineStr.mid(z - 1, 1); if (ch.at(0).isSpace() || ch == QLatin1String("/") || ch == QLatin1String(">")) { return tag.right(tag.length() - 1); } if (z == lineStr.length()) { tag += ch; return tag.right(tag.length() - 1); } tag += ch; } } } y--; col = kv.document()->line(y).length(); } while (y >= 0); return QString(); } /** * Check if cursor is inside an attribute value, that is * if '="' is on the left, and if it's nearer than "<" or ">". * * @Return the attribute name or "" if we're outside an attribute * value. * * Note: only call when insideTag() == true. * TODO: allow whitespace around "=" */ QString PluginKateXMLToolsCompletionModel::insideAttribute(KTextEditor::View &kv) { int line, col; kv.cursorPosition().position(line, col); int y = line; // another variable because uint <-> int uint x = 0; QString lineStr; QString ch; do { lineStr = kv.document()->line(y); for (x = col; x > 0; x--) { ch = lineStr.mid(x - 1, 1); QString chLeft = lineStr.mid(x - 2, 1); // TODO: allow whitespace if (isQuote(ch) && chLeft == QLatin1String("=")) { break; } else if (isQuote(ch) && chLeft != QLatin1String("=")) { return QString(); } else if (ch == QLatin1String("<") || ch == QLatin1String(">")) { return QString(); } } y--; col = kv.document()->line(y).length(); } while (!isQuote(ch)); // look for next white space on the left to get the tag name QString attr; for (int z = x; z >= 0; z--) { ch = lineStr.mid(z - 1, 1); if (ch.at(0).isSpace()) { break; } if (z == 0) { // start of line == whitespace attr += ch; break; } attr = ch + attr; } return attr.left(attr.length() - 2); } /** * Find the parent element for the current cursor position. That is, * go left and find the first opening element that's not closed yet, * ignoring empty elements. * Examples: If cursor is at "X", the correct parent element is "p": *

foo bar X *

foo bar X *

foo bar X */ QString PluginKateXMLToolsCompletionModel::getParentElement(KTextEditor::View &kv, int skipCharacters) { enum { parsingText, parsingElement, parsingElementBoundary, parsingNonElement, parsingAttributeDquote, parsingAttributeSquote, parsingIgnore } parseState; parseState = (skipCharacters > 0) ? parsingIgnore : parsingText; int nestingLevel = 0; int line, col; kv.cursorPosition().position(line, col); QString str = kv.document()->line(line); while (true) { // move left a character if (!col--) { do { if (!line--) { return QString(); // reached start of document } str = kv.document()->line(line); col = str.length(); } while (!col); --col; } ushort ch = str.at(col).unicode(); switch (parseState) { case parsingIgnore: // ignore the specified number of characters parseState = (--skipCharacters > 0) ? parsingIgnore : parsingText; break; case parsingText: switch (ch) { case '<': // hmm... we were actually inside an element return QString(); case '>': // we just hit an element boundary parseState = parsingElementBoundary; break; } break; case parsingElement: switch (ch) { case '"': // attribute ( double quoted ) parseState = parsingAttributeDquote; break; case '\'': // attribute ( single quoted ) parseState = parsingAttributeSquote; break; case '/': // close tag parseState = parsingNonElement; ++nestingLevel; break; case '<': // we just hit the start of the element... if (nestingLevel--) { break; } QString tag = str.mid(col + 1); for (uint pos = 0, len = tag.length(); pos < len; ++pos) { ch = tag.at(pos).unicode(); if (ch == ' ' || ch == '\t' || ch == '>') { tag.truncate(pos); break; } } return tag; } break; case parsingElementBoundary: switch (ch) { case '?': // processing instruction case '-': // comment case '/': // empty element parseState = parsingNonElement; break; case '"': parseState = parsingAttributeDquote; break; case '\'': parseState = parsingAttributeSquote; break; case '<': // empty tag ( bad XML ) parseState = parsingText; break; default: parseState = parsingElement; } break; case parsingAttributeDquote: if (ch == '"') { parseState = parsingElement; } break; case parsingAttributeSquote: if (ch == '\'') { parseState = parsingElement; } break; case parsingNonElement: if (ch == '<') { parseState = parsingText; } break; } } } /** * Return true if the tag is neither a closing tag * nor an empty tag, nor a comment, nor processing instruction. */ bool PluginKateXMLToolsCompletionModel::isOpeningTag(const QString &tag) { return (!isClosingTag(tag) && !isEmptyTag(tag) && !tag.startsWith(QLatin1String("")); } /** * Return true if ch is a single or double quote. Expects ch to be of length 1. */ bool PluginKateXMLToolsCompletionModel::isQuote(const QString &ch) { return (ch == QLatin1String("\"") || ch == QLatin1String("'")); } // ======================================================================== // Tools: /// Get string describing current mode QString PluginKateXMLToolsCompletionModel::currentModeToString() const { switch (m_mode) { case entities: return i18n("XML entities"); case attributevalues: return i18n("XML attribute values"); case attributes: return i18n("XML attributes"); case elements: case closingtag: return i18n("XML elements"); default: break; } return QString(); } /** Sort a QStringList case-insensitively. Static. TODO: make it more simple. */ QStringList PluginKateXMLToolsCompletionModel::sortQStringList(QStringList list) { // Sort list case-insensitive. This looks complicated but using a QMap // is even suggested by the Qt documentation. QMap mapList; - for (QStringList::Iterator it = list.begin(); it != list.end(); ++it) { - QString str = *it; + for (const auto& str : qAsConst(list)) { if (mapList.contains(str.toLower())) { // do not override a previous value, e.g. "Auml" and "auml" are two different // entities, but they should be sorted next to each other. // TODO: currently it's undefined if e.g. "A" or "a" comes first, it depends on // the meta DTD ( really? it seems to work okay?!? ) mapList[str.toLower() + '_'] = str; } else { mapList[str.toLower()] = str; } } list.clear(); QMap::Iterator it; // Qt doc: "the items are alphabetically sorted [by key] when iterating over the map": for (it = mapList.begin(); it != mapList.end(); ++it) { list.append(it.value()); } return list; } //BEGIN InsertElement dialog InsertElement::InsertElement(const QStringList & completions, QWidget * parent) : QDialog(parent) { setWindowTitle(i18n("Insert XML Element")); QVBoxLayout *topLayout = new QVBoxLayout(this); // label QString text = i18n("Enter XML tag name and attributes (\"<\", \">\" and closing tag will be supplied):"); QLabel *label = new QLabel(text, this); label->setWordWrap(true); // combo box m_cmbElements = new KHistoryComboBox(this); static_cast(m_cmbElements)->setHistoryItems(completions, true); connect(m_cmbElements->lineEdit(), &QLineEdit::textChanged, this, &InsertElement::slotHistoryTextChanged); // button box QDialogButtonBox * box = new QDialogButtonBox(this); box->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); m_okButton = box->button(QDialogButtonBox::Ok); m_okButton->setDefault(true); connect(box, &QDialogButtonBox::accepted, this, &InsertElement::accept); connect(box, &QDialogButtonBox::rejected, this, &InsertElement::reject); // fill layout topLayout->addWidget(label); topLayout->addWidget(m_cmbElements); topLayout->addWidget(box); m_cmbElements->setFocus(); // make sure the ok button is enabled/disabled correctly slotHistoryTextChanged(m_cmbElements->lineEdit()->text()); } InsertElement::~InsertElement() { } void InsertElement::slotHistoryTextChanged(const QString &text) { m_okButton->setEnabled(!text.isEmpty()); } QString InsertElement::text() const { return m_cmbElements->currentText(); } //END InsertElement dialog #include "plugin_katexmltools.moc" // kate: space-indent on; indent-width 4; replace-tabs on; mixed-indent off; diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index dc18327b0..d1ffed20f 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -1,4 +1,9 @@ -# docs -ecm_optional_add_subdirectory( kate ) -ecm_optional_add_subdirectory( katepart ) -ecm_optional_add_subdirectory( kwrite ) +find_package(KF5DocTools QUIET) + +if(NOT KF5DocTools_FOUND) + return() +endif() + +ecm_optional_add_subdirectory(kate) +ecm_optional_add_subdirectory(katepart) +ecm_optional_add_subdirectory(kwrite) diff --git a/doc/kate/CMakeLists.txt b/doc/kate/CMakeLists.txt index 9ba3c4b8f..ecb940f51 100644 --- a/doc/kate/CMakeLists.txt +++ b/doc/kate/CMakeLists.txt @@ -1,4 +1,10 @@ -########### install files ############### +kdoctools_create_handbook( + index.docbook + INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en + SUBDIR kate +) -kdoctools_create_handbook (index.docbook INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en SUBDIR kate) -kdoctools_create_manpage (man-kate.1.docbook 1 INSTALL_DESTINATION ${MAN_INSTALL_DIR}) +kdoctools_create_manpage( + man-kate.1.docbook 1 + INSTALL_DESTINATION ${MAN_INSTALL_DIR} +) diff --git a/doc/katepart/CMakeLists.txt b/doc/katepart/CMakeLists.txt index 6b7fc4323..77c4f6d55 100644 --- a/doc/katepart/CMakeLists.txt +++ b/doc/katepart/CMakeLists.txt @@ -1,4 +1,5 @@ -########### install files ############### -# -# -kdoctools_create_handbook (index.docbook INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en SUBDIR katepart) +kdoctools_create_handbook( + index.docbook + INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en + SUBDIR katepart +) diff --git a/doc/katepart/development.docbook b/doc/katepart/development.docbook index 763116ec3..93e5046e9 100644 --- a/doc/katepart/development.docbook +++ b/doc/katepart/development.docbook @@ -1,2864 +1,2878 @@ &TC.Hollingsworth; &TC.Hollingsworth.mail; Extending &katepart; Introduction Like any advanced text editor component, &katepart; offers a variety of ways to extend its functionality. You can write simple scripts to add functionality with JavaScript. Finally, once you have extended &katepart;, you are welcome to join us and share your enhancements with the world! Working with Syntax Highlighting Overview Syntax Highlighting is what makes the editor automatically display text in different styles/colors, depending on the function of the string in relation to the purpose of the file. In program source code for example, control statements may be rendered bold, while data types and comments get different colors from the rest of the text. This greatly enhances the readability of the text, and thus helps the author to be more efficient and productive. A C++ function, rendered with syntax highlighting. A C++ function, rendered with syntax highlighting. The same C++ function, without highlighting. The same C++ function, without highlighting. Of the two examples, which is easiest to read? &kappname; comes with a flexible, configurable and capable system for doing syntax highlighting, and the standard distribution provides definitions for a wide range of programming, scripting and markup languages and other text file formats. In addition you can provide your own definitions in simple &XML; files. &kappname; will automatically detect the right syntax rules when you open a file, based on the &MIME; Type of the file, determined by its extension, or, if it has none, the contents. Should you experience a bad choice, you can manually set the syntax to use from the ToolsHighlighting menu. The styles and colors used by each syntax highlight definition can be configured using the Highlighting Text Styles tab of the Config Dialog, while the &MIME; Types and file extensions it should be used for are handled by the Modes & Filetypes tab. Syntax highlighting is there to enhance the readability of correct text, but you cannot trust it to validate your text. Marking text for syntax is difficult depending on the format you are using, and in some cases the authors of the syntax rules will be proud if 98% of text gets correctly rendered, though most often you need a rare style to see the incorrect 2%. The &kappname; Syntax Highlight System This section will discuss the &kappname; syntax highlighting mechanism in more detail. It is for you if you want to know about it, or if you want to change or create syntax definitions. How it Works Whenever you open a file, one of the first things the &kappname; editor does is detect which syntax definition to use for the file. While reading the text of the file, and while you type away in it, the syntax highlighting system will analyze the text using the rules defined by the syntax definition and mark in it where different contexts and styles begin and end. When you type in the document, the new text is analyzed and marked on the fly, so that if you delete a character that is marked as the beginning or end of a context, the style of surrounding text changes accordingly. The syntax definitions used by the &kappname; Syntax Highlighting System are &XML; files, containing Rules for detecting the role of text, organized into context blocks Keyword lists Style Item definitions When analyzing the text, the detection rules are evaluated in the order in which they are defined, and if the beginning of the current string matches a rule, the related context is used. The start point in the text is moved to the final point at which that rule matched and a new loop of the rules begins, starting in the context set by the matched rule. Rules The detection rules are the heart of the highlighting detection system. A rule is a string, character or regular expression against which to match the text being analyzed. It contains information about which style to use for the matching part of the text. It may switch the working context of the system either to an explicitly mentioned context or to the previous context used by the text. Rules are organized in context groups. A context group is used for main text concepts within the format, for example quoted text strings or comment blocks in program source code. This ensures that the highlighting system does not need to loop through all rules when it is not necessary, and that some character sequences in the text can be treated differently depending on the current context. Contexts may be generated dynamically to allow the usage of instance specific data in rules. Context Styles and Keywords In some programming languages, integer numbers are treated differently from floating point ones by the compiler (the program that converts the source code to a binary executable), and there may be characters having a special meaning within a quoted string. In such cases, it makes sense to render them differently from the surroundings so that they are easy to identify while reading the text. So even if they do not represent special contexts, they may be seen as such by the syntax highlighting system, so that they can be marked for different rendering. A syntax definition may contain as many styles as required to cover the concepts of the format it is used for. In many formats, there are lists of words that represent a specific concept. For example, in programming languages, control statements are one concept, data type names another, and built in functions of the language a third. The &kappname; Syntax Highlighting System can use such lists to detect and mark words in the text to emphasize concepts of the text formats. Default Styles If you open a C++ source file, a &Java; source file and an HTML document in &kappname;, you will see that even though the formats are different, and thus different words are chosen for special treatment, the colors used are the same. This is because &kappname; has a predefined list of Default Styles which are employed by the individual syntax definitions. This makes it easy to recognize similar concepts in different text formats. For example, comments are present in almost any programming, scripting or markup language, and when they are rendered using the same style in all languages, you do not have to stop and think to identify them within the text. All styles in a syntax definition use one of the default styles. A few syntax definitions use more styles than there are defaults, so if you use a format often, it may be worth launching the configuration dialog to see if some concepts use the same style. For example, there is only one default style for strings, but as the Perl programming language operates with two types of strings, you can enhance the highlighting by configuring those to be slightly different. All available default styles will be explained later. The Highlight Definition &XML; Format Overview &kappname; uses the Syntax-Highlighting framework from &kde-frameworks;. The default highlighting xml files shipped with &kappname; are compiled into the Syntax-Highlighting library by default. This section is an overview of the Highlight Definition &XML; format. Based on a small example it will describe the main components and their meaning and usage. The next section will go into detail with the highlight detection rules. The formal definition, also known as the XSD you find in Syntax Highlighting repository in the file language.xsd Custom .xml highlight definition files are located in org.kde.syntax-highlighting/syntax/ in your user folder found with qtpaths which usually is $HOME/.local/share On &Windows; these files are located %USERPROFILE%/AppData/Local/org.kde.syntax-highlighting/syntax. %USERPROFILE% usually expands to C:\\Users\\user. Main sections of &kappname; Highlight Definition files A highlighting file contains a header that sets the XML version: <?xml version="1.0" encoding="UTF-8"?> The root of the definition file is the element language. Available attributes are: Required attributes: name sets the name of the language. It appears in the menus and dialogs afterwards. section specifies the category. extensions defines file extensions, such as "*.cpp;*.h" version specifies the current revision of the definition file in terms of an integer number. Whenever you change a highlighting definition file, make sure to increase this number. kateversion specifies the latest supported &kappname; version. Optional attributes: mimetype associates files &MIME; type. casesensitive defines, whether the keywords are case sensitive or not. priority is necessary if another highlight definition file uses the same extensions. The higher priority will win. author contains the name of the author and his email-address. license contains the license, usually the MIT license for new syntax-highlighting files. style contains the provided language and is used by the indenters for the attribute required-syntax-style. indenter defines which indenter will be used by default. Available indenters are: ada, normal, cstyle, cmake, haskell, latex, lilypond, lisp, lua, pascal, python, replicode, ruby and xml. hidden defines whether the name should appear in &kappname;'s menus. So the next line may look like this: <language name="C++" version="1" kateversion="2.4" section="Sources" extensions="*.cpp;*.h" /> Next comes the highlighting element, which contains the optional element list and the required elements contexts and itemDatas. list elements contain a list of keywords. In this case the keywords are class and const. You can add as many lists as you need. +Since &kde-frameworks; 5.53, a list can include keywords from another +list or language/file, using the include element. +## is used to separate the list name and the language +definition name, in the same way as in the IncludeRules rule. +This is useful to avoid duplicating keyword lists, if you need to include the keywords +of another language/file. For example, the othername list +contains the str keyword and all the keywords of the +types list, which belongs to the ISO C++ +language. The contexts element contains all contexts. The first context is by default the start of the highlighting. There are two rules in the context Normal Text, which match the list of keywords with the name somename and a rule that detects a quote and switches the context to string. To learn more about rules read the next chapter. The third part is the itemDatas element. It contains all color and font styles needed by the contexts and rules. In this example, the itemData Normal Text, String and Keyword are used. <highlighting> <list name="somename"> - <item> class </item> - <item> const </item> + <item>class</item> + <item>const</item> + </list> + <list name="othername"> + <item>str</item> + <include>types##ISO C++</include> </list> <contexts> <context attribute="Normal Text" lineEndContext="#pop" name="Normal Text" > <keyword attribute="Keyword" context="#stay" String="somename" /> + <keyword attribute="Keyword" context="#stay" String="othername" /> <DetectChar attribute="String" context="string" char="&quot;" /> </context> <context attribute="String" lineEndContext="#stay" name="string" > <DetectChar attribute="String" context="#pop" char="&quot;" /> </context> </contexts> <itemDatas> <itemData name="Normal Text" defStyleNum="dsNormal" /> <itemData name="Keyword" defStyleNum="dsKeyword" /> <itemData name="String" defStyleNum="dsString" /> </itemDatas> </highlighting> The last part of a highlight definition is the optional general section. It may contain information about keywords, code folding, comments and indentation. The comment section defines with what string a single line comment is introduced. You also can define a multiline comment using multiLine with the additional attribute end. This is used if the user presses the corresponding shortcut for comment/uncomment. The keywords section defines whether keyword lists are case sensitive or not. Other attributes will be explained later. <general> <comments> <comment name="singleLine" start="#"/> </comments> <keywords casesensitive="1"/> </general> </language> The Sections in Detail This part will describe all available attributes for contexts, itemDatas, keywords, comments, code folding and indentation. The element context belongs in the group contexts. A context itself defines context specific rules such as what should happen if the highlight system reaches the end of a line. Available attributes are: name states the context name. Rules will use this name to specify the context to switch to if the rule matches. lineEndContext defines the context the highlight system switches to if it reaches the end of a line. This may either be a name of another context, #stay to not switch the context (⪚. do nothing) or #pop which will cause it to leave this context. It is possible to use for example #pop#pop#pop to pop three times, or even #pop#pop!OtherContext to pop two times and switch to the context named OtherContext. lineEmptyContext defines the context if an empty line is encountered. Default: #stay. fallthrough defines if the highlight system switches to the context specified in fallthroughContext if no rule matches. Default: false. fallthroughContext specifies the next context if no rule matches. The element itemData is in the group itemDatas. It defines the font style and colors. So it is possible to define your own styles and colors. However, we recommend you stick to the default styles if possible so that the user will always see the same colors used in different languages. Though, sometimes there is no other way and it is necessary to change color and font attributes. The attributes name and defStyleNum are required, the others are optional. Available attributes are: name sets the name of the itemData. Contexts and rules will use this name in their attribute attribute to reference an itemData. defStyleNum defines which default style to use. Available default styles are explained in detail later. color defines a color. Valid formats are '#rrggbb' or '#rgb'. selColor defines the selection color. italic if true, the text will be italic. bold if true, the text will be bold. underline if true, the text will be underlined. strikeout if true, the text will be struck out. spellChecking if true, the text will be spellchecked. The element keywords in the group general defines keyword properties. Available attributes are: casesensitive may be true or false. If true, all keywords are matched case sensitively. weakDeliminator is a list of characters that do not act as word delimiters. For example, the dot '.' is a word delimiter. Assume a keyword in a list contains a dot, it will only match if you specify the dot as a weak delimiter. additionalDeliminator defines additional delimiters. wordWrapDeliminator defines characters after which a line wrap may occur. Default delimiters and word wrap delimiters are the characters .():!+,-<=>%&*/;?[]^{|}~\, space (' ') and tabulator ('\t'). The element comment in the group comments defines comment properties which are used for ToolsComment and ToolsUncomment. Available attributes are: name is either singleLine or multiLine. If you choose multiLine the attributes end and region are required. start defines the string used to start a comment. In C++ this would be "/*". end defines the string used to close a comment. In C++ this would be "*/". region should be the name of the foldable multiline comment. Assume you have beginRegion="Comment" ... endRegion="Comment" in your rules, you should use region="Comment". This way uncomment works even if you do not select all the text of the multiline comment. The cursor only must be in the multiline comment. The element folding in the group general defines code folding properties. Available attributes are: indentationsensitive if true, the code folding markers will be added indentation based, as in the scripting language Python. Usually you do not need to set it, as it defaults to false. Available Default Styles Default Styles were already explained, as a short summary: Default styles are predefined font and color styles. General default styles: dsNormal, when no special highlighting is required. dsKeyword, built-in language keywords. dsFunction, function calls and definitions. dsVariable, if applicable: variable names (e.g. $someVar in PHP/Perl). dsControlFlow, control flow keywords like if, else, switch, break, return, yield, ... dsOperator, operators like + - * / :: < > dsBuiltIn, built-in functions, classes, and objects. dsExtension, common extensions, such as Qt classes and functions/macros in C++ and Python. dsPreprocessor, preprocessor statements or macro definitions. dsAttribute, annotations such as @override and __declspec(...). String-related default styles: dsChar, single characters, such as 'x'. dsSpecialChar, chars with special meaning in strings such as escapes, substitutions, or regex operators. dsString, strings like "hello world". dsVerbatimString, verbatim or raw strings like 'raw \backlash' in Perl, CoffeeScript, and shells, as well as r'\raw' in Python. dsSpecialString, SQL, regexes, HERE docs, LaTeX math mode, ... dsImport, import, include, require of modules. Number-related default styles: dsDataType, built-in data types like int, void, u64. dsDecVal, decimal values. dsBaseN, values with a base other than 10. dsFloat, floating point values. dsConstant, built-in and user defined constants like PI. Comment and documentation-related default styles: dsComment, comments. dsDocumentation, /** Documentation comments */ or """docstrings""". dsAnnotation, documentation commands like @param, @brief. dsCommentVar, the variable names used in above commands, like "foobar" in @param foobar. dsRegionMarker, region markers like //BEGIN, //END in comments. Other default styles: dsInformation, notes and tips like @note in doxygen. dsWarning, warnings like @warning in doxygen. dsAlert, special words like TODO, FIXME, XXXX. dsError, error highlighting and wrong syntax. dsOthers, when nothing else fits. Highlight Detection Rules This section describes the syntax detection rules. Each rule can match zero or more characters at the beginning of the string they are tested against. If the rule matches, the matching characters are assigned the style or attribute defined by the rule, and a rule may ask that the current context is switched. A rule looks like this: <RuleName attribute="(identifier)" context="(identifier)" [rule specific attributes] /> The attribute identifies the style to use for matched characters by name, and the context identifies the context to use from here. The context can be identified by: An identifier, which is the name of the other context. An order telling the engine to stay in the current context (#stay), or to pop back to a previous context used in the string (#pop). To go back more steps, the #pop keyword can be repeated: #pop#pop#pop An order followed by an exclamation mark (!) and an identifier, which will make the engine first follow the order and then switch to the other context, e.g. #pop#pop!OtherContext. Rule specific attributes varies and are described in the following sections. Common attributes All rules have the following attributes in common and are available whenever (common attributes) appears. attribute and context are required attributes, all others are optional. attribute: An attribute maps to a defined itemData. context: Specify the context to which the highlighting system switches if the rule matches. beginRegion: Start a code folding block. Default: unset. endRegion: Close a code folding block. Default: unset. lookAhead: If true, the highlighting system will not process the matches length. Default: false. firstNonSpace: Match only, if the string is the first non-whitespace in the line. Default: false. column: Match only, if the column matches. Default: unset. Dynamic rules Some rules allow the optional attribute dynamic of type boolean that defaults to false. If dynamic is true, a rule can use placeholders representing the text matched by a regular expression rule that switched to the current context in its string or char attributes. In a string, the placeholder %N (where N is a number) will be replaced with the corresponding capture N from the calling regular expression. In a char the placeholder must be a number N and it will be replaced with the first character of the corresponding capture N from the calling regular expression. Whenever a rule allows this attribute it will contain a (dynamic). dynamic: may be (true|false). The Rules in Detail DetectChar Detect a single specific character. Commonly used for example to find the ends of quoted strings. <DetectChar char="(character)" (common attributes) (dynamic) /> The char attribute defines the character to match. Detect2Chars Detect two specific characters in a defined order. <Detect2Chars char="(character)" char1="(character)" (common attributes) /> The char attribute defines the first character to match, char1 the second. AnyChar Detect one character of a set of specified characters. <AnyChar String="(string)" (common attributes) /> The String attribute defines the set of characters. StringDetect Detect an exact string. <StringDetect String="(string)" [insensitive="true|false"] (common attributes) (dynamic) /> The String attribute defines the string to match. The insensitive attribute defaults to false and is passed to the string comparison function. If the value is true insensitive comparing is used. WordDetect Detect an exact string but additionally require word boundaries such as a dot '.' or a whitespace on the beginning and the end of the word. Think of \b<string>\b in terms of a regular expression, but it is faster than the rule RegExpr. <WordDetect String="(string)" [insensitive="true|false"] (common attributes) /> The String attribute defines the string to match. The insensitive attribute defaults to false and is passed to the string comparison function. If the value is true insensitive comparing is used. Since: Kate 3.5 (KDE 4.5) RegExpr Matches against a regular expression. <RegExpr String="(string)" [insensitive="true|false"] [minimal="true|false"] (common attributes) (dynamic) /> The String attribute defines the regular expression. insensitive defaults to false and is passed to the regular expression engine. minimal defaults to false and is passed to the regular expression engine. Because the rules are always matched against the beginning of the current string, a regular expression starting with a caret (^) indicates that the rule should only be matched against the start of a line. See Regular Expressions for more information on those. keyword Detect a keyword from a specified list. <keyword String="(list name)" (common attributes) /> The String attribute identifies the keyword list by name. A list with that name must exist. The highlighting system processes keyword rules in a very optimized way. This makes it an absolute necessity that any keywords to be matched need to be surrounded by defined delimiters, either implied (the default delimiters), or explicitly specified within the additionalDeliminator property of the keywords tag. If a keyword to be matched shall contain a delimiter character, this respective character must be added to the weakDeliminator property of the keywords tag. This character will then loose its delimiter property in all keyword rules. Int Detect an integer number. <Int (common attributes) /> This rule has no specific attributes. Float Detect a floating point number. <Float (common attributes) /> This rule has no specific attributes. HlCOct Detect an octal point number representation. <HlCOct (common attributes) /> This rule has no specific attributes. HlCHex Detect a hexadecimal number representation. <HlCHex (common attributes) /> This rule has no specific attributes. HlCStringChar Detect an escaped character. <HlCStringChar (common attributes) /> This rule has no specific attributes. It matches literal representations of characters commonly used in program code, for example \n (newline) or \t (TAB). The following characters will match if they follow a backslash (\): abefnrtv"'?\. Additionally, escaped hexadecimal numbers such as for example \xff and escaped octal numbers, for example \033 will match. HlCChar Detect an C character. <HlCChar (common attributes) /> This rule has no specific attributes. It matches C characters enclosed in a tick (Example: 'c'). The ticks may be a simple character or an escaped character. See HlCStringChar for matched escaped character sequences. RangeDetect Detect a string with defined start and end characters. <RangeDetect char="(character)" char1="(character)" (common attributes) /> char defines the character starting the range, char1 the character ending the range. Useful to detect for example small quoted strings and the like, but note that since the highlighting engine works on one line at a time, this will not find strings spanning over a line break. LineContinue Matches a specified char at the end of a line. <LineContinue (common attributes) [char="\"] /> char optional character to match, default is backslash ('\'). New since KDE 4.13. This rule is useful for switching context at end of line. This is needed for example in C/C++ to continue macros or strings. IncludeRules Include rules from another context or language/file. <IncludeRules context="contextlink" [includeAttrib="true|false"] /> The context attribute defines which context to include. If it is a simple string it includes all defined rules into the current context, example: <IncludeRules context="anotherContext" /> If the string contains a ## the highlight system will look for a context from another language definition with the given name, for example <IncludeRules context="String##C++" /> would include the context String from the C++ highlighting definition. If includeAttrib attribute is true, change the destination attribute to the one of the source. This is required to make, for example, commenting work, if text matched by the included context is a different highlight from the host context. DetectSpaces Detect whitespaces. <DetectSpaces (common attributes) /> This rule has no specific attributes. Use this rule if you know that there can be several whitespaces ahead, for example in the beginning of indented lines. This rule will skip all whitespace at once, instead of testing multiple rules and skipping one at a time due to no match. DetectIdentifier Detect identifier strings (as a regular expression: [a-zA-Z_][a-zA-Z0-9_]*). <DetectIdentifier (common attributes) /> This rule has no specific attributes. Use this rule to skip a string of word characters at once, rather than testing with multiple rules and skipping one at a time due to no match. Tips & Tricks Once you have understood how the context switching works it will be easy to write highlight definitions. Though you should carefully check what rule you choose in what situation. Regular expressions are very mighty, but they are slow compared to the other rules. So you may consider the following tips. If you only match two characters use Detect2Chars instead of StringDetect. The same applies to DetectChar. Regular expressions are easy to use but often there is another much faster way to achieve the same result. Consider you only want to match the character '#' if it is the first character in the line. A regular expression based solution would look like this: <RegExpr attribute="Macro" context="macro" String="^\s*#" /> You can achieve the same much faster in using: <DetectChar attribute="Macro" context="macro" char="#" firstNonSpace="true" /> If you want to match the regular expression '^#' you can still use DetectChar with the attribute column="0". The attribute column counts characters, so a tabulator is only one character. You can switch contexts without processing characters. Assume that you want to switch context when you meet the string */, but need to process that string in the next context. The below rule will match, and the lookAhead attribute will cause the highlighter to keep the matched string for the next context. <Detect2Chars attribute="Comment" context="#pop" char="*" char1="/" lookAhead="true" /> Use DetectSpaces if you know that many whitespaces occur. Use DetectIdentifier instead of the regular expression '[a-zA-Z_]\w*'. Use default styles whenever you can. This way the user will find a familiar environment. Look into other XML-files to see how other people implement tricky rules. You can validate every XML file by using the command validatehl.sh language.xsd mySyntax.xml. The files validatehl.sh and language.xsd are available in Syntax Highlighting repository. If you repeat complex regular expression very often you can use ENTITIES. Example: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE language SYSTEM "language.dtd" [ <!ENTITY myref "[A-Za-z_:][\w.:_-]*"> ]> Now you can use &myref; instead of the regular expression. Scripting with JavaScript The &kappname; editor component is easily extensible by writing scripts. The scripting language is ECMAScript (widely known as JavaScript). &kappname; supports two kinds of scripts: indentation and command line scripts. Indentation Scripts Indentation scripts - also referred as indenters - automatically indent the source code while typing text. As an example, after hitting the return key the indentation level often increases. The following sections describe step by step how to create the skeleton for a simple indenter. As a first step, create a new *.js file called ⪚ javascript.js in the local home folder $XDG_DATA_HOME/katepart5/script/indentation. Therein, the environment variable XDG_DATA_HOME typically expands to either ~/.local or ~/.local/share. On &Windows; these files are located in %USER%\AppData\Local\katepart5\indentation. %USERPROFILE% usually expands to C:\\Users\\user. The Indentation Script Header The header of the file javascript.js is embedded as JSON at the beginning of the document as follows: var katescript = { "name": "JavaScript", "author": "Example Name <example.name@some.address.org>", "license": "BSD License", "revision": 1, "kate-version": "5.1", "required-syntax-style": "javascript", "indent-languages": ["javascript"], "priority": 0, }; // kate-script-header, must be at the start of the file without comments Each entry is explained in detail now: name [required]: This is the indenter name that appears in the menu ToolsIndentation and in the configuration dialog. author [optional]: The author's name and contact information. license [optional]: Short form of the license, such as BSD License or LGPLv3. revision [required]: The revision of the script. This number should be increased whenever the script is modified. kate-version [required]: Minimum required &kappname; version. required-syntax-style [optional]: The required syntax style, which matches the specified style in syntax highlighting files. This is important for indenters that rely on specific highlight information in the document. If a required syntax style is specified, the indenter is available only when the appropriate highlighter is active. This prevents undefined behavior caused by using the indenter without the expected highlighting schema. For instance, the Ruby indenter makes use of this in the files ruby.js and ruby.xml. indent-languages [optional]: JSON array of syntax styles the indenter can indent correctly, ⪚: ["c++", "java"]. priority [optional]: If several indenters are suited for a certain highlighted file, the priority decides which indenter is chosen as default indenter. The Indenter Source Code Having specified the header this section explains how the indentation scripting itself works. The basic skeleton of the body looks like this: // required katepart js libraries, e.g. range.js if you use Range require ("range.js"); triggerCharacters = "{}/:;"; function indent(line, indentWidth, ch) { // called for each newline (ch == '\n') and all characters specified in // the global variable triggerCharacters. When calling ToolsAlign // the variable ch is empty, i.e. ch == ''. // // see also: Scripting API return -2; } The function indent() has three parameters: line: the line that has to be indented indentWidth: the indentation width in number of spaces ch: either a newline character (ch == '\n'), the trigger character specified in triggerCharacters or empty if the user invoked the action ToolsAlign. The return value of the indent() function specifies how the line will be indented. If the return value is a simple integer number, it is interpreted as follows: return value -2: do nothing return value -1: keep indentation (searches for previous non-blank line) return value 0: numbers >= 0 specify the indentation depth in spaces Alternatively, an array of two elements can be returned: return [ indent, align ]; In this case, the first element is the indentation depth as above with the same meaning of the special values. However, the second element is an absolute value representing a column for alignment. If this value is higher than the indent value, the difference represents a number of spaces to be added after the indentation of the first parameter. Otherwise, the second number is ignored. Using tabs and spaces for indentation is often referred to as mixed mode. Consider the following example: Assume using tabs to indent, and tab width is set to 4. Here, <tab> represents a tab and '.' a space: 1: <tab><tab>foobar("hello", 2: <tab><tab>......."world"); When indenting line 2, the indent() function returns [8, 15]. As result, two tabs are inserted to indent to column 8, and 7 spaces are added to align the second parameter under the first, so that it stays aligned if the file is viewed with a different tab width. A default &kde; installation ships &kappname; with several indenters. The corresponding JavaScript source code can be found in $XDG_DATA_DIRS/katepart5/script/indentation. On &Windows; these files are located in %USER%\AppData\Local\katepart5\indentation. %USER% usually expands to C:\\Users\\user. Developing an indenter requires reloading the scripts to see whether the changes behave appropriately. Instead of restarting the application, simply switch to the command line and invoke the command reload-scripts. If you develop useful scripts please consider contributing to the &kappname; Project by contacting the mailing list. Command Line Scripts As it is hard to satisfy everyone's needs, &kappname; supports little helper tools for quick text manipulation through the built-in command line. For instance, the command sort is implemented as a script. This section explains how to create *.js files to extend &kappname; with arbitrary helper scripts. Command line scripts are located in the same folder as indentation scripts. So as a first step, create a new *.js file called myutils.js in the local home folder $XDG_DATA_HOME/katepart5/script/commands. Therein, the environment variable XDG_DATA_HOME typically expands to either ~/.local or ~/.local/share. On &Windows; these files are located in %USER%\AppData\Local\katepart5\commands. %USER% usually expands to C:\\Users\\user. The Command Line Script Header The header of each command line script is embedded in JSON at the beginning of the script as follows: var katescript = { "author": "Example Name <example.name@some.address.org>", "license": "LGPLv2+", "revision": 1, "kate-version": "5.1", "functions": ["sort", "moveLinesDown"], "actions": [ { "function": "sort", "name": "Sort Selected Text", "category": "Editing", "interactive": "false" }, { "function": "moveLinesDown", "name": "Move Lines Down", "category": "Editing", "shortcut": "Ctrl+Shift+Down", "interactive": "false" } ] }; // kate-script-header, must be at the start of the file without comments Each entry is explained in detail now: author [optional]: The author's name and contact information. license [optional]: Short form of the license, such as BSD License or LGPLv2. revision [required]: The revision of the script. This number should be increased whenever the script is modified. kate-version [required]: Minimum required &kappname; version. functions [required]: JSON array of commands in the script. actions [optional]: JSON Array of JSON objects that defines the actions that appear in the application menu. Detailed information is provided in the section Binding Shortcuts. Since the value of functions is a JSON array, a single script is able to contain an arbitrary number of command line commands. Each function is available through &kappname;'s built-in command line. The Script Source Code All functions specified in the header have to be implemented in the script. For instance, the script file from the example above needs to implement the two functions sort and moveLinesDown. All functions have the following syntax: // required katepart js libraries, e.g. range.js if you use Range require ("range.js"); function <name>(arg1, arg2, ...) { // ... implementation, see also: Scripting API } Arguments in the command line are passed to the function as arg1, arg2, etc. In order to provide documentation for each command, simply implement the 'help' function as follows: function help(cmd) { if (cmd == "sort") { return i18n("Sort the selected text."); } else if (cmd == "...") { // ... } } Executing help sort in the command line then calls this help function with the argument cmd set to the given command, &ie; cmd == "sort". &kappname; then presents the returned text as documentation to the user. Make sure to translate the strings. Developing a command line script requires reloading the scripts to see whether the changes behave appropriately. Instead of restarting the application, simply switch to the command line and invoke the command reload-scripts. Binding Shortcuts In order to make the scripts accessible in the application menu and assign shortcuts, the script needs to provide an appropriate script header. In the above example, both functions sort and moveLinesDown appear in the menu due to the following part in the script header: var katescript = { ... "actions": [ { "function": "sort", "name": "Sort Selected Text", "icon": "", "category": "Editing", "interactive": "false" }, { "function": "moveLinesDown", "name": "Move Lines Down", "icon": "", "category": "Editing", "shortcut": "Ctrl+Shift+Down", "interactive": "false" } ] }; The fields for one action are as follows: function [required]: The function that should appear in the menu Tools Scripts. name [required]: The text appears in the script menu. icon [optional]: The icon appears next to the text in the menu. All &kde; icon names can be used here. category [optional]: If a category is specified, the script appears in a submenu. shortcut [optional]: The shortcut given here is the default shortcut. Example: Ctrl+Alt+t. See the Qt documentation for further details. interactive [optional]: If the script needs user input in the command line, set this to true. If you develop useful scripts please consider contributing to the &kappname; Project by contacting the mailing list. Scripting API The scripting API presented here is available to all scripts, &ie; indentation scripts and command line commands. The Cursor and Range classes are provided by library files in $XDG_DATA_DIRS/katepart5/libraries. If you want to use them in your script, which needs to use some of the Document or View functions, please include the necessary library by using: // required katepart js libraries, e.g. range.js if you use Range require ("range.js"); To extend the standard scripting API with your own functions and prototypes simply create a new file in &kde;'s local configuration folder $XDG_DATA_HOME/katepart5/libraries and include it into your script using: require ("myscriptnamehere.js"); On &Windows; these files are located in %USER%\AppData\Local\katepart5\libraries. %USER% usually expands to C:\\Users\\user. To extend existing prototypes like Cursor or Range, the recommended way is to not modify the global *.js files. Instead, change the Cursor prototype in JavaScript after the cursor.js is included into your script via require. Cursors and Ranges As &kappname; is a text editor, all the scripting API is based on cursors and ranges whenever possible. A Cursor is a simple (line, column) tuple representing a text position in the document. A Range spans text from a starting cursor position to an ending cursor position. The API is explained in detail in the next sections. The Cursor Prototype Cursor(); Constructor. Returns a Cursor at position (0, 0). Example: var cursor = new Cursor(); Cursor(int line, int column); Constructor. Returns a Cursor at position (line, column). Example: var cursor = new Cursor(3, 42); Cursor(Cursor other); Copy constructor. Returns a copy of the cursor other. Example: var copy = new Cursor(other); Cursor Cursor.clone(); Returns a clone of the cursor. Example: var clone = cursor.clone(); Cursor.setPosition(int line, int column); Sets the cursor position to line and column. Since: &kde; 4.11 bool Cursor.isValid(); Check whether the cursor is valid. The cursor is invalid, if line and/or column are set to -1. Example: var valid = cursor.isValid(); Cursor Cursor.invalid(); Returns a new invalid cursor located at (-1, -1). Example: var invalidCursor = cursor.invalid(); int Cursor.compareTo(Cursor other); Compares this cursor to the cursor other. Returns -1, if this cursor is located before the cursor other, 0, if both cursors equal and +1, if this cursor is located after the cursor other. bool Cursor.equals(Cursor other); Returns true, if this cursor and the cursor other are equal, otherwise false. String Cursor.toString(); Returns the cursor as a string of the form Cursor(line, column). The Range Prototype Range(); Constructor. Calling new Range() returns a Range at (0, 0) - (0, 0). Range(Cursor start, Cursor end); Constructor. Calling new Range(start, end) returns the Range (start, end). Range(int startLine, int startColumn, int endLine, int endColumn); Constructor. Calling new Range(startLine, startColumn, endLine, endColumn) returns the Range from (startLine, startColumn) to (endLine, endColumn). Range(Range other); Copy constructor. Returns a copy of Range other. Range Range.clone(); Returns a clone of the range. Example: var clone = range.clone(); bool Range.isEmpty(); Returns true, if the start and end cursors are equal. Example: var empty = range.isEmpty(); Since: &kde; 4.11 bool Range.isValid(); Returns true, if both start and end cursor are valid, otherwise false. Example: var valid = range.isValid(); Range Range.invalid(); Returns the Range from (-1, -1) to (-1, -1). bool Range.contains(Cursor cursor); Returns true, if this range contains the cursor position, otherwise false. bool Range.contains(Range other); Returns true, if this range contains the Range other, otherwise false. bool Range.containsColumn(int column); Returns true, if column is in the half open interval [start.column, end.column), otherwise false. bool Range.containsLine(int line); Returns true, if line is in the half open interval [start.line, end.line), otherwise false. bool Range.overlaps(Range other); Returns true, if this range and the range other share a common region, otherwise false. bool Range.overlapsLine(int line); Returns true, if line is in the interval [start.line, end.line], otherwise false. bool Range.overlapsColumn(int column); Returns true, if column is in the interval [start.column, end.column], otherwise false. bool Range.onSingleLine(); Returns true, if the range starts and ends at the same line, &ie; if Range.start.line == Range.end.line. Since: &kde; 4.9 bool Range.equals(Range other); Returns true, if this range and the Range other are equal, otherwise false. String Range.toString(); Returns the range as a string of the form Range(Cursor(line, column), Cursor(line, column)). Global Functions This section lists all global functions. Reading & Including Files String read(String file); Will search the given file relative to the katepart/script/files directory and return its content as a string. void require(String file); Will search the given file relative to the katepart/script/libraries directory and evaluate it. require is internally guarded against multiple inclusions of the same file. Since: &kde; 4.10 Debugging void debug(String text); Prints text to stdout in the console launching the application. Translation In order to support full localization, there are several functions to translate strings in scripts, namely i18n, i18nc, i18np and i18ncp. These functions behave exactly like &kde;'s translation functions. The translation functions translate the wrapped strings through &kde;'s translation system to the language used in the application. Strings in scripts being developed in the official &kappname; sources are automatically extracted and translatable. In other words, as a &kappname; developer you do not have to bother with message extraction and translation. It should be noted though, that the translation only works inside the &kde; infrastructure, &ie;, new strings in 3rd-party scripts developed outside of &kde; are not translated. Therefore, please consider contributing your scripts to &kate; such that proper translation is possible. void i18n(String text, arg1, ...); Translates text into the language used by the application. The arguments arg1, ..., are optional and used to replace the placeholders %1, %2, etc. void i18nc(String context, String text, arg1, ...); Translates text into the language used by the application. Additionally, the string context is visible to translators so they can provide a better translation. The arguments arg1, ..., are optional and used to replace the placeholders %1, %2, etc. void i18np(String singular, String plural, int number, arg1, ...); Translates either singular or plural into the language used by the application, depending on the given number. The arguments arg1, ..., are optional and used to replace the placeholders %1, %2, etc. void i18ncp(String context, String singular, String plural, int number, arg1, ...); Translates either singular or plural into the language used by the application, depending on the given number. Additionally, the string context is visible to translators so they can provide a better translation. The arguments arg1, ..., are optional and used to replace the placeholders %1, %2, etc. The View API Whenever a script is being executed, there is a global variable view representing the current active editor view. The following is a list of all available View functions. Cursor view.cursorPosition() Returns the current cursor position in the view. void view.setCursorPosition(int line, int column); void view.setCursorPosition(Cursor cursor); Set the current cursor position to either (line, column) or to the given cursor. Cursor view.virtualCursorPosition(); Returns the virtual cursor position with each tab counting the corresponding amount of spaces depending on the current tab width. void view.setVirtualCursorPosition(int line, int column); void view.setVirtualCursorPosition(Cursor cursor); Set the current virtual cursor position to (line, column) or to the given cursor. String view.selectedText(); Returns the selected text. If no text is selected, the returned string is empty. bool view.hasSelection(); Returns true, if the view has selected text, otherwise false. Range view.selection(); Returns the selected text range. The returned range is invalid if there is no selected text. void view.setSelection(Range range); Set the selected text to the given range. void view.removeSelectedText(); Remove the selected text. If the view does not have any selected text, this does nothing. void view.selectAll(); Selects the entire text in the document. void view.clearSelection(); Clears the text selection without removing the text. object view.executeCommand(String command, String args, Range range); Executes the command line command command with the optional arguments args and the optional range. The returned object has a boolean property object.ok that indicates whether execution of the command was successful. In case of an error, the string object.status contains an error message. Since: &kde-frameworks; 5.50 The Document API Whenever a script is being executed, there is a global variable document representing the current active document. The following is a list of all available Document functions. String document.fileName(); Returns the document's filename or an empty string for unsaved text buffers. String document.url(); Returns the document's full url or an empty string for unsaved text buffers. String document.mimeType(); Returns the document's mime type or the mime type application/octet-stream if no appropriate mime type could be found. String document.encoding(); Returns the currently used encoding to save the file. String document.highlightingMode(); Returns the global highlighting mode used for the whole document. String document.highlightingModeAt(Cursor pos); Returns the highlighting mode used at the given position in the document. Array document.embeddedHighlightingModes(); Returns an array of highlighting modes embedded in this document. bool document.isModified(); Returns true, if the document has unsaved changes (modified), otherwise false. String document.text(); Returns the entire content of the document in a single text string. Newlines are marked with the newline character \n. String document.text(int fromLine, int fromColumn, int toLine, int toColumn); String document.text(Cursor from, Cursor to); String document.text(Range range); Returns the text in the given range. It is recommended to use the cursor and range based version for better readability of the source code. String document.line(int line); Returns the given text line as string. The string is empty if the requested line is out of range. String document.wordAt(int line, int column); String document.wordAt(Cursor cursor); Returns the word at the given cursor position. Range document.wordRangeAt(int line, int column); Range document.wordRangeAt(Cursor cursor); Return the range of the word at the given cursor position. The returned range is invalid (see Range.isValid()), if the text position is after the end of a line. If there is no word at the given cursor, an empty range is returned. Since: &kde; 4.9 String document.charAt(int line, int column); String document.charAt(Cursor cursor); Returns the character at the given cursor position. String document.firstChar(int line); Returns the first character in the given line that is not a whitespace. The first character is at column 0. If the line is empty or only contains whitespace characters, the returned string is empty. String document.lastChar(int line); Returns the last character in the given line that is not a whitespace. If the line is empty or only contains whitespace characters, the returned string is empty. bool document.isSpace(int line, int column); bool document.isSpace(Cursor cursor); Returns true, if the character at the given cursor position is a whitespace, otherwise false. bool document.matchesAt(int line, int column, String text); bool document.matchesAt(Cursor cursor, String text); Returns true, if the given text matches at the corresponding cursor position, otherwise false. bool document.startsWith(int line, String text, bool skipWhiteSpaces); Returns true, if the line starts with text, otherwise false. The argument skipWhiteSpaces controls whether leading whitespaces are ignored. bool document.endsWith(int line, String text, bool skipWhiteSpaces); Returns true, if the line ends with text, otherwise false. The argument skipWhiteSpaces controls whether trailing whitespaces are ignored. bool document.setText(String text); Sets the entire document text. bool document.clear(); Removes the entire text in the document. bool document.truncate(int line, int column); bool document.truncate(Cursor cursor); Truncate the given line at the given column or cursor position. Returns true on success, or false if the given line is not part of the document range. bool document.insertText(int line, int column, String text); bool document.insertText(Cursor cursor, String text); Inserts the text at the given cursor position. Returns true on success, or false, if the document is in read-only mode. bool document.removeText(int fromLine, int fromColumn, int toLine, int toColumn); bool document.removeText(Cursor from, Cursor to); bool document.removeText(Range range); Removes the text in the given range. Returns true on success, or false, if the document is in read-only mode. bool document.insertLine(int line, String text); Inserts text in the given line. Returns true on success, or false, if the document is in read-only mode or the line is not in the document range. bool document.removeLine(int line); Removes the given text line. Returns true on success, or false, if the document is in read-only mode or the line is not in the document range. bool document.wrapLine(int line, int column); bool document.wrapLine(Cursor cursor); Wraps the line at the given cursor position. Returns true on success, otherwise false, ⪚ if line < 0. Since: &kde; 4.9 void document.joinLines(int startLine, int endLine); Joins the lines from startLine to endLine. Two succeeding text lines are always separated with a single space. int document.lines(); Returns the number of lines in the document. bool document.isLineModified(int line); Returns true, if line currently contains unsaved data. Since: &kde; 5.0 bool document.isLineSaved(int line); Returns true, if line was changed, but the document was saved. Hence, the line currently does not contain any unsaved data. Since: &kde; 5.0 bool document.isLineTouched(int line); Returns true, if line currently contains unsaved data or was changed before. Since: &kde; 5.0 bool document.findTouchedLine(int startLine, bool down); Search for the next touched line starting at line. The search is performed either upwards or downwards depending on the search direction specified in down. Since: &kde; 5.0 int document.length(); Returns the number of characters in the document. int document.lineLength(int line); Returns the line's length. void document.editBegin(); Starts an edit group for undo/redo grouping. Make sure to always call editEnd() as often as you call editBegin(). Calling editBegin() internally uses a reference counter, &ie;, this call can be nested. void document.editEnd(); Ends an edit group. The last call of editEnd() (&ie; the one for the first call of editBegin()) finishes the edit step. int document.firstColumn(int line); Returns the first non-whitespace column in the given line. If there are only whitespaces in the line, the return value is -1. int document.lastColumn(int line); Returns the last non-whitespace column in the given line. If there are only whitespaces in the line, the return value is -1. int document.prevNonSpaceColumn(int line, int column); int document.prevNonSpaceColumn(Cursor cursor); Returns the column with a non-whitespace character starting at the given cursor position and searching backwards. int document.nextNonSpaceColumn(int line, int column); int document.nextNonSpaceColumn(Cursor cursor); Returns the column with a non-whitespace character starting at the given cursor position and searching forwards. int document.prevNonEmptyLine(int line); Returns the next non-empty line containing non-whitespace characters searching backwards. int document.nextNonEmptyLine(int line); Returns the next non-empty line containing non-whitespace characters searching forwards. bool document.isInWord(String character, int attribute); Returns true, if the given character with the given attribute can be part of a word, otherwise false. bool document.canBreakAt(String character, int attribute); Returns true, if the given character with the given attribute is suited to wrap a line, otherwise false. bool document.canComment(int startAttribute, int endAttribute); Returns true, if a range starting and ending with the given attributes is suited to be commented out, otherwise false. String document.commentMarker(int attribute); Returns the comment marker for single line comments for a given attribute. String document.commentStart(int attribute); Returns the comment marker for the start of multi-line comments for a given attribute. String document.commentEnd(int attribute); Returns the comment marker for the end of multi-line comments for a given attribute. Range document.documentRange(); Returns a range that encompasses the whole document. Cursor documentEnd(); Returns a cursor positioned at the last column of the last line in the document. bool isValidTextPosition(int line, int column); bool isValidTextPosition(Cursor cursor); Returns true, if the given cursor position is positioned at a valid text position. A text position is valid only if it locate at the start, in the middle, or the end of a valid line. Further, a text position is invalid if it is located in a Unicode surrogate. Since: &kde; 5.0 int document.attribute(int line, int column); int document.attribute(Cursor cursor); Returns the attribute at the given cursor position. bool document.isAttribute(int line, int column, int attribute); bool document.isAttribute(Cursor cursor, int attribute); Returns true, if the attribute at the given cursor position equals attribute, otherwise false. String document.attributeName(int line, int column); String document.attributeName(Cursor cursor); Returns the attribute name as human readable text. This is equal to the itemData name in the syntax highlighting files. bool document.isAttributeName(int line, int column, String name); bool document.isAttributeName(Cursor cursor, String name); Returns true, if the attribute name at a certain cursor position matches the given name, otherwise false. String document.variable(String key); Returns the value of the requested document variable key. If the document variable does not exist, the return value is an empty string. void document.setVariable(String key, String value); Set the value of the requested document variable key. See also: Kate document variables Since: &kde; 4.8 int document.firstVirtualColumn(int line); Returns the virtual column of the first non-whitespace character in the given line or -1, if the line is empty or contains only whitespace characters. int document.lastVirtualColumn(int line); Returns the virtual column of the last non-whitespace character in the given line or -1, if the line is empty or contains only whitespace characters. int document.toVirtualColumn(int line, int column); int document.toVirtualColumn(Cursor cursor); Cursor document.toVirtualCursor(Cursor cursor); Converts the given real cursor position to a virtual cursor position, either returning an int or a Cursor object. int document.fromVirtualColumn(int line, int virtualColumn); int document.fromVirtualColumn(Cursor virtualCursor); Cursor document.fromVirtualCursor(Cursor virtualCursor); Converts the given virtual cursor position to a real cursor position, either returning an int or a Cursor object. Cursor document.anchor(int line, int column, Char character); Cursor document.anchor(Cursor cursor, Char character); Searches backward for the given character starting from the given cursor. As an example, if '(' is passed as character, this function will return the position of the opening '('. This reference counting, &ie; other '(...)' are ignored. Cursor document.rfind(int line, int column, String text, int attribute = -1); Cursor document.rfind(Cursor cursor, String text, int attribute = -1); Find searching backwards the given text with the appropriate attribute. The argument attribute is ignored if it is set to -1. The returned cursor is invalid, if the text could not be found. int document.defStyleNum(int line, int column); int document.defStyleNum(Cursor cursor); Returns the default style used at the given cursor position. bool document.isCode(int line, int column); bool document.isCode(Cursor cursor); Returns true, if the attribute at the given cursor position is not equal to all of the following styles: dsComment, dsString, dsRegionMarker, dsChar, dsOthers. bool document.isComment(int line, int column); bool document.isComment(Cursor cursor); Returns true, if the attribute of the character at the cursor position is dsComment, otherwise false. bool document.isString(int line, int column); bool document.isString(Cursor cursor); Returns true, if the attribute of the character at the cursor position is dsString, otherwise false. bool document.isRegionMarker(int line, int column); bool document.isRegionMarker(Cursor cursor); Returns true, if the attribute of the character at the cursor position is dsRegionMarker, otherwise false. bool document.isChar(int line, int column); bool document.isChar(Cursor cursor); Returns true, if the attribute of the character at the cursor position is dsChar, otherwise false. bool document.isOthers(int line, int column); bool document.isOthers(Cursor cursor); Returns true, if the attribute of the character at the cursor position is dsOthers, otherwise false. The Editor API In addition to the document and view API, there is a general editor API that provides functions for general editor scripting functionality. String editor.clipboardText(); Returns the text that currently is in the global clipboard. Since: &kde-frameworks; 5.50 String editor.clipboardHistory(); The editor holds a clipboard history that contains up to 10 clipboard entries. This function returns all entries that currently are in the clipboard history. Since: &kde-frameworks; 5.50 void editor.setClipboardText(String text); Set the contents of the clipboard to text. The text will be added to the clipboard history. Since: &kde-frameworks; 5.50 diff --git a/doc/kwrite/CMakeLists.txt b/doc/kwrite/CMakeLists.txt index 5f8071fe3..6bf40bfb9 100644 --- a/doc/kwrite/CMakeLists.txt +++ b/doc/kwrite/CMakeLists.txt @@ -1,3 +1,5 @@ -########### install files ############### - -kdoctools_create_handbook (index.docbook INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en SUBDIR kwrite) +kdoctools_create_handbook( + index.docbook + INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en + SUBDIR kwrite +) diff --git a/kate/CMakeLists.txt b/kate/CMakeLists.txt index 107c77270..bdc67bf4e 100644 --- a/kate/CMakeLists.txt +++ b/kate/CMakeLists.txt @@ -1,154 +1,168 @@ -# -# The Kate Application -# -project(kate) - -# Load the frameworks we need -find_package(KF5 REQUIRED COMPONENTS DBusAddons GuiAddons) - -# includes -include_directories( - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/session - ${CMAKE_CURRENT_SOURCE_DIR}/qtsingleapplication -) +# We use an intermediate static library because linking tests directly to an executable is not +# possible with CMake yet. +add_library(kate-lib STATIC "") + +configure_file(config.h.in config.h) -# collect the needed source files -set (KATE_LIBRARY_SRCS - kateappadaptor.cpp - kateapp.cpp - kateconfigdialog.cpp - kateconfigplugindialogpage.cpp - katedocmanager.cpp - katefileactions.cpp - katemainwindow.cpp - katepluginmanager.cpp - kateviewmanager.cpp - kateviewspace.cpp - katesavemodifieddialog.cpp - katemwmodonhddialog.cpp - katecolorschemechooser.cpp - katequickopenmodel.cpp - katetabbutton.cpp - katetabbar.cpp - - # session - session/katesessionsaction.cpp - session/katesessionmanager.cpp - session/katesessionmanagedialog.cpp - session/katesession.cpp - - katemdi.cpp - katerunninginstanceinfo.cpp - katequickopen.cpp - katewaiter.h +include(GenerateExportHeader) +generate_export_header( + kate-lib + EXPORT_FILE_NAME katetests_export.h + EXPORT_MACRO_NAME KATE_TESTS_EXPORT ) -ki18n_wrap_ui(KATE_LIBRARY_SRCS - ui/sessionconfigwidget.ui - session/katesessionmanagedialog.ui +target_include_directories( + kate-lib + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/session + ${CMAKE_CURRENT_SOURCE_DIR}/qtsingleapplication + ${CMAKE_CURRENT_BINARY_DIR} # katetests_export.h + config.h ) -qt5_add_resources( KATE_LIBRARY_SRCS data/kate.qrc ) +find_package( + KF5 + QUIET + REQUIRED + COMPONENTS + TextEditor + IconThemes + WindowSystem + DBusAddons + Crash + OPTIONAL_COMPONENTS + Activities +) -add_library(kdeinit_kate STATIC ${KATE_LIBRARY_SRCS}) -target_link_libraries(kdeinit_kate -PUBLIC +target_link_libraries( + kate-lib + PUBLIC KF5::TextEditor - KF5::I18n KF5::IconThemes - KF5::WindowSystem - KF5::GuiAddons + KF5::WindowSystem KF5::DBusAddons - KF5::Crash) + KF5::Crash +) if(KF5Activities_FOUND) - target_link_libraries(kdeinit_kate - PUBLIC - KF5::Activities) + target_link_libraries(kate-lib PUBLIC KF5::Activities) endif() -generate_export_header(kdeinit_kate - EXPORT_FILE_NAME kateprivate_export.h - EXPORT_MACRO_NAME KATE_TESTS_EXPORT -) - -# collect icons -set(KATE_ICONS_PNG - ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-apps-kate.png - ${CMAKE_CURRENT_SOURCE_DIR}/icons/22-apps-kate.png - ${CMAKE_CURRENT_SOURCE_DIR}/icons/32-apps-kate.png - ${CMAKE_CURRENT_SOURCE_DIR}/icons/48-apps-kate.png - ${CMAKE_CURRENT_SOURCE_DIR}/icons/64-apps-kate.png - ${CMAKE_CURRENT_SOURCE_DIR}/icons/128-apps-kate.png -) +ki18n_wrap_ui(UI_SOURCES ui/sessionconfigwidget.ui session/katesessionmanagedialog.ui) +target_sources(kate-lib PRIVATE ${UI_SOURCES}) -set(KATE_ICONS_SVG -${CMAKE_CURRENT_SOURCE_DIR}/icons/sc-apps-kate.svgz +set(ICONS_PNG + ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-apps-kate.png + ${CMAKE_CURRENT_SOURCE_DIR}/icons/22-apps-kate.png + ${CMAKE_CURRENT_SOURCE_DIR}/icons/32-apps-kate.png + ${CMAKE_CURRENT_SOURCE_DIR}/icons/48-apps-kate.png + ${CMAKE_CURRENT_SOURCE_DIR}/icons/64-apps-kate.png + ${CMAKE_CURRENT_SOURCE_DIR}/icons/128-apps-kate.png ) -# application only sources -set (KATE_APP_SRCS - main.cpp +set(ICONS_SVG ${CMAKE_CURRENT_SOURCE_DIR}/icons/sc-apps-kate.svgz) + +# Add icon files to the application's source files to have CMake bundle them in the executable. +ecm_add_app_icon(ICONS_SOURCES ICONS ${ICONS_PNG}) +target_sources(kate-lib PRIVATE ${ICONS_SOURCES}) + +target_sources( + kate-lib + PRIVATE + data/kate.qrc + + session/katesession.cpp + session/katesessionmanagedialog.cpp + session/katesessionmanager.cpp + session/katesessionsaction.cpp + + kateapp.cpp + kateappadaptor.cpp + katecolorschemechooser.cpp + kateconfigdialog.cpp + kateconfigplugindialogpage.cpp + katedocmanager.cpp + katefileactions.cpp + katemainwindow.cpp + katemdi.cpp + katemwmodonhddialog.cpp + katepluginmanager.cpp + katequickopen.cpp + katequickopenmodel.cpp + katerunninginstanceinfo.cpp + katesavemodifieddialog.cpp + katetabbar.cpp + katetabbutton.cpp + kateviewmanager.cpp + kateviewspace.cpp + katewaiter.cpp ) -# use single application instead of dbus on mac + windows -if (APPLE OR WIN32) - set(singleapp_SRCS - qtsingleapplication/qtlocalpeer.cpp - qtsingleapplication/qtsingleapplication.cpp - qtsingleapplication/qtlockedfile.cpp - ) - - if(WIN32) - set(singleapp_SRCS ${singleapp_SRCS} qtsingleapplication/qtlockedfile_win.cpp) - else() - set(singleapp_SRCS ${singleapp_SRCS} qtsingleapplication/qtlockedfile_unix.cpp) - endif() - - add_definitions("-DUSE_QT_SINGLE_APP") - - set(KATE_APP_SRCS ${KATE_APP_SRCS} ${singleapp_SRCS}) +# Use a single application on MacOS + Windows instead of dbus. +if(APPLE OR WIN32) + target_compile_definitions(kate-lib PRIVATE USE_QT_SINGLE_APP) + + target_sources( + kate-lib + PRIVATE + qtsingleapplication/qtlocalpeer.cpp + qtsingleapplication/qtsingleapplication.cpp + qtsingleapplication/qtlockedfile.cpp + ) + + if(WIN32) + target_sources(kate-lib PRIVATE qtsingleapplication/qtlockedfile_win.cpp) + else() + target_sources(kate-lib PRIVATE qtsingleapplication/qtlockedfile_unix.cpp) + endif() endif() -# add icons to application sources, to have them bundled -ecm_add_app_icon(KATE_APP_SRCS ICONS ${KATE_ICONS_PNG}) +# Executable only adds the main definition. +add_executable(kate-bin main.cpp) +target_link_libraries(kate-bin PRIVATE kate-lib) -# create executable -add_executable(kate ${KATE_APP_SRCS}) -target_link_libraries(kate kdeinit_kate) +set_property( + TARGET kate-bin + PROPERTY OUTPUT_NAME kate +) -# own plist magic for mac os +# See https://cmake.org/cmake/help/v3.15/prop_tgt/MACOSX_BUNDLE_INFO_PLIST.html if(APPLE) - # own plist template - set_target_properties (kate PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/data/MacOSXBundleInfo.plist.in) - - # the MacOSX bundle display name property (CFBundleDisplayName) is not currently supported by cmake, - # so has to be set for all targets in this cmake file - set(MACOSX_BUNDLE_DISPLAY_NAME Kate) - set_target_properties(kate PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Kate") - set_target_properties(kate PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Kate") - set_target_properties(kate PROPERTIES MACOSX_BUNDLE_DISPLAY_NAME "Kate") - set_target_properties(kate PROPERTIES MACOSX_BUNDLE_INFO_STRING "Kate - Advanced Text Editor") - set_target_properties(kate PROPERTIES MACOSX_BUNDLE_LONG_VERSION_STRING "Kate ${KDE_APPLICATIONS_VERSION}") - set_target_properties(kate PROPERTIES MACOSX_BUNDLE_SHORT_VERSION_STRING "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}") - set_target_properties(kate PROPERTIES MACOSX_BUNDLE_BUNDLE_VERSION "${KDE_APPLICATIONS_VERSION}") - set_target_properties(kate PROPERTIES MACOSX_BUNDLE_COPYRIGHT "2000-2016 The Kate Authors") + set_property( + TARGET kate-bin + PROPERTY MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/data/MacOSXBundleInfo.plist.in + ) + + # These are substituted by CMake into plist.in. + set(MACOSX_BUNDLE_DISPLAY_NAME "Kate") + set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Kate") + set(MACOSX_BUNDLE_BUNDLE_NAME "Kate") + set(MACOSX_BUNDLE_DISPLAY_NAME "Kate") + set(MACOSX_BUNDLE_INFO_STRING "Kate - Advanced Text Editor") + set(MACOSX_BUNDLE_LONG_VERSION_STRING "Kate ${KDE_APPLICATIONS_VERSION}") + set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}") + set(MACOSX_BUNDLE_BUNDLE_VERSION "${KDE_APPLICATIONS_VERSION}") + set(MACOSX_BUNDLE_COPYRIGHT "2000-2016 The Kate Authors") endif() -# install executable -install(TARGETS kate ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(TARGETS kate-bin ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) -# desktop file -install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/data/org.kde.kate.desktop DESTINATION ${XDG_APPS_INSTALL_DIR}) +ecm_install_icons( + ICONS ${ICONS_PNG} ${ICONS_SVG} + DESTINATION ${ICON_INSTALL_DIR} + THEME hicolor +) -# appdata -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/data/org.kde.kate.appdata.xml DESTINATION ${CMAKE_INSTALL_METAINFODIR}) +install( + PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/data/org.kde.kate.desktop + DESTINATION ${XDG_APPS_INSTALL_DIR} +) -# install icons -ecm_install_icons(ICONS ${KATE_ICONS_PNG} ${KATE_ICONS_SVG} DESTINATION ${ICON_INSTALL_DIR} THEME hicolor) +install( + FILES ${CMAKE_CURRENT_SOURCE_DIR}/data/org.kde.kate.appdata.xml + DESTINATION ${CMAKE_INSTALL_METAINFODIR} +) -# automatic unit tests -if (BUILD_TESTING) - add_subdirectory(autotests) +if(BUILD_TESTING) + add_subdirectory(autotests) endif() diff --git a/kate/autotests/CMakeLists.txt b/kate/autotests/CMakeLists.txt index efc57c085..511da575c 100644 --- a/kate/autotests/CMakeLists.txt +++ b/kate/autotests/CMakeLists.txt @@ -1,17 +1,25 @@ - include(ECMMarkAsTest) +find_package(Qt5Test QUIET REQUIRED) + macro(kate_executable_tests) foreach(_testname ${ARGN}) add_executable(${_testname} ${_testname}.cpp) + + target_link_libraries( + ${_testname} + PRIVATE + kate-lib + Qt5::Test + ) + add_test(NAME kateapp-${_testname} COMMAND ${_testname}) - target_link_libraries(${_testname} kdeinit_kate Qt5::Test) ecm_mark_as_test(${_testname}) - endforeach(_testname) -endmacro(kate_executable_tests) + endforeach() +endmacro() kate_executable_tests( session_test session_manager_test sessions_action_test ) diff --git a/kate/autotests/session_manager_test.cpp b/kate/autotests/session_manager_test.cpp index 6e1131f87..ca41188e0 100644 --- a/kate/autotests/session_manager_test.cpp +++ b/kate/autotests/session_manager_test.cpp @@ -1,168 +1,178 @@ /* This file is part of the KDE project * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "session_manager_test.h" #include "katesessionmanager.h" #include "kateapp.h" #include #include #include #include #include QTEST_MAIN(KateSessionManagerTest) void KateSessionManagerTest::initTestCase() { - m_app = new KateApp(QCommandLineParser()); // FIXME: aaaah, why, why, why?! + /** + * init resources from our static lib + */ + Q_INIT_RESOURCE(kate); + + // we need an application object, as session loading will trigger modifications to that + m_app = new KateApp(QCommandLineParser()); + m_app->sessionManager()->activateAnonymousSession(); } void KateSessionManagerTest::cleanupTestCase() { delete m_app; } void KateSessionManagerTest::init() { m_tempdir = new QTemporaryDir; QVERIFY(m_tempdir->isValid()); m_manager = new KateSessionManager(this, m_tempdir->path()); } void KateSessionManagerTest::cleanup() { delete m_manager; delete m_tempdir; } void KateSessionManagerTest::basic() { QCOMPARE(m_manager->sessionsDir(), m_tempdir->path()); QCOMPARE(m_manager->sessionList().size(), 0); - QCOMPARE(m_manager->activeSession()->isAnonymous(), true); + QVERIFY(m_manager->activateAnonymousSession()); + QVERIFY(m_manager->activeSession()); } void KateSessionManagerTest::activateNewNamedSession() { const QString sessionName = QStringLiteral("hello_world"); QVERIFY(m_manager->activateSession(sessionName, false, false)); QCOMPARE(m_manager->sessionList().size(), 1); KateSession::Ptr s = m_manager->activeSession(); QCOMPARE(s->name(), sessionName); QCOMPARE(s->isAnonymous(), false); const QString sessionFile = m_tempdir->path() + QLatin1Char('/') + sessionName + QLatin1String(".katesession"); QCOMPARE(s->config()->name(), sessionFile); } void KateSessionManagerTest::anonymousSessionFile() { const QString anonfile = QDir().cleanPath(m_tempdir->path() + QLatin1String("/../anonymous.katesession")); + QVERIFY(m_manager->activateAnonymousSession()); QVERIFY(m_manager->activeSession()->isAnonymous()); QCOMPARE(m_manager->activeSession()->config()->name(), anonfile); } void KateSessionManagerTest::urlizeSessionFile() { const QString sessionName = QStringLiteral("hello world/#"); m_manager->activateSession(sessionName, false, false); KateSession::Ptr s = m_manager->activeSession(); const QString sessionFile = m_tempdir->path() + QLatin1String("/hello%20world%2F%23.katesession"); QCOMPARE(s->config()->name(), sessionFile); } void KateSessionManagerTest::deleteSession() { m_manager->activateSession(QStringLiteral("foo")); KateSession::Ptr s = m_manager->activeSession(); m_manager->activateSession(QStringLiteral("bar")); QCOMPARE(m_manager->sessionList().size(), 2); m_manager->deleteSession(s); QCOMPARE(m_manager->sessionList().size(), 1); } void KateSessionManagerTest::deleteActiveSession() { m_manager->activateSession(QStringLiteral("foo")); KateSession::Ptr s = m_manager->activeSession(); QCOMPARE(m_manager->sessionList().size(), 1); m_manager->deleteSession(s); QCOMPARE(m_manager->sessionList().size(), 1); } void KateSessionManagerTest::renameSession() { m_manager->activateSession(QStringLiteral("foo")); KateSession::Ptr s = m_manager->activeSession(); QCOMPARE(m_manager->sessionList().size(), 1); const QString newName = QStringLiteral("bar"); m_manager->renameSession(s, newName); // non-collision path QCOMPARE(s->name(), newName); QCOMPARE(m_manager->sessionList().size(), 1); QCOMPARE(m_manager->sessionList().first(), s); } void KateSessionManagerTest::saveActiveSessionWithAnynomous() { + QVERIFY(m_manager->activateAnonymousSession()); QVERIFY(m_manager->activeSession()->isAnonymous()); QVERIFY(m_manager->sessionList().size() == 0); QCOMPARE(m_manager->saveActiveSession(), true); QCOMPARE(m_manager->activeSession()->isAnonymous(), true); QCOMPARE(m_manager->activeSession()->name(), QString()); QCOMPARE(m_manager->sessionList().size(), 0); } void KateSessionManagerTest::deletingSessionFilesUnderRunningApp() { m_manager->activateSession(QStringLiteral("foo")); m_manager->activateSession(QStringLiteral("bar")); QVERIFY(m_manager->sessionList().size() == 2); QVERIFY(m_manager->activeSession()->name() == QLatin1String("bar")); const QString file = m_tempdir->path() + QLatin1String("/foo.katesession"); QVERIFY(QFile(file).remove()); QTRY_COMPARE_WITH_TIMEOUT(m_manager->sessionList().size(), 1, 1000); // that should be enough for KDirWatch to kick in QCOMPARE(m_manager->activeSession()->name(), QLatin1String("bar")); } void KateSessionManagerTest::startNonEmpty() { m_manager->activateSession(QStringLiteral("foo")); m_manager->activateSession(QStringLiteral("bar")); KateSessionManager m(this, m_tempdir->path()); QCOMPARE(m.sessionList().size(), 2); } diff --git a/kate/autotests/sessions_action_test.cpp b/kate/autotests/sessions_action_test.cpp index 7382cbdc9..b10ba1d87 100644 --- a/kate/autotests/sessions_action_test.cpp +++ b/kate/autotests/sessions_action_test.cpp @@ -1,98 +1,105 @@ /* This file is part of the KDE project * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "sessions_action_test.h" #include "katesessionsaction.h" #include "katesessionmanager.h" #include "kateapp.h" #include #include #include #include QTEST_MAIN(KateSessionsActionTest) void KateSessionsActionTest::initTestCase() { - m_app = new KateApp(QCommandLineParser()); // FIXME: aaaah, why, why, why?! + /** + * init resources from our static lib + */ + Q_INIT_RESOURCE(kate); + + // we need an application object, as session loading will trigger modifications to that + m_app = new KateApp(QCommandLineParser()); + m_app->sessionManager()->activateAnonymousSession(); } void KateSessionsActionTest::cleanupTestCase() { delete m_app; } void KateSessionsActionTest::init() { m_tempdir = new QTemporaryDir; QVERIFY(m_tempdir->isValid()); m_manager = new KateSessionManager(this, m_tempdir->path()); m_ac = new KateSessionsAction(QStringLiteral("menu"), this, m_manager); } void KateSessionsActionTest::cleanup() { delete m_ac; delete m_manager; delete m_tempdir; } void KateSessionsActionTest::basic() { m_ac->slotAboutToShow(); QCOMPARE(m_ac->isEnabled(), false); QList actions = m_ac->sessionsGroup->actions(); QCOMPARE(actions.size(), 0); } void KateSessionsActionTest::limit() { for (int i = 0; i < 14; i++) { m_manager->activateSession(QStringLiteral("session %1").arg(i)); } QCOMPARE(m_manager->activeSession()->isAnonymous(), false); QCOMPARE(m_manager->sessionList().size(), 14); QCOMPARE(m_ac->isEnabled(), true); m_ac->slotAboutToShow(); QList actions = m_ac->sessionsGroup->actions(); QCOMPARE(actions.size(), 10); } void KateSessionsActionTest::timesorted() { /* m_manager->activateSession("one", false, false); m_manager->saveActiveSession(); m_manager->activateSession("two", false, false); m_manager->saveActiveSession(); m_manager->activateSession("three", false, false); m_manager->saveActiveSession(); KateSessionList list = m_manager->sessionList(); QCOMPARE(list.size(), 3); TODO: any idea how to test this simply and not calling utime()? */ } diff --git a/config.h.cmake b/kate/config.h.in similarity index 72% rename from config.h.cmake rename to kate/config.h.in index 9220caf40..47f3bca97 100644 --- a/config.h.cmake +++ b/kate/config.h.in @@ -1,11 +1,10 @@ #ifndef KATE_CONFIG_H #define KATE_CONFIG_H /* config.h. Generated by cmake from config.h.cmake */ #define KATE_VERSION "${KDE_APPLICATIONS_VERSION}" -#cmakedefine HAVE_CTERMID 1 -#cmakedefine KActivities_FOUND 1 +#cmakedefine KF5Activities_FOUND #endif diff --git a/kate/data/org.kde.kate.appdata.xml b/kate/data/org.kde.kate.appdata.xml index 9b69b38a9..5ffd318a1 100644 --- a/kate/data/org.kde.kate.appdata.xml +++ b/kate/data/org.kde.kate.appdata.xml @@ -1,940 +1,940 @@ org.kde.kate.desktop CC0-1.0 LGPL-2.1+ Kate كيت Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate Kate ਕੇਟ Kate Kate Kate Kate Kate Kate Кејт Kate Кејт Kate Kate Kate Kate xxKatexx Kate Kate

Advanced Text Editor محرّر نصوص متقدّم Editor avanzáu de testu Napredni uređivač teksta Editor de text avançat Editor de text avançat Pokročilý textový editor Avanceret teksteditor Erweiterter Texteditor Προηγμένος επεξεργαστής κειμένου Advanced Text Editor Editor de texto avanzado Täiustatud tekstiredaktor Testu editore aurreratua Kehittynyt tekstimuokkain Éditeur de texte avancé Editor avanzado de textos Speciális szövegszerkesztő Editor avantiate de texto Pengedit Teks Tingkat Lanjut Editor di testi avanzato 고급 텍스트 편집기 Avansert skriveprogram Geavanceerde teksteditor Avansert skriveprogram ਤਕਨੀਕੀ ਟੈਕਸਟ ਐਡੀਟਰ Zaawansowany edytor tekstu Editor de Texto Avançado Editor de Texto Avançado Улучшенный текстовый редактор Pokročilý textový editor Napreden urejevalnik besedil Напредни уређивач текста Napredni uređivač teksta Напредни уређивач текста Napredni uređivač teksta Avancerad texteditor Gelişmiş Metin Düzenleyici Потужний текстовий редактор xxAdvanced Text Editorxx 高级文本编辑器 進階文字編輯器

Kate is a multi-document, multi-view text editor by KDE. It features stuff like codefolding, syntaxhighlighting, dynamic word wrap, an embedded console, an extensive plugin interface and some preliminary scripting support.

El Kate és un editor de text multidocument i multivista, creat per la comunitat KDE. Té funcionalitats com el plegat de codi, ressaltat de la sintaxi, ajust dinàmic de les paraules, una consola incrustada, una interfície extensa de connectors i una implementació preliminar per a crear scripts.

El Kate és un editor de text multidocument i multivista, creat per la comunitat KDE. Té funcionalitats com el plegat de codi, ressaltat de la sintaxi, ajust dinàmic de les paraules, una consola incrustada, una interfície extensa de connectors i una implementació preliminar per a crear scripts.

Kate je textový editor od KDE pro práci s více dokumenty najednou. Podporuje vlastnosti jako skládání kódu, zvýraznění syntaxe, dynamické zalamování slov, vloženou konzoli, rozsáhlou podporu modulů a základní podporu pro skriptování.

Kate er en multidokument-, multivisnings-teksteditor af KDE. Den har blandt andet kodefoldning, syntaksfremhævning, dynamisk ombrydning af ord, en indlejret konsol, en omfattende plugingrænseflade og foreløbig nogen understøttelse af scripting.

Kate ist ein KDE-Editor für mehrere Dokumente und Ansichten. Funktionen wie Quelltextausblendung, Syntaxhervorhebung, dynamischer Zeilenumbruch, eine eingebettete Konsole und eine umfangreiche Schnittstelle für Module sowie vorläufige Unterstützung für Skripte sind enthalten.

Το Kate είναι ένας πολλών εγγράφων, πολλών προβολών επεξεργαστής κειμένου από το KDE. Περιέχει λειτουργίες όπως απόκρυψη κώδικα, επισήμανση σύνταξης, δυναμική αναδίπλωση λέξεων, ενσωματωμένη κονσόλα, εκτεταμένο περιβάλλον χρήσης προσθέτων και κάποια βασική υποστήριξη συγγραφής σεναρίων.

Kate is a multi-document, multi-view text editor by KDE. It features stuff like code folding, syntax highlighting, dynamic word wrap, an embedded console, an extensive plugin interface and some preliminary scripting support.

Kate es un editor de texto creado por KDE que puede abrir varios documentos a la vez y que cuenta con varios modos de vista. Entre otras funcionalidades dispone de plegado de código, resaltado de sintaxis, justificado de línea dinámico, consola integrada, una amplia interfaz para complementos y admite scripts de manera preliminar.

Kate KDEk eginiko testu-editore dokumentu-anitza, ikuspegi-anitza da. Kode-tolestea, sintaxi-nabarmentzea, itzulbiratze-dinamikoa, kapsulatutako kontsola, osagarri interfaze zabal bat eta hastapenetan dagoen script bidez hedatzeko euskarria gisako eginbideak eskaintzen ditu.

Kate on KDE:n usean tiedoston ja näkymän tekstimuokkain, jossa on on koodin laskostus, syntaksikorostus, dynaaminen rivitys, upotettu konsoli, kattava liitännäisrajapinta sekä alustava skriptaustuki.

Kate est un éditeur de texte multi-documents et multi-vues pour KDE. Il offre des fonctionnalités comme le repliement de code, la coloration syntaxique, le retour à la ligne automatique, une console intégrée, une interface complète pour modules complémentaires, et un début de prise en charge d'automatisation par scripts.

Kate é un editor de texto creado por KDE que permite traballar en varios documentos e con varias vistas ao mesmo tempo. Goza de moitas funcionalidades, como pregado de código, salientado de sintaxe, axuste dinámico de texto, un terminal integrado, unha extensa interface para o desenvolvemento de complementos e funcionalidades preliminares de scripting.

Kate es un editor de texto per multi-documento, multi-vista. Illo ha characteristicas como codefolding (plicamento de codice), dynamic word wrap (excision dynamic de parolas), un consolle incorpporate, un interfacie de plugin extensive e alcun supporto preliminari per le scripting.

Kate adalah editor teks multi-dokumen multi-tampilan oleh KDE. Ini fitur hal-hal seperti codefolding, syntaxhighlighting, bungkus kata dinamis, konsol tertanam, antarmuka plugin yang luas dan beberapa dukungan scripting awal.

Kate è un editor di testi multi-documento e multi-vista di KDE. È dotato di funzionalità come il raggruppamento del codice, l'evidenziazione della sintassi, a capo automatico dinamico, una console integrata, un'interfaccia potenziabile tramite estensioni e qualche supporto preliminare per la creazione di script.

Kate는 KDE의 다문서 다화면 텍스트 편집기입니다. 코드 접기, 구문 강조, 동적 줄 바꿈, 내장된 콘솔, 플러그인 인터페이스, 스크립트 기능을 지원합니다.

Kate is een multi-document, multi-view tekstbewerker door KDE. Het bevat functies zoals opvouwen van code, accentuering van syntaxis, dynamisch afbreken van de regel, een ingebed console, een extensieve plug-in-interface en enige beginnende ondersteuning voor scripts.

Kate er eit skriveprogram frå KDE som kan handsama fleire dokument og fleire visingar samtidig. Det har funksjonar som kodefalding, syntaksmerking, dymamisk linjebryting, ein innebygd terminalemulator, eit omfattande grensesnitt for programtillegg og enkel skriptstøtte.

Kate jest wielodokumentowym, wielowidokowym edytorem tekstu stworzonym przez KDE. Umożliwia zwijanie kodu, podświetlanie składni, dynamiczne zawijanie wyrazów, osadzoną konsolę, rozbudowany interfejs dla wtyczek i zapewnia podstawową obsługę skryptów.

O Kate é um editor de texto multi-documentos e multi-janelas do KDE. Oferece algumas funcionalidades como a dobragem de código, realce de sintaxe, mudanças de linha dinâmicas, uma consola incorporada, uma interface em 'plugins' alargada e algum suporte preliminar para programação.

Kate é um editor de texto multi-documentos e multi-janelas para o KDE. Oferece algumas funcionalidades como a dobradura de código, realce de sintaxe, quebra de linha dinâmica, um console incorporado, uma interface de plugins extensa e um suporte preliminar para criação de scripts.

Kate — текстовый редактор от KDE, позволяющий работать с несколькими документами одновременно. Реализует сворачивание блоков кода, подсветка синтаксиса, динамический перенос строк, встроенный терминал, интерфейс для написания модулей и базовую поддержку сценариев.

Kate je viacdokumentový, viacpohľadový textový editor pre KDE. Podporuje zabaľovanie kódu, zvýrazňovanie syntaxe, dynamické zalamovanie slov, vstavanú konzolu, rozšíriteľnú architektúru pluginov a základnú podporu skriptovania.

Kate je urejevalnik besedil s strani KDE, s podporo več dokumentom in pogledom naenkrat. Podpira zvijanje kode, poudarjanje skladnje, dinamičen prelom besedila. Ima vgrajeno konzolo, razširljiv vmesnik za vstavke, vsebuje pa tudi podporo skriptom.

Кејт је вишедокументски, вишеприказни уређивач текста из КДЕ‑а. Поседује могућности као што су сажимање кода, истицање синтаксе, динамички прелом текста, угнежђени терминал, опсежно сучеље за прикључке и основна подршка за скриптовање.

Kate je višedokumentski, višeprikazni uređivač teksta iz KDE‑a. Poseduje mogućnosti kao što su sažimanje koda, isticanje sintakse, dinamički prelom teksta, ugnežđeni terminal, opsežno sučelje za priključke i osnovna podrška za skriptovanje.

Кејт је вишедокументски, вишеприказни уређивач текста из КДЕ‑а. Поседује могућности као што су сажимање кода, истицање синтаксе, динамички прелом текста, угнежђени терминал, опсежно сучеље за прикључке и основна подршка за скриптовање.

Kate je višedokumentski, višeprikazni uređivač teksta iz KDE‑a. Poseduje mogućnosti kao što su sažimanje koda, isticanje sintakse, dinamički prelom teksta, ugnežđeni terminal, opsežno sučelje za priključke i osnovna podrška za skriptovanje.

Kate är en texteditor av KDE med flera dokument och multipla vyer. Den har funktioner som kodvikning, syntaxfärgläggning, dynamisk radbrytning, en inbäddad terminal, ett omfattande insticksgränssnitt och visst preliminärt stöd för skript.

Kate KDE tarafından çoklu belge ile çalışabilen, çoklu görünümlü bir metin düzenleyicisidir. Kod katlama, sözdizimi vurgulaması, dinamik sözcük kaydırma, gömülü bir uç birim, geniş eklenti arayüzü ve bazı basit betik desteği özellikleri vardır.

Kate — текстовий редактор від KDE, у якому передбачено можливість одночасної роботи з декількома документами на декількох панелях перегляду. У програмі передбачено багато корисних можливостей, зокрема згортання коду, підсвічування синтаксичних конструкцій, динамічне перенесення рядків, вбудована консоль, гнучкий інтерфейс для роботи з додатками та базова підтримка роботи зі скриптами.

xxKate is a multi-document, multi-view text editor by KDE. It features stuff like codefolding, syntaxhighlighting, dynamic word wrap, an embedded console, an extensive plugin interface and some preliminary scripting support.xx

Kate 是一个多文档,多视图的文本编辑器,是 KDE 的一部分。它的特性包括代码折叠,语法高亮,动态文字折行,嵌入式命令行终端,可扩展的插件接口和一些初步的脚本支持。

Kate 是 KDE 內的進階編輯器,可同時編輯多個文件。它的功能包括像是源碼封裝,語法突顯,動態折行,嵌入主控台,延伸外掛程式介面與一些基本的文稿支援等等。

Features:

الميزات:

Carauterístiques:

Svojstva:

Característiques:

Característiques:

Vlastnosti:

Funktioner:

Funktionen:

Χαρακτηριστικά:

Features:

Funcionalidades:

Omadused:

Eginbideak:

Ominaisuuksia:

Fonctionnalités :

Funcionalidades:

Szolgáltatások:

Characteristicas:

Fitur:

Funzionalità:

기능:

Funksjoner:

Mogelijkheden:

Funksjonar:

ਫੀਚਰ:

Możliwości:

Funcionalidades:

Funcionalidades:

Возможности:

Funkcie:

Zmožnosti:

Могућности:

Mogućnosti:

Могућности:

Mogućnosti:

Funktioner:

Özellikler:

Можливості:

xxFeatures:xx

功能:

功能:

  • MDI, window splitting, window tabbing
  • واجهة لعدّة مستندات، تقسيم وتلسين للنّوافذ
  • -
  • MDI, divisió de finestres, canvi de finestres per pestanyes
  • +
  • MDI, divisió de finestres, finestres en pestanyes
  • MDI, divisió de finestres, canvi de finestres per pestanyes
  • MDI, dělení oken, skládání oken do karet
  • MDI, vinduesopdeling, fanebladsivindue
  • MDI (Multiple Document Interface = Benutzeroberfläche mit mehreren gleichzeitig geöffneten Dokumenten), geteilte Fenster, Unterfenster
  • MDI, διαίρεση παραθύρων, παραθυρικές καρτέλες
  • MDI, window splitting, window tabbing
  • MDI, división de ventana, pestañas de ventana
  • MDI, akende poolitamine, akende esitamine kaartidena
  • MDI, leiho-zatitzea, leihoak fitxa gisa antolatzea
  • Monen tiedoston käyttöliittymä (MDI), ikkunoiden jakaminen ja välilehdet
  • Interface multi-documents, fenêtres scindées, organisation par onglets
  • -
  • Capacidade para traballar en varios documentos á vez, coa posibilidade de dividir a xanela e usar lapelas.
  • +
  • Interface para varios documentos, división das xanelas, xanelas con lapelas
  • MDI, ablakfelosztás, ablaklapok
  • MDI, window splitting (division de fenestra), window tabbing (schedas de fenestra)
  • MDI, pemisahan window, penambahan tab window
  • MDI, divisione della finestra, schede delle finestre
  • MDI, 창 나누기, 탭 기능
  • MDI, vindusdeling, vindusfaner
  • MDI, splitsing van vensters, tabbladen
  • MDI, vindaugsdeling, vindaugsfaner
  • MDI, ਵਿੰਡੋ ਵੰਡਣਾ, ਵਿੰਡੋ ਟੈਬ ਦੇ ਰੂਪ ਵਿੱਚ ਰੱਖਣਾ
  • MDI, dzielenie okna, okna w kartach
  • MDI, divisão de janelas, janelas em páginas
  • MDI, divisão de janelas, janelas em abas
  • Работа с несколькими документами одновременно, разделение окон, вкладки
  • MDI, rozdeľovanie okien, záložky okien
  • MDI, razdelitev oken, zavihki oken
  • МДИ, подела прозора, прозори под језичцима
  • MDI, podela prozora, prozori pod jezičcima
  • МДИ, подела прозора, прозори под језичцима
  • MDI, podela prozora, prozori pod jezičcima
  • Flerfönster, fönsterdelning, fönsterflikar
  • MDI, pencere bölme, sekmeli pencereler
  • Інтерфейс для одночасної роботи з багатьма документами, поділ вікна програми, можливість роботи із вкладками.
  • xxMDI, window splitting, window tabbingxx
  • 多文档界面 (MDI),窗口拆分,标签页
  • MDI,視窗分割,視窗分頁
  • Spell checking
  • تدقيق الهجاء
  • Comprobación ortográfica
  • Provjera ispravnog pisanja riječi
  • Verificació ortogràfica
  • Verificació ortogràfica
  • Kontrola pravopisu
  • Stavekontrol
  • Rechtschreibprüfung
  • Ορθογραφία
  • Spell checking
  • Revisión de la ortografía
  • Õigekirja kontroll
  • Ortografia-egiaztatzea
  • Oikeinkirjoituksen tarkistus
  • Vérification orthographique
  • Corrección ortográfica.
  • Helyesírás-ellenőrzés
  • Controlo Orthographic
  • Pengecekan ejaan
  • Controllo ortografico
  • 맞춤법 검사
  • Stavekontroll
  • Spellingcontrole
  • Stavekontroll
  • ਸ਼ਬਦ-ਜੋੜ ਜਾਂਚ
  • Sprawdzanie pisowni
  • Verificação ortográfica
  • Verificação ortográfica
  • Проверка орфографии
  • Kontrola pravopisu
  • Preverjanje črkovanja
  • провера правописа
  • provera pravopisa
  • провера правописа
  • provera pravopisa
  • Stavningskontroll
  • Yazım denetimi
  • Перевірка правопису.
  • xxSpell checkingxx
  • 拼写检查
  • 拼字檢查
  • CR, CRLF, LF newline support
  • دعم أسطر CR، وCRLF وLF الجديدة
  • Implementació de línia nova CR, CRLF, LF
  • Implementació de línia nova CR, CRLF, LF
  • Podpora odřádkování CR, CRLF i LF
  • Understøttelse af CR-, CRLF-, LF-linjeskift
  • Unterstützung für CR, CRLF, LF am Zeilenende
  • CR, CRLF, LF για υποστήριξη αλλαγής γραμμής
  • CR, CRLF, LF newline support
  • Admite fin de línea CR, CRLF, LF
  • CR, CRLF, LF reavahetuse toetus
  • CR, CRLF, LF lerro-amaierak onartzen ditu
  • Tuki useille rivinvaihdoille: CR, CRLF ja LF
  • Prise en charge des retours à la ligne CR, CRLF, LF
  • Compatíbel cos fins de liña CR, CRLF e LF.
  • CR, CRLF, LF új sor támogatás
  • CR, CRLF, LF supporto de nove linea
  • Dukungan CR, CRLF, LF newline
  • Supporto ritorno a capo CR, CRLF, LF
  • CR, CRLF, LF 새 줄지 원
  • Støtte for ny linje med CR, CRLF eller LF
  • Ondersteuning van CR, CRLF, LF nieuwe-regel
  • Støtte for linjeskift med CR, CRLF eller LF
  • CR, CRLF, LF ਨਵੀਂ-ਲਾਈਨ ਸਹਿਯੋਗ
  • Obsługa znaków nowego wiersza CR, CRLF, LF
  • suporte de mudanças de linha com CR, CRLF, LF
  • Suporte de mudança de linha com CR, CRLF, LF
  • Поддержка переносов строк CR, CRLF, LF
  • Podpora nových riadkov CR, CRLF, LF
  • Podpora novim vrsticam vrste CR, CRLF in LF
  • подршка за различите крајеве редова (ЦР, ЛФ, ЦР+ЛФ)
  • podrška za različite krajeve redova (CR, LF, CR+LF)
  • подршка за различите крајеве редова (ЦР, ЛФ, ЦР+ЛФ)
  • podrška za različite krajeve redova (CR, LF, CR+LF)
  • Stöd för nyrad med CR, CRLF, LF
  • CR, CRLF, LF yeni satır desteği
  • Підтримка форматів розриву рядків CR, CRLF та LF.
  • xxCR, CRLF, LF newline supportxx
  • CR, CRLF, LF 换行支持
  • CR, CRLF, LF 等換行符號支援
  • Encoding support (utf-8, utf-16, ascii etc.)
  • دعم التّرميز (utf-8، وutf-16 وآسكي وغيرها)
  • Sofitu pa codificaciones (utf-8, utf-16, ascii... etc)
  • Admet codificacions (UTF-8, UTF-16, ASCII, etc.)
  • Admet codificacions (UTF-8, UTF-16, ASCII, etc.)
  • Podpora kódování (UTF-8, UTF-16, ASCII a další)
  • Understøttelse af kodning (utf-8, utf-16, ascii osv.)
  • Unterstützung für Kodierungen (utf-8, utf-16, ascii usw.)
  • Υποστήριξη κωδικοποίησης (utf-8, utf-16, ascii etc.)
  • Encoding support (utf-8, utf-16, ascii etc.)
  • Admite codificación ( (utf-8, utf-16, ascii, etc.)
  • Kodeeringute toetus (utf-8, utf-16, ascii jne.)
  • Kodeketa euskarria (utf-8, utf-16, ascii etab.)
  • Merkistökoodaustuki (UTF-8, UTF-16, ASCII jne.)
  • Prise en charge de l'encodage (utf-8, utf-16, ascii etc.)
  • Compatíbel con varios sistemas de codificación de texto (UTF-8, UTF-16, ASCII, etc.).
  • Különböző kódolások támogatása (UTF-8, UTF-16, ASCII, stb)
  • Supporto de codifica (utf-8, utf-16, ascii etc.)
  • Dukungan pengkodean (utf-8, utf-16, ascii dll.)
  • Supporto di codifica (utf-8, utf-16, ascii, ecc.)
  • 인코딩 지원(UTF-8, UTF-16, ASCII 등)
  • Støtte for tegnkoding (utf-8. utf-16, ascii osv.)
  • Ondersteuning voor codering (utf-8, utf-16, ascii etc.)
  • Støtte for teiknkoding (UTF-8, UTF-16, ASCII, ….)
  • ਇੰਕੋਡਿੰਗ ਸਹਿਯੋਗ (utf-8, utf-16, ascii ਆਦਿ)
  • Obsługa kodowań (utf-8, utf-16, ascii itp.)
  • Suporte para codificações de caracteres (utf-8, utf-16, ascii etc.)
  • Suporte para codificação de caracteres (utf-8, utf-16, ascii, etc.)
  • Поддержка различных кодировок (UTF-8, UTF-16, ASCII и других)
  • Podpora kódovania (utf-8, utf-16, ascii atď.)
  • Podpora kodiranju (utf-8, utf-16, ascii, itd.)
  • подршка за кодирања (УТФ‑8, УТФ‑16, аски, итд.)
  • podrška za kodiranja (UTF‑8, UTF‑16, ASCII, itd.)
  • подршка за кодирања (УТФ‑8, УТФ‑16, аски, итд.)
  • podrška za kodiranja (UTF‑8, UTF‑16, ASCII, itd.)
  • Kodningsstöd (UTF-8, UTF-16, ASCII etc.)
  • Kodlama desteği (utf-8, utf-16, ascii vb.)
  • Підтримка різноманітних кодувань (utf-8, utf-16, ascii тощо).
  • xxEncoding support (utf-8, utf-16, ascii etc.)xx
  • 编码支持 (UTF-8,UTF-16,ASCII 等)
  • 編碼支援(UTF-8,UTF-16,ASCII 等)
  • Encoding conversion
  • التّحويل بين التّرميزات
  • Conversió de codificació
  • Conversió de codificació
  • Převod kódování
  • Konvertering af kodning
  • Umwandlung von Kodierungen
  • Μετατροπή κωδικοποίησης
  • Encoding conversion
  • Conversión de codificación
  • Kodeeringute teisendamine
  • Kodeketa arteko bihurketa
  • Merkistökoodauksen muuntaminen
  • Conversion d'encodage
  • Conversión entre sistemas de codificación.
  • Kódolások közti konverzió
  • Conversion de codifica
  • Konversi pengkodean
  • Conversione di codifica
  • 인코딩 변환
  • Kodekonvertering
  • Conversie van codering
  • Støtte for teinkodingskonvertering
  • ਇੰਕੋਡਿੰਗ ਤਬਦੀਲੀ
  • Przekształcanie kodowań
  • Conversão de codificações
  • Conversão de codificação de caracteres
  • Преобразование кодировки
  • Konverzia kódovania
  • Pretvorba kodiranja
  • претварање кодирања
  • pretvaranje kodiranja
  • претварање кодирања
  • pretvaranje kodiranja
  • Kodningskonvertering
  • Kodlama dönüştürme
  • Підтримка можливості перетворення даних у різних кодуваннях.
  • xxEncoding conversionxx
  • 编码转换
  • 編碼轉換
  • Regular expression based find & replace
  • بحث واستبدال مبنيّ على التّعابير النّمطيّة
  • Expressió regular basada en cerca i substitució
  • Expressió regular basada en cerca i substitució
  • Funkce Najít a nahradit s podporou regulárních výrazů
  • Find og erstart, baseret på regulært udtryk
  • Reguläre Ausdrücke für Suchen und Ersetzen
  • Κανονικές εκφράσεις σε find & replace
  • Regular expression based find & replace
  • Función de buscar y sustituir basada en expresiones regulares
  • Regulaaravaldiste põhine otsimine ja asendamine
  • Adierazpen erregularretan oinarritutako aurkitu eta ordeztea
  • Säännöllisiin lausekkeisiin perustuva Etsi ja korvaa
  • Rechercher & remplacé utilisant les expressions rationnelles
  • Funcionalidade de atopar e substituír baseada en expresións regulares.
  • Reguláris kifejezéseken alapuló keresés és csere
  • Trovar basate sur expression regular expression based & reimplaciar
  • Ekspresi reguler berdasarkan cari & ganti
  • Ricerche e sostituzioni basate sulle espressioni regolari
  • 정규 표현식 기반 찾기 및 바꾸기
  • Finn og erstatt basert på regulære uttrykk
  • Op reguliere expressies gebaseerd zoeken & vervangen
  • «Finn og byt ut» basert på regulære uttrykk
  • ਨਿਯਮਤ ਸਮੀਕਰਨ ਦੇ ਨਾਲ ਲੈਸ ਖੋਜ ਤੇ ਤਬਦੀਲ ਕਰਨਾ
  • Znajdź i zastąp oparte na wyrażeniach regularnych
  • Pesquisa & substituição com base em expressões regulares
  • Pesquisa e substituição com base em expressões regulares
  • Поиск и замена с возможностью использования регулярных выражений
  • Regulárne výrazy založené na hľadaj a nahraď
  • Iskanje in zamenjava temelječa na regularnih izrazih
  • тражење и замена помоћу регуларних израза
  • traženje i zamena pomoću regularnih izraza
  • тражење и замена помоћу регуларних израза
  • traženje i zamena pomoću regularnih izraza
  • Sök och ersätt baserad på reguljära uttryck
  • Bul & değiştir için düzenli ifade desteği
  • Пошук і заміна фрагментів тексту на основі формальних виразів.
  • xxRegular expression based find & replacexx
  • 基于正则表达式的查找和替换
  • 搜尋與取代支援正則表達式
  • Powerful syntax highlighting and bracket matching
  • دعم قويّ لإبراز الصّياغة ومطابقة الأقواس
  • Ressaltat potent de sintaxi i concordança de parèntesis
  • Ressaltat potent de sintaxi i concordança de parèntesis
  • Pokročilé zvýrazňování syntaxe a párování závorek ve zdrojovém kódu
  • Kraftfuld fremhævning af syntaks og match af vinkelparenteser
  • Leistungsfähige Syntaxhervorhebung und Übereinstimmung von Klammern
  • Ισχυρό εργαλείο επισήμανσης σύνταξης και ταιριάσματος αγκυλών
  • Powerful syntax highlighting and bracket matching
  • Potente resaltado de sintaxis y de emparejado de paréntesis
  • Võimas süntaksi esiletõstmine ja sulgude sobitamine
  • Sintaxi nabarmentze eta parentesien bat-etortze ahaltsua
  • Tehokas syntaksin korostus; mukana myös toisiaan vastaavien sulkeiden korostus
  • Coloration syntaxique puissante et correspondances des parenthèses
  • Potente realzado de sintaxe e de parellas de parénteses.
  • Hatékony szintaxiskiemelés és zárójel-párosítás
  • Potente evidentiation de syntaxhe e coincidentia de parenthesis
  • Penyorotan sintaks dan pencocokan tandakurungsiku yang ampuh
  • Potente evidenziazione della sintassi e verifica delle parentesi
  • 강력한 구문 강조 및 괄호 일치
  • Kraftig syntaksfremheving og parentesparing
  • Krachtige syntaxisaccentuering en overeenkomende haakjes
  • Kraftig syntaksmerking og markering av parentespar
  • Zaawansowane podświetlanie składni i par nawiasów
  • Realce de sintaxe e correspondência de parêntesis
  • Realce de sintaxe e correspondência de parênteses
  • Подсветка синтаксиса и поиск парных скобок
  • Silné zvýrazňovanie syntaxe a párovanie zátvoriek
  • Zmogljivo poudarjanje skladnje in oklepajev
  • моћно истицање синтаксе и поклапање заграда
  • moćno isticanje sintakse i poklapanje zagrada
  • моћно истицање синтаксе и поклапање заграда
  • moćno isticanje sintakse i poklapanje zagrada
  • Kraftfull syntaxfärgläggning och parentesmatchning
  • Güçlü söz dizimi vurgulaması ve parantez eşleştirme
  • Потужні засоби підсвічування синтаксичних конструкцій та відповідних дужок.
  • xxPowerful syntax highlighting and bracket matchingxx
  • 强大的语法高亮和括号配对
  • 強大的語法突顯與括號對應
  • Code and text folding
  • لفّ النّصوص والأكواد
  • Plegat de codi i text
  • Plegat de codi i text
  • Skládání kódu a textu
  • Foldning af kode og tekst
  • Quelltext- und Textausblendung
  • Απόκρυψη κώδικα και κειμένου
  • Code and text folding
  • Plegado de código y de texto
  • Koodi ja teksti voltimine
  • Kode- eta testu-tolestea
  • Koodin ja tekstin laskostus
  • Repliement de code et de texte
  • Pregado de código e texto.
  • Kód- és szövegösszecsukás
  • Codice e texto plicabile
  • Pelipatan kode dan teks
  • Raggruppamento del codice e del testo
  • 코드 및 텍스트 접기
  • Kode- og tekstbryting
  • Code en tekst invouwen
  • Kode- og tekstfalding
  • ਕੋਡ ਅਤੇ ਟੈਕਸਟ ਸਮੇਟਣਾ
  • Zwijanie kodu i tekstu
  • Dobragem de código e texto
  • Dobradura de código e texto
  • Сворачивание блоков исходного кода
  • Zabaľovanie kódu a textu
  • Zvijanje kode in besedila
  • сажимање кода и текста
  • sažimanje koda i teksta
  • сажимање кода и текста
  • sažimanje koda i teksta
  • Kod- och textvikning
  • Kod ve metin katlama
  • Засоби згортання коду та тексту.
  • xxCode and text foldingxx
  • 代码和文本折叠
  • 程式碼與文字封裝
  • Infinite undo/redo support
  • دعم عدد متناهٍ من الإعادات والتّكرارات
  • Sofitu pa desfechura/refechura infinita
  • Admet desfer/refer infinits
  • Admet desfer/refer infinits
  • Nekonečná funkce Zpět/Vpřed
  • Understøttelse af uendeligt antal fortryd/gendan
  • Unterstützung für unbegrenztes Zurücknehmen und Wiederherstellen
  • Υποστήριξη για απεριόριστη αναίρεση/επαναφορά
  • Infinite undo/redo support
  • Función hacer y deshacer infinita
  • Piiranguteta tagasivõtmiste ja uuestitegemiste toetus
  • Desegin/berregin mugagabea onartzen du
  • Tuki rajattomalle kumoamiselle ja uudelleen tekemiselle
  • Prise en charge de l'annulation / refaire à l'infini
  • Funcionalidade de desfacer e refacer sen limitacións.
  • Végtelen visszavonás/újra végrehajtás
  • Supporto per annullation e re-facer infinite
  • Dukungan urungkan/lanjurkan tak terbatas
  • Supporto annulla/rifai infinito
  • 무한 실행 취소/다시 실행 지원
  • Ubegrenset støtte for angring/omgjøring
  • Ondersteuning van oneindig ongedaan maken/opnieuw
  • Uavgrensa støtte for angring/omgjering
  • ਬੇਅੰਤ ਵਾਪਸ/ਪਰਤਾਉਣ ਸਹਿਯੋਗ
  • Nieskończona historia cofania/przywracania
  • Suporte para desfazer/refazer ilimitado
  • Suporte para desfazer/refazer sem limite de ações
  • Поддержка бесконечной глубины отмены и повтора действий
  • Nekonečná podpora späť/znova
  • Podpora neskončno razveljavitvam/uveljavitvam
  • неограничено опозивање и понављање
  • neograničeno opozivanje i ponavljanje
  • неограничено опозивање и понављање
  • neograničeno opozivanje i ponavljanje
  • Obegränsat stöd för ångra och gör om
  • Sınırsız geri alma/yeniden yapma desteği
  • Підтримка нескінченного буфера скасування і повторення дій.
  • xxInfinite undo/redo supportxx
  • 无限制撤销/重做支持
  • 無限的復原/重做支援
  • Block selection mode
  • وضع التحديد الكتليّ
  • Mode de selecció per blocs
  • Mode de selecció per blocs
  • Režim blokového výběru
  • Blokmarkeringstilstand
  • Blockauswahlmodus
  • Λειτουργία επιλογής μπλοκ
  • Block selection mode
  • Modo de selección de bloque
  • Plokivaliku režiim
  • Bloke-hautapen modua
  • Lohkovalintatila
  • Mode de sélection par bloc
  • Modo de selección por bloques.
  • Blokkos kijelölési mód
  • Modo de selection de bloco
  • Mode seleksi blok
  • Modalità di selezione a blocchi
  • 블록 선택 모드
  • Blokkmerkingsmodus
  • Blokselectiemodus
  • Blokkmerkingsmodus
  • Tryb zaznaczania bloków
  • Modo de selecção em bloco
  • Modo de seleção de blocos
  • Режим блочного выделения
  • Režim výberu bloku
  • Način bločne izbire
  • режим блоковског избора
  • režim blokovskog izbora
  • режим блоковског избора
  • režim blokovskog izbora
  • Blockmarkeringsläge
  • Blok seçim kipi
  • Режим прямокутного вибору.
  • xxBlock selection modexx
  • 块选择模式
  • 區塊選擇模式
  • Auto indentation
  • الإزاحة الآليّة
  • Sangráu automáticu
  • Automatsko uvlačenje
  • Sagnat automàtic
  • Sagnat automàtic
  • Automaticky odsadit
  • Auto-indrykning
  • Automatisches einrücken
  • Αυτόματη χρήση εσοχών
  • Auto indentation
  • Sangrado automático
  • Automaatne taandus
  • Koskatze automatikoa
  • Automaattinen sisennys
  • Indentation automatique
  • Sangrado automático.
  • Automatikus behúzás
  • Auto indentation
  • Indentasi otomatis
  • Rientro automatico
  • 자동 들여쓰기
  • Automatisk innrykk
  • Automatisch inspringen
  • Automatiske innrykk
  • Automatyczne wcięcia
  • Indentação automática
  • Recuo automático
  • Автоматические отступы
  • Automatické odsadzovanie
  • Samodejno zamikanje
  • аутоматско увлачење
  • automatsko uvlačenje
  • аутоматско увлачење
  • automatsko uvlačenje
  • Automatisk indentering
  • Otomatik girintileme
  • Автовідступ.
  • xxAuto indentationxx
  • 自动缩进
  • 自動縮排
  • Auto completion support
  • دعم الإكمال الآليّ
  • Sofitu pa completáu automáticu
  • Admet compleció automàtica
  • Admet compleció automàtica
  • Podpora automatického doplňování
  • Understøttelse af automatisk fuldførelse
  • Unterstützung für Autovervollständigung
  • Υποστήριξη αυτόματης συμπλήρωσης
  • Auto completion support
  • Completado automático
  • Automaatse lõpetamise toetus
  • Osatze automatikoa onartzen du
  • Automaattitäydennyksen tuki
  • Prise en charge du complètement automatique
  • Funcionalidade de completación automática.
  • Automatikus kiegészítés
  • Supporto de auto completion
  • Dukungan penyelesaian otomatis
  • Supporto completamento automatico
  • 자동 완성 지원
  • Autofullføring
  • Ondersteuning voor automatische aanvulling
  • Støtte for autofullføring
  • Obsługa samouzupełniania
  • Modo de completação automática
  • Suporte para complementação automática
  • Поддержка автоматического дополнения текста
  • Podpora automatického ukončovania
  • Podpora samodejnemu dopolnjevanju
  • аутоматска допуна
  • automatska dopuna
  • аутоматска допуна
  • automatska dopuna
  • Stöd för automatisk komplettering
  • Otomatik tamamlama desteği
  • Підтримка автодоповнення.
  • xxAuto completion supportxx
  • 自动补全支持
  • 自動補完支援
  • Shell integration
  • التّكامل مع الصّدفة
  • Integración cola shell
  • Integració de l'intèrpret d'ordres
  • Integració de l'intèrpret d'ordres
  • Integrace shellu
  • Skal-integration
  • Einbindung einer Shell
  • Ενοποίηση κελύφους
  • Shell integration
  • Integración con la línea de órdenes
  • Shelliga lõimimine
  • Shell-arekin bateratzea
  • Komentotulkki-integraatio
  • Intégration avec l'interpréteur de commande
  • Integración co intérprete de ordes.
  • Parancsértelmező integrációja
  • Integration de shell
  • Integrasi dengan Shell
  • Integrazione con la shell
  • 셸 통합
  • Skall-integrasjon
  • Integratie van de shell
  • Skalintegrering
  • ਸ਼ੈੱਲ ਨਾਲ ਜੋੜ
  • Integracja z powłoką
  • Integração com a consola
  • Integração com o console
  • Вызов функций из командной строки
  • Integrácia shellu
  • Vgrajena lupina
  • уклапање шкољке
  • uklapanje školjke
  • уклапање шкољке
  • uklapanje školjke
  • Skalintegrering
  • Kabuk tümleştirmesi
  • Інтеграція з командною оболонкою.
  • xxShell integrationxx
  • Shell 集成
  • Shell 整合
  • Wide protocol support (http, ftp, ssh, webdav etc.) using kioslaves
  • دعم واسع للموافيق (http، وftp، وssh وwebdav وغيرها) عبر «عبيد KIO»
  • Implementació àmplia de protocols (http, ftp, ssh, webdav, etc.) usant kioslaves
  • Implementació àmplia de protocols (http, ftp, ssh, webdav, etc.) usant kioslaves
  • Široká podpora protokolů pomocí kioslaves (http, ftp, ssh, webdav atd.)
  • Bred understøttelse af protokoller (http, ftp, ssh, webdav osv.) ved brug af kioslaves
  • Unterstützung für viele Protokolle (http, ftp, ssh, webdav usw.) unter Verwendung von Ein-/Ausgabemodulen
  • Ευρεία υποστήριξη σε πρωτόκολλα (http, ftp, ssh, webdav κτλ.) με χρήση kioslaves
  • Wide protocol support (http, ftp, ssh, webdav etc.) using kioslaves
  • Admite numerosos protocolos mediante el uso de kioslaves (http, ftp, ssh, webdav, etc.)
  • Laialdane protokollide (http, ftp, ssh, webdav jne.) toetus KIO moodulite vahendusel
  • Protokolo-euskarri zabala (http, ftp, ssh, webdav etab.) «kioslave»(e)ak erabiliz
  • Laaja yhteyskäytäntötuki (HTTP, FTP, SSH, WebDAV jne.) käyttäen KIO-palveluita
  • Prise en charge de protocoles variés (http, ftp, ssh, webdav etc.) en utilisant le module d'entrée / sortie.
  • Compatibilidade con numerosos protocolos (HTTP, FTP, SSH, WebDAV, etc.) mediante KIO.
  • Protokollok (HTTP, FTP, SSH, Webdav, stb) széleskörű támogatása KIO-szolgáltatások használatával
  • Supporto de protocollo large (http, ftp, ssh, webdav etc.) usante kioslaves
  • Dukungan protokol yang luas (http, ftp, ssh, webdav dll.) menggunakan kioslaves
  • Ampio supporto di protocolli (http, ftp, ssh, WebDAV, ecc.) utilizzando i kioslave
  • KIO 슬레이브를 사용한 다양한 프로토콜 지원(HTTP, FTP, SSH, WebDAV 등)
  • Bred protokollstøtte (http, ftp, ssh, webdav osv.) ved bruk av kioslaver
  • Brede ondersteuning van protocollen (http, ftp, ssh, webdav etc.) met kioslaves
  • Omfattande protokollstøtte (http, ftp, ssh, webdav, …) ved bruk av KIO-slavar
  • Szeroka obsługa protokołów (http, ftp, ssh, webdav itp.) wykorzystująca kioslaves
  • Suporte abrangente de protocolos (HTTP, FTP, SSH, WebDAV etc.) com os kioslaves
  • Suporte abrangente de protocolos (HTTP, FTP, SSH, WebDAV, etc.) usando kioslaves
  • Поддержка множества файловых протоколов (HTTP, FTP, SSH, WebDAV и других) посредством kioslaves
  • Široká podpora protokolov (http, ftp, ssh, webdav atď.) pomocou kioslaves
  • Podpora za širok nabor protokolov (http, ftp, ssh, webdav, itd.) z uporabo kioslaves
  • подршка за многобројне протоколе преко У/И захвата (ХТТП, ФТП, ССХ, вебДАВ, итд.)
  • podrška za mnogobrojne protokole preko U/I zahvata (HTTP, FTP, SSH, WebDAV, itd.)
  • подршка за многобројне протоколе преко У/И захвата (ХТТП, ФТП, ССХ, вебДАВ, итд.)
  • podrška za mnogobrojne protokole preko U/I zahvata (HTTP, FTP, SSH, WebDAV, itd.)
  • Brett protokollstöd (HTTP, FTP, SSH, WebDav, etc.) med användning av I/O-slavar
  • Kioslaves kullanımıyla geniş protokol (http, ftp, ssh, webdav vb.) desteği
  • Широка підтримка протоколів (http, ftp, ssh, webdav тощо) на основі допоміжних засобів KIO.
  • xxWide protocol support (http, ftp, ssh, webdav etc.) using kioslavesxx
  • 通过 KIOSlave 支持广泛的协议 (HTTP, FTP, SSH, WebDAV 等)
  • 用 kioslave 提供廣泛的協定支援(http, ftp, ssh, webdav 等等)
  • Plugin architecture for the application and editor component
  • بنية ملحقات للتّطبيق ومكوّن المحرّر
  • Arquiteutura de plugins pa l'aplicación y el componente del editor
  • Arquitectura de connectors per a l'aplicació i el component d'edició
  • Arquitectura de connectors per a l'aplicació i el component d'edició
  • Podpora rozšiřujících modulů pro aplikaci editor
  • Plugin-arkitektur til programmet og editorkomponenten
  • Modul-Architektur für das Programm und die Editorkomponente
  • Αρχιτεκτονική προσθέτων για το συστατικό της εφαρμογής και του επεξεργαστή
  • Plugin architecture for the application and editor component
  • Arquitectura de complementos para el componente de aplicación y de editor.
  • Pluginapõhine ülesehitus nii rakenduses kui ka redaktorikomponendis
  • Osagarrien arkitektura aplikazioarentzako eta editore osagaiarentzako
  • Liitännäisarkkitehtuuri sovellukselle ja muokkainosalle
  • Architecture à modules complémentaires pour l'application et le composant éditeur
  • Arquitectura de complementos para o aplicativo e o compoñente do editor.
  • Bővítmények támogatása az alkalmazásban és a szerkesztő komponensben
  • Archiutectura de plugin per le componente de editor e application
  • Arsitektur pengaya untuk aplikasi dan komponen penyunting
  • Architettura con estensioni per i componenti dell'applicazione e dell'editor
  • 프로그램과 편집기 구성 요소 플러그인 구조
  • Modularkitektur for programmet og redigeringskomponenten
  • Plug-in-architectuur voor de applicatie- en editor-component
  • Støtte for programtillegg til programmet og redigeringskomponenten
  • ਐਪਲੀਕੇਸ਼ਨ ਅਤੇ ਐਡੀਟਰ ਭਾਗਾਂ ਲਈ ਪਲੱਗਇਨ ਢਾਂਚਾ
  • Architektura wtyczek dla programów i składników edytora
  • Arquitectura modular para o componente de edição e a aplicação
  • Arquitetura modular para o aplicativo e componente de edição
  • Модульная архитектура приложения и компонента редактора
  • Architektúra pluginov pre aplikáciu a komponent editora
  • Arhitektura vstavkov za program in sestavni del urejevalnika
  • архитектура прикључка за компоненте програма и уређивача
  • arhitektura priključka za komponente programa i uređivača
  • архитектура прикључка за компоненте програма и уређивача
  • arhitektura priključka za komponente programa i uređivača
  • Insticksarkitektur för programmet och editorkomponenten
  • Uygulama ve düzenleyici bileşeni için eklenti mimarisi
  • Архітектура роботи з додатками для компонентів програми та редактора.
  • xxPlugin architecture for the application and editor componentxx
  • 为应用程序和编辑器组件而生的插件架构
  • 用於應用程式與編輯器組件的外掛架構
  • Customizable shortcuts
  • اختصارات يمكن تخصيصها
  • Atayos personalizables
  • Dreceres personalitzables
  • Dreceres personalitzables
  • Nastavitelné klávesové zkratky
  • Genveje som kan tilpasses
  • Benutzerdefinierte Kurzbefehle
  • Προσαρμόσιμες συντομεύσεις
  • Customisable shortcuts
  • Accesos rápidos personalizables
  • Kohandatavad kiirklahvid
  • Norbere erara egoki ditzakeen lasterbide
  • Mukautettavat pikanäppäimet
  • Raccourcis personnalisables
  • Atallos personalizábeis.
  • Testre szabható gyorsbillentyűk
  • Vias breve personalisabile
  • Pintasan dapat dikustomisasi
  • Scorciatoie personalizzabili
  • 사용자 정의 가능한 단축키
  • Brukerstyrte snarveier
  • Aanpasbare sneltoetsen
  • Tilpassbare snarvegar
  • Dostosowywalne skróty
  • Atalhos de teclado personalizados
  • Atalhos de teclado personalizados
  • Настраиваемые комбинации клавиш
  • Prispôsobiteľné skratky
  • Prilagodljive bližnjice
  • прилагодљиве пречице
  • prilagodljive prečice
  • прилагодљиве пречице
  • prilagodljive prečice
  • Anpassningsbara genvägar
  • Özelleştirilebilir kısayollar
  • Придатні до налаштовування клавіатурні скорочення.
  • xxCustomizable shortcutsxx
  • 自定义快捷键
  • 自定快捷鍵
  • Integrated command line
  • سطر أوامر مدمج
  • Llinia de comandos integrada
  • Línia d'ordres integrada
  • Línia d'ordres integrada
  • Integrovaný příkazový řádek
  • Ingegreret kommandolinje
  • Integrierte Befehlszeile
  • Ενοποιημένη γραμμή εντολών
  • Integrated command line
  • Línea de órdenes integrada
  • Lõimitud käsurida
  • Bateratutako komando-lerroa
  • Integroitu komentorivi
  • Ligne de commande intégrée
  • Liña de ordes integrada.
  • Integrált parancssor
  • Linea de commando integrate
  • Baris perintah yang terintegrasi
  • Riga di comando integrata
  • 내장 명령행
  • Integrert kommandolinje
  • Geïntegreerde opdrachtregel
  • Integrert kommandolinje
  • ਵਿਚੇ ਮੌਜੂਦ ਕਮਾਂਡ ਲਾਈਨ
  • Zintegrowany wiersz poleceń
  • Linha de comandos integrada
  • Linha de comando integrada
  • Встроенный командный терминал
  • Integrovaný príkazový riadok
  • Vgrajena ukazna vrstica
  • уклопљена командна линија
  • uklopljena komandna linija
  • уклопљена командна линија
  • uklopljena komandna linija
  • Integrerad kommandorad
  • Tümleşik komut satırı
  • Інтегрований засіб роботи з командним рядком.
  • xxIntegrated command linexx
  • 集成命令行
  • 整合指令行
  • Scriptable using JavaScript
  • يمكن سكربتته عبر جافاسكربت
  • Es pot crear scripts usant JavaScript
  • Es pot crear scripts usant JavaScript
  • Podpora skriptování v JavaScriptu
  • Scriptbar ved brug af JavaScript
  • Skriptfähig über JavaScript
  • Συγγραφή σεναρίων με JavaScript
  • Scriptable using JavaScript
  • Uso de scripts de JavaScript
  • Skriptimine JavaScriptiga
  • Javascript erabiliz hedagarria
  • Skriptattavissa JavaScriptillä
  • Automatisable en utilisant JavaScript
  • A súa funcionalidade pódese ampliar mediante scripts escritos en JavaScript.
  • Szkriptelhetőség JavaScripttel
  • Scribibile per scripts usante JavaScript
  • Skrip menggunakan JavaScript
  • Creazione di script con JavaScript
  • JavaScript를 사용한 스크립트
  • Kan skriptes med Javascript
  • JavaScript te gebruiken in scripts
  • Støtte for JavaScript-støtte
  • Możliwość używania skryptów JavaScript
  • Programável através de JavaScript
  • Possibilidade de uso de scripts feitos em JavaScript
  • Поддержка сценариев на языке JavaScript
  • Skriptovateľný pomocou JavaScriptu
  • Podpora skriptom z uporabo JavaScript
  • скриптовање помоћу јаваскрипта
  • skriptovanje pomoću JavaScripta
  • скриптовање помоћу јаваскрипта
  • skriptovanje pomoću JavaScripta
  • Skriptbar med användning av Javascript
  • JavaScript kullanılarak betikleştirilebilir
  • Можливість керування програмою за допомогою скриптів мовою JavaScript.
  • xxScriptable using JavaScriptxx
  • 支持 JavaScript 脚本化
  • 可使用 JavaScript
http://kate-editor.org/ https://bugs.kde.org/enter_bug.cgi?format=guided&product=kate http://docs.kde.org/stable/en/applications/kate/index.html https://www.kde.org/community/donations/?app=kate&source=appdata Editing code with Kate Edición de códigu con Kate Edició de codi amb el Kate Edició de codi amb el Kate Bearbeitung von Quelltext mit Kate Επεξεργασία κώδικα με το Kate Editing code with Kate Edición de código con Kate Kate-rekin kodea editatzea Koodin muokkaus Katella Édition de code avec Kate Editando código con Kate Modificante codice con Kate Pengeditan kode menggunakan Kate Modificare il codice con Kate Kate로 코드 편집 Broncode bewerken met Kate Redigering av kjeldekode med Kate Edytowanie kodu z Kate Edição de código com o Kate Editando código no Kate Редактирование исходного текста программы в Kate Úprava kódu s Kate Redigera kod med Kate Kate ile kod düzenleme Редагування коду за допомогою Kate xxEditing code with Katexx 使用 Kate 编辑代码 使用 Kate 編輯程式碼 http://kde.org/images/screenshots/kate.png KDE kate diff --git a/kate/kateapp.cpp b/kate/kateapp.cpp index 9a3ecca22..19318980f 100644 --- a/kate/kateapp.cpp +++ b/kate/kateapp.cpp @@ -1,491 +1,486 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2002 Joseph Wenninger This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateapp.h" #include "kateviewmanager.h" #include "katemainwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../urlinfo.h" /** * singleton instance pointer */ static KateApp *appSelf = Q_NULLPTR; Q_LOGGING_CATEGORY(LOG_KATE, "kate", QtWarningMsg) KateApp::KateApp(const QCommandLineParser &args) : m_args(args) , m_wrapper(appSelf = this) , m_docManager(this) , m_adaptor(this) , m_pluginManager(this) , m_sessionManager(this) { /** * re-route some signals to application wrapper */ connect(&m_docManager, &KateDocManager::documentCreated, &m_wrapper, &KTextEditor::Application::documentCreated); connect(&m_docManager, &KateDocManager::documentWillBeDeleted, &m_wrapper, &KTextEditor::Application::documentWillBeDeleted); connect(&m_docManager, &KateDocManager::documentDeleted, &m_wrapper, &KTextEditor::Application::documentDeleted); connect(&m_docManager, &KateDocManager::aboutToCreateDocuments, &m_wrapper, &KTextEditor::Application::aboutToCreateDocuments); connect(&m_docManager, &KateDocManager::documentsCreated, &m_wrapper, &KTextEditor::Application::documentsCreated); /** * handle mac os x like file open request via event filter */ qApp->installEventFilter(this); } KateApp::~KateApp() { /** * unregister from dbus before we get unusable... */ if (QDBusConnection::sessionBus().interface()) { m_adaptor.emitExiting(); QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/MainApplication")); } /** * delete all main windows before the document manager & co. die */ while (!m_mainWindows.isEmpty()) { // mainwindow itself calls KateApp::removeMainWindow(this) delete m_mainWindows[0]; } } KateApp *KateApp::self() { return appSelf; } bool KateApp::init() { // set KATE_PID for use in child processes qputenv("KATE_PID", QStringLiteral("%1").arg(QCoreApplication::applicationPid()).toLatin1().constData()); // handle restore different if (qApp->isSessionRestored()) { restoreKate(); } else { // let us handle our command line args and co ;) // we can exit here if session chooser decides if (!startupKate()) { // session chooser told to exit kate return false; } } - // application dbus interface - if (QDBusConnection::sessionBus().interface()) { - QDBusConnection::sessionBus().registerObject(QStringLiteral("/MainApplication"), this); - } - return true; } void KateApp::restoreKate() { KConfig *sessionConfig = KConfigGui::sessionConfig(); // activate again correct session!!! QString lastSession(sessionConfig->group("General").readEntry("Last Session", QString())); sessionManager()->activateSession(lastSession, false, false); // plugins KateApp::self()->pluginManager()->loadConfig(sessionConfig); // restore the files we need m_docManager.restoreDocumentList(sessionConfig); // restore all windows ;) for (int n = 1; KMainWindow::canBeRestored(n); n++) { newMainWindow(sessionConfig, QString::number(n)); } // oh, no mainwindow, create one, should not happen, but make sure ;) if (mainWindowsCount() == 0) { newMainWindow(); } } bool KateApp::startupKate() { // user specified session to open if (m_args.isSet(QStringLiteral("start"))) { sessionManager()->activateSession(m_args.value(QStringLiteral("start")), false); } else if (m_args.isSet(QStringLiteral("startanon"))) { sessionManager()->activateAnonymousSession(); } else if (!m_args.isSet(QStringLiteral("stdin")) && (m_args.positionalArguments().count() == 0)) { // only start session if no files specified // let the user choose session if possible if (!sessionManager()->chooseSession()) { // we will exit kate now, notify the rest of the world we are done KStartupInfo::appStarted(KStartupInfo::startupId()); return false; } } else { sessionManager()->activateAnonymousSession(); } // oh, no mainwindow, create one, should not happen, but make sure ;) if (mainWindowsCount() == 0) { newMainWindow(); } // notify about start KStartupInfo::setNewStartupId(activeKateMainWindow(), KStartupInfo::startupId()); QTextCodec *codec = m_args.isSet(QStringLiteral("encoding")) ? QTextCodec::codecForName(m_args.value(QStringLiteral("encoding")).toUtf8()) : nullptr; bool tempfileSet = m_args.isSet(QStringLiteral("tempfile")); KTextEditor::Document *doc = nullptr; const QString codec_name = codec ? QString::fromLatin1(codec->name()) : QString(); // Bug 397913: Reverse the order here so the new tabs are opened in same order as the files were passed in on the command line QString positionalArgument; for (int i = m_args.positionalArguments().count() - 1; i >= 0; --i) { positionalArgument = m_args.positionalArguments().at(i); UrlInfo info(positionalArgument); // this file is no local dir, open it, else warn bool noDir = !info.url.isLocalFile() || !QFileInfo(info.url.toLocalFile()).isDir(); if (noDir) { doc = openDocUrl(info.url, codec_name, tempfileSet); if (info.cursor.isValid()) { setCursor(info.cursor.line(), info.cursor.column()); } else if (info.url.hasQuery()) { QUrlQuery q(info.url); QString lineStr = q.queryItemValue(QStringLiteral("line")); QString columnStr = q.queryItemValue(QStringLiteral("column")); int line = lineStr.toInt(); if (line > 0) line--; int column = columnStr.toInt(); if (column > 0) column--; setCursor(line, column); } } else { KMessageBox::sorry(activeKateMainWindow(), i18n("The file '%1' could not be opened: it is not a normal file, it is a folder.", info.url.toString())); } } // handle stdin input if (m_args.isSet(QStringLiteral("stdin"))) { QTextStream input(stdin, QIODevice::ReadOnly); // set chosen codec if (codec) { input.setCodec(codec); } QString line; QString text; do { line = input.readLine(); text.append(line + QLatin1Char('\n')); } while (!line.isNull()); openInput(text, codec_name); } else if (doc) { activeKateMainWindow()->viewManager()->activateView(doc); } int line = 0; int column = 0; bool nav = false; if (m_args.isSet(QStringLiteral("line"))) { line = m_args.value(QStringLiteral("line")).toInt() - 1; nav = true; } if (m_args.isSet(QStringLiteral("column"))) { column = m_args.value(QStringLiteral("column")).toInt() - 1; nav = true; } if (nav && activeKateMainWindow()->viewManager()->activeView()) { activeKateMainWindow()->viewManager()->activeView()->setCursorPosition(KTextEditor::Cursor(line, column)); } activeKateMainWindow()->setAutoSaveSettings(); return true; } void KateApp::shutdownKate(KateMainWindow *win) { if (!win->queryClose_internal()) { return; } sessionManager()->saveActiveSession(true); /** * all main windows will be cleaned up * in the KateApp destructor after the event * loop is left */ QApplication::quit(); } KatePluginManager *KateApp::pluginManager() { return &m_pluginManager; } KateDocManager *KateApp::documentManager() { return &m_docManager; } KateSessionManager *KateApp::sessionManager() { return &m_sessionManager; } bool KateApp::openUrl(const QUrl &url, const QString &encoding, bool isTempFile) { return openDocUrl(url, encoding, isTempFile); } bool KateApp::isOnActivity(const QString &activity) { for (const auto& window : m_mainWindows) { const KWindowInfo info(window->winId(), nullptr, NET::WM2Activities); const auto activities = info.activities(); // handle special case of "on all activities" if (activities.isEmpty() || activities.contains(activity)) return true; } return false; } KTextEditor::Document *KateApp::openDocUrl(const QUrl &url, const QString &encoding, bool isTempFile) { KateMainWindow *mainWindow = activeKateMainWindow(); if (!mainWindow) { return nullptr; } QTextCodec *codec = encoding.isEmpty() ? nullptr : QTextCodec::codecForName(encoding.toLatin1()); // this file is no local dir, open it, else warn bool noDir = !url.isLocalFile() || !QFileInfo(url.toLocalFile()).isDir(); KTextEditor::Document *doc = nullptr; if (noDir) { // open a normal file if (codec) { doc = mainWindow->viewManager()->openUrl(url, QString::fromLatin1(codec->name()), true, isTempFile); } else { doc = mainWindow->viewManager()->openUrl(url, QString(), true, isTempFile); } } else KMessageBox::sorry(mainWindow, i18n("The file '%1' could not be opened: it is not a normal file, it is a folder.", url.url())); return doc; } bool KateApp::setCursor(int line, int column) { KateMainWindow *mainWindow = activeKateMainWindow(); if (!mainWindow) { return false; } if (auto v = mainWindow->viewManager()->activeView()) { v->removeSelection(); v->setCursorPosition(KTextEditor::Cursor(line, column)); } return true; } bool KateApp::openInput(const QString &text, const QString &encoding) { activeKateMainWindow()->viewManager()->openUrl(QUrl(), encoding, true); if (!activeKateMainWindow()->viewManager()->activeView()) { return false; } KTextEditor::Document *doc = activeKateMainWindow()->viewManager()->activeView()->document(); if (!doc) { return false; } return doc->setText(text); } KateMainWindow *KateApp::newMainWindow(KConfig *sconfig_, const QString &sgroup_) { KConfig *sconfig = sconfig_ ? sconfig_ : KSharedConfig::openConfig().data(); QString sgroup = !sgroup_.isEmpty() ? sgroup_ : QStringLiteral("MainWindow0"); KateMainWindow *mainWindow = new KateMainWindow(sconfig, sgroup); mainWindow->show(); return mainWindow; } void KateApp::addMainWindow(KateMainWindow *mainWindow) { m_mainWindows.push_back(mainWindow); } void KateApp::removeMainWindow(KateMainWindow *mainWindow) { m_mainWindows.removeAll(mainWindow); } KateMainWindow *KateApp::activeKateMainWindow() { if (m_mainWindows.isEmpty()) { return nullptr; } int n = m_mainWindows.indexOf(static_cast((static_cast(QCoreApplication::instance())->activeWindow()))); if (n < 0) { n = 0; } return m_mainWindows[n]; } int KateApp::mainWindowsCount() const { return m_mainWindows.size(); } int KateApp::mainWindowID(KateMainWindow *window) { return m_mainWindows.indexOf(window); } KateMainWindow *KateApp::mainWindow(int n) { if (n < m_mainWindows.size()) { return m_mainWindows[n]; } return nullptr; } void KateApp::emitDocumentClosed(const QString &token) { m_adaptor.emitDocumentClosed(token); } KTextEditor::Plugin *KateApp::plugin(const QString &name) { return m_pluginManager.plugin(name); } bool KateApp::eventFilter(QObject *obj, QEvent *event) { /** * handle mac os like file open */ if (event->type() == QEvent::FileOpen) { /** * try to open and activate the new document, like we would do for stuff * opened via dbus */ QFileOpenEvent *foe = static_cast(event); KTextEditor::Document *doc = openDocUrl(foe->url(), QString(), false); if (doc && activeKateMainWindow()) { activeKateMainWindow()->viewManager()->activateView(doc); } return true; } /** * else: pass over to default implementation */ return QObject::eventFilter(obj, event); } void KateApp::remoteMessageReceived(const QString &message, QObject *) { /** * try to parse message, ignore if no object */ const QJsonDocument jsonMessage = QJsonDocument::fromJson(message.toUtf8()); if (!jsonMessage.isObject()) return; /** * open all passed urls */ const QJsonArray urls = jsonMessage.object().value(QLatin1String("urls")).toArray(); Q_FOREACH(QJsonValue urlObject, urls) { /** * get url meta data */ const QUrl url = urlObject.toObject().value(QLatin1String("url")).toVariant().toUrl(); const int line = urlObject.toObject().value(QLatin1String("line")).toVariant().toInt(); const int column = urlObject.toObject().value(QLatin1String("column")).toVariant().toInt(); /** * open file + set line/column if requested */ openUrl(url, QString(), false); if (line >= 0 && column >= 0) { setCursor(line, column); } } // try to activate current window m_adaptor.activate(); } diff --git a/kate/kateapp.h b/kate/kateapp.h index d593fb9fc..29ddee44d 100644 --- a/kate/kateapp.h +++ b/kate/kateapp.h @@ -1,374 +1,376 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2002 Joseph Wenninger This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KATE_APP_H__ #define __KATE_APP_H__ #include -#include "kateprivate_export.h" +#include "katetests_export.h" #include "katemainwindow.h" #include "katedocmanager.h" #include "katepluginmanager.h" #include "katesessionmanager.h" #include "kateappadaptor.h" #include #include class KateSessionManager; class KateMainWindow; class KatePluginManager; class KateDocManager; class KateAppCommands; class KateAppAdaptor; class QCommandLineParser; /** * Kate Application * This class represents the core kate application object */ class KATE_TESTS_EXPORT KateApp : public QObject { Q_OBJECT /** * constructors & accessor to app object + plugin interface for it */ public: /** * application constructor */ KateApp(const QCommandLineParser &args); /** * get kate inited * @return false, if application should exit */ bool init(); /** * application destructor */ ~KateApp() override; /** * static accessor to avoid casting ;) * @return app instance */ static KateApp *self(); /** * KTextEditor::Application wrapper * @return KTextEditor::Application wrapper. */ KTextEditor::Application *wrapper() { return &m_wrapper; } /** * kate init */ private: /** * restore a old kate session */ void restoreKate(); /** * try to start kate * @return success, if false, kate should exit */ bool startupKate(); /** * kate shutdown */ public: /** * shutdown kate application * @param win mainwindow which is used for dialogs */ void shutdownKate(KateMainWindow *win); /** * other accessors for global unique instances */ public: /** * accessor to plugin manager * @return plugin manager instance */ KatePluginManager *pluginManager(); /** * accessor to document manager * @return document manager instance */ KateDocManager *documentManager(); /** * accessor to session manager * @return session manager instance */ KateSessionManager *sessionManager(); /** * window management */ public: /** * create a new main window, use given config if any for restore * @param sconfig session config object * @param sgroup session group for this window * @return new constructed main window */ KateMainWindow *newMainWindow(KConfig *sconfig = nullptr, const QString &sgroup = QString()); /** * add the mainwindow given * should be called in mainwindow constructor * @param mainWindow window to remove */ void addMainWindow(KateMainWindow *mainWindow); /** * removes the mainwindow given, DOES NOT DELETE IT * should be called in mainwindow destructor * @param mainWindow window to remove */ void removeMainWindow(KateMainWindow *mainWindow); /** * give back current active main window * can only be 0 at app start or exit * @return current active main window */ KateMainWindow *activeKateMainWindow(); /** * give back number of existing main windows * @return number of main windows */ int mainWindowsCount() const; /** * give back the window you want * @param n window index * @return requested main window */ KateMainWindow *mainWindow(int n); int mainWindowID(KateMainWindow *window); /** * some stuff for the dcop API */ public: /** * open url with given encoding * used by kate if --use given * @param url filename * @param encoding encoding name * @param isTempFile whether the file is temporary * @return success */ bool openUrl(const QUrl &url, const QString &encoding, bool isTempFile); /** * checks if the current instance is in a given activity * @param activity activity to check * @return true if the window is in the given activity, false otherwise */ bool isOnActivity(const QString &activity); KTextEditor::Document *openDocUrl(const QUrl &url, const QString &encoding, bool isTempFile); void emitDocumentClosed(const QString &token); /** * position cursor in current active view * will clear selection * @param line line to set * @param column column to set * @return success */ bool setCursor(int line, int column); /** * helper to handle stdin input * open a new document/view, fill it with the text given * @param text text to fill in the new doc/view * @param encoding encoding to set for the document, if any * @return success */ bool openInput(const QString &text, const QString &encoding); // // KTextEditor::Application interface, called by wrappers via invokeMethod // public Q_SLOTS: /** * Get a list of all main windows. * @return all main windows */ QList mainWindows() { // assemble right list QList windows; - for (int i = 0; i < m_mainWindows.size(); ++i) { - windows.push_back(m_mainWindows[i]->wrapper()); + windows.reserve(m_mainWindows.size()); + + for (const auto mainWindow : qAsConst(m_mainWindows)) { + windows.push_back(mainWindow->wrapper()); } return windows; } /** * Accessor to the active main window. * \return a pointer to the active mainwindow */ KTextEditor::MainWindow *activeMainWindow() { // either return wrapper or nullptr if (KateMainWindow *a = activeKateMainWindow()) { return a->wrapper(); } return nullptr; } /** * Get a list of all documents that are managed by the application. * This might contain less documents than the editor has in his documents () list. * @return all documents the application manages */ QList documents() { return m_docManager.documentList(); } /** * Get the document with the URL \p url. * if multiple documents match the searched url, return the first found one... * \param url the document's URL * \return the document with the given \p url or NULL, if none found */ KTextEditor::Document *findUrl(const QUrl &url) { return m_docManager.findDocument(url); } /** * Open the document \p url with the given \p encoding. * if the url is empty, a new empty document will be created * \param url the document's url * \param encoding the preferred encoding. If encoding is QString() the * encoding will be guessed or the default encoding will be used. * \return a pointer to the created document */ KTextEditor::Document *openUrl(const QUrl &url, const QString &encoding = QString()) { return m_docManager.openUrl(url, encoding); } /** * Close the given \p document. If the document is modified, user will be asked if he wants that. * \param document the document to be closed * \return \e true on success, otherwise \e false */ bool closeDocument(KTextEditor::Document *document) { return m_docManager.closeDocument(document); } /** * Close a list of documents. If any of them are modified, user will be asked if he wants that. * Use this, if you want to close multiple documents at once, as the application might * be able to group the "do you really want that" dialogs into one. * \param documents list of documents to be closed * \return \e true on success, otherwise \e false */ bool closeDocuments(const QList &documents) { return m_docManager.closeDocumentList(documents); } /** * Get a plugin for the plugin with with identifier \p name. * \param name the plugin's name * \return pointer to the plugin if a plugin with \p name is loaded, otherwise nullptr */ KTextEditor::Plugin *plugin(const QString &name); /** * Ask app to quit. The app might interact with the user and decide that * quitting is not possible and return false. * * \return true if the app could quit */ bool quit() { shutdownKate(activeKateMainWindow()); return true; } /** * A message is received from an external instance, if we use QtSingleApplication * * \p message is a serialized message (at the moment just the file list separated by ';') * \p socket is the QLocalSocket used for the communication */ void remoteMessageReceived(const QString &message, QObject *socket); protected: /** * Event filter for QApplication to handle mac os like file open */ bool eventFilter(QObject *obj, QEvent *event) override; private: /** * kate's command line args */ const QCommandLineParser &m_args; /** * known main windows */ QList m_mainWindows; /** * Wrapper of application for KTextEditor */ KTextEditor::Application m_wrapper; /** * document manager */ KateDocManager m_docManager; /** * dbus interface */ KateAppAdaptor m_adaptor; /** * plugin manager */ KatePluginManager m_pluginManager; /** * session manager */ KateSessionManager m_sessionManager; }; #endif diff --git a/kate/kateappadaptor.cpp b/kate/kateappadaptor.cpp index 742ff833d..e990b8955 100644 --- a/kate/kateappadaptor.cpp +++ b/kate/kateappadaptor.cpp @@ -1,133 +1,142 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateappadaptor.h" #include "kateapp.h" #include "katesessionmanager.h" #include "katedocmanager.h" #include "katemainwindow.h" #include "katedebug.h" + +#include #include +#include + +/** + * add the adapter to the global application instance to have + * it auto-register with KDBusService, see bug 410742 + */ KateAppAdaptor::KateAppAdaptor(KateApp *app) - : QDBusAbstractAdaptor(app) + : QDBusAbstractAdaptor(qApp) , m_app(app) {} void KateAppAdaptor::activate() { KateMainWindow *win = m_app->activeKateMainWindow(); if (!win) { return; } // like QtSingleApplication win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->raise(); win->activateWindow(); // try to raise window, see bug 407288 - KWindowSystem::raiseWindow(win->winId()); + KStartupInfo::setNewStartupId(win, KStartupInfo::startupId()); + KWindowSystem::activateWindow(win->effectiveWinId()); } bool KateAppAdaptor::openUrl(const QString &url, const QString &encoding) { return m_app->openUrl(QUrl(url), encoding, false); } bool KateAppAdaptor::openUrl(const QString &url, const QString &encoding, bool isTempFile) { qCDebug(LOG_KATE) << "openURL"; return m_app->openUrl(QUrl(url), encoding, isTempFile); } bool KateAppAdaptor::isOnActivity(const QString &activity) { return m_app->isOnActivity(activity); } //----------- QString KateAppAdaptor::tokenOpenUrl(const QString &url, const QString &encoding) { KTextEditor::Document *doc = m_app->openDocUrl(QUrl(url), encoding, false); if (!doc) { return QStringLiteral("ERROR"); } return QStringLiteral("%1").arg((qptrdiff)doc); } QString KateAppAdaptor::tokenOpenUrl(const QString &url, const QString &encoding, bool isTempFile) { qCDebug(LOG_KATE) << "openURL"; KTextEditor::Document *doc = m_app->openDocUrl(QUrl(url), encoding, isTempFile); if (!doc) { return QStringLiteral("ERROR"); } return QStringLiteral("%1").arg((qptrdiff)doc); } QString KateAppAdaptor::tokenOpenUrlAt(const QString &url, int line, int column, const QString &encoding, bool isTempFile) { qCDebug(LOG_KATE) << "openURLAt"; KTextEditor::Document *doc = m_app->openDocUrl(QUrl(url), encoding, isTempFile); if (!doc) { return QStringLiteral("ERROR"); } m_app->setCursor(line, column); return QStringLiteral("%1").arg((qptrdiff)doc); } //-------- bool KateAppAdaptor::setCursor(int line, int column) { return m_app->setCursor(line, column); } bool KateAppAdaptor::openInput(const QString &text, const QString &encoding) { return m_app->openInput(text, encoding); } bool KateAppAdaptor::activateSession(const QString &session) { return m_app->sessionManager()->activateSession(session); } int KateAppAdaptor::desktopNumber() { KWindowInfo appInfo(m_app->activeKateMainWindow()->winId(), NET::WMDesktop); return appInfo.desktop(); } QString KateAppAdaptor::activeSession() { return m_app->sessionManager()->activeSession()->name(); } void KateAppAdaptor::emitExiting() { emit exiting(); } void KateAppAdaptor::emitDocumentClosed(const QString &token) { documentClosed(token); } diff --git a/kate/kateconfigdialog.cpp b/kate/kateconfigdialog.cpp index bfbc9bb40..47e854c37 100644 --- a/kate/kateconfigdialog.cpp +++ b/kate/kateconfigdialog.cpp @@ -1,417 +1,467 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2002 Joseph Wenninger Copyright (C) 2007 Mirko Stocker This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateconfigdialog.h" #include "ui_sessionconfigwidget.h" #include "katemainwindow.h" #include "katedocmanager.h" #include "katepluginmanager.h" #include "kateconfigplugindialogpage.h" #include "kateviewmanager.h" #include "kateapp.h" #include "katesessionmanager.h" #include "katedebug.h" #include "katequickopenmodel.h" #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include KateConfigDialog::KateConfigDialog(KateMainWindow *parent, KTextEditor::View *view) : KPageDialog(parent) , m_mainWindow(parent) , m_view(view) { setFaceType(Tree); setWindowTitle(i18n("Configure")); setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel | QDialogButtonBox::Help); setObjectName(QStringLiteral("configdialog")); KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup cgGeneral = KConfigGroup(config, "General"); buttonBox()->button(QDialogButtonBox::Apply)->setEnabled(false); KPageWidgetItem *applicationItem = addPage(new QWidget, i18n("Application")); applicationItem->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other"))); applicationItem->setHeader(i18n("Application Options")); applicationItem->setCheckable(false); applicationItem->setEnabled(false); m_applicationPage = applicationItem; //BEGIN General page QFrame *generalFrame = new QFrame; KPageWidgetItem *item = addSubPage(applicationItem, generalFrame, i18n("General")); item->setHeader(i18n("General Options")); item->setIcon(QIcon::fromTheme(QStringLiteral("go-home"))); setCurrentPage(item); QVBoxLayout *layout = new QVBoxLayout(generalFrame); layout->setContentsMargins(0, 0, 0, 0); // GROUP with the one below: "Behavior" QGroupBox *buttonGroup = new QGroupBox(i18n("&Behavior"), generalFrame); QVBoxLayout *vbox = new QVBoxLayout; layout->addWidget(buttonGroup); // modified files notification m_modNotifications = new QCheckBox( i18n("Wa&rn about files modified by foreign processes"), buttonGroup); m_modNotifications->setChecked(parent->modNotificationEnabled()); m_modNotifications->setWhatsThis(i18n( "If enabled, when Kate receives focus you will be asked what to do with " "files that have been modified on the hard disk. If not enabled, you will " "be asked what to do with a file that has been modified on the hard disk only " "when that file is tried to be saved.")); connect(m_modNotifications, &QCheckBox::toggled, this, &KateConfigDialog::slotChanged); vbox->addWidget(m_modNotifications); // Closing last file closes Kate m_modCloseAfterLast = new QCheckBox( i18n("Close Kate entirely when the last file is closed"), buttonGroup); m_modCloseAfterLast->setChecked(parent->modCloseAfterLast()); m_modCloseAfterLast->setWhatsThis(i18n( "If enabled, Kate will shutdown when the last file being edited is closed, " "otherwise a blank page will open so that you can start a new file.")); connect(m_modCloseAfterLast, &QCheckBox::toggled, this, &KateConfigDialog::slotChanged); vbox->addWidget(m_modCloseAfterLast); buttonGroup->setLayout(vbox); // GROUP with the one below: "Meta-information" buttonGroup = new QGroupBox(i18n("Meta-Information"), generalFrame); vbox = new QVBoxLayout; layout->addWidget(buttonGroup); // save meta infos m_saveMetaInfos = new QCheckBox(buttonGroup); m_saveMetaInfos->setText(i18n("Keep &meta-information past sessions")); m_saveMetaInfos->setChecked(KateApp::self()->documentManager()->getSaveMetaInfos()); m_saveMetaInfos->setWhatsThis(i18n( "Check this if you want document configuration like for example " "bookmarks to be saved past editor sessions. The configuration will be " "restored if the document has not changed when reopened.")); connect(m_saveMetaInfos, &QCheckBox::toggled, this, &KateConfigDialog::slotChanged); vbox->addWidget(m_saveMetaInfos); // meta infos days QFrame *metaInfos = new QFrame(buttonGroup); QHBoxLayout *hlayout = new QHBoxLayout(metaInfos); metaInfos->setEnabled(KateApp::self()->documentManager()->getSaveMetaInfos()); QLabel *label = new QLabel(i18n("&Delete unused meta-information after:"), metaInfos); hlayout->addWidget(label); m_daysMetaInfos = new KPluralHandlingSpinBox(metaInfos); m_daysMetaInfos->setMaximum(180); m_daysMetaInfos->setSpecialValueText(i18nc("The special case of 'Delete unused meta-information after'", "(never)")); m_daysMetaInfos->setSuffix(ki18ncp("The suffix of 'Delete unused meta-information after'", " day", " days")); m_daysMetaInfos->setValue(KateApp::self()->documentManager()->getDaysMetaInfos()); hlayout->addWidget(m_daysMetaInfos); label->setBuddy(m_daysMetaInfos); connect(m_saveMetaInfos, &QCheckBox::toggled, metaInfos, &QFrame::setEnabled); connect(m_daysMetaInfos, static_cast(&KPluralHandlingSpinBox::valueChanged), this, &KateConfigDialog::slotChanged); vbox->addWidget(metaInfos); buttonGroup->setLayout(vbox); // quick search buttonGroup = new QGroupBox(i18n("&Quick Open"), generalFrame); - hlayout = new QHBoxLayout(buttonGroup); + vbox = new QVBoxLayout; + buttonGroup->setLayout(vbox); + // quick open match mode + hlayout = new QHBoxLayout; label = new QLabel(i18n("&Match Mode:"), buttonGroup); hlayout->addWidget(label); m_cmbQuickOpenMatchMode = new QComboBox(buttonGroup); hlayout->addWidget(m_cmbQuickOpenMatchMode); label->setBuddy(m_cmbQuickOpenMatchMode); m_cmbQuickOpenMatchMode->addItem(i18n("Filename"), QVariant(KateQuickOpenModel::Columns::FileName)); m_cmbQuickOpenMatchMode->addItem(i18n("Filepath"), QVariant(KateQuickOpenModel::Columns::FilePath)); m_cmbQuickOpenMatchMode->setCurrentIndex(m_cmbQuickOpenMatchMode->findData(m_mainWindow->quickOpenMatchMode())); m_mainWindow->setQuickOpenMatchMode(m_cmbQuickOpenMatchMode->currentData().toInt()); connect(m_cmbQuickOpenMatchMode, static_cast(&QComboBox::currentIndexChanged), this, &KateConfigDialog::slotChanged); + vbox->addLayout(hlayout); + // quick open list mode + hlayout = new QHBoxLayout; + label = new QLabel(i18n("&List Mode:"), buttonGroup); + hlayout->addWidget(label); + m_cmbQuickOpenListMode = new QComboBox(buttonGroup); + hlayout->addWidget(m_cmbQuickOpenListMode); + label->setBuddy(m_cmbQuickOpenListMode); + m_cmbQuickOpenListMode->addItem(i18n("Current Project Files"), QVariant(KateQuickOpenModel::List::CurrentProject)); + m_cmbQuickOpenListMode->addItem(i18n("All Projects Files"), QVariant(KateQuickOpenModel::List::AllProjects)); + m_cmbQuickOpenListMode->setCurrentIndex(m_cmbQuickOpenListMode->findData(m_mainWindow->quickOpenListMode())); + m_mainWindow->setQuickOpenListMode(static_cast(m_cmbQuickOpenListMode->currentData().toInt())); + connect(m_cmbQuickOpenListMode, static_cast(&QComboBox::currentIndexChanged), this, &KateConfigDialog::slotChanged); + vbox->addLayout(hlayout); layout->addWidget(buttonGroup); layout->addStretch(1); // :-] works correct without autoadd //END General page //BEGIN Session page QWidget *sessionsPage = new QWidget(); item = addSubPage(applicationItem, sessionsPage, i18n("Sessions")); item->setHeader(i18n("Session Management")); item->setIcon(QIcon::fromTheme(QStringLiteral("view-history"))); sessionConfigUi = new Ui::SessionConfigWidget(); sessionConfigUi->setupUi(sessionsPage); // restore view config sessionConfigUi->restoreVC->setChecked( cgGeneral.readEntry("Restore Window Configuration", true) ); connect(sessionConfigUi->restoreVC, &QCheckBox::toggled, this, &KateConfigDialog::slotChanged); sessionConfigUi->spinBoxRecentFilesCount->setValue(recentFilesMaxCount()); connect(sessionConfigUi->spinBoxRecentFilesCount, static_cast(&QSpinBox::valueChanged), this, &KateConfigDialog::slotChanged); QString sesStart (cgGeneral.readEntry ("Startup Session", "manual")); if (sesStart == QStringLiteral("new")) sessionConfigUi->startNewSessionRadioButton->setChecked (true); else if (sesStart == QStringLiteral("last")) sessionConfigUi->loadLastUserSessionRadioButton->setChecked (true); else sessionConfigUi->manuallyChooseSessionRadioButton->setChecked (true); connect(sessionConfigUi->startNewSessionRadioButton, &QRadioButton::toggled, this, &KateConfigDialog::slotChanged); connect(sessionConfigUi->loadLastUserSessionRadioButton, &QRadioButton::toggled, this, &KateConfigDialog::slotChanged); connect(sessionConfigUi->manuallyChooseSessionRadioButton, &QRadioButton::toggled, this, &KateConfigDialog::slotChanged); //END Session page //BEGIN Plugins page QFrame *page = new QFrame(this); QVBoxLayout *vlayout = new QVBoxLayout(page); vlayout->setContentsMargins(0, 0, 0, 0); vlayout->setSpacing(0); KateConfigPluginPage *configPluginPage = new KateConfigPluginPage(page, this); vlayout->addWidget(configPluginPage); connect(configPluginPage, &KateConfigPluginPage::changed, this, &KateConfigDialog::slotChanged); item = addSubPage(applicationItem, page, i18n("Plugins")); item->setHeader(i18n("Plugin Manager")); item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-plugin"))); KatePluginList &pluginList(KateApp::self()->pluginManager()->pluginList()); foreach(const KatePluginInfo & plugin, pluginList) { if (plugin.load) { addPluginPage(plugin.plugin); } } //END Plugins page // editor widgets from kwrite/kwdialog m_editorPage = addPage(new QWidget, i18n("Editor Component")); m_editorPage->setIcon(QIcon::fromTheme(QStringLiteral("accessories-text-editor"))); m_editorPage->setHeader(i18n("Editor Component Options")); m_editorPage->setCheckable(false); m_editorPage->setEnabled(false); addEditorPages(); connect(this, &KateConfigDialog::accepted, this, &KateConfigDialog::slotApply); connect(buttonBox()->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &KateConfigDialog::slotApply); connect(buttonBox()->button(QDialogButtonBox::Help), &QPushButton::clicked, this, &KateConfigDialog::slotHelp); connect(this, &KateConfigDialog::currentPageChanged, this, &KateConfigDialog::slotCurrentPageChanged); - m_dataChanged = false; resize(minimumSizeHint()); + + // ensure no stray signals already set this! + m_dataChanged = false; } KateConfigDialog::~KateConfigDialog() { delete sessionConfigUi; } void KateConfigDialog::addEditorPages() { for (int i = 0; i < KTextEditor::Editor::instance()->configPages(); ++i) { KTextEditor::ConfigPage *page = KTextEditor::Editor::instance()->configPage(i, this); connect(page, &KTextEditor::ConfigPage::changed, this, &KateConfigDialog::slotChanged); m_editorPages.push_back(page); KPageWidgetItem *item = addSubPage(m_editorPage, page, page->name()); item->setHeader(page->fullName()); item->setIcon(page->icon()); } } void KateConfigDialog::addPluginPage(KTextEditor::Plugin *plugin) { for (int i = 0; i < plugin->configPages(); i++) { QFrame *page = new QFrame(); QVBoxLayout *layout = new QVBoxLayout(page); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); KTextEditor::ConfigPage *cp = plugin->configPage(i, page); page->layout()->addWidget(cp); KPageWidgetItem *item = addSubPage(m_applicationPage, page, cp->name()); item->setHeader(cp->fullName()); item->setIcon(cp->icon()); PluginPageListItem *info = new PluginPageListItem; info->plugin = plugin; info->pageParent = page; info->pluginPage = cp; info->idInPlugin = i; info->pageWidgetItem = item; connect(info->pluginPage, &KTextEditor::ConfigPage::changed, this, &KateConfigDialog::slotChanged); m_pluginPages.insert(item, info); } } void KateConfigDialog::slotCurrentPageChanged(KPageWidgetItem *current, KPageWidgetItem * /*before*/) { PluginPageListItem *info = m_pluginPages[current]; if (!info) { return; } if (info->pluginPage) { return; } qCDebug(LOG_KATE) << "creating config page (should not get here)"; info->pluginPage = info->plugin->configPage(info->idInPlugin, info->pageParent); info->pageParent->layout()->addWidget(info->pluginPage); info->pluginPage->show(); connect(info->pluginPage, &KTextEditor::ConfigPage::changed, this, &KateConfigDialog::slotChanged); } void KateConfigDialog::removePluginPage(KTextEditor::Plugin *plugin) { QList remove; for (QHash::const_iterator it = m_pluginPages.constBegin(); it != m_pluginPages.constEnd(); ++it) { PluginPageListItem *pli = it.value(); if (!pli) { continue; } if (pli->plugin == plugin) { remove.append(it.key()); } } qCDebug(LOG_KATE) << remove.count(); while (!remove.isEmpty()) { KPageWidgetItem *wItem = remove.takeLast(); PluginPageListItem *pItem = m_pluginPages.take(wItem); delete pItem->pluginPage; delete pItem->pageParent; removePage(wItem); delete pItem; } } void KateConfigDialog::slotApply() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); // if data changed apply the kate app stuff if (m_dataChanged) { KConfigGroup cg = KConfigGroup(config, "General"); cg.writeEntry("Restore Window Configuration", sessionConfigUi->restoreVC->isChecked()); cg.writeEntry("Recent File List Entry Count", sessionConfigUi->spinBoxRecentFilesCount->value()); if (sessionConfigUi->startNewSessionRadioButton->isChecked()) { cg.writeEntry("Startup Session", "new"); } else if (sessionConfigUi->loadLastUserSessionRadioButton->isChecked()) { cg.writeEntry("Startup Session", "last"); } else { cg.writeEntry("Startup Session", "manual"); } cg.writeEntry("Save Meta Infos", m_saveMetaInfos->isChecked()); KateApp::self()->documentManager()->setSaveMetaInfos(m_saveMetaInfos->isChecked()); cg.writeEntry("Days Meta Infos", m_daysMetaInfos->value()); KateApp::self()->documentManager()->setDaysMetaInfos(m_daysMetaInfos->value()); cg.writeEntry("Modified Notification", m_modNotifications->isChecked()); m_mainWindow->setModNotificationEnabled(m_modNotifications->isChecked()); cg.writeEntry("Close After Last", m_modCloseAfterLast->isChecked()); m_mainWindow->setModCloseAfterLast(m_modCloseAfterLast->isChecked()); cg.writeEntry("Quick Open Search Mode", m_cmbQuickOpenMatchMode->currentData().toInt()); m_mainWindow->setQuickOpenMatchMode(m_cmbQuickOpenMatchMode->currentData().toInt()); + cg.writeEntry("Quick Open List Mode", m_cmbQuickOpenListMode->currentData().toInt()); + m_mainWindow->setQuickOpenListMode(static_cast(m_cmbQuickOpenListMode->currentData().toInt())); + // patch document modified warn state const QList &docs = KateApp::self()->documentManager()->documentList(); foreach(KTextEditor::Document * doc, docs) if (qobject_cast(doc)) { qobject_cast(doc)->setModifiedOnDiskWarning(!m_modNotifications->isChecked()); } m_mainWindow->saveOptions(); // save plugin config !! KateSessionManager *sessionmanager = KateApp::self()->sessionManager(); KConfig *sessionConfig = sessionmanager->activeSession()->config(); KateApp::self()->pluginManager()->writeConfig(sessionConfig); } foreach(PluginPageListItem * plugin, m_pluginPages) { if (!plugin) { continue; } if (plugin->pluginPage) { plugin->pluginPage->apply(); } } // apply ktexteditor pages foreach(KTextEditor::ConfigPage * page, m_editorPages) page->apply(); config->sync(); m_dataChanged = false; buttonBox()->button(QDialogButtonBox::Apply)->setEnabled(false); } void KateConfigDialog::slotChanged() { m_dataChanged = true; buttonBox()->button(QDialogButtonBox::Apply)->setEnabled(true); } void KateConfigDialog::showAppPluginPage(KTextEditor::Plugin *p, uint id) { foreach(PluginPageListItem * plugin, m_pluginPages) { if ((plugin->plugin == p) && (id == plugin->idInPlugin)) { setCurrentPage(plugin->pageWidgetItem); break; } } } void KateConfigDialog::slotHelp() { QDesktopServices::openUrl(QUrl(QStringLiteral("help:/"))); } int KateConfigDialog::recentFilesMaxCount() { int maxItems = KConfigGroup(KSharedConfig::openConfig(), "General").readEntry("Recent File List Entry Count", 10); return maxItems; } +void KateConfigDialog::closeEvent(QCloseEvent *event) +{ + if (!m_dataChanged) { + event->accept(); + return; + } + + const auto response = KMessageBox::warningYesNoCancel(this, + i18n("You have unsaved changes. Do you want to apply the changes or discard them?"), + i18n("Warning"), + KStandardGuiItem::save(), + KStandardGuiItem::discard(), + KStandardGuiItem::cancel()); + switch (response) { + case KMessageBox::Yes: + slotApply(); + Q_FALLTHROUGH(); + case KMessageBox::No: + event->accept(); + break; + case KMessageBox::Cancel: + event->ignore(); + break; + default: + break; + } +} diff --git a/kate/kateconfigdialog.h b/kate/kateconfigdialog.h index 990d71ed8..e0cea7beb 100644 --- a/kate/kateconfigdialog.h +++ b/kate/kateconfigdialog.h @@ -1,101 +1,106 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2002 Joseph Wenninger Copyright (C) 2007 Mirko Stocker This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __kate_configdialog_h__ #define __kate_configdialog_h__ #include #include #include #include #include #include class QCheckBox; class QComboBox; class QSpinBox; class KateMainWindow; class KPluralHandlingSpinBox; namespace Ui { class SessionConfigWidget; } struct PluginPageListItem { KTextEditor::Plugin *plugin; uint idInPlugin; KTextEditor::ConfigPage *pluginPage; QWidget *pageParent; KPageWidgetItem *pageWidgetItem; }; class KateConfigDialog : public KPageDialog { Q_OBJECT public: KateConfigDialog(KateMainWindow *parent, KTextEditor::View *view); ~KateConfigDialog() override; public: // static /** * Reads the value from the given open config. If not present in config yet then * the default value 10 is used. */ static int recentFilesMaxCount(); public: void addPluginPage(KTextEditor::Plugin *plugin); void removePluginPage(KTextEditor::Plugin *plugin); void showAppPluginPage(KTextEditor::Plugin *plugin, uint id); + protected Q_SLOTS: void slotApply(); void slotChanged(); void slotHelp(); void slotCurrentPageChanged(KPageWidgetItem *current, KPageWidgetItem *before); +protected: + void closeEvent(QCloseEvent *event) override; + private: KateMainWindow *m_mainWindow; KTextEditor::View *m_view; - bool m_dataChanged; + bool m_dataChanged = false; QCheckBox *m_modNotifications; QCheckBox *m_modCloseAfterLast; QCheckBox *m_saveMetaInfos; KPluralHandlingSpinBox *m_daysMetaInfos; QComboBox * m_cmbQuickOpenMatchMode; + QComboBox * m_cmbQuickOpenListMode; // Sessions Page Ui::SessionConfigWidget *sessionConfigUi; QHash m_pluginPages; QList m_editorPages; KPageWidgetItem *m_applicationPage; KPageWidgetItem *m_editorPage; void addEditorPages(); }; #endif diff --git a/kate/kateconfigplugindialogpage.cpp b/kate/kateconfigplugindialogpage.cpp index 209037c95..41d48d9b3 100644 --- a/kate/kateconfigplugindialogpage.cpp +++ b/kate/kateconfigplugindialogpage.cpp @@ -1,125 +1,125 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2002 Joseph Wenninger Copyright (C) 2007 Mirko Stocker This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateconfigplugindialogpage.h" #include "kateapp.h" #include "katepluginmanager.h" #include "kateconfigdialog.h" #include #include class KatePluginListItem : public QTreeWidgetItem { public: KatePluginListItem(bool checked, KatePluginInfo *info); KatePluginInfo *info() const { return mInfo; } protected: void stateChange(bool); private: KatePluginInfo *mInfo; }; KatePluginListItem::KatePluginListItem(bool checked, KatePluginInfo *info) : QTreeWidgetItem() , mInfo(info) { setCheckState(0, checked ? Qt::Checked : Qt::Unchecked); } KatePluginListView::KatePluginListView(QWidget *parent) : QTreeWidget(parent) { setRootIsDecorated(false); connect(this, &KatePluginListView::itemChanged, this, &KatePluginListView::stateChanged); } void KatePluginListView::stateChanged(QTreeWidgetItem *item) { emit stateChange(static_cast(item), item->checkState(0) == Qt::Checked); } KateConfigPluginPage::KateConfigPluginPage(QWidget *parent, KateConfigDialog *dialog) : QFrame(parent) , myDialog(dialog) { QVBoxLayout *layout = new QVBoxLayout(this); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); KatePluginListView *listView = new KatePluginListView(this); layout->addWidget(listView); QStringList headers; headers << i18n("Name") << i18n("Description"); listView->setHeaderLabels(headers); listView->setWhatsThis(i18n("Here you can see all available Kate plugins. Those with a check mark are loaded, and will be loaded again the next time Kate is started.")); KatePluginList &pluginList(KateApp::self()->pluginManager()->pluginList()); - for (KatePluginList::iterator it = pluginList.begin(); it != pluginList.end(); ++it) { - QTreeWidgetItem *item = new KatePluginListItem(it->load, &(*it)); - item->setText(0, it->metaData.name()); - item->setText(1, it->metaData.description()); + for (auto& pluginInfo : pluginList) { + QTreeWidgetItem *item = new KatePluginListItem(pluginInfo.load, &pluginInfo); + item->setText(0, pluginInfo.metaData.name()); + item->setText(1, pluginInfo.metaData.description()); listView->addTopLevelItem(item); } listView->resizeColumnToContents(0); listView->sortByColumn(0, Qt::AscendingOrder); connect(listView, &KatePluginListView::stateChange, this, &KateConfigPluginPage::stateChange); } void KateConfigPluginPage::stateChange(KatePluginListItem *item, bool b) { if (b) { loadPlugin(item); } else { unloadPlugin(item); } emit changed(); } void KateConfigPluginPage::loadPlugin(KatePluginListItem *item) { const bool ok = KateApp::self()->pluginManager()->loadPlugin(item->info()); if (!ok) { return; } KateApp::self()->pluginManager()->enablePluginGUI(item->info()); myDialog->addPluginPage(item->info()->plugin); item->setCheckState(0, Qt::Checked); } void KateConfigPluginPage::unloadPlugin(KatePluginListItem *item) { myDialog->removePluginPage(item->info()->plugin); KateApp::self()->pluginManager()->unloadPlugin(item->info()); item->setCheckState(0, Qt::Unchecked); } diff --git a/kate/katedocmanager.cpp b/kate/katedocmanager.cpp index 390163ba4..9b0c84fea 100644 --- a/kate/katedocmanager.cpp +++ b/kate/katedocmanager.cpp @@ -1,593 +1,598 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2002 Joseph Wenninger Copyright (C) 2007 Flavio Castelli This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "katedocmanager.h" #include "kateapp.h" #include "katemainwindow.h" #include "kateviewmanager.h" #include "katesavemodifieddialog.h" #include "katedebug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include KateDocManager::KateDocManager(QObject *parent) : QObject(parent) , m_metaInfos(QStringLiteral("katemetainfos"), KConfig::NoGlobals) , m_saveMetaInfos(true) , m_daysMetaInfos(0) { // set our application wrapper KTextEditor::Editor::instance()->setApplication(KateApp::self()->wrapper()); // create one doc, we always have at least one around! createDoc(); } KateDocManager::~KateDocManager() { // write metainfos? if (m_saveMetaInfos) { // saving meta-infos when file is saved is not enough, we need to do it once more at the end saveMetaInfos(m_docList); // purge saved filesessions if (m_daysMetaInfos > 0) { const QStringList groups = m_metaInfos.groupList(); QDateTime def(QDate(1970, 1, 1)); - for (QStringList::const_iterator it = groups.begin(); it != groups.end(); ++it) { - QDateTime last = m_metaInfos.group(*it).readEntry("Time", def); + for (const auto& group : groups) { + QDateTime last = m_metaInfos.group(group).readEntry("Time", def); if (last.daysTo(QDateTime::currentDateTimeUtc()) > m_daysMetaInfos) { - m_metaInfos.deleteGroup(*it); + m_metaInfos.deleteGroup(group); } } } } qDeleteAll(m_docInfos); } KTextEditor::Document *KateDocManager::createDoc(const KateDocumentInfo &docInfo) { KTextEditor::Document *doc = KTextEditor::Editor::instance()->createDocument(this); // turn of the editorpart's own modification dialog, we have our own one, too! const KConfigGroup generalGroup(KSharedConfig::openConfig(), "General"); bool ownModNotification = generalGroup.readEntry("Modified Notification", false); if (qobject_cast(doc)) { qobject_cast(doc)->setModifiedOnDiskWarning(!ownModNotification); } m_docList.append(doc); m_docInfos.insert(doc, new KateDocumentInfo(docInfo)); // connect internal signals... connect(doc, &KTextEditor::Document::modifiedChanged, this, &KateDocManager::slotModChanged1); connect(doc, SIGNAL(modifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)), this, SLOT(slotModifiedOnDisc(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason))); // we have a new document, show it the world emit documentCreated(doc); emit documentCreatedViewManager(doc); // return our new document return doc; } KateDocumentInfo *KateDocManager::documentInfo(KTextEditor::Document *doc) { return m_docInfos.contains(doc) ? m_docInfos[doc] : nullptr; } -KTextEditor::Document *KateDocManager::findDocument(const QUrl &url) const +static QUrl +normalizeUrl(const QUrl & url) { - QUrl u(url.adjusted(QUrl::NormalizePathSegments)); - // Resolve symbolic links for local files (done anyway in KTextEditor) - if (u.isLocalFile()) { - QString normalizedUrl = QFileInfo(u.toLocalFile()).canonicalFilePath(); + if (url.isLocalFile()) { + QString normalizedUrl = QFileInfo(url.toLocalFile()).canonicalFilePath(); if (!normalizedUrl.isEmpty()) { - u = QUrl::fromLocalFile(normalizedUrl); + return QUrl::fromLocalFile(normalizedUrl); } } - foreach(KTextEditor::Document * it, m_docList) { + // else: cleanup only the .. stuff + return url.adjusted(QUrl::NormalizePathSegments); +} + +KTextEditor::Document *KateDocManager::findDocument(const QUrl &url) const +{ + const QUrl u(normalizeUrl(url)); + for(KTextEditor::Document *it : m_docList) { if (it->url() == u) { return it; } } - return nullptr; } QList KateDocManager::openUrls(const QList &urls, const QString &encoding, bool isTempFile, const KateDocumentInfo &docInfo) { QList docs; emit aboutToCreateDocuments(); foreach(const QUrl & url, urls) { docs << openUrl(url, encoding, isTempFile, docInfo); } emit documentsCreated(docs); return docs; } KTextEditor::Document *KateDocManager::openUrl(const QUrl &url, const QString &encoding, bool isTempFile, const KateDocumentInfo &docInfo) { // special handling: if only one unmodified empty buffer in the list, // keep this buffer in mind to close it after opening the new url KTextEditor::Document *untitledDoc = nullptr; if ((documentList().count() == 1) && (!documentList().at(0)->isModified() && documentList().at(0)->url().isEmpty())) { untitledDoc = documentList().first(); } // // create new document // - QUrl u(url.adjusted(QUrl::NormalizePathSegments)); + const QUrl u(normalizeUrl(url)); KTextEditor::Document *doc = nullptr; // always new document if url is empty... if (!u.isEmpty()) { doc = findDocument(u); } if (!doc) { if (untitledDoc) { // reuse the untitled document which is not needed auto & info = m_docInfos.find(untitledDoc).value(); delete info; info = new KateDocumentInfo(docInfo); doc = untitledDoc; } else { doc = createDoc(docInfo); } if (!encoding.isEmpty()) { doc->setEncoding(encoding); } if (!u.isEmpty()) { doc->openUrl(u); loadMetaInfos(doc, u); } } // // if needed, register as temporary file // if (isTempFile && u.isLocalFile()) { QFileInfo fi(u.toLocalFile()); if (fi.exists()) { m_tempFiles[doc] = qMakePair(u, fi.lastModified()); qCDebug(LOG_KATE) << "temporary file will be deleted after use unless modified: " << u; } } return doc; } bool KateDocManager::closeDocuments(const QList &documents, bool closeUrl) { if (documents.isEmpty()) { return false; } saveMetaInfos(documents); emit aboutToDeleteDocuments(documents); int last = 0; bool success = true; foreach(KTextEditor::Document * doc, documents) { if (closeUrl && !doc->closeUrl()) { success = false; // get out on first error break; } if (closeUrl && m_tempFiles.contains(doc)) { QFileInfo fi(m_tempFiles[ doc ].first.toLocalFile()); if (fi.lastModified() <= m_tempFiles[ doc ].second || KMessageBox::questionYesNo(KateApp::self()->activeKateMainWindow(), i18n("The supposedly temporary file %1 has been modified. " "Do you want to delete it anyway?", m_tempFiles[ doc ].first.url(QUrl::PreferLocalFile)), i18n("Delete File?")) == KMessageBox::Yes) { KIO::del(m_tempFiles[ doc ].first, KIO::HideProgressInfo); qCDebug(LOG_KATE) << "Deleted temporary file " << m_tempFiles[ doc ].first; m_tempFiles.remove(doc); } else { m_tempFiles.remove(doc); qCDebug(LOG_KATE) << "The supposedly temporary file " << m_tempFiles[ doc ].first.url() << " have been modified since loaded, and has not been deleted."; } } KateApp::self()->emitDocumentClosed(QString::number((qptrdiff)doc)); // document will be deleted, soon emit documentWillBeDeleted(doc); // really delete the document and its infos delete m_docInfos.take(doc); delete m_docList.takeAt(m_docList.indexOf(doc)); // document is gone, emit our signals emit documentDeleted(doc); last++; } /** * never ever empty the whole document list * do this before documentsDeleted is emitted, to have no flicker */ if (m_docList.isEmpty()) { createDoc(); } emit documentsDeleted(documents.mid(last)); return success; } bool KateDocManager::closeDocument(KTextEditor::Document *doc, bool closeUrl) { if (!doc) { return false; } QList documents; documents.append(doc); return closeDocuments(documents, closeUrl); } bool KateDocManager::closeDocumentList(QList documents) { QList modifiedDocuments; foreach(KTextEditor::Document * document, documents) { if (document->isModified()) { modifiedDocuments.append(document); } } if (modifiedDocuments.size() > 0 && !KateSaveModifiedDialog::queryClose(nullptr, modifiedDocuments)) { return false; } return closeDocuments(documents, false); // Do not show save/discard dialog } bool KateDocManager::closeAllDocuments(bool closeUrl) { /** * just close all documents */ return closeDocuments(m_docList, closeUrl); } bool KateDocManager::closeOtherDocuments(KTextEditor::Document *doc) { /** * close all documents beside the passed one */ QList documents = m_docList; documents.removeOne(doc); return closeDocuments(documents); } /** * Find all modified documents. * @return Return the list of all modified documents. */ QList KateDocManager::modifiedDocumentList() { QList modified; foreach(KTextEditor::Document * doc, m_docList) { if (doc->isModified()) { modified.append(doc); } } return modified; } bool KateDocManager::queryCloseDocuments(KateMainWindow *w) { int docCount = m_docList.count(); foreach(KTextEditor::Document * doc, m_docList) { if (doc->url().isEmpty() && doc->isModified()) { int msgres = KMessageBox::warningYesNoCancel(w, i18n("

The document '%1' has been modified, but not saved.

" "

Do you want to save your changes or discard them?

", doc->documentName()), i18n("Close Document"), KStandardGuiItem::save(), KStandardGuiItem::discard()); if (msgres == KMessageBox::Cancel) { return false; } if (msgres == KMessageBox::Yes) { const QUrl url = QFileDialog::getSaveFileUrl(w, i18n("Save As")); if (!url.isEmpty()) { if (!doc->saveAs(url)) { return false; } } else { return false; } } } else { if (!doc->queryClose()) { return false; } } } // document count changed while queryClose, abort and notify user if (m_docList.count() > docCount) { KMessageBox::information(w, i18n("New file opened while trying to close Kate, closing aborted."), i18n("Closing Aborted")); return false; } return true; } void KateDocManager::saveAll() { foreach(KTextEditor::Document * doc, m_docList) if (doc->isModified()) { doc->documentSave(); } } void KateDocManager::saveSelected(const QList &docList) { foreach(KTextEditor::Document * doc, docList) { if (doc->isModified()) { doc->documentSave(); } } } void KateDocManager::reloadAll() { // reload all docs that are NOT modified on disk foreach(KTextEditor::Document * doc, m_docList) { if (! documentInfo(doc)->modifiedOnDisc) { doc->documentReload(); } } // take care of all documents that ARE modified on disk KateApp::self()->activeKateMainWindow()->showModOnDiskPrompt(); } void KateDocManager::closeOrphaned() { QList documents; foreach(KTextEditor::Document * doc, m_docList) { KateDocumentInfo *info = documentInfo(doc); if (info && !info->openSuccess) { documents.append(doc); } } closeDocuments(documents); } void KateDocManager::saveDocumentList(KConfig *config) { KConfigGroup openDocGroup(config, "Open Documents"); openDocGroup.writeEntry("Count", m_docList.count()); int i = 0; foreach(KTextEditor::Document * doc, m_docList) { KConfigGroup cg(config, QStringLiteral("Document %1").arg(i)); doc->writeSessionConfig(cg); i++; } } void KateDocManager::restoreDocumentList(KConfig *config) { KConfigGroup openDocGroup(config, "Open Documents"); unsigned int count = openDocGroup.readEntry("Count", 0); if (count == 0) { return; } QProgressDialog progress; progress.setWindowTitle(i18n("Starting Up")); progress.setLabelText(i18n("Reopening files from the last session...")); progress.setModal(true); progress.setCancelButton(nullptr); progress.setRange(0, count); for (unsigned int i = 0; i < count; i++) { KConfigGroup cg(config, QStringLiteral("Document %1").arg(i)); KTextEditor::Document *doc = nullptr; if (i == 0) { doc = m_docList.first(); } else { doc = createDoc(); } connect(doc, SIGNAL(completed()), this, SLOT(documentOpened())); connect(doc, &KParts::ReadOnlyPart::canceled, this, &KateDocManager::documentOpened); doc->readSessionConfig(cg); progress.setValue(i); } } void KateDocManager::slotModifiedOnDisc(KTextEditor::Document *doc, bool b, KTextEditor::ModificationInterface::ModifiedOnDiskReason reason) { if (m_docInfos.contains(doc)) { m_docInfos[doc]->modifiedOnDisc = b; m_docInfos[doc]->modifiedOnDiscReason = reason; slotModChanged1(doc); } } /** * Load file's meta-information if the checksum didn't change since last time. */ bool KateDocManager::loadMetaInfos(KTextEditor::Document *doc, const QUrl &url) { if (!m_saveMetaInfos) { return false; } if (!m_metaInfos.hasGroup(url.toDisplayString())) { return false; } const QByteArray checksum = doc->checksum().toHex(); bool ok = true; if (!checksum.isEmpty()) { KConfigGroup urlGroup(&m_metaInfos, url.toDisplayString()); const QString old_checksum = urlGroup.readEntry("Checksum"); if (QString::fromLatin1(checksum) == old_checksum) { QSet flags; if (documentInfo(doc)->openedByUser) { flags << QStringLiteral ("SkipEncoding"); } flags << QStringLiteral ("SkipUrl"); doc->readSessionConfig(urlGroup, flags); } else { urlGroup.deleteGroup(); ok = false; } m_metaInfos.sync(); } return ok && doc->url() == url; } /** * Save file's meta-information if doc is in 'unmodified' state */ void KateDocManager::saveMetaInfos(const QList &documents) { /** * skip work if no meta infos wanted */ if (!m_saveMetaInfos) { return; } /** * store meta info for all non-modified documents which have some checksum */ const QDateTime now = QDateTime::currentDateTimeUtc(); foreach(KTextEditor::Document * doc, documents) { /** * skip modified docs */ if (doc->isModified()) { continue; } const QByteArray checksum = doc->checksum().toHex(); if (!checksum.isEmpty()) { /** * write the group with checksum and time */ KConfigGroup urlGroup(&m_metaInfos, doc->url().toString()); urlGroup.writeEntry("Checksum", QString::fromLatin1(checksum)); urlGroup.writeEntry("Time", now); /** * write document session config */ doc->writeSessionConfig(urlGroup); } } /** * sync to not loose data */ m_metaInfos.sync(); } void KateDocManager::slotModChanged(KTextEditor::Document *doc) { QList documents; documents.append(doc); saveMetaInfos(documents); } void KateDocManager::slotModChanged1(KTextEditor::Document *doc) { QMetaObject::invokeMethod(KateApp::self()->activeKateMainWindow(), "queueModifiedOnDisc", Qt::QueuedConnection, Q_ARG(KTextEditor::Document *, doc)); } void KateDocManager::documentOpened() { KColorScheme colors(QPalette::Active); KTextEditor::Document *doc = qobject_cast(sender()); if (!doc) { return; // should never happen, but who knows } disconnect(doc, SIGNAL(completed()), this, SLOT(documentOpened())); disconnect(doc, &KParts::ReadOnlyPart::canceled, this, &KateDocManager::documentOpened); // Only set "no success" when doc is empty to avoid close of files // with other trouble when do closeOrphaned() if (doc->openingError() && doc->isEmpty()) { KateDocumentInfo *info = documentInfo(doc); if (info) { info->openSuccess = false; } } } diff --git a/kate/katemainwindow.cpp b/kate/katemainwindow.cpp index 743849f07..3b2fd5800 100644 --- a/kate/katemainwindow.cpp +++ b/kate/katemainwindow.cpp @@ -1,1278 +1,1297 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2001 Anders Lund Copyright (C) 2007 Flavio Castelli This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ //BEGIN Includes #include "katemainwindow.h" #include "kateconfigdialog.h" #include "katedocmanager.h" #include "katepluginmanager.h" #include "kateconfigplugindialogpage.h" #include "kateviewmanager.h" #include "kateapp.h" #include "katesavemodifieddialog.h" #include "katemwmodonhddialog.h" #include "katesessionsaction.h" #include "katesessionmanager.h" #include "kateviewspace.h" #include "katequickopen.h" #include "kateupdatedisabler.h" #include "katedebug.h" #include "katecolorschemechooser.h" #include "katefileactions.h" #include "katequickopenmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //END KateMwModOnHdDialog *KateMainWindow::s_modOnHdDialog = nullptr; KateContainerStackedLayout::KateContainerStackedLayout(QWidget *parent) : QStackedLayout(parent) {} QSize KateContainerStackedLayout::sizeHint() const { if (currentWidget()) { return currentWidget()->sizeHint(); } return QStackedLayout::sizeHint(); } QSize KateContainerStackedLayout::minimumSize() const { if (currentWidget()) { return currentWidget()->minimumSize(); } return QStackedLayout::minimumSize(); } KateMainWindow::KateMainWindow(KConfig *sconfig, const QString &sgroup) : KateMDI::MainWindow(nullptr) , m_modignore(false) , m_wrapper(new KTextEditor::MainWindow(this)) { /** * we don't want any flicker here */ KateUpdateDisabler disableUpdates (this); /** * get and set config revision */ static const int currentConfigRevision = 10; const int readConfigRevision = KConfigGroup(KSharedConfig::openConfig(), "General").readEntry("Config Revision", 0); KConfigGroup(KSharedConfig::openConfig(), "General").writeEntry("Config Revision", currentConfigRevision); const bool firstStart = readConfigRevision < currentConfigRevision; // start session restore if needed startRestore(sconfig, sgroup); // setup most important actions first, needed by setupMainWindow setupImportantActions(); // setup the most important widgets setupMainWindow(); // setup the actions setupActions(); setStandardToolBarMenuEnabled(true); setXMLFile(QStringLiteral("kateui.rc")); createShellGUI(true); //qCDebug(LOG_KATE) << "****************************************************************************" << sconfig; // register mainwindow in app KateApp::self()->addMainWindow(this); // enable plugin guis KateApp::self()->pluginManager()->enableAllPluginsGUI(this, sconfig); // caption update Q_FOREACH (auto doc, KateApp::self()->documentManager()->documentList()) { slotDocumentCreated(doc); } connect(KateApp::self()->documentManager(), &KateDocManager::documentCreated, this, &KateMainWindow::slotDocumentCreated); readOptions(); if (sconfig) { m_viewManager->restoreViewConfiguration(KConfigGroup(sconfig, sgroup)); } finishRestore(); m_fileOpenRecent->loadEntries(KConfigGroup(sconfig, "Recent Files")); setAcceptDrops(true); connect(KateApp::self()->sessionManager(), SIGNAL(sessionChanged()), this, SLOT(updateCaption())); connect(this, &KateMDI::MainWindow::sigShowPluginConfigPage, this, &KateMainWindow::showPluginConfigPage); // prior to this there was (possibly) no view, therefore not context menu. // Hence, we have to take care of the menu bar here toggleShowMenuBar(false); // on first start: deactivate toolbar if (firstStart) toolBar(QStringLiteral("mainToolBar"))->hide(); // pass focus to first view! if (m_viewManager->activeView()) m_viewManager->activeView()->setFocus(); } KateMainWindow::~KateMainWindow() { // first, save our fallback window size ;) KConfigGroup cfg(KSharedConfig::openConfig(), "MainWindow"); KWindowConfig::saveWindowSize(windowHandle(), cfg); // save other options ;=) saveOptions(); // unregister mainwindow in app KateApp::self()->removeMainWindow(this); // disable all plugin guis, delete all pluginViews KateApp::self()->pluginManager()->disableAllPluginsGUI(this); // manually delete quick open, it's event filters will cause crash otherwise later delete m_quickOpen; m_quickOpen = nullptr; // delete the view manager, before KateMainWindow's wrapper is dead delete m_viewManager; m_viewManager = nullptr; // kill the wrapper object, now that all views are dead delete m_wrapper; m_wrapper = nullptr; } QSize KateMainWindow::sizeHint() const { /** * have some useful size hint, else we have mini windows per default */ return (QSize(640, 480).expandedTo(minimumSizeHint())); } void KateMainWindow::setupImportantActions() { m_paShowStatusBar = KStandardAction::showStatusbar(this, SLOT(toggleShowStatusBar()), actionCollection()); m_paShowStatusBar->setWhatsThis(i18n("Use this command to show or hide the view's statusbar")); m_paShowMenuBar = KStandardAction::showMenubar(this, SLOT(toggleShowMenuBar()), actionCollection()); m_paShowTabBar = new KToggleAction(i18n("Show &Tabs"), this); actionCollection()->addAction(QStringLiteral("settings_show_tab_bar"), m_paShowTabBar); connect(m_paShowTabBar, &QAction::toggled, this, &KateMainWindow::toggleShowTabBar); m_paShowTabBar->setWhatsThis(i18n("Use this command to show or hide the tabs for the views")); m_paShowPath = new KToggleAction(i18n("Sho&w Path in Titlebar"), this); actionCollection()->addAction(QStringLiteral("settings_show_full_path"), m_paShowPath); connect(m_paShowPath, SIGNAL(toggled(bool)), this, SLOT(updateCaption())); m_paShowPath->setWhatsThis(i18n("Show the complete document path in the window caption")); // Load themes actionCollection()->addAction(QStringLiteral("colorscheme_menu"), new KateColorSchemeChooser(actionCollection())); QAction * a = actionCollection()->addAction(KStandardAction::Back, QStringLiteral("view_prev_tab")); a->setText(i18n("&Previous Tab")); a->setWhatsThis(i18n("Focus the previous tab.")); actionCollection()->setDefaultShortcuts(a, a->shortcuts() << KStandardShortcut::tabPrev()); connect(a, &QAction::triggered, this, &KateMainWindow::slotFocusPrevTab); a = actionCollection()->addAction(KStandardAction::Forward, QStringLiteral("view_next_tab")); a->setText(i18n("&Next Tab")); a->setWhatsThis(i18n("Focus the next tab.")); actionCollection()->setDefaultShortcuts(a, a->shortcuts() << KStandardShortcut::tabNext()); connect(a, &QAction::triggered, this, &KateMainWindow::slotFocusNextTab); // the quick open action is used by the KateViewSpace "quick open button" a = actionCollection()->addAction(QStringLiteral("view_quick_open")); a->setIcon(QIcon::fromTheme(QStringLiteral("quickopen"))); a->setText(i18n("&Quick Open")); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_O)); connect(a, &QAction::triggered, this, &KateMainWindow::slotQuickOpen); a->setWhatsThis(i18n("Open a form to quick open documents.")); } void KateMainWindow::setupMainWindow() { setToolViewStyle(KMultiTabBar::KDEV3ICON); /** * create central stacked widget with its children */ m_mainStackedWidget = new QStackedWidget(centralWidget()); centralWidget()->layout()->addWidget(m_mainStackedWidget); (static_cast(centralWidget()->layout()))->setStretchFactor(m_mainStackedWidget, 100); m_quickOpen = new KateQuickOpen(m_mainStackedWidget, this); m_mainStackedWidget->addWidget(m_quickOpen); m_viewManager = new KateViewManager(m_mainStackedWidget, this); m_mainStackedWidget->addWidget(m_viewManager); // make view manager default visible! m_mainStackedWidget->setCurrentWidget(m_viewManager); m_bottomViewBarContainer = new QWidget(centralWidget()); centralWidget()->layout()->addWidget(m_bottomViewBarContainer); m_bottomContainerStack = new KateContainerStackedLayout(m_bottomViewBarContainer); } void KateMainWindow::setupActions() { QAction *a; actionCollection()->addAction(KStandardAction::New, QStringLiteral("file_new"), m_viewManager, SLOT(slotDocumentNew())) ->setWhatsThis(i18n("Create a new document")); actionCollection()->addAction(KStandardAction::Open, QStringLiteral("file_open"), m_viewManager, SLOT(slotDocumentOpen())) ->setWhatsThis(i18n("Open an existing document for editing")); m_fileOpenRecent = KStandardAction::openRecent(m_viewManager, SLOT(openUrl(QUrl)), this); m_fileOpenRecent->setMaxItems(KateConfigDialog::recentFilesMaxCount()); actionCollection()->addAction(m_fileOpenRecent->objectName(), m_fileOpenRecent); m_fileOpenRecent->setWhatsThis(i18n("This lists files which you have opened recently, and allows you to easily open them again.")); a = actionCollection()->addAction(QStringLiteral("file_save_all")); a->setIcon(QIcon::fromTheme(QStringLiteral("document-save-all"))); a->setText(i18n("Save A&ll")); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_L)); connect(a, &QAction::triggered, KateApp::self()->documentManager(), &KateDocManager::saveAll); a->setWhatsThis(i18n("Save all open, modified documents to disk.")); a = actionCollection()->addAction(QStringLiteral("file_reload_all")); a->setText(i18n("&Reload All")); connect(a, &QAction::triggered, KateApp::self()->documentManager(), &KateDocManager::reloadAll); a->setWhatsThis(i18n("Reload all open documents.")); a = actionCollection()->addAction(QStringLiteral("file_copy_filepath")); a->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); a->setText(i18n("Copy File &Path")); connect(a, &QAction::triggered, KateApp::self()->documentManager(), [this]() { auto&& view = viewManager()->activeView(); KateFileActions::copyFilePathToClipboard(view->document()); }); a->setWhatsThis(i18n("Copies the file path of the current file to clipboard.")); a = actionCollection()->addAction(QStringLiteral("file_open_containing_folder")); a->setIcon(QIcon::fromTheme(QStringLiteral("document-open-folder"))); a->setText(i18n("&Open Containing Folder")); connect(a, &QAction::triggered, KateApp::self()->documentManager(), [this]() { auto&& view = viewManager()->activeView(); KateFileActions::openContainingFolder(view->document()); }); a->setWhatsThis(i18n("Copies the file path of the current file to clipboard.")); a = actionCollection()->addAction(QStringLiteral("file_rename")); a->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); a->setText(i18nc("@action:inmenu", "Rename...")); connect(a, &QAction::triggered, KateApp::self()->documentManager(), [this]() { auto&& view = viewManager()->activeView(); KateFileActions::renameDocumentFile(this, view->document()); }); a->setWhatsThis(i18n("Renames the file belonging to the current document.")); a = actionCollection()->addAction(QStringLiteral("file_delete")); a->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); a->setText(i18nc("@action:inmenu", "Delete")); connect(a, &QAction::triggered, KateApp::self()->documentManager(), [this]() { auto&& view = viewManager()->activeView(); KateFileActions::deleteDocumentFile(this, view->document()); }); a->setWhatsThis(i18n("Deletes the file belonging to the current document.")); a = actionCollection()->addAction(QStringLiteral("file_properties")); a->setIcon(QIcon::fromTheme(QStringLiteral("dialog-object-properties"))); a->setText(i18n("Properties")); connect(a, &QAction::triggered, KateApp::self()->documentManager(), [this]() { auto&& view = viewManager()->activeView(); KateFileActions::openFilePropertiesDialog(view->document()); }); a->setWhatsThis(i18n("Deletes the file belonging to the current document.")); a = actionCollection()->addAction(QStringLiteral("file_compare")); a->setText(i18n("Compare")); connect(a, &QAction::triggered, KateApp::self()->documentManager(), [this]() { QMessageBox::information(this, i18n("Compare"), i18n("Use the Tabbar context menu to compare two documents")); }); a->setWhatsThis(i18n("Shows a hint how to compare documents.")); a = actionCollection()->addAction(QStringLiteral("file_close_orphaned")); a->setText(i18n("Close Orphaned")); connect(a, &QAction::triggered, KateApp::self()->documentManager(), &KateDocManager::closeOrphaned); a->setWhatsThis(i18n("Close all documents in the file list that could not be reopened, because they are not accessible anymore.")); a = actionCollection()->addAction(KStandardAction::Close, QStringLiteral("file_close"), m_viewManager, SLOT(slotDocumentClose())); a->setIcon(QIcon::fromTheme(QStringLiteral("document-close"))); a->setWhatsThis(i18n("Close the current document.")); a = actionCollection()->addAction(QStringLiteral("file_close_other")); a->setText(i18n("Close Other")); connect(a, SIGNAL(triggered()), this, SLOT(slotDocumentCloseOther())); a->setWhatsThis(i18n("Close other open documents.")); a = actionCollection()->addAction(QStringLiteral("file_close_all")); a->setText(i18n("Clos&e All")); connect(a, &QAction::triggered, this, &KateMainWindow::slotDocumentCloseAll); a->setWhatsThis(i18n("Close all open documents.")); a = actionCollection()->addAction(KStandardAction::Quit, QStringLiteral("file_quit")); // Qt::QueuedConnection: delay real shutdown, as we are inside menu action handling (bug #185708) connect(a, &QAction::triggered, this, &KateMainWindow::slotFileQuit, Qt::QueuedConnection); a->setWhatsThis(i18n("Close this window")); a = actionCollection()->addAction(QStringLiteral("view_new_view")); a->setIcon(QIcon::fromTheme(QStringLiteral("window-new"))); a->setText(i18n("&New Window")); connect(a, &QAction::triggered, this, &KateMainWindow::newWindow); a->setWhatsThis(i18n("Create a new Kate view (a new window with the same document list).")); m_showFullScreenAction = KStandardAction::fullScreen(nullptr, nullptr, this, this); actionCollection()->addAction(m_showFullScreenAction->objectName(), m_showFullScreenAction); connect(m_showFullScreenAction, &QAction::toggled, this, &KateMainWindow::slotFullScreen); documentOpenWith = new KActionMenu(i18n("Open W&ith"), this); actionCollection()->addAction(QStringLiteral("file_open_with"), documentOpenWith); documentOpenWith->setWhatsThis(i18n("Open the current document using another application registered for its file type, or an application of your choice.")); connect(documentOpenWith->menu(), &QMenu::aboutToShow, this, &KateMainWindow::mSlotFixOpenWithMenu); connect(documentOpenWith->menu(), &QMenu::triggered, this, &KateMainWindow::slotOpenWithMenuAction); a = KStandardAction::keyBindings(this, SLOT(editKeys()), actionCollection()); a->setWhatsThis(i18n("Configure the application's keyboard shortcut assignments.")); a = KStandardAction::configureToolbars(this, SLOT(slotEditToolbars()), actionCollection()); a->setWhatsThis(i18n("Configure which items should appear in the toolbar(s).")); QAction *settingsConfigure = KStandardAction::preferences(this, SLOT(slotConfigure()), actionCollection()); settingsConfigure->setWhatsThis(i18n("Configure various aspects of this application and the editing component.")); if (KateApp::self()->pluginManager()->pluginList().count() > 0) { a = actionCollection()->addAction(QStringLiteral("help_plugins_contents")); a->setText(i18n("&Plugins Handbook")); connect(a, &QAction::triggered, this, &KateMainWindow::pluginHelp); a->setWhatsThis(i18n("This shows help files for various available plugins.")); } a = actionCollection()->addAction(QStringLiteral("help_about_editor")); a->setText(i18n("&About Editor Component")); connect(a, &QAction::triggered, this, &KateMainWindow::aboutEditor); connect(m_viewManager, &KateViewManager::viewChanged, this, &KateMainWindow::slotWindowActivated); connect(m_viewManager, &KateViewManager::viewChanged, this, &KateMainWindow::slotUpdateOpenWith); connect(m_viewManager, &KateViewManager::viewChanged, this, &KateMainWindow::slotUpdateActionsNeedingUrl); connect(m_viewManager, &KateViewManager::viewChanged, this, &KateMainWindow::slotUpdateBottomViewBar); // re-route signals to our wrapper connect(m_viewManager, &KateViewManager::viewChanged, m_wrapper, &KTextEditor::MainWindow::viewChanged); connect(m_viewManager, &KateViewManager::viewCreated, m_wrapper, &KTextEditor::MainWindow::viewCreated); connect(this, &KateMainWindow::unhandledShortcutOverride, m_wrapper, &KTextEditor::MainWindow::unhandledShortcutOverride); slotWindowActivated(); // session actions a = actionCollection()->addAction(QStringLiteral("sessions_new")); a->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); a->setText(i18nc("Menu entry Session->New", "&New")); // Qt::QueuedConnection to avoid deletion of code that is executed when reducing the amount of mainwindows. (bug #227008) connect(a, &QAction::triggered, KateApp::self()->sessionManager(), &KateSessionManager::sessionNew, Qt::QueuedConnection); a = actionCollection()->addAction(QStringLiteral("sessions_save")); a->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); a->setText(i18n("&Save Session")); connect(a, &QAction::triggered, KateApp::self()->sessionManager(), &KateSessionManager::sessionSave); a = actionCollection()->addAction(QStringLiteral("sessions_save_as")); a->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); a->setText(i18n("Save Session &As...")); connect(a, &QAction::triggered, KateApp::self()->sessionManager(), &KateSessionManager::sessionSaveAs); a = actionCollection()->addAction(QStringLiteral("sessions_manage")); a->setIcon(QIcon::fromTheme(QStringLiteral("view-choose"))); a->setText(i18n("&Manage Sessions...")); // Qt::QueuedConnection to avoid deletion of code that is executed when reducing the amount of mainwindows. (bug #227008) connect(a, &QAction::triggered, KateApp::self()->sessionManager(), &KateSessionManager::sessionManage, Qt::QueuedConnection); // quick open menu ;) a = new KateSessionsAction(i18n("&Quick Open Session"), this); actionCollection()->addAction(QStringLiteral("sessions_list"), a); } void KateMainWindow::slotDocumentCloseAll() { if (KateApp::self()->documentManager()->documentList().size() >= 1 && KMessageBox::warningContinueCancel(this, i18n("This will close all open documents. Are you sure you want to continue?"), i18n("Close all documents"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("closeAll")) != KMessageBox::Cancel) { if (queryClose_internal()) { KateApp::self()->documentManager()->closeAllDocuments(false); } } } void KateMainWindow::slotDocumentCloseOther(KTextEditor::Document *document) { if (KateApp::self()->documentManager()->documentList().size() > 1 && KMessageBox::warningContinueCancel(this, i18n("This will close all open documents beside the current one. Are you sure you want to continue?"), i18n("Close all documents beside current one"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("closeOther")) != KMessageBox::Cancel) { if (queryClose_internal(document)) { KateApp::self()->documentManager()->closeOtherDocuments(document); } } } void KateMainWindow::slotDocumentCloseSelected(const QList &docList) { QList documents; foreach(KTextEditor::Document * doc, docList) { if (queryClose_internal(doc)) { documents.append(doc); } } KateApp::self()->documentManager()->closeDocuments(documents); } void KateMainWindow::slotDocumentCloseOther() { slotDocumentCloseOther(m_viewManager->activeView()->document()); } bool KateMainWindow::queryClose_internal(KTextEditor::Document *doc) { int documentCount = KateApp::self()->documentManager()->documentList().size(); if (! showModOnDiskPrompt()) { return false; } QList modifiedDocuments = KateApp::self()->documentManager()->modifiedDocumentList(); modifiedDocuments.removeAll(doc); bool shutdown = (modifiedDocuments.count() == 0); if (!shutdown) { shutdown = KateSaveModifiedDialog::queryClose(this, modifiedDocuments); } if (KateApp::self()->documentManager()->documentList().size() > documentCount) { KMessageBox::information(this, i18n("New file opened while trying to close Kate, closing aborted."), i18n("Closing Aborted")); shutdown = false; } return shutdown; } /** * queryClose(), take care that after the last mainwindow the stuff is closed */ bool KateMainWindow::queryClose() { // session saving, can we close all views ? // just test, not close them actually if (qApp->isSavingSession()) { return queryClose_internal(); } // normal closing of window // allow to close all windows until the last without restrictions if (KateApp::self()->mainWindowsCount() > 1) { return true; } // last one: check if we can close all documents, try run // and save docs if we really close down ! if (queryClose_internal()) { KateApp::self()->sessionManager()->saveActiveSession(true); return true; } return false; } void KateMainWindow::newWindow() { KateApp::self()->newMainWindow(KateApp::self()->sessionManager()->activeSession()->config()); } void KateMainWindow::slotEditToolbars() { KConfigGroup cfg(KSharedConfig::openConfig(), "MainWindow"); saveMainWindowSettings(cfg); KEditToolBar dlg(factory()); connect(&dlg, &KEditToolBar::newToolBarConfig, this, &KateMainWindow::slotNewToolbarConfig); dlg.exec(); } void KateMainWindow::reloadXmlGui() { for (KTextEditor::Document* doc : KateApp::self()->documentManager()->documentList()) { doc->reloadXML(); for (KTextEditor::View* view : doc->views()) { view->reloadXML(); } } } void KateMainWindow::slotNewToolbarConfig() { applyMainWindowSettings(KConfigGroup(KSharedConfig::openConfig(), "MainWindow")); // we need to reload all View's XML Gui from disk to ensure toolbar // changes are applied to all views. reloadXmlGui(); } void KateMainWindow::slotFileQuit() { KateApp::self()->shutdownKate(this); } void KateMainWindow::slotFileClose() { m_viewManager->slotDocumentClose(); } void KateMainWindow::slotOpenDocument(const QUrl &url) { m_viewManager->openUrl(url, QString(), true, false); } void KateMainWindow::readOptions() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); const KConfigGroup generalGroup(config, "General"); m_modNotification = generalGroup.readEntry("Modified Notification", false); m_modCloseAfterLast = generalGroup.readEntry("Close After Last", false); KateApp::self()->documentManager()->setSaveMetaInfos(generalGroup.readEntry("Save Meta Infos", true)); KateApp::self()->documentManager()->setDaysMetaInfos(generalGroup.readEntry("Days Meta Infos", 30)); m_paShowPath->setChecked(generalGroup.readEntry("Show Full Path in Title", false)); m_paShowStatusBar->setChecked(generalGroup.readEntry("Show Status Bar", true)); m_paShowMenuBar->setChecked(generalGroup.readEntry("Show Menu Bar", true)); m_paShowTabBar->setChecked(generalGroup.readEntry("Show Tab Bar", true)); m_quickOpen->setMatchMode(generalGroup.readEntry("Quick Open Search Mode", (int)KateQuickOpenModel::Columns::FileName)); + int listMode = generalGroup.readEntry("Quick Open List Mode", (int)KateQuickOpenModel::List::CurrentProject); + m_quickOpen->setListMode(static_cast(listMode)); // emit signal to hide/show statusbars toggleShowStatusBar(); toggleShowTabBar(); } void KateMainWindow::saveOptions() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup generalGroup(config, "General"); generalGroup.writeEntry("Save Meta Infos", KateApp::self()->documentManager()->getSaveMetaInfos()); generalGroup.writeEntry("Days Meta Infos", KateApp::self()->documentManager()->getDaysMetaInfos()); generalGroup.writeEntry("Show Full Path in Title", m_paShowPath->isChecked()); generalGroup.writeEntry("Show Status Bar", m_paShowStatusBar->isChecked()); generalGroup.writeEntry("Show Menu Bar", m_paShowMenuBar->isChecked()); generalGroup.writeEntry("Show Tab Bar", m_paShowTabBar->isChecked()); } void KateMainWindow::toggleShowMenuBar(bool showMessage) { if (m_paShowMenuBar->isChecked()) { menuBar()->show(); removeMenuBarActionFromContextMenu(); } else { if (showMessage) { const QString accel = m_paShowMenuBar->shortcut().toString(); KMessageBox::information(this, i18n("This will hide the menu bar completely." " You can show it again by typing %1.", accel), i18n("Hide menu bar"), QStringLiteral("HideMenuBarWarning")); } menuBar()->hide(); addMenuBarActionToContextMenu(); } } void KateMainWindow::addMenuBarActionToContextMenu() { if (m_viewManager->activeView()) { m_viewManager->activeView()->contextMenu()->addAction(m_paShowMenuBar); } } void KateMainWindow::removeMenuBarActionFromContextMenu() { if (m_viewManager->activeView()) { m_viewManager->activeView()->contextMenu()->removeAction(m_paShowMenuBar); } } void KateMainWindow::toggleShowStatusBar() { emit statusBarToggled(); } bool KateMainWindow::showStatusBar() { return m_paShowStatusBar->isChecked(); } void KateMainWindow::toggleShowTabBar() { emit tabBarToggled(); } bool KateMainWindow::showTabBar() { return m_paShowTabBar->isChecked(); } void KateMainWindow::slotWindowActivated() { if (m_viewManager->activeView()) { updateCaption(m_viewManager->activeView()->document()); } // show view manager in any case if (m_mainStackedWidget->currentWidget() != m_viewManager) { m_mainStackedWidget->setCurrentWidget(m_viewManager); } // update proxy centralWidget()->setFocusProxy(m_viewManager->activeView()); } void KateMainWindow::slotUpdateOpenWith() { if (m_viewManager->activeView()) { documentOpenWith->setEnabled(!m_viewManager->activeView()->document()->url().isEmpty()); } else { documentOpenWith->setEnabled(false); } } void KateMainWindow::slotUpdateActionsNeedingUrl() { auto&& view = viewManager()->activeView(); const bool hasUrl = view && !view->document()->url().isEmpty(); action("file_copy_filepath")->setEnabled(hasUrl); action("file_open_containing_folder")->setEnabled(hasUrl); action("file_rename")->setEnabled(hasUrl); action("file_delete")->setEnabled(hasUrl); action("file_properties")->setEnabled(hasUrl); } void KateMainWindow::dragEnterEvent(QDragEnterEvent *event) { if (!event->mimeData()) { return; } const bool accept = event->mimeData()->hasUrls() || event->mimeData()->hasText(); event->setAccepted(accept); } void KateMainWindow::dropEvent(QDropEvent *event) { slotDropEvent(event); } void KateMainWindow::slotDropEvent(QDropEvent *event) { if (event->mimeData() == nullptr) { return; } // // are we dropping files? // if (event->mimeData()->hasUrls()) { QList textlist = event->mimeData()->urls(); // Try to get the KTextEditor::View that sent this, and activate it, so that the file opens in the // view where it was dropped KTextEditor::View *kVsender = qobject_cast(QObject::sender()); if (kVsender != nullptr) { QWidget *parent = kVsender->parentWidget(); if (parent != nullptr) { KateViewSpace *vs = qobject_cast(parent->parentWidget()); if (vs != nullptr) { m_viewManager->setActiveSpace(vs); } } } foreach(const QUrl & url, textlist) { // if url has no file component, try and recursively scan dir KFileItem kitem(url); kitem.setDelayedMimeTypes(true); if (kitem.isDir()) { if (KMessageBox::questionYesNo(this, i18n("You dropped the directory %1 into Kate. " "Do you want to load all files contained in it ?", url.url()), i18n("Load files recursively?")) == KMessageBox::Yes) { KIO::ListJob *list_job = KIO::listRecursive(url, KIO::DefaultFlags, false); connect(list_job, &KIO::ListJob::entries, this, &KateMainWindow::slotListRecursiveEntries); } } else { m_viewManager->openUrl(url); } } } // // or are we dropping text? // else if (event->mimeData()->hasText()) { KTextEditor::Document *doc = KateApp::self()->documentManager()->createDoc(); doc->setText(event->mimeData()->text()); m_viewManager->activateView(doc); } } void KateMainWindow::slotListRecursiveEntries(KIO::Job *job, const KIO::UDSEntryList &list) { const QUrl dir = static_cast(job)->url(); foreach(const KIO::UDSEntry & entry, list) { if (!entry.isDir()) { QUrl url(dir); url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + QLatin1Char('/') + entry.stringValue(KIO::UDSEntry::UDS_NAME)); m_viewManager->openUrl(url); } } } void KateMainWindow::editKeys() { KShortcutsDialog dlg(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this); QList clients = guiFactory()->clients(); foreach(KXMLGUIClient * client, clients) { // FIXME there appear to be invalid clients after session switching // qCDebug(LOG_KATE)<<"adding client to shortcut editor"; // qCDebug(LOG_KATE)<actionCollection(); // qCDebug(LOG_KATE)<componentData().aboutData(); // qCDebug(LOG_KATE)<componentData().aboutData()->programName(); dlg.addCollection(client->actionCollection(), client->componentName()); } dlg.configure(); // reloadXML gui clients, to ensure all clients are up-to-date reloadXmlGui(); } void KateMainWindow::openUrl(const QString &name) { m_viewManager->openUrl(QUrl(name)); } void KateMainWindow::slotConfigure() { showPluginConfigPage(nullptr, 0); } void KateMainWindow::showPluginConfigPage(KTextEditor::Plugin *configpageinterface, uint id) { if (!m_viewManager->activeView()) { return; } KateConfigDialog *dlg = new KateConfigDialog(this, m_viewManager->activeView()); if (configpageinterface) { dlg->showAppPluginPage(configpageinterface, id); } if (dlg->exec() == QDialog::Accepted) { m_fileOpenRecent->setMaxItems(KateConfigDialog::recentFilesMaxCount()); } delete dlg; m_viewManager->reactivateActiveView(); // gui (toolbars...) needs to be updated, because // of possible changes that the configure dialog // could have done on it, specially for plugins. } QUrl KateMainWindow::activeDocumentUrl() { // anders: i make this one safe, as it may be called during // startup (by the file selector) KTextEditor::View *v = m_viewManager->activeView(); if (v) { return v->document()->url(); } return QUrl(); } void KateMainWindow::mSlotFixOpenWithMenu() { // dh: in bug #307699, this slot is called when launching the Kate application // unfortunately, no one ever could reproduce except users. KTextEditor::View *activeView = m_viewManager->activeView(); if (! activeView) { return; } // cleanup menu QMenu *menu = documentOpenWith->menu(); menu->clear(); // get a list of appropriate services. QMimeDatabase db; QMimeType mime = db.mimeTypeForName(activeView->document()->mimeType()); //qCDebug(LOG_KATE) << "mime type: " << mime.name(); QAction *a = nullptr; - KService::List offers = KMimeTypeTrader::self()->query(mime.name(), QStringLiteral("Application")); + const KService::List offers = KMimeTypeTrader::self()->query(mime.name(), QStringLiteral("Application")); // add all default open-with-actions except "Kate" - for (KService::List::Iterator it = offers.begin(); it != offers.end(); ++it) { - KService::Ptr service = *it; + for (const auto& service : offers) { if (service->name() == QStringLiteral("Kate")) { continue; } a = menu->addAction(QIcon::fromTheme(service->icon()), service->name()); a->setData(service->entryPath()); } // append "Other..." to call the KDE "open with" dialog. a = documentOpenWith->menu()->addAction(i18n("&Other...")); a->setData(QString()); } void KateMainWindow::slotOpenWithMenuAction(QAction *a) { QList list; list.append(m_viewManager->activeView()->document()->url()); const QString openWith = a->data().toString(); if (openWith.isEmpty()) { // display "open with" dialog KOpenWithDialog dlg(list); if (dlg.exec()) { KRun::runService(*dlg.service(), list, this); } return; } KService::Ptr app = KService::serviceByDesktopPath(openWith); if (app) { KRun::runService(*app, list, this); } else { KMessageBox::error(this, i18n("Application '%1' not found.", openWith), i18n("Application not found")); } } void KateMainWindow::pluginHelp() { KHelpClient::invokeHelp(QString(), QStringLiteral("kate-plugins")); } void KateMainWindow::aboutEditor() { KAboutApplicationDialog ad(KTextEditor::Editor::instance()->aboutData(), this); ad.exec(); } void KateMainWindow::slotFullScreen(bool t) { KToggleFullScreenAction::setFullScreen(this, t); QMenuBar *mb = menuBar(); if (t) { QToolButton *b = new QToolButton(mb); b->setDefaultAction(m_showFullScreenAction); b->setSizePolicy(QSizePolicy(QSizePolicy::Minimum,QSizePolicy::Ignored)); b->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); mb->setCornerWidget(b,Qt::TopRightCorner); b->setVisible(true); b->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); } else { QWidget *w=mb->cornerWidget(Qt::TopRightCorner); if (w) w->deleteLater(); } } bool KateMainWindow::showModOnDiskPrompt() { KTextEditor::Document *doc; DocVector list; list.reserve(KateApp::self()->documentManager()->documentList().size()); foreach(doc, KateApp::self()->documentManager()->documentList()) { if (KateApp::self()->documentManager()->documentInfo(doc)->modifiedOnDisc && doc->isModified()) { list.append(doc); } } if (!list.isEmpty() && !m_modignore) { KateMwModOnHdDialog mhdlg(list, this); m_modignore = true; bool res = mhdlg.exec(); m_modignore = false; return res; } return true; } void KateMainWindow::slotDocumentCreated(KTextEditor::Document *doc) { connect(doc, SIGNAL(modifiedChanged(KTextEditor::Document*)), this, SLOT(updateCaption(KTextEditor::Document*))); connect(doc, SIGNAL(readWriteChanged(KTextEditor::Document*)), this, SLOT(updateCaption(KTextEditor::Document*))); connect(doc, SIGNAL(documentNameChanged(KTextEditor::Document*)), this, SLOT(updateCaption(KTextEditor::Document*))); connect(doc, SIGNAL(documentUrlChanged(KTextEditor::Document*)), this, SLOT(updateCaption(KTextEditor::Document*))); connect(doc, &KTextEditor::Document::documentNameChanged, this, &KateMainWindow::slotUpdateOpenWith); updateCaption(doc); } void KateMainWindow::updateCaption() { if (m_viewManager->activeView()) { updateCaption(m_viewManager->activeView()->document()); } } void KateMainWindow::updateCaption(KTextEditor::Document *doc) { if (!m_viewManager->activeView()) { setCaption(QString(), false); return; } // block signals from inactive docs if (!((KTextEditor::Document *)m_viewManager->activeView()->document() == doc)) { return; } QString c; if (m_viewManager->activeView()->document()->url().isEmpty() || (!m_paShowPath || !m_paShowPath->isChecked())) { c = ((KTextEditor::Document *)m_viewManager->activeView()->document())->documentName(); } else { c = m_viewManager->activeView()->document()->url().toString(QUrl::PreferLocalFile); const QString homePath = QDir::homePath(); if (c.startsWith(homePath)) { c = QStringLiteral("~") + c.right(c.length() - homePath.length()); } } QString sessName = KateApp::self()->sessionManager()->activeSession()->name(); if (!sessName.isEmpty()) { sessName = QStringLiteral("%1: ").arg(sessName); } QString readOnlyCaption; if (!m_viewManager->activeView()->document()->isReadWrite()) { readOnlyCaption = i18n(" [read only]"); } setCaption(sessName + c + readOnlyCaption + QStringLiteral(" [*]"), m_viewManager->activeView()->document()->isModified()); } void KateMainWindow::saveProperties(KConfigGroup &config) { saveSession(config); // store all plugin view states int id = KateApp::self()->mainWindowID(this); foreach(const KatePluginInfo & item, KateApp::self()->pluginManager()->pluginList()) { if (item.plugin && pluginViews().contains(item.plugin)) { if (auto interface = qobject_cast (pluginViews().value(item.plugin))) { KConfigGroup group(config.config(), QStringLiteral("Plugin:%1:MainWindow:%2").arg(item.saveName()).arg(id)); interface->writeSessionConfig(group); } } } - m_fileOpenRecent->saveEntries(KConfigGroup(config.config(), "Recent Files")); + saveOpenRecent(config.config()); m_viewManager->saveViewConfiguration(config); } void KateMainWindow::readProperties(const KConfigGroup &config) { // KDE5: TODO startRestore should take a const KConfigBase*, or even just a const KConfigGroup&, // but this propagates down to interfaces/kate/plugin.h so all plugins have to be ported KConfigBase *configBase = const_cast(config.config()); startRestore(configBase, config.name()); // perhaps enable plugin guis KateApp::self()->pluginManager()->enableAllPluginsGUI(this, configBase); finishRestore(); - m_fileOpenRecent->loadEntries(KConfigGroup(config.config(), "Recent Files")); + loadOpenRecent(config.config()); m_viewManager->restoreViewConfiguration(config); } +void KateMainWindow::saveOpenRecent(KConfig *config) { + m_fileOpenRecent->saveEntries(KConfigGroup(config, "Recent Files")); +} + +void KateMainWindow::loadOpenRecent(const KConfig *config) { + m_fileOpenRecent->loadEntries(KConfigGroup(config, "Recent Files")); +} + void KateMainWindow::saveGlobalProperties(KConfig *sessionConfig) { KateApp::self()->documentManager()->saveDocumentList(sessionConfig); KConfigGroup cg(sessionConfig, "General"); cg.writeEntry("Last Session", KateApp::self()->sessionManager()->activeSession()->name()); // save plugin config !! KateApp::self()->pluginManager()->writeConfig(sessionConfig); } void KateMainWindow::saveWindowConfig(const KConfigGroup &_config) { KConfigGroup config(_config); saveMainWindowSettings(config); KWindowConfig::saveWindowSize(windowHandle(), config); config.writeEntry("WindowState", int(((KParts::MainWindow *)this)->windowState())); config.sync(); } void KateMainWindow::restoreWindowConfig(const KConfigGroup &config) { setWindowState(Qt::WindowNoState); applyMainWindowSettings(config); KWindowConfig::restoreWindowSize(windowHandle(), config); setWindowState(QFlags(config.readEntry("WindowState", int(Qt::WindowActive)))); } void KateMainWindow::slotUpdateBottomViewBar() { //qCDebug(LOG_KATE)<<"slotUpdateHorizontalViewBar()"<activeView(); BarState bs = m_bottomViewBarMapping[view]; if (bs.bar() && bs.state()) { m_bottomContainerStack->setCurrentWidget(bs.bar()); m_bottomContainerStack->currentWidget()->show(); m_bottomViewBarContainer->show(); } else { QWidget *wid = m_bottomContainerStack->currentWidget(); if (wid) { wid->hide(); } //qCDebug(LOG_KATE)<hide(); } } void KateMainWindow::queueModifiedOnDisc(KTextEditor::Document *doc) { if (!m_modNotification) { return; } KateDocumentInfo *docInfo = KateApp::self()->documentManager()->documentInfo(doc); if (!docInfo) { return; } bool modOnDisk = (uint)docInfo->modifiedOnDisc; if (s_modOnHdDialog == nullptr && modOnDisk) { DocVector list; list.append(doc); s_modOnHdDialog = new KateMwModOnHdDialog(list, this); m_modignore = true; KWindowSystem::setOnAllDesktops(s_modOnHdDialog->winId(), true); s_modOnHdDialog->exec(); delete s_modOnHdDialog; // s_modOnHdDialog is set to 0 in destructor of KateMwModOnHdDialog (jowenn!!!) m_modignore = false; } else if (s_modOnHdDialog != nullptr) { s_modOnHdDialog->addDocument(doc); } } bool KateMainWindow::event(QEvent *e) { if (e->type() == QEvent::ShortcutOverride) { QKeyEvent *k = static_cast(e); emit unhandledShortcutOverride(k); } return KateMDI::MainWindow::event(e); } QObject *KateMainWindow::pluginView(const QString &name) { KTextEditor::Plugin *plugin = KateApp::self()->pluginManager()->plugin(name); if (!plugin) { return nullptr; } return m_pluginViews.contains(plugin) ? m_pluginViews.value(plugin) : nullptr; } void KateMainWindow::mousePressEvent(QMouseEvent *e) { switch(e->button()) { case Qt::ForwardButton: slotFocusNextTab(); break; case Qt::BackButton: slotFocusPrevTab(); break; default: ; } } void KateMainWindow::slotFocusPrevTab() { if (m_viewManager->activeViewSpace()) { m_viewManager->activeViewSpace()->focusPrevTab(); } } void KateMainWindow::slotFocusNextTab() { if (m_viewManager->activeViewSpace()) { m_viewManager->activeViewSpace()->focusNextTab(); } } void KateMainWindow::slotQuickOpen() { /** * toggle back to view manager when when quick open is already shown */ if (m_mainStackedWidget->currentWidget() == m_quickOpen) { m_mainStackedWidget->setCurrentWidget(m_viewManager); centralWidget()->setFocusProxy(m_viewManager); return; } /** * show quick open and pass focus to it */ m_quickOpen->update(); m_mainStackedWidget->setCurrentWidget(m_quickOpen); centralWidget()->setFocusProxy(m_quickOpen); m_quickOpen->setFocus(); } QWidget *KateMainWindow::createToolView(KTextEditor::Plugin *plugin, const QString &identifier, KTextEditor::MainWindow::ToolViewPosition pos, const QIcon &icon, const QString &text) { // FIXME KF5 return KateMDI::MainWindow::createToolView(plugin, identifier, (KMultiTabBar::KMultiTabBarPosition)(pos), icon.pixmap(QSize(16, 16)), text); } bool KateMainWindow::moveToolView(QWidget *widget, KTextEditor::MainWindow::ToolViewPosition pos) { if (!qobject_cast(widget)) { return false; } // FIXME KF5 return KateMDI::MainWindow::moveToolView(qobject_cast(widget), (KMultiTabBar::KMultiTabBarPosition)(pos)); } bool KateMainWindow::showToolView(QWidget *widget) { if (!qobject_cast(widget)) { return false; } return KateMDI::MainWindow::showToolView(qobject_cast(widget)); } bool KateMainWindow::hideToolView(QWidget *widget) { if (!qobject_cast(widget)) { return false; } return KateMDI::MainWindow::hideToolView(qobject_cast(widget)); } void KateMainWindow::setQuickOpenMatchMode(int mode) { m_quickOpen->setMatchMode(mode); } int KateMainWindow::quickOpenMatchMode() { return m_quickOpen->matchMode(); } + +void KateMainWindow::setQuickOpenListMode(KateQuickOpenModel::List mode) +{ + m_quickOpen->setListMode(mode); +} + +KateQuickOpenModel::List KateMainWindow::quickOpenListMode() const +{ + return m_quickOpen->listMode(); +} diff --git a/kate/katemainwindow.h b/kate/katemainwindow.h index c09c5353c..c628f251e 100644 --- a/kate/katemainwindow.h +++ b/kate/katemainwindow.h @@ -1,593 +1,600 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2001 Anders Lund This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KATE_MAINWINDOW_H__ #define __KATE_MAINWINDOW_H__ #include "katemdi.h" #include "kateviewmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include class QMenu; namespace KIO { class UDSEntry; typedef class QList UDSEntryList; } class KFileItem; class KRecentFilesAction; class KateViewManager; class KateMwModOnHdDialog; class KateQuickOpen; +enum KateQuickOpenModelList : int; // Helper layout class to always provide minimum size class KateContainerStackedLayout : public QStackedLayout { Q_OBJECT public: KateContainerStackedLayout(QWidget *parent); QSize sizeHint() const override; QSize minimumSize() const override; }; class KateMainWindow : public KateMDI::MainWindow, virtual public KParts::PartBase { Q_OBJECT public: /** * Construct the window and restore its state from given config if any * @param sconfig session config for this window, 0 if none * @param sgroup session config group to use */ KateMainWindow(KConfig *sconfig, const QString &sgroup); /** * Destruct the nice window */ ~KateMainWindow() override; /** * Accessor methodes for interface and child objects */ public: KateViewManager *viewManager() { return m_viewManager; } /** * KTextEditor::MainWindow wrapper * @return KTextEditor::MainWindow wrapper. */ KTextEditor::MainWindow *wrapper() { return m_wrapper; } public: /** Returns the URL of the current document. * anders: I add this for use from the file selector. */ QUrl activeDocumentUrl(); /** * Prompts the user for what to do with files that are modified on disk if any. * This is optionally run when the window receives focus, and when the last * window is closed. * @return true if no documents are modified on disk, or all documents were * handled by the dialog; otherwise (the dialog was canceled) false. */ bool showModOnDiskPrompt(); public: /*reimp*/ void readProperties(const KConfigGroup &config) override; /*reimp*/ void saveProperties(KConfigGroup &config) override; /*reimp*/ void saveGlobalProperties(KConfig *sessionConfig) override; + void saveOpenRecent(KConfig *config); + void loadOpenRecent(const KConfig *config); + public: bool queryClose_internal(KTextEditor::Document *doc = nullptr); /** * save the settings, size and state of this window in * the provided config group */ void saveWindowConfig(const KConfigGroup &); /** * restore the settings, size and state of this window from * the provided config group. */ void restoreWindowConfig(const KConfigGroup &); /** * save some global options to katerc */ void saveOptions(); private: /** * Setup actions which pointers are needed already in setupMainWindow */ void setupImportantActions(); void setupMainWindow(); void setupActions(); bool queryClose() override; void addMenuBarActionToContextMenu(); void removeMenuBarActionFromContextMenu(); /** * read some global options from katerc */ void readOptions(); void dragEnterEvent(QDragEnterEvent *) override; void dropEvent(QDropEvent *) override; public Q_SLOTS: void slotFileClose(); void slotFileQuit(); void queueModifiedOnDisc(KTextEditor::Document *doc); void slotFocusPrevTab(); void slotFocusNextTab(); /** * Show quick open */ void slotQuickOpen(); /** * Overwrite size hint for better default window sizes * @return size hint */ QSize sizeHint() const override; /** * slots used for actions in the menus/toolbars * or internal signal connections */ private Q_SLOTS: void newWindow(); void slotConfigure(); void slotOpenWithMenuAction(QAction *a); void slotEditToolbars(); void slotNewToolbarConfig(); void slotUpdateOpenWith(); void slotUpdateActionsNeedingUrl(); void slotOpenDocument(const QUrl &); void slotDropEvent(QDropEvent *); void editKeys(); void mSlotFixOpenWithMenu(); void reloadXmlGui(); /* to update the caption */ void slotDocumentCreated(KTextEditor::Document *doc); void updateCaption(KTextEditor::Document *doc); // calls updateCaption(doc) with the current document void updateCaption(); void pluginHelp(); void aboutEditor(); void slotFullScreen(bool); void slotListRecursiveEntries(KIO::Job *job, const KIO::UDSEntryList &list); private Q_SLOTS: void toggleShowMenuBar(bool showMessage = true); void toggleShowStatusBar(); void toggleShowTabBar(); public: bool showStatusBar(); bool showTabBar(); Q_SIGNALS: void statusBarToggled(); void tabBarToggled(); void unhandledShortcutOverride(QEvent *e); public: void openUrl(const QString &name = QString()); QHash &pluginViews() { return m_pluginViews; } QWidget *bottomViewBarContainer() { return m_bottomViewBarContainer; } void addToBottomViewBarContainer(KTextEditor::View *view, QWidget *bar) { m_bottomContainerStack->addWidget(bar); m_bottomViewBarMapping[view] = BarState(bar); } void hideBottomViewBarForView(KTextEditor::View *view) { BarState &state = m_bottomViewBarMapping[view]; if (state.bar()) { m_bottomContainerStack->setCurrentWidget(state.bar()); state.bar()->hide(); state.setState(false); } m_bottomViewBarContainer->hide(); } void showBottomViewBarForView(KTextEditor::View *view) { BarState &state = m_bottomViewBarMapping[view]; if (state.bar()) { m_bottomContainerStack->setCurrentWidget(state.bar()); state.bar()->show(); state.setState(true); m_bottomViewBarContainer->show(); } } void deleteBottomViewBarForView(KTextEditor::View *view) { BarState state = m_bottomViewBarMapping.take(view); if (state.bar()) { if (m_bottomContainerStack->currentWidget() == state.bar()) { m_bottomViewBarContainer->hide(); } delete state.bar(); } } bool modNotificationEnabled() const { return m_modNotification; } void setModNotificationEnabled(bool e) { m_modNotification = e; } bool modCloseAfterLast() const { return m_modCloseAfterLast; } void setModCloseAfterLast(bool e) { m_modCloseAfterLast = e; } void setQuickOpenMatchMode(int mode); int quickOpenMatchMode(); + void setQuickOpenListMode(KateQuickOpenModelList mode); + KateQuickOpenModelList quickOpenListMode() const; + KRecentFilesAction *fileOpenRecent() const { return m_fileOpenRecent; } // // KTextEditor::MainWindow interface, get called by invokeMethod from our wrapper object! // public Q_SLOTS: /** * get the toplevel widget. * \return the real main window widget. */ QWidget *window() { return this; } /** * Accessor to the XMLGUIFactory. * \return the mainwindow's KXMLGUIFactory. */ KXMLGUIFactory *guiFactory() override { return KateMDI::MainWindow::guiFactory(); } /** * Get a list of all views for this main window. * @return all views */ QList views() { return viewManager()->views(); } /** * Access the active view. * \return active view */ KTextEditor::View *activeView() { return viewManager()->activeView(); } /** * Activate the view with the corresponding \p document. * If none exist for this document, create one * \param document the document * \return activated view of this document */ KTextEditor::View *activateView(KTextEditor::Document *document) { return viewManager()->activateView(document); } /** * Open the document \p url with the given \p encoding. * \param url the document's url * \param encoding the preferred encoding. If encoding is QString() the * encoding will be guessed or the default encoding will be used. * \return a pointer to the created view for the new document, if a document * with this url is already existing, its view will be activated */ KTextEditor::View *openUrl(const QUrl &url, const QString &encoding = QString()) { return viewManager()->openUrlWithView(url, encoding); } /** * Close selected view * \param view the view * \return true if view was closed */ bool closeView(KTextEditor::View *view) { m_viewManager->closeView(view); return true; } /** * Close the split view where the given view is contained. * \param view the view. * \return true if the split view was closed. */ bool closeSplitView(KTextEditor::View *view) { m_viewManager->closeViewSpace(view); return true; } /** * @returns true if the two given views share the same split view, * false otherwise. */ bool viewsInSameSplitView(KTextEditor::View *view1, KTextEditor::View *view2) { return m_viewManager->viewsInSameViewSpace(view1, view2); } /** * Split current view space according to \p orientation * \param orientation in which line split the view */ void splitView(Qt::Orientation orientation) { m_viewManager->splitViewSpace(nullptr, orientation); } /** * Try to create a view bar for the given view. * Its parameter is the view for which we want a view bar * @return suitable widget that can host view bars widgets or nullptr */ QWidget *createViewBar(KTextEditor::View *) { return bottomViewBarContainer(); } /** * Delete the view bar for the given view. * @param view view for which we want an view bar */ void deleteViewBar(KTextEditor::View *view) { deleteBottomViewBarForView(view); } /** * Add a widget to the view bar. * @param view view for which the view bar is used * @param bar bar widget, shall have the viewBarParent() as parent widget */ void addWidgetToViewBar(KTextEditor::View *view, QWidget *bar) { addToBottomViewBarContainer(view, bar); } /** * Show the view bar for the given view * @param view view for which the view bar is used */ void showViewBar(KTextEditor::View *view) { showBottomViewBarForView(view); } /** * Hide the view bar for the given view * @param view view for which the view bar is used */ void hideViewBar(KTextEditor::View *view) { hideBottomViewBarForView(view); } /** * Create a new toolview with unique \p identifier at side \p pos * with \p icon and caption \p text. Use the returned widget to embedd * your widgets. * \param plugin which owns this tool view * \param identifier unique identifier for this toolview * \param pos position for the toolview, if we are in session restore, * this is only a preference * \param icon icon to use in the sidebar for the toolview * \param text translated text (i18n()) to use in addition to icon * \return created toolview on success, otherwise NULL */ QWidget *createToolView(KTextEditor::Plugin *plugin, const QString &identifier, KTextEditor::MainWindow::ToolViewPosition pos, const QIcon &icon, const QString &text); /** * Move the toolview \p widget to position \p pos. * \param widget the toolview to move, where the widget was constructed * by createToolView(). * \param pos new position to move widget to * \return \e true on success, otherwise \e false */ bool moveToolView(QWidget *widget, KTextEditor::MainWindow::ToolViewPosition pos); /** * Show the toolview \p widget. * \param widget the toolview to show, where the widget was constructed * by createToolView(). * \return \e true on success, otherwise \e false * \todo add focus parameter: bool showToolView (QWidget *widget, bool giveFocus ); */ bool showToolView(QWidget *widget); /** * Hide the toolview \p widget. * \param widget the toolview to hide, where the widget was constructed * by createToolView(). * \return \e true on success, otherwise \e false */ bool hideToolView(QWidget *widget); /** * Get a plugin view for the plugin with with identifier \p name. * \param name the plugin's name * \return pointer to the plugin view if a plugin with \p name is loaded and has a view for this mainwindow, * otherwise NULL */ QObject *pluginView(const QString &name); private Q_SLOTS: void slotUpdateBottomViewBar(); private Q_SLOTS: void slotDocumentCloseAll(); void slotDocumentCloseOther(); void slotDocumentCloseOther(KTextEditor::Document *document); void slotDocumentCloseSelected(const QList &); private: /** * Notify about file modifications from other processes? */ bool m_modNotification; /** * Shutdown Kate after last file is closed */ bool m_modCloseAfterLast; /** * stacked widget containing the central area, aka view manager, quickopen, ... */ QStackedWidget *m_mainStackedWidget; /** * quick open to fast switch documents */ KateQuickOpen *m_quickOpen; /** * keeps track of views */ KateViewManager *m_viewManager; KRecentFilesAction *m_fileOpenRecent; KActionMenu *documentOpenWith; KToggleAction *settingsShowFileselector; KToggleAction *m_showFullScreenAction; bool m_modignore; // all plugin views for this mainwindow, used by the pluginmanager QHash m_pluginViews; // options: show statusbar + show path KToggleAction *m_paShowPath; KToggleAction *m_paShowMenuBar; KToggleAction *m_paShowStatusBar; KToggleAction *m_paShowTabBar; QWidget *m_bottomViewBarContainer; KateContainerStackedLayout *m_bottomContainerStack; class BarState { public: BarState(): m_bar(nullptr), m_state(false) {} BarState(QWidget *bar): m_bar(bar), m_state(false) {} ~BarState() {} QWidget *bar() { return m_bar; } bool state() { return m_state; } void setState(bool state) { m_state = state; } private: QWidget *m_bar; bool m_state; }; QHash m_bottomViewBarMapping; public: static void unsetModifiedOnDiscDialogIfIf(KateMwModOnHdDialog *diag) { if (s_modOnHdDialog == diag) { s_modOnHdDialog = nullptr; } } private: static KateMwModOnHdDialog *s_modOnHdDialog; /** * Wrapper of main window for KTextEditor */ KTextEditor::MainWindow *m_wrapper; public Q_SLOTS: void showPluginConfigPage(KTextEditor::Plugin *configpageinterface, uint id); void slotWindowActivated(); protected: bool event(QEvent *e) override; void mousePressEvent(QMouseEvent *e) override; }; #endif diff --git a/kate/katemdi.cpp b/kate/katemdi.cpp index 495d1a5d9..5a84be013 100644 --- a/kate/katemdi.cpp +++ b/kate/katemdi.cpp @@ -1,1056 +1,1055 @@ /* This file is part of the KDE libraries Copyright (C) 2005 Christoph Cullmann Copyright (C) 2002, 2003 Joseph Wenninger GUIClient partly based on ktoolbarhandler.cpp: Copyright (C) 2002 Simon Hausmann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "katemdi.h" #include "katedebug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include namespace KateMDI { //BEGIN TOGGLETOOLVIEWACTION // ToggleToolViewAction::ToggleToolViewAction(const QString &text, ToolView *tv, QObject *parent) : KToggleAction(text, parent) , m_tv(tv) { connect(this, &ToggleToolViewAction::toggled, this, &ToggleToolViewAction::slotToggled); connect(m_tv, &ToolView::toolVisibleChanged, this, &ToggleToolViewAction::toolVisibleChanged); setChecked(m_tv->toolVisible()); } ToggleToolViewAction::~ToggleToolViewAction() {} void ToggleToolViewAction::toolVisibleChanged(bool) { if (isChecked() != m_tv->toolVisible()) { setChecked(m_tv->toolVisible()); } } void ToggleToolViewAction::slotToggled(bool t) { if (t) { m_tv->mainWindow()->showToolView(m_tv); m_tv->setFocus(); } else { m_tv->mainWindow()->hideToolView(m_tv); } } //END TOGGLETOOLVIEWACTION //BEGIN GUICLIENT static const QString actionListName = QStringLiteral("kate_mdi_view_actions"); // please don't use QStringLiteral since it can't be used with a concatenated string parameter on all platforms static const QString guiDescription = QStringLiteral("" "" "" " " " " " " "" ""); GUIClient::GUIClient(MainWindow *mw) : QObject(mw) , KXMLGUIClient(mw) , m_mw(mw) { connect(m_mw->guiFactory(), &KXMLGUIFactory::clientAdded, this, &GUIClient::clientAdded); if (domDocument().documentElement().isNull()) { QString completeDescription = guiDescription.arg(actionListName); setXML(completeDescription, false /*merge*/); } m_toolMenu = new KActionMenu(i18n("Tool &Views"), this); actionCollection()->addAction(QStringLiteral("kate_mdi_toolview_menu"), m_toolMenu); m_showSidebarsAction = new KToggleAction(i18n("Show Side&bars"), this); actionCollection()->addAction(QStringLiteral("kate_mdi_sidebar_visibility"), m_showSidebarsAction); actionCollection()->setDefaultShortcut(m_showSidebarsAction, Qt::CTRL | Qt::ALT | Qt::SHIFT | Qt::Key_F); m_showSidebarsAction->setChecked(m_mw->sidebarsVisible()); connect(m_showSidebarsAction, &KToggleAction::toggled, m_mw, &MainWindow::setSidebarsVisible); m_toolMenu->addAction(m_showSidebarsAction); QAction *sep_act = new QAction(this); sep_act->setSeparator(true); m_toolMenu->addAction(sep_act); // read shortcuts actionCollection()->setConfigGroup(QStringLiteral("Shortcuts")); actionCollection()->readSettings(); actionCollection()->addAssociatedWidget(m_mw); foreach(QAction * action, actionCollection()->actions()) action->setShortcutContext(Qt::WidgetWithChildrenShortcut); } GUIClient::~GUIClient() {} void GUIClient::updateSidebarsVisibleAction() { m_showSidebarsAction->setChecked(m_mw->sidebarsVisible()); } void GUIClient::registerToolView(ToolView *tv) { QString aname = QLatin1String("kate_mdi_toolview_") + tv->id; // try to read the action shortcut QList shortcuts; KSharedConfigPtr cfg = KSharedConfig::openConfig(); QString shortcutString = cfg->group("Shortcuts").readEntry(aname, QString()); foreach(const QString & shortcut, shortcutString.split(QLatin1String(";"))) { shortcuts << QKeySequence::fromString(shortcut); } KToggleAction *a = new ToggleToolViewAction(i18n("Show %1", tv->text), tv, this); actionCollection()->setDefaultShortcuts(a, shortcuts); actionCollection()->addAction(aname, a); m_toolViewActions.append(a); m_toolMenu->addAction(a); m_toolToAction.insert(tv, a); updateActions(); } void GUIClient::unregisterToolView(ToolView *tv) { QAction *a = m_toolToAction[tv]; if (!a) { return; } m_toolViewActions.removeAt(m_toolViewActions.indexOf(a)); delete a; m_toolToAction.remove(tv); updateActions(); } void GUIClient::clientAdded(KXMLGUIClient *client) { if (client == this) { updateActions(); } } void GUIClient::updateActions() { if (!factory()) { return; } unplugActionList(actionListName); QList addList; addList.append(m_toolMenu); plugActionList(actionListName, addList); } //END GUICLIENT //BEGIN TOOLVIEW ToolView::ToolView(MainWindow *mainwin, Sidebar *sidebar, QWidget *parent) : QFrame(parent) , m_mainWin(mainwin) , m_sidebar(sidebar) , m_toolbar(nullptr) , m_toolVisible(false) , persistent(false) { // try to fix resize policy QSizePolicy policy(QSizePolicy::Preferred, QSizePolicy::Preferred); policy.setRetainSizeWhenHidden(true); setSizePolicy(policy); QVBoxLayout *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); m_toolbar = new KToolBar(this); m_toolbar->setVisible(false); m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); } QSize ToolView::sizeHint() const { return size(); } QSize ToolView::minimumSizeHint() const { return QSize(160, 160); } ToolView::~ToolView() { m_mainWin->toolViewDeleted(this); } void ToolView::setToolVisible(bool vis) { if (m_toolVisible == vis) { return; } m_toolVisible = vis; emit toolVisibleChanged(m_toolVisible); } bool ToolView::toolVisible() const { return m_toolVisible; } void ToolView::childEvent(QChildEvent *ev) { // set the widget to be focus proxy if possible if ((ev->type() == QEvent::ChildAdded) && qobject_cast(ev->child())) { QWidget *widget = qobject_cast(ev->child()); setFocusProxy(widget); layout()->addWidget(widget); } QFrame::childEvent(ev); } void ToolView::actionEvent(QActionEvent* event) { QFrame::actionEvent(event); if (event->type() == QEvent::ActionAdded) { m_toolbar->addAction(event->action()); } else if (event->type() == QEvent::ActionRemoved) { m_toolbar->removeAction(event->action()); } m_toolbar->setVisible(!m_toolbar->actions().isEmpty()); } //END TOOLVIEW //BEGIN SIDEBAR Sidebar::Sidebar(KMultiTabBar::KMultiTabBarPosition pos, MainWindow *mainwin, QWidget *parent) : KMultiTabBar(pos, parent) , m_mainWin(mainwin) , m_splitter(nullptr) , m_ownSplit(nullptr) , m_lastSize(0) { hide(); } Sidebar::~Sidebar() {} void Sidebar::setSplitter(QSplitter *sp) { m_splitter = sp; m_ownSplit = new QSplitter((position() == KMultiTabBar::Top || position() == KMultiTabBar::Bottom) ? Qt::Horizontal : Qt::Vertical, m_splitter); m_ownSplit->setChildrenCollapsible(false); m_ownSplit->hide(); } ToolView *Sidebar::addWidget(const QIcon &icon, const QString &text, ToolView *widget) { static int id = 0; if (widget) { if (widget->sidebar() == this) { return widget; } widget->sidebar()->removeWidget(widget); } int newId = ++id; appendTab(icon, newId, text); if (!widget) { widget = new ToolView(m_mainWin, this, m_ownSplit); widget->hide(); widget->icon = icon; widget->text = text; } else { widget->hide(); widget->setParent(m_ownSplit); widget->m_sidebar = this; } // save its pos ;) widget->persistent = false; m_idToWidget.insert(newId, widget); m_widgetToId.insert(widget, newId); m_toolviews.push_back(widget); // widget => size, for correct size restoration after hide/show // starts with invalid size m_widgetToSize.insert(widget, QSize()); show(); connect(tab(newId), SIGNAL(clicked(int)), this, SLOT(tabClicked(int))); tab(newId)->installEventFilter(this); tab(newId)->setToolTip(QString()); return widget; } bool Sidebar::removeWidget(ToolView *widget) { if (!m_widgetToId.contains(widget)) { return false; } removeTab(m_widgetToId[widget]); m_idToWidget.remove(m_widgetToId[widget]); m_widgetToId.remove(widget); m_widgetToSize.remove(widget); m_toolviews.removeAt(m_toolviews.indexOf(widget)); bool anyVis = false; QMapIterator it(m_idToWidget); while (it.hasNext()) { it.next(); if ((anyVis = it.value()->isVisible())) { break; } } if (m_idToWidget.isEmpty()) { m_ownSplit->hide(); hide(); } else if (!anyVis) { m_ownSplit->hide(); } return true; } bool Sidebar::showWidget(ToolView *widget) { if (!m_widgetToId.contains(widget)) { return false; } // hide other non-persistent views QMapIterator it(m_idToWidget); while (it.hasNext()) { it.next(); if ((it.value() != widget) && !it.value()->persistent) { hideWidget(it.value()); } } setTab(m_widgetToId[widget], true); /** * resize to right size again and show, else artifacts */ if (m_widgetToSize[widget].isValid()) { widget->resize(m_widgetToSize[widget]); } /** * resize to right size again and show, else artifacts * same as for widget, both needed */ if (m_preHideSize.isValid()) { widget->resize(m_preHideSize); m_ownSplit->resize(m_preHideSize); } m_ownSplit->show(); widget->show(); /** * we are visible again! */ widget->setToolVisible(true); return true; } bool Sidebar::hideWidget(ToolView *widget) { if (!m_widgetToId.contains(widget)) { return false; } bool anyVis = false; updateLastSize(); QMapIterator it(m_idToWidget); while (it.hasNext()) { it.next(); if (it.value() == widget) { // remember size and hide if (widget->isVisible()) { m_widgetToSize[widget] = widget->size(); } } else if ((anyVis = it.value()->isVisible())) { break; } } widget->hide(); // lower tab setTab(m_widgetToId[widget], false); if (!anyVis) { if (m_ownSplit->isVisible()) { m_preHideSize = m_ownSplit->size(); } m_ownSplit->hide(); } widget->setToolVisible(false); return true; } void Sidebar::tabClicked(int i) { ToolView *w = m_idToWidget[i]; if (!w) { return; } if (isTabRaised(i)) { showWidget(w); w->setFocus(); } else { hideWidget(w); } } bool Sidebar::eventFilter(QObject *obj, QEvent *ev) { if (ev->type() == QEvent::ContextMenu) { QContextMenuEvent *e = (QContextMenuEvent *) ev; KMultiTabBarTab *bt = dynamic_cast(obj); if (bt) { //qCDebug(LOG_KATE) << "Request for popup"; m_popupButton = bt->id(); ToolView *w = m_idToWidget[m_popupButton]; if (w) { QMenu *menu = new QMenu(this); if (!w->plugin.isNull()) { if (w->plugin.data()->configPages() > 0) { menu->addAction(i18n("Configure ..."))->setData(20); } } menu->addSection(QIcon::fromTheme(QStringLiteral("view_remove")), i18n("Behavior")); menu->addAction(w->persistent ? QIcon::fromTheme(QStringLiteral("view-restore")) : QIcon::fromTheme(QStringLiteral("view-fullscreen")), w->persistent ? i18n("Make Non-Persistent") : i18n("Make Persistent")) -> setData(10); menu->addSection(QIcon::fromTheme(QStringLiteral("move")), i18n("Move To")); if (position() != 0) { menu->addAction(QIcon::fromTheme(QStringLiteral("go-previous")), i18n("Left Sidebar"))->setData(0); } if (position() != 1) { menu->addAction(QIcon::fromTheme(QStringLiteral("go-next")), i18n("Right Sidebar"))->setData(1); } if (position() != 2) { menu->addAction(QIcon::fromTheme(QStringLiteral("go-up")), i18n("Top Sidebar"))->setData(2); } if (position() != 3) { menu->addAction(QIcon::fromTheme(QStringLiteral("go-down")), i18n("Bottom Sidebar"))->setData(3); } connect(menu, &QMenu::triggered, this, &Sidebar::buttonPopupActivate); menu->exec(e->globalPos()); delete menu; return true; } } } return false; } void Sidebar::setVisible(bool visible) { // visible==true means show-request if (visible && (m_idToWidget.isEmpty() || !m_mainWin->sidebarsVisible())) { return; } KMultiTabBar::setVisible(visible); } void Sidebar::buttonPopupActivate(QAction *a) { int id = a->data().toInt(); ToolView *w = m_idToWidget[m_popupButton]; if (!w) { return; } // move ids if (id < 4) { // move + show ;) m_mainWin->moveToolView(w, (KMultiTabBar::KMultiTabBarPosition) id); m_mainWin->showToolView(w); } // toggle persistent if (id == 10) { w->persistent = !w->persistent; } // configure actionCollection if (id == 20) { if (!w->plugin.isNull()) { if (w->plugin.data()->configPages() > 0) { emit sigShowPluginConfigPage(w->plugin.data(), 0); } } } } void Sidebar::updateLastSize() { QList s = m_splitter->sizes(); int i = 0; if ((position() == KMultiTabBar::Right || position() == KMultiTabBar::Bottom)) { i = 2; } // little threshold if (s[i] > 2) { m_lastSize = s[i]; } } class TmpToolViewSorter { public: ToolView *tv; unsigned int pos; }; void Sidebar::restoreSession(KConfigGroup &config) { // get the last correct placed toolview int firstWrong = 0; for (; firstWrong < m_toolviews.size(); ++firstWrong) { ToolView *tv = m_toolviews[firstWrong]; int pos = config.readEntry(QStringLiteral("Kate-MDI-ToolView-%1-Sidebar-Position").arg(tv->id), firstWrong); if (pos != firstWrong) { break; } } // we need to reshuffle, ahhh :( if (firstWrong < m_toolviews.size()) { // first: collect the items to reshuffle QList toSort; for (int i = firstWrong; i < m_toolviews.size(); ++i) { TmpToolViewSorter s; s.tv = m_toolviews[i]; s.pos = config.readEntry(QStringLiteral("Kate-MDI-ToolView-%1-Sidebar-Position").arg(m_toolviews[i]->id), i); toSort.push_back(s); } // now: sort the stuff we need to reshuffle for (int m = 0; m < toSort.size(); ++m) for (int n = m + 1; n < toSort.size(); ++n) if (toSort[n].pos < toSort[m].pos) { TmpToolViewSorter tmp = toSort[n]; toSort[n] = toSort[m]; toSort[m] = tmp; } // then: remove this items from the button bar // do this backwards, to minimize the relayout efforts for (int i = m_toolviews.size() - 1; i >= (int)firstWrong; --i) { removeTab(m_widgetToId[m_toolviews[i]]); } // insert the reshuffled things in order :) for (int i = 0; i < toSort.size(); ++i) { ToolView *tv = toSort[i].tv; m_toolviews[firstWrong + i] = tv; // readd the button int newId = m_widgetToId[tv]; appendTab(tv->icon, newId, tv->text); connect(tab(newId), SIGNAL(clicked(int)), this, SLOT(tabClicked(int))); tab(newId)->installEventFilter(this); tab(newId)->setToolTip(QString()); // reshuffle in splitter: move to last m_ownSplit->addWidget(tv); } } // update last size if needed updateLastSize(); // restore the own splitter sizes QList s = config.readEntry(QStringLiteral("Kate-MDI-Sidebar-%1-Splitter").arg(position()), QList()); m_ownSplit->setSizes(s); // show only correct toolviews, remember persistent values ;) bool anyVis = false; - for (int i = 0; i < m_toolviews.size(); ++i) { - ToolView *tv = m_toolviews[i]; - + for (auto tv : qAsConst(m_toolviews)) { tv->persistent = config.readEntry(QStringLiteral("Kate-MDI-ToolView-%1-Persistent").arg(tv->id), false); tv->setToolVisible(config.readEntry(QStringLiteral("Kate-MDI-ToolView-%1-Visible").arg(tv->id), false)); if (!anyVis) { anyVis = tv->toolVisible(); } setTab(m_widgetToId[tv], tv->toolVisible()); if (tv->toolVisible()) { tv->show(); } else { tv->hide(); } } if (anyVis) { m_ownSplit->show(); } else { m_ownSplit->hide(); } } void Sidebar::saveSession(KConfigGroup &config) { // store the own splitter sizes QList s = m_ownSplit->sizes(); config.writeEntry(QStringLiteral("Kate-MDI-Sidebar-%1-Splitter").arg(position()), s); // store the data about all toolviews in this sidebar ;) for (int i = 0; i < m_toolviews.size(); ++i) { ToolView *tv = m_toolviews[i]; config.writeEntry(QStringLiteral("Kate-MDI-ToolView-%1-Position").arg(tv->id), int(tv->sidebar()->position())); config.writeEntry(QStringLiteral("Kate-MDI-ToolView-%1-Sidebar-Position").arg(tv->id), i); config.writeEntry(QStringLiteral("Kate-MDI-ToolView-%1-Visible").arg(tv->id), tv->toolVisible()); config.writeEntry(QStringLiteral("Kate-MDI-ToolView-%1-Persistent").arg(tv->id), tv->persistent); } } //END SIDEBAR //BEGIN MAIN WINDOW MainWindow::MainWindow(QWidget *parentWidget) : KParts::MainWindow(parentWidget, Qt::Window) , m_sidebarsVisible(true) , m_restoreConfig(nullptr) , m_guiClient(new GUIClient(this)) { // init the internal widgets QFrame *hb = new QFrame(this); QHBoxLayout *hlayout = new QHBoxLayout(hb); hlayout->setContentsMargins(0, 0, 0, 0); hlayout->setSpacing(0); setCentralWidget(hb); m_sidebars[KMultiTabBar::Left] = new Sidebar(KMultiTabBar::Left, this, hb); hlayout->addWidget(m_sidebars[KMultiTabBar::Left]); m_hSplitter = new QSplitter(Qt::Horizontal, hb); hlayout->addWidget(m_hSplitter); m_sidebars[KMultiTabBar::Left]->setSplitter(m_hSplitter); QFrame *vb = new QFrame(m_hSplitter); QVBoxLayout *vlayout = new QVBoxLayout(vb); vlayout->setContentsMargins(0, 0, 0, 0); vlayout->setSpacing(0); m_hSplitter->setCollapsible(m_hSplitter->indexOf(vb), false); m_hSplitter->setStretchFactor(m_hSplitter->indexOf(vb), 1); m_sidebars[KMultiTabBar::Top] = new Sidebar(KMultiTabBar::Top, this, vb); vlayout->addWidget(m_sidebars[KMultiTabBar::Top]); m_vSplitter = new QSplitter(Qt::Vertical, vb); vlayout->addWidget(m_vSplitter); m_sidebars[KMultiTabBar::Top]->setSplitter(m_vSplitter); m_centralWidget = new QWidget(m_vSplitter); m_centralWidget->setLayout(new QVBoxLayout); m_centralWidget->layout()->setSpacing(0); m_centralWidget->layout()->setContentsMargins(0, 0, 0, 0); m_vSplitter->setCollapsible(m_vSplitter->indexOf(m_centralWidget), false); m_vSplitter->setStretchFactor(m_vSplitter->indexOf(m_centralWidget), 1); m_sidebars[KMultiTabBar::Bottom] = new Sidebar(KMultiTabBar::Bottom, this, vb); vlayout->addWidget(m_sidebars[KMultiTabBar::Bottom]); m_sidebars[KMultiTabBar::Bottom]->setSplitter(m_vSplitter); m_sidebars[KMultiTabBar::Right] = new Sidebar(KMultiTabBar::Right, this, hb); hlayout->addWidget(m_sidebars[KMultiTabBar::Right]); m_sidebars[KMultiTabBar::Right]->setSplitter(m_hSplitter); - for (int i = 0; i < 4; i++) { - connect(m_sidebars[i], &Sidebar::sigShowPluginConfigPage, this, &MainWindow::sigShowPluginConfigPage); + for (const auto sidebar : qAsConst(m_sidebars)) { + connect(sidebar, &Sidebar::sigShowPluginConfigPage, this, &MainWindow::sigShowPluginConfigPage); } } MainWindow::~MainWindow() { // cu toolviews qDeleteAll(m_toolviews); // seems like we really should delete this by hand ;) delete m_centralWidget; // cleanup the sidebars - for (unsigned int i = 0; i < 4; ++i) { - delete m_sidebars[i]; + for (auto sidebar : qAsConst(m_sidebars)) { + delete sidebar; } } QWidget *MainWindow::centralWidget() const { return m_centralWidget; } ToolView *MainWindow::createToolView(KTextEditor::Plugin *plugin, const QString &identifier, KMultiTabBar::KMultiTabBarPosition pos, const QIcon &icon, const QString &text) { if (m_idToWidget[identifier]) { return nullptr; } // try the restore config to figure out real pos if (m_restoreConfig && m_restoreConfig->hasGroup(m_restoreGroup)) { KConfigGroup cg(m_restoreConfig, m_restoreGroup); pos = (KMultiTabBar::KMultiTabBarPosition) cg.readEntry(QStringLiteral("Kate-MDI-ToolView-%1-Position").arg(identifier), int(pos)); } ToolView *v = m_sidebars[pos]->addWidget(icon, text, nullptr); v->id = identifier; v->plugin = plugin; m_idToWidget.insert(identifier, v); m_toolviews.push_back(v); // register for menu stuff m_guiClient->registerToolView(v); return v; } ToolView *MainWindow::toolView(const QString &identifier) const { return m_idToWidget[identifier]; } void MainWindow::toolViewDeleted(ToolView *widget) { if (!widget) { return; } if (widget->mainWindow() != this) { return; } // unregister from menu stuff m_guiClient->unregisterToolView(widget); widget->sidebar()->removeWidget(widget); m_idToWidget.remove(widget->id); m_toolviews.removeAt(m_toolviews.indexOf(widget)); } void MainWindow::setSidebarsVisible(bool visible) { bool old_visible = m_sidebarsVisible; m_sidebarsVisible = visible; m_sidebars[0]->setVisible(visible); m_sidebars[1]->setVisible(visible); m_sidebars[2]->setVisible(visible); m_sidebars[3]->setVisible(visible); m_guiClient->updateSidebarsVisibleAction(); // show information message box, if the users hides the sidebars if (old_visible && (!m_sidebarsVisible)) { KMessageBox::information(this, i18n("You are about to hide the sidebars. With " "hidden sidebars it is not possible to directly " "access the tool views with the mouse anymore, " "so if you need to access the sidebars again " "invoke View > Tool Views > Show Sidebars " "in the menu. It is still possible to show/hide " "the tool views with the assigned shortcuts."), QString(), QStringLiteral("Kate hide sidebars notification message")); } } bool MainWindow::sidebarsVisible() const { return m_sidebarsVisible; } void MainWindow::setToolViewStyle(KMultiTabBar::KMultiTabBarStyle style) { m_sidebars[0]->setStyle(style); m_sidebars[1]->setStyle(style); m_sidebars[2]->setStyle(style); m_sidebars[3]->setStyle(style); } KMultiTabBar::KMultiTabBarStyle MainWindow::toolViewStyle() const { // all sidebars have the same style, so just take Top return m_sidebars[KMultiTabBar::Top]->tabStyle(); } bool MainWindow::moveToolView(ToolView *widget, KMultiTabBar::KMultiTabBarPosition pos) { if (!widget || widget->mainWindow() != this) { return false; } // try the restore config to figure out real pos if (m_restoreConfig && m_restoreConfig->hasGroup(m_restoreGroup)) { KConfigGroup cg(m_restoreConfig, m_restoreGroup); pos = (KMultiTabBar::KMultiTabBarPosition) cg.readEntry(QStringLiteral("Kate-MDI-ToolView-%1-Position").arg(widget->id), int(pos)); } m_sidebars[pos]->addWidget(widget->icon, widget->text, widget); return true; } bool MainWindow::showToolView(ToolView *widget) { if (!widget || widget->mainWindow() != this) { return false; } // skip this if happens during restoring, or we will just see flicker if (m_restoreConfig && m_restoreConfig->hasGroup(m_restoreGroup)) { return true; } return widget->sidebar()->showWidget(widget); } bool MainWindow::hideToolView(ToolView *widget) { if (!widget || widget->mainWindow() != this) { return false; } // skip this if happens during restoring, or we will just see flicker if (m_restoreConfig && m_restoreConfig->hasGroup(m_restoreGroup)) { return true; } const bool ret = widget->sidebar()->hideWidget(widget); m_centralWidget->setFocus(); return ret; } void MainWindow::startRestore(KConfigBase *config, const QString &group) { // first save this stuff m_restoreConfig = config; m_restoreGroup = group; if (!m_restoreConfig || !m_restoreConfig->hasGroup(m_restoreGroup)) { // if no config around, set already now sane default sizes // otherwise, set later in ::finishRestore(), since it does not work // if set already now (see bug #164438) QList hs = (QList() << 200 << 100 << 200); QList vs = (QList() << 150 << 100 << 200); m_sidebars[0]->setLastSize(hs[0]); m_sidebars[1]->setLastSize(hs[2]); m_sidebars[2]->setLastSize(vs[0]); m_sidebars[3]->setLastSize(vs[2]); m_hSplitter->setSizes(hs); m_vSplitter->setSizes(vs); return; } // apply size once, to get sizes ready ;) KConfigGroup cg(m_restoreConfig, m_restoreGroup); KWindowConfig::restoreWindowSize(windowHandle(), cg); setToolViewStyle((KMultiTabBar::KMultiTabBarStyle)cg.readEntry("Kate-MDI-Sidebar-Style", (int)toolViewStyle())); // after reading m_sidebarsVisible, update the GUI toggle action m_sidebarsVisible = cg.readEntry("Kate-MDI-Sidebar-Visible", true); m_guiClient->updateSidebarsVisibleAction(); } void MainWindow::finishRestore() { if (!m_restoreConfig) { return; } if (m_restoreConfig->hasGroup(m_restoreGroup)) { // apply all settings, like toolbar pos and more ;) KConfigGroup cg(m_restoreConfig, m_restoreGroup); applyMainWindowSettings(cg); // reshuffle toolviews only if needed - for (int i = 0; i < m_toolviews.size(); ++i) { - KMultiTabBar::KMultiTabBarPosition newPos = (KMultiTabBar::KMultiTabBarPosition) cg.readEntry(QStringLiteral("Kate-MDI-ToolView-%1-Position").arg(m_toolviews[i]->id), int(m_toolviews[i]->sidebar()->position())); + for (const auto tv : qAsConst(m_toolviews)) { + KMultiTabBar::KMultiTabBarPosition newPos = (KMultiTabBar::KMultiTabBarPosition) cg.readEntry(QStringLiteral("Kate-MDI-ToolView-%1-Position").arg(tv->id), int(tv->sidebar()->position())); - if (m_toolviews[i]->sidebar()->position() != newPos) { - moveToolView(m_toolviews[i], newPos); + if (tv->sidebar()->position() != newPos) { + moveToolView(tv, newPos); } } // restore the sidebars - for (unsigned int i = 0; i < 4; ++i) { - m_sidebars[i]->restoreSession(cg); + for (auto sidebar : qAsConst(m_sidebars)) { + sidebar->restoreSession(cg); } // restore splitter sizes QList hs = (QList() << 200 << 100 << 200); QList vs = (QList() << 150 << 100 << 200); // get main splitter sizes ;) hs = cg.readEntry("Kate-MDI-H-Splitter", hs); vs = cg.readEntry("Kate-MDI-V-Splitter", vs); m_sidebars[0]->setLastSize(hs[0]); m_sidebars[1]->setLastSize(hs[2]); m_sidebars[2]->setLastSize(vs[0]); m_sidebars[3]->setLastSize(vs[2]); m_hSplitter->setSizes(hs); m_vSplitter->setSizes(vs); } // clear this stuff, we are done ;) m_restoreConfig = nullptr; m_restoreGroup.clear(); } void MainWindow::saveSession(KConfigGroup &config) { saveMainWindowSettings(config); // save main splitter sizes ;) QList hs = m_hSplitter->sizes(); QList vs = m_vSplitter->sizes(); if (hs[0] <= 2 && !m_sidebars[0]->splitterVisible()) { hs[0] = m_sidebars[0]->lastSize(); } if (hs[2] <= 2 && !m_sidebars[1]->splitterVisible()) { hs[2] = m_sidebars[1]->lastSize(); } if (vs[0] <= 2 && !m_sidebars[2]->splitterVisible()) { vs[0] = m_sidebars[2]->lastSize(); } if (vs[2] <= 2 && !m_sidebars[3]->splitterVisible()) { vs[2] = m_sidebars[3]->lastSize(); } config.writeEntry("Kate-MDI-H-Splitter", hs); config.writeEntry("Kate-MDI-V-Splitter", vs); // save sidebar style config.writeEntry("Kate-MDI-Sidebar-Style", (int)toolViewStyle()); config.writeEntry("Kate-MDI-Sidebar-Visible", m_sidebarsVisible); // save the sidebars - for (unsigned int i = 0; i < 4; ++i) { - m_sidebars[i]->saveSession(config); + for (auto sidebar : qAsConst(m_sidebars)) { + sidebar->saveSession(config); } } //END MAIN WINDOW } // namespace KateMDI diff --git a/kate/katemwmodonhddialog.cpp b/kate/katemwmodonhddialog.cpp index d9cc3e411..c76770438 100644 --- a/kate/katemwmodonhddialog.cpp +++ b/kate/katemwmodonhddialog.cpp @@ -1,375 +1,375 @@ /* This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2004, Anders Lund */ #include "katemwmodonhddialog.h" #include "kateapp.h" #include "katedocmanager.h" #include "katemainwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include class KateDocItem : public QTreeWidgetItem { public: KateDocItem(KTextEditor::Document *doc, const QString &status, QTreeWidget *tw) : QTreeWidgetItem(tw), document(doc) { setText(0, doc->url().toString()); setText(1, status); if (! doc->isModified()) { setCheckState(0, Qt::Checked); } else { setCheckState(0, Qt::Unchecked); } } ~KateDocItem() override {} KTextEditor::Document *document; }; KateMwModOnHdDialog::KateMwModOnHdDialog(DocVector docs, QWidget *parent, const char *name) : QDialog(parent), m_proc(nullptr), m_diffFile(nullptr), m_blockAddDocument(false) { setWindowTitle(i18n("Documents Modified on Disk")); setObjectName(QString::fromLatin1(name)); setModal(true); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); // Message QHBoxLayout *hb = new QHBoxLayout; mainLayout->addLayout(hb); // dialog text QLabel *icon = new QLabel(this); hb->addWidget(icon); icon->setPixmap(DesktopIcon(QStringLiteral("dialog-warning"))); QLabel *t = new QLabel(i18n( "The documents listed below have changed on disk.

Select one " "or more at once, and press an action button until the list is empty.

"), this); hb->addWidget(t); hb->setStretchFactor(t, 1000); // Document list twDocuments = new QTreeWidget(this); mainLayout->addWidget(twDocuments); QStringList header; header << i18n("Filename") << i18n("Status on Disk"); twDocuments->setHeaderLabels(header); twDocuments->setSelectionMode(QAbstractItemView::SingleSelection); twDocuments->setRootIsDecorated(false); m_stateTexts << QString() << i18n("Modified") << i18n("Created") << i18n("Deleted"); - for (int i = 0; i < docs.size(); i++) { - new KateDocItem(docs[i], m_stateTexts[(uint)KateApp::self()->documentManager()->documentInfo(docs[i])->modifiedOnDiscReason ], twDocuments); + for (auto& doc : qAsConst(docs)) { + new KateDocItem(doc, m_stateTexts[(uint)KateApp::self()->documentManager()->documentInfo(doc)->modifiedOnDiscReason ], twDocuments); } twDocuments->header()->setStretchLastSection(false); twDocuments->header()->setSectionResizeMode(0, QHeaderView::Stretch); twDocuments->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); connect(twDocuments, &QTreeWidget::currentItemChanged, this, &KateMwModOnHdDialog::slotSelectionChanged); // Diff line hb = new QHBoxLayout; mainLayout->addLayout(hb); btnDiff = new QPushButton(QIcon::fromTheme(QStringLiteral("document-preview")), i18n("&View Difference"), this); btnDiff->setWhatsThis(i18n( "Calculates the difference between the editor contents and the disk " "file for the selected document, and shows the difference with the " "default application. Requires diff(1).")); hb->addWidget(btnDiff); connect(btnDiff, &QPushButton::clicked, this, &KateMwModOnHdDialog::slotDiff); // Dialog buttons QDialogButtonBox *buttons = new QDialogButtonBox(this); mainLayout->addWidget(buttons); QPushButton *ignoreButton = new QPushButton(QIcon::fromTheme(QStringLiteral("dialog-warning")), i18n("&Ignore Changes")); ignoreButton->setToolTip(i18n("Remove modified flag from selected documents")); buttons->addButton(ignoreButton, QDialogButtonBox::RejectRole); connect(ignoreButton, &QPushButton::clicked, this, &KateMwModOnHdDialog::slotIgnore); QPushButton *overwriteButton = new QPushButton; KGuiItem::assign(overwriteButton, KStandardGuiItem::overwrite()); overwriteButton->setToolTip(i18n("Overwrite selected documents, discarding disk changes")); buttons->addButton(overwriteButton, QDialogButtonBox::DestructiveRole); connect(overwriteButton, &QPushButton::clicked, this, &KateMwModOnHdDialog::slotOverwrite); QPushButton *reloadButton = new QPushButton(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("&Reload")); reloadButton->setDefault(true); reloadButton->setToolTip(i18n("Reload selected documents from disk")); buttons->addButton(reloadButton, QDialogButtonBox::DestructiveRole); connect(reloadButton, &QPushButton::clicked, this, &KateMwModOnHdDialog::slotReload); slotSelectionChanged(nullptr, nullptr); } KateMwModOnHdDialog::~KateMwModOnHdDialog() { KateMainWindow::unsetModifiedOnDiscDialogIfIf(this); if (m_proc) { m_proc->kill(); m_proc->waitForFinished(); delete m_proc; m_proc = Q_NULLPTR; } if (m_diffFile) { m_diffFile->setAutoRemove(true); delete m_diffFile; m_diffFile = Q_NULLPTR; } } void KateMwModOnHdDialog::slotIgnore() { handleSelected(Ignore); } void KateMwModOnHdDialog::slotOverwrite() { handleSelected(Overwrite); } void KateMwModOnHdDialog::slotReload() { handleSelected(Reload); } void KateMwModOnHdDialog::handleSelected(int action) { // don't alter the treewidget via addDocument, we modify it here! m_blockAddDocument = true; // collect all items we can remove QList itemsToDelete; for (QTreeWidgetItemIterator it(twDocuments); *it; ++it) { KateDocItem *item = (KateDocItem *) * it; if (item->checkState(0) == Qt::Checked) { KTextEditor::ModificationInterface::ModifiedOnDiskReason reason = KateApp::self()->documentManager()->documentInfo(item->document)->modifiedOnDiscReason; bool success = true; if (KTextEditor::ModificationInterface *iface = qobject_cast(item->document)) { iface->setModifiedOnDisk(KTextEditor::ModificationInterface::OnDiskUnmodified); } switch (action) { case Overwrite: success = item->document->save(); if (! success) { KMessageBox::sorry(this, i18n("Could not save the document \n'%1'", item->document->url().toString())); } break; case Reload: item->document->documentReload(); break; default: break; } if (success) { itemsToDelete.append(item); } else { if (KTextEditor::ModificationInterface *iface = qobject_cast(item->document)) { iface->setModifiedOnDisk(reason); } } } } // remove the marked items, addDocument is blocked, this is save! for (int i = 0; i < itemsToDelete.count(); ++i) { delete itemsToDelete[i]; } // any documents left unhandled? if (! twDocuments->topLevelItemCount()) { accept(); } // allow addDocument again m_blockAddDocument = false; } void KateMwModOnHdDialog::slotSelectionChanged(QTreeWidgetItem *current, QTreeWidgetItem *) { KateDocItem *currentDocItem = static_cast(current); // set the diff button enabled btnDiff->setEnabled(currentDocItem && KateApp::self()->documentManager()->documentInfo(currentDocItem->document)->modifiedOnDiscReason != KTextEditor::ModificationInterface::OnDiskDeleted); } // ### the code below is slightly modified from kdelibs/kate/part/katedialogs, // class KateModOnHdPrompt. void KateMwModOnHdDialog::slotDiff() { if (!btnDiff->isEnabled()) { // diff button already pressed, proc not finished yet return; } if (! twDocuments->currentItem()) { return; } KTextEditor::Document *doc = (static_cast(twDocuments->currentItem()))->document; // don't try to diff a deleted file if (KateApp::self()->documentManager()->documentInfo(doc)->modifiedOnDiscReason == KTextEditor::ModificationInterface::OnDiskDeleted) { return; } if (m_diffFile) { return; } m_diffFile = new QTemporaryFile(); m_diffFile->open(); // Start a KProcess that creates a diff m_proc = new KProcess(this); m_proc->setOutputChannelMode(KProcess::MergedChannels); *m_proc << QStringLiteral("diff") << QStringLiteral("-ub") << QStringLiteral("-") << doc->url().toLocalFile(); connect(m_proc, &KProcess::readyRead, this, &KateMwModOnHdDialog::slotDataAvailable); connect(m_proc, static_cast(&KProcess::finished), this, &KateMwModOnHdDialog::slotPDone); setCursor(Qt::WaitCursor); btnDiff->setEnabled(false); m_proc->start(); QTextStream ts(m_proc); int lastln = doc->lines() - 1; for (int l = 0; l < lastln; ++l) { ts << doc->line(l) << QLatin1Char('\n'); } ts << doc->line(lastln); ts.flush(); m_proc->closeWriteChannel(); } void KateMwModOnHdDialog::slotDataAvailable() { m_diffFile->write(m_proc->readAll()); } void KateMwModOnHdDialog::slotPDone() { setCursor(Qt::ArrowCursor); slotSelectionChanged(twDocuments->currentItem(), nullptr); const QProcess::ExitStatus es = m_proc->exitStatus(); delete m_proc; m_proc = nullptr; if (es != QProcess::NormalExit) { KMessageBox::sorry(this, i18n("The diff command failed. Please make sure that " "diff(1) is installed and in your PATH."), i18n("Error Creating Diff")); delete m_diffFile; m_diffFile = nullptr; return; } if (m_diffFile->size() == 0) { KMessageBox::information(this, i18n("Ignoring amount of white space changed, the files are identical."), i18n("Diff Output")); delete m_diffFile; m_diffFile = nullptr; return; } m_diffFile->setAutoRemove(false); QUrl url = QUrl::fromLocalFile(m_diffFile->fileName()); delete m_diffFile; m_diffFile = nullptr; // KRun::runUrl should delete the file, once the client exits KRun::runUrl(url, QStringLiteral("text/x-patch"), this, KRun::RunFlags(KRun::DeleteTemporaryFiles)); } void KateMwModOnHdDialog::addDocument(KTextEditor::Document *doc) { // guard this e.g. during handleSelected if (m_blockAddDocument) return; for (QTreeWidgetItemIterator it(twDocuments); *it; ++it) { KateDocItem *item = (KateDocItem *) * it; if (item->document == doc) { delete item; break; } } uint reason = (uint)KateApp::self()->documentManager()->documentInfo(doc)->modifiedOnDiscReason; if (reason) { new KateDocItem(doc, m_stateTexts[reason], twDocuments); } if (! twDocuments->topLevelItemCount()) { accept(); } } void KateMwModOnHdDialog::keyPressEvent(QKeyEvent *event) { if (event->modifiers() == 0) { if (event->key() == Qt::Key_Escape) { event->accept(); return; } } QDialog::keyPressEvent(event); } void KateMwModOnHdDialog::closeEvent(QCloseEvent *e) { if (! twDocuments->topLevelItemCount()) { QDialog::closeEvent(e); } else { e->ignore(); } } diff --git a/kate/katepluginmanager.cpp b/kate/katepluginmanager.cpp index 1dfb4ea63..6fea0cb96 100644 --- a/kate/katepluginmanager.cpp +++ b/kate/katepluginmanager.cpp @@ -1,362 +1,358 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2001 Anders Lund This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "config.h" - #include "katepluginmanager.h" #include "kateapp.h" #include "katemainwindow.h" #include "katedebug.h" #include #include #include #include #include #include #include QString KatePluginInfo::saveName() const { return QFileInfo(metaData.fileName()).baseName(); } +bool KatePluginInfo::operator<(const KatePluginInfo &other) const +{ + if (sortOrder != other.sortOrder) + return sortOrder < other.sortOrder; + + return saveName() < other.saveName(); +} + KatePluginManager::KatePluginManager(QObject *parent) : QObject(parent) { setupPluginList(); } KatePluginManager::~KatePluginManager() { - // than unload the plugins unloadAllPlugins(); } void KatePluginManager::setupPluginList() { - /** - * get all KTextEditor/Plugins - */ - const QVector plugins = KPluginLoader::findPlugins(QStringLiteral("ktexteditor"), [](const KPluginMetaData & md) { - return md.serviceTypes().contains(QStringLiteral("KTextEditor/Plugin")); - }); - - /** - * move them to our internal data structure, - * activate some plugins per default, - * the following list is ordered alphabetically by plugin name - * (this is not a technical need; just to have some order) - */ - QSet defaultPlugins; - - defaultPlugins.insert(QStringLiteral("cuttlefishplugin")); // this comes with package plasma5-sdk but it won't hurt to list it here (activate by right click in the text area) + // activate a hand-picked list of plugins per default, give them a hand-picked sort order for loading + const QMap defaultPlugins { + { QStringLiteral("katefiletreeplugin"), -1000 } + , { QStringLiteral("katesearchplugin"), -900 } + , { QStringLiteral("kateprojectplugin"), -800 } + , { QStringLiteral("tabswitcherplugin"), -100 } + , { QStringLiteral("textfilterplugin"), -100 } #ifndef WIN32 - defaultPlugins.insert(QStringLiteral("katefilebrowserplugin")); // currently works badly on Windows - defaultPlugins.insert(QStringLiteral("katekonsoleplugin")); // currently does not work on Windows at all + , { QStringLiteral("katefilebrowserplugin"), -100 } // currently works badly on Windows + , { QStringLiteral("katekonsoleplugin"), -100 } // currently does not work on Windows at all #endif - defaultPlugins.insert(QStringLiteral("katefiletreeplugin")); - defaultPlugins.insert(QStringLiteral("kateprojectplugin")); - defaultPlugins.insert(QStringLiteral("katesearchplugin")); - //defaultPlugins.insert(QLatin1String("ktexteditorpreviewplugin")); // the feature is nice and needed, but in its current state we should not present it by default - defaultPlugins.insert(QStringLiteral("tabswitcherplugin")); - defaultPlugins.insert(QStringLiteral("textfilterplugin")); + }; + // handle all install KTextEditor plugins m_pluginList.clear(); - QVectorIterator i(plugins); QSet unique; - while (i.hasNext()) { + const QVector plugins = KPluginLoader::findPlugins(QStringLiteral("ktexteditor"), [](const KPluginMetaData & md) { + return md.serviceTypes().contains(QStringLiteral("KTextEditor/Plugin")); + }); + for (const auto &pluginMetaData : plugins) { KatePluginInfo info; - info.metaData = i.next(); + info.metaData = pluginMetaData; // only load plugins once, even if found multiple times! if (unique.contains(info.saveName())) continue; info.defaultLoad = defaultPlugins.contains(info.saveName()); + info.sortOrder = defaultPlugins.value(info.saveName()); info.load = false; info.plugin = nullptr; m_pluginList.push_back(info); unique.insert (info.saveName()); } - /** - * construct fast lookup map - */ + // sort to ensure some deterministic plugin load order, this is important for tool-view creation order + std::sort(m_pluginList.begin(), m_pluginList.end()); + + // construct fast lookup map, do this after vector has final size, resize will invalidate the pointers! m_name2Plugin.clear(); - for (int i = 0; i < m_pluginList.size(); ++i) { - m_name2Plugin[m_pluginList[i].saveName()] = &(m_pluginList[i]); + for (auto& pluginInfo : m_pluginList) { + m_name2Plugin[pluginInfo.saveName()] = &pluginInfo; } } void KatePluginManager::loadConfig(KConfig *config) { // first: unload the plugins unloadAllPlugins(); /** * ask config object */ if (config) { KConfigGroup cg = KConfigGroup(config, QStringLiteral("Kate Plugins")); // disable all plugin if no config, beside the ones marked as default load - for (int i = 0; i < m_pluginList.size(); ++i) { - m_pluginList[i].load = cg.readEntry(m_pluginList[i].saveName(), m_pluginList[i].defaultLoad); + for (auto& pluginInfo: m_pluginList) { + pluginInfo.load = cg.readEntry(pluginInfo.saveName(), pluginInfo.defaultLoad); } } /** * load plugins */ - for (KatePluginList::iterator it = m_pluginList.begin(); it != m_pluginList.end(); ++it) { - if (it->load) { + for (auto& pluginInfo : m_pluginList) { + if (pluginInfo.load) { /** * load plugin + trigger update of GUI for already existing main windows */ - loadPlugin(&(*it)); - enablePluginGUI(&(*it)); + loadPlugin(&pluginInfo); + enablePluginGUI(&pluginInfo); // restore config - if (auto interface = qobject_cast (it->plugin)) { - KConfigGroup group(config, QStringLiteral("Plugin:%1:").arg(it->saveName())); + if (auto interface = qobject_cast (pluginInfo.plugin)) { + KConfigGroup group(config, QStringLiteral("Plugin:%1:").arg(pluginInfo.saveName())); interface->readSessionConfig(group); } } } } void KatePluginManager::writeConfig(KConfig *config) { Q_ASSERT(config); KConfigGroup cg = KConfigGroup(config, QStringLiteral("Kate Plugins")); foreach(const KatePluginInfo & plugin, m_pluginList) { QString saveName = plugin.saveName(); cg.writeEntry(saveName, plugin.load); // save config if (auto interface = qobject_cast (plugin.plugin)) { KConfigGroup group(config, QStringLiteral("Plugin:%1:").arg(saveName)); interface->writeSessionConfig(group); } } } void KatePluginManager::unloadAllPlugins() { - for (KatePluginList::iterator it = m_pluginList.begin(); it != m_pluginList.end(); ++it) { - if (it->plugin) { - unloadPlugin(&(*it)); + for (auto& pluginInfo : m_pluginList) { + if (pluginInfo.plugin) { + unloadPlugin(&pluginInfo); } } } void KatePluginManager::enableAllPluginsGUI(KateMainWindow *win, KConfigBase *config) { - for (KatePluginList::iterator it = m_pluginList.begin(); it != m_pluginList.end(); ++it) { - if (it->plugin) { - enablePluginGUI(&(*it), win, config); + for (auto& pluginInfo : m_pluginList) { + if (pluginInfo.plugin) { + enablePluginGUI(&pluginInfo, win, config); } } } void KatePluginManager::disableAllPluginsGUI(KateMainWindow *win) { - for (KatePluginList::iterator it = m_pluginList.begin(); it != m_pluginList.end(); ++it) { - if (it->plugin) { - disablePluginGUI(&(*it), win); + for (auto& pluginInfo : m_pluginList) { + if (pluginInfo.plugin) { + disablePluginGUI(&pluginInfo, win); } } } bool KatePluginManager::loadPlugin(KatePluginInfo *item) { /** * try to load the plugin */ auto factory = KPluginLoader(item->metaData.fileName()).factory(); if (factory) { item->plugin = factory->create(this, QVariantList() << item->saveName()); } item->load = item->plugin != nullptr; /** * tell the world about the success */ if (item->plugin) { emit KateApp::self()->wrapper()->pluginCreated(item->saveName(), item->plugin); } return item->plugin != nullptr; } void KatePluginManager::unloadPlugin(KatePluginInfo *item) { disablePluginGUI(item); delete item->plugin; KTextEditor::Plugin *plugin = item->plugin; item->plugin = nullptr; item->load = false; emit KateApp::self()->wrapper()->pluginDeleted(item->saveName(), plugin); } void KatePluginManager::enablePluginGUI(KatePluginInfo *item, KateMainWindow *win, KConfigBase *config) { // plugin around at all? if (!item->plugin) { return; } // lookup if there is already a view for it.. QObject *createdView = nullptr; if (!win->pluginViews().contains(item->plugin)) { // create the view + try to correctly load shortcuts, if it's a GUI Client createdView = item->plugin->createView(win->wrapper()); if (createdView) { win->pluginViews().insert(item->plugin, createdView); } } // load session config if needed if (config && win->pluginViews().contains(item->plugin)) { if (auto interface = qobject_cast (win->pluginViews().value(item->plugin))) { KConfigGroup group(config, QStringLiteral("Plugin:%1:MainWindow:0").arg(item->saveName())); interface->readSessionConfig(group); } } if (createdView) { emit win->wrapper()->pluginViewCreated(item->saveName(), createdView); } } void KatePluginManager::enablePluginGUI(KatePluginInfo *item) { // plugin around at all? if (!item->plugin) { return; } // enable the gui for all mainwindows... for (int i = 0; i < KateApp::self()->mainWindowsCount(); i++) { enablePluginGUI(item, KateApp::self()->mainWindow(i), nullptr); } } void KatePluginManager::disablePluginGUI(KatePluginInfo *item, KateMainWindow *win) { // plugin around at all? if (!item->plugin) { return; } // lookup if there is a view for it.. if (!win->pluginViews().contains(item->plugin)) { return; } // really delete the view of this plugin QObject *deletedView = win->pluginViews().value(item->plugin); delete deletedView; win->pluginViews().remove(item->plugin); emit win->wrapper()->pluginViewDeleted(item->saveName(), deletedView); } void KatePluginManager::disablePluginGUI(KatePluginInfo *item) { // plugin around at all? if (!item->plugin) { return; } // disable the gui for all mainwindows... for (int i = 0; i < KateApp::self()->mainWindowsCount(); i++) { disablePluginGUI(item, KateApp::self()->mainWindow(i)); } } KTextEditor::Plugin *KatePluginManager::plugin(const QString &name) { /** * name known? */ if (!m_name2Plugin.contains(name)) { return nullptr; } /** * real plugin instance, if any ;) */ return m_name2Plugin.value(name)->plugin; } bool KatePluginManager::pluginAvailable(const QString &name) { return m_name2Plugin.contains(name); } class KTextEditor::Plugin *KatePluginManager::loadPlugin(const QString &name, bool permanent) { /** * name known? */ if (!m_name2Plugin.contains(name)) { return nullptr; } /** * load, bail out on error */ loadPlugin(m_name2Plugin.value(name)); if (!m_name2Plugin.value(name)->plugin) { return nullptr; } /** * perhaps patch not load again back to "ok, load it once again on next loadConfig" */ m_name2Plugin.value(name)->load = permanent; return m_name2Plugin.value(name)->plugin; } void KatePluginManager::unloadPlugin(const QString &name, bool permanent) { /** * name known? */ if (!m_name2Plugin.contains(name)) { return; } /** * unload */ unloadPlugin(m_name2Plugin.value(name)); /** * perhaps patch load again back to "ok, load it once again on next loadConfig" */ m_name2Plugin.value(name)->load = !permanent; } diff --git a/kate/katepluginmanager.h b/kate/katepluginmanager.h index 27c895e40..3087b9551 100644 --- a/kate/katepluginmanager.h +++ b/kate/katepluginmanager.h @@ -1,104 +1,101 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2001 Anders Lund This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KATE_PLUGINMANAGER_H__ #define __KATE_PLUGINMANAGER_H__ #include #include #include #include #include #include class KConfig; class KateMainWindow; class KatePluginInfo { public: - KatePluginInfo() - : load(false) - , defaultLoad(false) - , plugin(nullptr) - {} - bool load; - bool defaultLoad; + bool load = false; + bool defaultLoad = false; KPluginMetaData metaData; - KTextEditor::Plugin *plugin; + KTextEditor::Plugin *plugin = nullptr; + int sortOrder = 0; QString saveName() const; + bool operator<(const KatePluginInfo &other) const; }; typedef QList KatePluginList; class KatePluginManager : public QObject { Q_OBJECT public: KatePluginManager(QObject *parent); ~KatePluginManager() override; void unloadAllPlugins(); void enableAllPluginsGUI(KateMainWindow *win, KConfigBase *config = nullptr); void disableAllPluginsGUI(KateMainWindow *win); void loadConfig(KConfig *); void writeConfig(KConfig *); bool loadPlugin(KatePluginInfo *item); void unloadPlugin(KatePluginInfo *item); void enablePluginGUI(KatePluginInfo *item, KateMainWindow *win, KConfigBase *config = nullptr); void enablePluginGUI(KatePluginInfo *item); void disablePluginGUI(KatePluginInfo *item, KateMainWindow *win); void disablePluginGUI(KatePluginInfo *item); inline KatePluginList &pluginList() { return m_pluginList; } KTextEditor::Plugin *plugin(const QString &name); bool pluginAvailable(const QString &name); KTextEditor::Plugin *loadPlugin(const QString &name, bool permanent = true); void unloadPlugin(const QString &name, bool permanent = true); private: void setupPluginList(); /** * all known plugins */ KatePluginList m_pluginList; /** * fast access map from name => plugin info * uses the info stored in the plugin list */ QMap m_name2Plugin; }; #endif diff --git a/kate/katequickopen.cpp b/kate/katequickopen.cpp index aa639de8b..86014d874 100644 --- a/kate/katequickopen.cpp +++ b/kate/katequickopen.cpp @@ -1,179 +1,193 @@ /* This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2007,2009 Joseph Wenninger */ #include "katequickopen.h" #include "katequickopenmodel.h" #include "katemainwindow.h" #include "kateviewmanager.h" #include "kateapp.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(QPointer) KateQuickOpen::KateQuickOpen(QWidget *parent, KateMainWindow *mainWindow) : QWidget(parent) , m_mainWindow(mainWindow) { QVBoxLayout *layout = new QVBoxLayout(); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); m_inputLine = new KLineEdit(); setFocusProxy(m_inputLine); m_inputLine->setPlaceholderText(i18n("Quick Open Search")); layout->addWidget(m_inputLine); m_listView = new QTreeView(); layout->addWidget(m_listView, 1); m_listView->setTextElideMode(Qt::ElideLeft); m_base_model = new KateQuickOpenModel(m_mainWindow, this); m_model = new QSortFilterProxyModel(this); m_model->setFilterRole(Qt::DisplayRole); m_model->setSortRole(Qt::DisplayRole); m_model->setFilterCaseSensitivity(Qt::CaseInsensitive); m_model->setSortCaseSensitivity(Qt::CaseInsensitive); m_model->setFilterKeyColumn(0); connect(m_inputLine, &KLineEdit::textChanged, m_model, &QSortFilterProxyModel::setFilterWildcard); connect(m_inputLine, &KLineEdit::returnPressed, this, &KateQuickOpen::slotReturnPressed); connect(m_model, &QSortFilterProxyModel::rowsInserted, this, &KateQuickOpen::reselectFirst); connect(m_model, &QSortFilterProxyModel::rowsRemoved, this, &KateQuickOpen::reselectFirst); connect(m_listView, &QTreeView::activated, this, &KateQuickOpen::slotReturnPressed); m_listView->setModel(m_model); m_model->setSourceModel(m_base_model); m_inputLine->installEventFilter(this); m_listView->installEventFilter(this); m_listView->setHeaderHidden(true); m_listView->setRootIsDecorated(false); } bool KateQuickOpen::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); if (obj == m_inputLine) { const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp) || (keyEvent->key() == Qt::Key_PageDown); if (forward2list) { QCoreApplication::sendEvent(m_listView, event); return true; } if (keyEvent->key() == Qt::Key_Escape) { m_mainWindow->slotWindowActivated(); m_inputLine->clear(); return true; } } else { const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp) && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab); if (forward2input) { QCoreApplication::sendEvent(m_inputLine, event); return true; } } } // hide on focus out, if neither input field nor list have focus! else if (event->type() == QEvent::FocusOut && !(m_inputLine->hasFocus() || m_listView->hasFocus())) { m_mainWindow->slotWindowActivated(); m_inputLine->clear(); return true; } return QWidget::eventFilter(obj, event); } void KateQuickOpen::reselectFirst() { - QModelIndex index = m_model->index(0, 0); + int first = 0; + if (m_mainWindow->viewManager()->sortedViews().size() > 1) + first = 1; + + QModelIndex index = m_model->index(first, 0); m_listView->setCurrentIndex(index); } void KateQuickOpen::update() { m_base_model->refresh(); m_listView->resizeColumnToContents(0); // If we have a very long file name we restrict the size of the first column // to take at most half of the space. Otherwise it would look odd. int colw0 = m_listView->header()->sectionSize(0); // file name int colw1 = m_listView->header()->sectionSize(1); // file path if (colw0 > colw1) { m_listView->setColumnWidth(0, (colw0 + colw1) / 2); } reselectFirst(); } void KateQuickOpen::slotReturnPressed() { const auto index = m_listView->model()->index(m_listView->currentIndex().row(), KateQuickOpenModel::Columns::FilePath); auto url = index.data(Qt::UserRole).toUrl(); m_mainWindow->wrapper()->openUrl(url); m_mainWindow->slotWindowActivated(); m_inputLine->clear(); } void KateQuickOpen::setMatchMode(int mode) { m_model->setFilterKeyColumn(mode); } int KateQuickOpen::matchMode() { return m_model->filterKeyColumn(); } + +void KateQuickOpen::setListMode(KateQuickOpenModel::List mode) +{ + m_base_model->setListMode(mode); +} + +KateQuickOpenModel::List KateQuickOpen::listMode() const +{ + return m_base_model->listMode(); +} diff --git a/kate/katequickopen.h b/kate/katequickopen.h index bc0dd556f..805e19a77 100644 --- a/kate/katequickopen.h +++ b/kate/katequickopen.h @@ -1,76 +1,80 @@ /* Copyright (C) 2007,2009 Joseph Wenninger This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KATE_QUICK_OPEN_H #define KATE_QUICK_OPEN_H #include class KateMainWindow; class KLineEdit; class QModelIndex; class QStandardItemModel; class QSortFilterProxyModel; class QTreeView; class KateQuickOpenModel; +enum KateQuickOpenModelList : int; class KateQuickOpen : public QWidget { Q_OBJECT public: KateQuickOpen(QWidget *parent, KateMainWindow *mainWindow); /** * update state * will fill model with current open documents, project documents, ... */ void update(); int matchMode(); void setMatchMode(int mode); + KateQuickOpenModelList listMode() const; + void setListMode(KateQuickOpenModelList mode); + protected: bool eventFilter(QObject *obj, QEvent *event) override; private Q_SLOTS: void reselectFirst(); /** * Return pressed, activate the selected document * and go back to background */ void slotReturnPressed(); private: KateMainWindow *m_mainWindow; QTreeView *m_listView; KLineEdit *m_inputLine; /** * our model we search in */ KateQuickOpenModel *m_base_model; /** * filtered model we search in */ QSortFilterProxyModel *m_model; }; #endif diff --git a/kate/katequickopenmodel.cpp b/kate/katequickopenmodel.cpp index 6a596090f..624452b7f 100644 --- a/kate/katequickopenmodel.cpp +++ b/kate/katequickopenmodel.cpp @@ -1,138 +1,138 @@ /* This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2018 Tomaz Canabrava */ #include "katequickopenmodel.h" #include "katemainwindow.h" #include "kateviewmanager.h" #include "kateapp.h" #include #include KateQuickOpenModel::KateQuickOpenModel(KateMainWindow *mainWindow, QObject *parent) : QAbstractTableModel (parent), m_mainWindow(mainWindow) { } int KateQuickOpenModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } return m_modelEntries.size(); } int KateQuickOpenModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return 2; } QVariant KateQuickOpenModel::data(const QModelIndex& idx, int role) const { if(! idx.isValid()) { return {}; } if (role != Qt::DisplayRole && role != Qt::FontRole && role != Qt::UserRole) { return {}; } auto entry = m_modelEntries.at(idx.row()); if (role == Qt::DisplayRole) { switch(idx.column()) { case Columns::FileName: return entry.fileName; case Columns::FilePath: return entry.filePath; } } else if (role == Qt::FontRole) { if (entry.bold) { QFont font; font.setBold(true); return font; } } else if (role == Qt::UserRole) { return entry.url; } return {}; } void KateQuickOpenModel::refresh() { QObject *projectView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin")); const QList sortedViews = m_mainWindow->viewManager()->sortedViews(); const QList openDocs = KateApp::self()->documentManager()->documentList(); - const QStringList projectDocs = projectView ? projectView->property("projectFiles").toStringList() : QStringList(); + const QStringList projectDocs = projectView + ? (m_listMode == CurrentProject + ? projectView->property("projectFiles") : projectView->property("allProjectsFiles")).toStringList() + : QStringList(); QVector allDocuments; - allDocuments.resize(sortedViews.size() + openDocs.size() + projectDocs.size()); + allDocuments.reserve(sortedViews.size() + openDocs.size() + projectDocs.size()); + size_t sort_id = (size_t)-1; for (auto *view : qAsConst(sortedViews)) { auto doc = view->document(); - allDocuments.push_back({ doc->url(), doc->documentName(), doc->url().toDisplayString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile), false }); + allDocuments.push_back({ doc->url(), doc->documentName(), doc->url().toDisplayString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile), true, sort_id --}); } - QStringList openedUrls; - openedUrls.reserve(openDocs.size()); for (auto *doc : qAsConst(openDocs)) { const auto normalizedUrl = doc->url().toString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile); - allDocuments.push_back({ doc->url(), doc->documentName(), normalizedUrl, false }); - openedUrls.push_back(normalizedUrl); + allDocuments.push_back({ doc->url(), doc->documentName(), normalizedUrl, true, 0 }); } for (const auto& file : qAsConst(projectDocs)) { QFileInfo fi(file); const auto localFile = QUrl::fromLocalFile(fi.absoluteFilePath()); allDocuments.push_back({ localFile, fi.fileName(), - localFile.toString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile), false }); + localFile.toString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile), false, 0 }); } /** Sort the arrays by filePath. */ - std::sort(std::begin(allDocuments), std::end(allDocuments), + std::stable_sort(std::begin(allDocuments), std::end(allDocuments), [](const ModelEntry& a, const ModelEntry& b) { return a.filePath < b.filePath; }); - /** remove Duplicates. */ + /** remove Duplicates. + * Note that the stable_sort above guarantees that the items that the + * bold/sort_id fields of the items added first are correctly preserved. + */ allDocuments.erase( std::unique(allDocuments.begin(), allDocuments.end(), [](const ModelEntry& a, const ModelEntry& b) { return a.filePath == b.filePath; }), std::end(allDocuments)); - for (auto& doc : allDocuments) { - if (Q_UNLIKELY(openedUrls.indexOf(doc.filePath) != -1)) { - doc.bold = true; - } - } - /** sort the arrays via boldness (open or not */ - std::sort(std::begin(allDocuments), std::end(allDocuments), + std::stable_sort(std::begin(allDocuments), std::end(allDocuments), [](const ModelEntry& a, const ModelEntry& b) { + if (a.bold == b.bold) + return a.sort_id > b.sort_id; return a.bold > b.bold; }); beginResetModel(); m_modelEntries = allDocuments; endResetModel(); } diff --git a/kate/katequickopenmodel.h b/kate/katequickopenmodel.h index 709918c5f..dcd3a5934 100644 --- a/kate/katequickopenmodel.h +++ b/kate/katequickopenmodel.h @@ -1,58 +1,67 @@ /* This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2018 Tomaz Canabrava */ #ifndef KATEQUICKOPENMODEL_H #define KATEQUICKOPENMODEL_H #include #include #include #include #include "katemainwindow.h" struct ModelEntry { QUrl url; // used for actually opening a selected file (local or remote) QString fileName; // display string for left column QString filePath; // display string for right column bool bold; // format line in bold text or not + size_t sort_id; }; +// needs to be defined outside of class to support forward declaration elsewhere +enum KateQuickOpenModelList : int { CurrentProject, AllProjects }; + class KateQuickOpenModel : public QAbstractTableModel { Q_OBJECT public: enum Columns : int { FileName, FilePath, Bold }; explicit KateQuickOpenModel(KateMainWindow *mainWindow, QObject *parent=nullptr); int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QVariant data(const QModelIndex& idx, int role) const override; void refresh(); + // add a convenient in-class alias + using List = KateQuickOpenModelList; + List listMode() const { return m_listMode; } + void setListMode(List mode) { m_listMode = mode; } private: QVector m_modelEntries; /* TODO: don't rely in a pointer to the main window. * this is bad engineering, but current code is too tight * on this and it's hard to untangle without breaking existing * code. */ KateMainWindow *m_mainWindow; + List m_listMode; }; #endif diff --git a/kate/kateviewmanager.cpp b/kate/kateviewmanager.cpp index c72e2484d..e69b86f1e 100644 --- a/kate/kateviewmanager.cpp +++ b/kate/kateviewmanager.cpp @@ -1,1244 +1,1255 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2001 Anders Lund This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ //BEGIN Includes #include "kateviewmanager.h" +#include "config.h" #include "kateapp.h" #include "katemainwindow.h" #include "kateviewspace.h" #include "kateupdatedisabler.h" #include #include #include #include #include #include #include #include #include #include #include -#include - -#ifdef KActivities_FOUND +#ifdef KF5Activities_FOUND #include #endif #include #include //END Includes static const qint64 FileSizeAboveToAskUserIfProceedWithOpen = 10 * 1024 * 1024; // 10MB should suffice KateViewManager::KateViewManager(QWidget *parentW, KateMainWindow *parent) : QSplitter(parentW) , m_mainWindow(parent) , m_blockViewCreationAndActivation(false) , m_activeViewRunning(false) , m_minAge(0) , m_guiMergedView(nullptr) { // while init m_init = true; + // we don't allow full collapse, see bug 366014 + setChildrenCollapsible(false); + // important, set them up, as we use them in other methodes setupActions(); KateViewSpace *vs = new KateViewSpace(this, nullptr); addWidget(vs); vs->setActive(true); m_viewSpaceList.append(vs); connect(this, &KateViewManager::viewChanged, this, &KateViewManager::slotViewChanged); connect(KateApp::self()->documentManager(), &KateDocManager::documentCreatedViewManager, this, &KateViewManager::documentCreated); /** * before document is really deleted: cleanup all views! */ connect(KateApp::self()->documentManager(), &KateDocManager::documentWillBeDeleted , this, &KateViewManager::documentWillBeDeleted); /** * handle document deletion transactions * disable view creation in between * afterwards ensure we have views ;) */ connect(KateApp::self()->documentManager(), &KateDocManager::aboutToDeleteDocuments , this, &KateViewManager::aboutToDeleteDocuments); connect(KateApp::self()->documentManager(), &KateDocManager::documentsDeleted , this, &KateViewManager::documentsDeleted); // register all already existing documents m_blockViewCreationAndActivation = true; const QList &docs = KateApp::self()->documentManager()->documentList(); foreach(KTextEditor::Document * doc, docs) { documentCreated(doc); } m_blockViewCreationAndActivation = false; // init done m_init = false; } KateViewManager::~KateViewManager() { /** * remove the single client that is registered at the factory, if any */ if (m_guiMergedView) { mainWindow()->guiFactory()->removeClient(m_guiMergedView); m_guiMergedView = nullptr; } } void KateViewManager::setupActions() { /** * view splitting */ m_splitViewVert = m_mainWindow->actionCollection()->addAction(QStringLiteral("view_split_vert")); m_splitViewVert->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right"))); m_splitViewVert->setText(i18n("Split Ve&rtical")); m_mainWindow->actionCollection()->setDefaultShortcut(m_splitViewVert, Qt::CTRL + Qt::SHIFT + Qt::Key_L); connect(m_splitViewVert, &QAction::triggered, this, &KateViewManager::slotSplitViewSpaceVert); m_splitViewVert->setWhatsThis(i18n("Split the currently active view vertically into two views.")); m_splitViewHoriz = m_mainWindow->actionCollection()->addAction(QStringLiteral("view_split_horiz")); m_splitViewHoriz->setIcon(QIcon::fromTheme(QStringLiteral("view-split-top-bottom"))); m_splitViewHoriz->setText(i18n("Split &Horizontal")); m_mainWindow->actionCollection()->setDefaultShortcut(m_splitViewHoriz, Qt::CTRL + Qt::SHIFT + Qt::Key_T); connect(m_splitViewHoriz, &QAction::triggered, this, &KateViewManager::slotSplitViewSpaceHoriz); m_splitViewHoriz->setWhatsThis(i18n("Split the currently active view horizontally into two views.")); m_closeView = m_mainWindow->actionCollection()->addAction(QStringLiteral("view_close_current_space")); m_closeView->setIcon(QIcon::fromTheme(QStringLiteral("view-close"))); m_closeView->setText(i18n("Cl&ose Current View")); m_mainWindow->actionCollection()->setDefaultShortcut(m_closeView, Qt::CTRL + Qt::SHIFT + Qt::Key_R); connect(m_closeView, &QAction::triggered, this, &KateViewManager::slotCloseCurrentViewSpace, Qt::QueuedConnection); m_closeView->setWhatsThis(i18n("Close the currently active split view")); m_closeOtherViews = m_mainWindow->actionCollection()->addAction(QStringLiteral("view_close_others")); m_closeOtherViews->setIcon(QIcon::fromTheme(QStringLiteral("view-close"))); m_closeOtherViews->setText(i18n("Close Inactive Views")); connect(m_closeOtherViews, &QAction::triggered, this, &KateViewManager::slotCloseOtherViews, Qt::QueuedConnection); m_closeOtherViews->setWhatsThis(i18n("Close every view but the active one")); m_hideOtherViews = m_mainWindow->actionCollection()->addAction(QStringLiteral("view_hide_others")); m_hideOtherViews->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen"))); m_hideOtherViews->setText(i18n("Hide Inactive Views")); m_hideOtherViews->setCheckable(true); connect(m_hideOtherViews, &QAction::triggered, this, &KateViewManager::slotHideOtherViews, Qt::QueuedConnection); m_hideOtherViews->setWhatsThis(i18n("Hide every view but the active one")); m_toggleSplitterOrientation = m_mainWindow->actionCollection()->addAction(QStringLiteral("view_split_toggle")); m_toggleSplitterOrientation->setText(i18n("Toggle Orientation")); connect(m_toggleSplitterOrientation, &QAction::triggered, this, &KateViewManager::toggleSplitterOrientation, Qt::QueuedConnection); m_toggleSplitterOrientation->setWhatsThis(i18n("Toggles the orientation of the current split view")); goNext = m_mainWindow->actionCollection()->addAction(QStringLiteral("go_next_split_view")); goNext->setText(i18n("Next Split View")); m_mainWindow->actionCollection()->setDefaultShortcut(goNext, Qt::Key_F8); connect(goNext, &QAction::triggered, this, &KateViewManager::activateNextView); goNext->setWhatsThis(i18n("Make the next split view the active one.")); goPrev = m_mainWindow->actionCollection()->addAction(QStringLiteral("go_prev_split_view")); goPrev->setText(i18n("Previous Split View")); m_mainWindow->actionCollection()->setDefaultShortcut(goPrev, Qt::SHIFT + Qt::Key_F8); connect(goPrev, &QAction::triggered, this, &KateViewManager::activatePrevView); goPrev->setWhatsThis(i18n("Make the previous split view the active one.")); QAction * a = m_mainWindow->actionCollection()->addAction(QStringLiteral("view_split_move_right")); a->setText(i18n("Move Splitter Right")); connect(a, &QAction::triggered, this, &KateViewManager::moveSplitterRight); a->setWhatsThis(i18n("Move the splitter of the current view to the right")); a = m_mainWindow->actionCollection()->addAction(QStringLiteral("view_split_move_left")); a->setText(i18n("Move Splitter Left")); connect(a, &QAction::triggered, this, &KateViewManager::moveSplitterLeft); a->setWhatsThis(i18n("Move the splitter of the current view to the left")); a = m_mainWindow->actionCollection()->addAction(QStringLiteral("view_split_move_up")); a->setText(i18n("Move Splitter Up")); connect(a, &QAction::triggered, this, &KateViewManager::moveSplitterUp); a->setWhatsThis(i18n("Move the splitter of the current view up")); a = m_mainWindow->actionCollection()->addAction(QStringLiteral("view_split_move_down")); a->setText(i18n("Move Splitter Down")); connect(a, &QAction::triggered, this, &KateViewManager::moveSplitterDown); a->setWhatsThis(i18n("Move the splitter of the current view down")); } void KateViewManager::updateViewSpaceActions() { m_closeView->setEnabled(m_viewSpaceList.count() > 1); m_closeOtherViews->setEnabled(m_viewSpaceList.count() > 1); m_toggleSplitterOrientation->setEnabled(m_viewSpaceList.count() > 1); goNext->setEnabled(m_viewSpaceList.count() > 1); goPrev->setEnabled(m_viewSpaceList.count() > 1); } void KateViewManager::slotDocumentNew() { createView(); } void KateViewManager::slotDocumentOpen() { // try to start dialog in useful dir: either dir of current doc or last used one KTextEditor::View * const cv = activeView(); QUrl startUrl = cv ? cv->document()->url() : QUrl(); if (startUrl.isValid()) { m_lastOpenDialogUrl = startUrl; } else { startUrl = m_lastOpenDialogUrl; } const QList urls = QFileDialog::getOpenFileUrls(m_mainWindow, i18n("Open File"), startUrl); /** * emit size warning, for local files */ QString fileListWithTooLargeFiles; Q_FOREACH(const QUrl & url, urls) { if (!url.isLocalFile()) { continue; } const auto size = QFile(url.toLocalFile()).size(); if (size > FileSizeAboveToAskUserIfProceedWithOpen) { fileListWithTooLargeFiles += QStringLiteral("
  • %1 (%2MB)
  • ").arg(url.fileName()).arg(size / 1024 / 1024); } } if (!fileListWithTooLargeFiles.isEmpty()) { const QString text = i18n("

    You are attempting to open one or more large files:

      %1

    Do you want to proceed?

    Beware that kate may stop responding for some time when opening large files.

    " , fileListWithTooLargeFiles); const auto ret = KMessageBox::warningYesNo(this, text, i18n("Opening Large File"), KStandardGuiItem::cont(), KStandardGuiItem::stop()); if (ret == KMessageBox::No) { return; } } // activate view of last opened document KateDocumentInfo docInfo; docInfo.openedByUser = true; if (KTextEditor::Document *lastID = openUrls(urls, QString(), false, docInfo)) { activateView(lastID); } } void KateViewManager::slotDocumentClose(KTextEditor::Document *document) { bool shutdownKate = m_mainWindow->modCloseAfterLast() && KateApp::self()->documentManager()->documentList().size() == 1; // close document if (KateApp::self()->documentManager()->closeDocument(document) && shutdownKate) { KateApp::self()->shutdownKate(m_mainWindow); } } void KateViewManager::slotDocumentClose() { // no active view, do nothing if (!activeView()) { return; } slotDocumentClose(activeView()->document()); } KTextEditor::Document *KateViewManager::openUrl(const QUrl &url, const QString &encoding, bool activate, bool isTempFile, const KateDocumentInfo &docInfo) { KTextEditor::Document *doc = KateApp::self()->documentManager()->openUrl(url, encoding, isTempFile, docInfo); if (!doc->url().isEmpty()) { m_mainWindow->fileOpenRecent()->addUrl(doc->url()); } if (activate) { activateView(doc); } return doc; } KTextEditor::Document *KateViewManager::openUrls(const QList &urls, const QString &encoding, bool isTempFile, const KateDocumentInfo &docInfo) { QList docs = KateApp::self()->documentManager()->openUrls(urls, encoding, isTempFile, docInfo); foreach(const KTextEditor::Document * doc, docs) { if (!doc->url().isEmpty()) { m_mainWindow->fileOpenRecent()->addUrl(doc->url()); } } return docs.isEmpty() ? nullptr : docs.last(); } KTextEditor::View *KateViewManager::openUrlWithView(const QUrl &url, const QString &encoding) { KTextEditor::Document *doc = KateApp::self()->documentManager()->openUrl(url, encoding); if (!doc) { return nullptr; } if (!doc->url().isEmpty()) { m_mainWindow->fileOpenRecent()->addUrl(doc->url()); } activateView(doc); return activeView(); } void KateViewManager::openUrl(const QUrl &url) { openUrl(url, QString()); } KateMainWindow *KateViewManager::mainWindow() { return m_mainWindow; } void KateViewManager::documentCreated(KTextEditor::Document *doc) { // forward to currently active view space activeViewSpace()->registerDocument(doc); // to update open recent files on saving connect(doc, &KTextEditor::Document::documentSavedOrUploaded, this, &KateViewManager::documentSavedOrUploaded); if (m_blockViewCreationAndActivation) { return; } if (!activeView()) { activateView(doc); } /** * check if we have any empty viewspaces and give them a view */ Q_FOREACH(KateViewSpace * vs, m_viewSpaceList) { if (!vs->currentView()) { createView(activeView()->document(), vs); } } } void KateViewManager::aboutToDeleteDocuments(const QList &) { /** * block view creation until the transaction is done * this shall not stack! */ Q_ASSERT (!m_blockViewCreationAndActivation); m_blockViewCreationAndActivation = true; /** * disable updates hard (we can't use KateUpdateDisabler here, we have delayed signal */ mainWindow()->setUpdatesEnabled(false); } void KateViewManager::documentsDeleted(const QList &) { /** * again allow view creation */ m_blockViewCreationAndActivation = false; /** * try to have active view around! */ if (!activeView() && !KateApp::self()->documentManager()->documentList().isEmpty()) { createView(KateApp::self()->documentManager()->documentList().last()); } /** * if we have one now, show them in all viewspaces that got empty! */ if (KTextEditor::View *const newActiveView = activeView()) { /** * check if we have any empty viewspaces and give them a view */ Q_FOREACH(KateViewSpace * vs, m_viewSpaceList) { if (!vs->currentView()) { createView(newActiveView->document(), vs); } } emit viewChanged(newActiveView); } /** * enable updates hard (we can't use KateUpdateDisabler here, we have delayed signal */ mainWindow()->setUpdatesEnabled(true); } void KateViewManager::documentSavedOrUploaded(KTextEditor::Document *doc, bool) { if (!doc->url().isEmpty()) { m_mainWindow->fileOpenRecent()->addUrl(doc->url()); } } KTextEditor::View *KateViewManager::createView(KTextEditor::Document *doc, KateViewSpace *vs) { if (m_blockViewCreationAndActivation) { return nullptr; } // create doc if (!doc) { doc = KateApp::self()->documentManager()->createDoc(); } /** * create view, registers its XML gui itself * pass the view the correct main window */ KTextEditor::View *view = (vs ? vs : activeViewSpace())->createView(doc); /** * remember this view, active == false, min age set * create activity resource */ m_views[view].active = false; m_views[view].lruAge = m_minAge--; -#ifdef KActivities_FOUND +#ifdef KF5Activities_FOUND m_views[view].activityResource = new KActivities::ResourceInstance(view->window()->winId(), view); m_views[view].activityResource->setUri(doc->url()); #endif // disable settings dialog action delete view->actionCollection()->action(QStringLiteral("set_confdlg")); delete view->actionCollection()->action(QStringLiteral("editor_options")); connect(view, SIGNAL(dropEventPass(QDropEvent*)), mainWindow(), SLOT(slotDropEvent(QDropEvent*))); connect(view, &KTextEditor::View::focusIn, this, &KateViewManager::activateSpace); viewCreated(view); if (!vs) { activateView(view); } return view; } bool KateViewManager::deleteView(KTextEditor::View *view) { if (!view) { return true; } KateViewSpace *viewspace = static_cast(view->parentWidget()->parentWidget()); viewspace->removeView(view); /** * deregister if needed */ if (m_guiMergedView == view) { mainWindow()->guiFactory()->removeClient(m_guiMergedView); m_guiMergedView = nullptr; } // remove view from mapping and memory !! m_views.remove(view); delete view; return true; } KateViewSpace *KateViewManager::activeViewSpace() { for (QList::const_iterator it = m_viewSpaceList.constBegin(); it != m_viewSpaceList.constEnd(); ++it) { if ((*it)->isActiveSpace()) { return *it; } } // none active, so use the first we grab if (!m_viewSpaceList.isEmpty()) { m_viewSpaceList.first()->setActive(true); return m_viewSpaceList.first(); } Q_ASSERT(false); return nullptr; } KTextEditor::View *KateViewManager::activeView() { if (m_activeViewRunning) { return nullptr; } m_activeViewRunning = true; QHashIterator it(m_views); while (it.hasNext()) { it.next(); if (it.value().active) { m_activeViewRunning = false; return it.key(); } } // if we get to here, no view isActive() // first, try to get one from activeViewSpace() KateViewSpace *vs = activeViewSpace(); if (vs && vs->currentView()) { activateView(vs->currentView()); m_activeViewRunning = false; return vs->currentView(); } // last attempt: just pick first if (!m_views.isEmpty()) { KTextEditor::View *v = m_views.begin().key(); activateView(v); m_activeViewRunning = false; return v; } m_activeViewRunning = false; // no views exists! return nullptr; } void KateViewManager::setActiveSpace(KateViewSpace *vs) { if (activeViewSpace()) { activeViewSpace()->setActive(false); } vs->setActive(true); } void KateViewManager::setActiveView(KTextEditor::View *view) { if (activeView()) { m_views[activeView()].active = false; } if (view) { m_views[view].active = true; } } void KateViewManager::activateSpace(KTextEditor::View *v) { if (!v) { return; } KateViewSpace *vs = static_cast(v->parentWidget()->parentWidget()); if (!vs->isActiveSpace()) { setActiveSpace(vs); activateView(v); } } void KateViewManager::reactivateActiveView() { KTextEditor::View *view = activeView(); if (view) { m_views[view].active = false; activateView(view); } } void KateViewManager::activateView(KTextEditor::View *view) { if (!view) { return; } Q_ASSERT (m_views.contains(view)); if (!m_views[view].active) { // avoid flicker KateUpdateDisabler disableUpdates (mainWindow()); if (!activeViewSpace()->showView(view)) { // since it wasn't found, give'em a new one createView(view->document()); return; } setActiveView(view); bool toolbarVisible = mainWindow()->toolBar()->isVisible(); if (toolbarVisible) { mainWindow()->toolBar()->hide(); // hide to avoid toolbar flickering } if (m_guiMergedView) { mainWindow()->guiFactory()->removeClient(m_guiMergedView); m_guiMergedView = nullptr; } if (!m_blockViewCreationAndActivation) { mainWindow()->guiFactory()->addClient(view); m_guiMergedView = view; } if (toolbarVisible) { mainWindow()->toolBar()->show(); } // remember age of this view m_views[view].lruAge = m_minAge--; emit viewChanged(view); -#ifdef KActivities_FOUND +#ifdef KF5Activities_FOUND // inform activity manager m_views[view].activityResource->setUri(view->document()->url()); m_views[view].activityResource->notifyFocusedIn(); #endif } } KTextEditor::View *KateViewManager::activateView(KTextEditor::Document *d) { // no doc with this id found if (!d) { return activeView(); } // activate existing view if possible if (activeViewSpace()->showView(d)) { activateView(activeViewSpace()->currentView()); return activeView(); } // create new view otherwise createView(d); return activeView(); } void KateViewManager::slotViewChanged() { if (activeView() && !activeView()->hasFocus()) { activeView()->setFocus(); } } void KateViewManager::activateNextView() { int i = m_viewSpaceList.indexOf(activeViewSpace()) + 1; if (i >= m_viewSpaceList.count()) { i = 0; } setActiveSpace(m_viewSpaceList.at(i)); activateView(m_viewSpaceList.at(i)->currentView()); } void KateViewManager::activatePrevView() { int i = m_viewSpaceList.indexOf(activeViewSpace()) - 1; if (i < 0) { i = m_viewSpaceList.count() - 1; } setActiveSpace(m_viewSpaceList.at(i)); activateView(m_viewSpaceList.at(i)->currentView()); } void KateViewManager::documentWillBeDeleted(KTextEditor::Document *doc) { /** * collect all views of that document that belong to this manager */ QList closeList; Q_FOREACH (KTextEditor::View *v, doc->views()) { if (m_views.contains(v)) { closeList.append(v); } } while (!closeList.isEmpty()) { deleteView(closeList.takeFirst()); } } void KateViewManager::closeView(KTextEditor::View *view) { /** * kill view we want to kill */ deleteView(view); /** * try to have active view around! */ if (!activeView() && !KateApp::self()->documentManager()->documentList().isEmpty()) { createView(KateApp::self()->documentManager()->documentList().last()); } /** * if we have one now, show them in all viewspaces that got empty! */ if (KTextEditor::View *const newActiveView = activeView()) { /** * check if we have any empty viewspaces and give them a view */ Q_FOREACH(KateViewSpace * vs, m_viewSpaceList) { if (!vs->currentView()) { createView(newActiveView->document(), vs); } } emit viewChanged(newActiveView); } } void KateViewManager::splitViewSpace(KateViewSpace *vs, // = 0 Qt::Orientation o) // = Qt::Horizontal { // emergency: fallback to activeViewSpace, and if still invalid, abort if (!vs) { vs = activeViewSpace(); } if (!vs) { return; } // get current splitter, and abort if null QSplitter *currentSplitter = qobject_cast(vs->parentWidget()); if (!currentSplitter) { return; } // avoid flicker KateUpdateDisabler disableUpdates (mainWindow()); // index where to insert new splitter/viewspace const int index = currentSplitter->indexOf(vs); // create new viewspace KateViewSpace *vsNew = new KateViewSpace(this); // only 1 children -> we are the root container. So simply set the orientation // and add the new view space, then correct the sizes to 50%:50% if (currentSplitter->count() == 1) { if (currentSplitter->orientation() != o) { currentSplitter->setOrientation(o); } QList sizes = currentSplitter->sizes(); sizes << (sizes.first() - currentSplitter->handleWidth()) / 2; sizes[0] = sizes[1]; currentSplitter->insertWidget(index, vsNew); currentSplitter->setSizes(sizes); } else { // create a new QSplitter and replace vs with the splitter. vs and newVS are // the new children of the new QSplitter QSplitter *newContainer = new QSplitter(o); + + // we don't allow full collapse, see bug 366014 + newContainer->setChildrenCollapsible(false); + QList currentSizes = currentSplitter->sizes(); newContainer->addWidget(vs); newContainer->addWidget(vsNew); currentSplitter->insertWidget(index, newContainer); newContainer->show(); // fix sizes of children of old and new splitter currentSplitter->setSizes(currentSizes); QList newSizes = newContainer->sizes(); newSizes[0] = (newSizes[0] + newSizes[1] - newContainer->handleWidth()) / 2; newSizes[1] = newSizes[0]; newContainer->setSizes(newSizes); } m_viewSpaceList.append(vsNew); activeViewSpace()->setActive(false); vsNew->setActive(true); vsNew->show(); createView((KTextEditor::Document *)activeView()->document()); updateViewSpaceActions(); } void KateViewManager::closeViewSpace(KTextEditor::View *view) { KateViewSpace *space; if (view) { space = static_cast(view->parentWidget()->parentWidget()); } else { space = activeViewSpace(); } removeViewSpace(space); } void KateViewManager::toggleSplitterOrientation() { KateViewSpace *vs = activeViewSpace(); if (!vs) { return; } // get current splitter, and abort if null QSplitter *currentSplitter = qobject_cast(vs->parentWidget()); if (!currentSplitter || (currentSplitter->count() == 1)) { return; } // avoid flicker KateUpdateDisabler disableUpdates (mainWindow()); // toggle orientation if (currentSplitter->orientation() == Qt::Horizontal) { currentSplitter->setOrientation(Qt::Vertical); } else { currentSplitter->setOrientation(Qt::Horizontal); } } bool KateViewManager::viewsInSameViewSpace(KTextEditor::View *view1, KTextEditor::View *view2) { if (!view1 || !view2) { return false; } if (m_viewSpaceList.size() == 1) { return true; } KateViewSpace *vs1 = static_cast(view1->parentWidget()->parentWidget()); KateViewSpace *vs2 = static_cast(view2->parentWidget()->parentWidget()); return vs1 && (vs1 == vs2); } void KateViewManager::removeViewSpace(KateViewSpace *viewspace) { // abort if viewspace is 0 if (!viewspace) { return; } // abort if this is the last viewspace if (m_viewSpaceList.count() < 2) { return; } // get current splitter QSplitter *currentSplitter = qobject_cast(viewspace->parentWidget()); // no splitter found, bah if (!currentSplitter) { return; } // // 1. get LRU document list from current viewspace // 2. delete current view space // 3. add LRU documents from deleted viewspace to new active viewspace // // backup LRU list const QVector lruDocumntsList = viewspace->lruDocumentList(); // avoid flicker KateUpdateDisabler disableUpdates (mainWindow()); // delete views of the viewspace while (viewspace->currentView()) { deleteView(viewspace->currentView()); } // cu viewspace m_viewSpaceList.removeAt(m_viewSpaceList.indexOf(viewspace)); delete viewspace; // at this point, the splitter has exactly 1 child Q_ASSERT(currentSplitter->count() == 1); // if we are not the root splitter, move the child one level up and delete // the splitter then. if (currentSplitter != this) { // get parent splitter QSplitter *parentSplitter = qobject_cast(currentSplitter->parentWidget()); // only do magic if found ;) if (parentSplitter) { int index = parentSplitter->indexOf(currentSplitter); // save current splitter size, as the removal of currentSplitter looses the info QList parentSizes = parentSplitter->sizes(); parentSplitter->insertWidget(index, currentSplitter->widget(0)); delete currentSplitter; // now restore the sizes again parentSplitter->setSizes(parentSizes); } } else if (QSplitter *splitter = qobject_cast(currentSplitter->widget(0))) { // we are the root splitter and have only one child, which is also a splitter // -> eliminate the redundant splitter and move both children into the root splitter QList sizes = splitter->sizes(); // adapt splitter orientation to the splitter we are about to delete currentSplitter->setOrientation(splitter->orientation()); currentSplitter->addWidget(splitter->widget(0)); currentSplitter->addWidget(splitter->widget(0)); delete splitter; currentSplitter->setSizes(sizes); } // merge docuemnts of closed view space activeViewSpace()->mergeLruList(lruDocumntsList); // find the view that is now active. KTextEditor::View *v = activeViewSpace()->currentView(); if (v) { activateView(v); } updateViewSpaceActions(); emit viewChanged(v); } void KateViewManager::slotCloseOtherViews() { // avoid flicker KateUpdateDisabler disableUpdates(mainWindow()); const KateViewSpace *active = activeViewSpace(); foreach(KateViewSpace * v, m_viewSpaceList) { if (active != v) { removeViewSpace(v); } } } void KateViewManager::slotHideOtherViews(bool hideOthers) { // avoid flicker KateUpdateDisabler disableUpdates(mainWindow()); const KateViewSpace *active = activeViewSpace(); foreach(KateViewSpace * v, m_viewSpaceList) { if (active != v) { v->setVisible(!hideOthers); } } // disable the split actions, if we are in single-view-mode m_splitViewVert->setDisabled(hideOthers); m_splitViewHoriz->setDisabled(hideOthers); m_closeView->setDisabled(hideOthers); m_closeOtherViews->setDisabled(hideOthers); m_toggleSplitterOrientation->setDisabled(hideOthers); } /** * session config functions */ void KateViewManager::saveViewConfiguration(KConfigGroup &config) { // set Active ViewSpace to 0, just in case there is none active (would be // strange) and config somehow has previous value set config.writeEntry("Active ViewSpace", 0); m_splitterIndex = 0; saveSplitterConfig(this, config.config(), config.name()); } void KateViewManager::restoreViewConfiguration(const KConfigGroup &config) { /** * remove the single client that is registered at the factory, if any */ if (m_guiMergedView) { mainWindow()->guiFactory()->removeClient(m_guiMergedView); m_guiMergedView = nullptr; } /** * delete viewspaces, they will delete the views */ qDeleteAll(m_viewSpaceList); m_viewSpaceList.clear(); /** * delete mapping of now deleted views */ m_views.clear(); /** * kill all previous existing sub-splitters, just to be sure * e.g. important if one restores a config in an existing window with some splitters */ while (count() > 0) { delete widget(0); } // reset lru history, too! m_minAge = 0; // start recursion for the root splitter (Splitter 0) restoreSplitter(config.config(), config.name() + QStringLiteral("-Splitter 0"), this, config.name()); // finally, make the correct view from the last session active int lastViewSpace = config.readEntry("Active ViewSpace", 0); if (lastViewSpace > m_viewSpaceList.size()) { lastViewSpace = 0; } if (lastViewSpace >= 0 && lastViewSpace < m_viewSpaceList.size()) { setActiveSpace(m_viewSpaceList.at(lastViewSpace)); // activate correct view (wish #195435, #188764) activateView(m_viewSpaceList.at(lastViewSpace)->currentView()); // give view the focus to avoid focus stealing by toolviews / plugins m_viewSpaceList.at(lastViewSpace)->currentView()->setFocus(); } // emergency if (m_viewSpaceList.empty()) { // kill bad children while (count()) { delete widget(0); } KateViewSpace *vs = new KateViewSpace(this, nullptr); addWidget(vs); vs->setActive(true); m_viewSpaceList.append(vs); /** * activate at least one document! */ activateView(KateApp::self()->documentManager()->documentList().last()); if (!vs->currentView()) { createView(activeView()->document(), vs); } } updateViewSpaceActions(); } QString KateViewManager::saveSplitterConfig(QSplitter *s, KConfigBase *configBase, const QString &viewConfGrp) { /** * avoid to export invisible view spaces * else they will stick around for ever in sessions * bug 358266 - code initially done during load * bug 381433 - moved code to save */ /** * create new splitter name, might be not used */ const auto grp = QString(viewConfGrp + QStringLiteral("-Splitter %1")).arg(m_splitterIndex); ++m_splitterIndex; // a QSplitter has two children, either QSplitters and/or KateViewSpaces // special case: root splitter might have only one child (just for info) QStringList childList; const auto sizes = s->sizes(); for (int it = 0; it < s->count(); ++it) { // skip empty sized invisible ones, if not last one, we need one thing at least if ((sizes[it] == 0) && ((it + 1 < s->count()) || !childList.empty())) continue; // For KateViewSpaces, ask them to save the file list. auto obj = s->widget(it); if (auto kvs = qobject_cast(obj)) { childList.append(QString(viewConfGrp + QStringLiteral("-ViewSpace %1")).arg(m_viewSpaceList.indexOf(kvs))); kvs->saveConfig(configBase, m_viewSpaceList.indexOf(kvs), viewConfGrp); // save active viewspace if (kvs->isActiveSpace()) { KConfigGroup viewConfGroup(configBase, viewConfGrp); viewConfGroup.writeEntry("Active ViewSpace", m_viewSpaceList.indexOf(kvs)); } } // for QSplitters, recurse else if (auto splitter = qobject_cast(obj)) { childList.append(saveSplitterConfig(splitter, configBase, viewConfGrp)); } } // if only one thing, skip splitter config export, if not top splitter if ((s != this) && (childList.size() == 1)) return childList.at(0); // Save sizes, orient, children for this splitter KConfigGroup config(configBase, grp); config.writeEntry("Sizes", sizes); config.writeEntry("Orientation", int(s->orientation())); config.writeEntry("Children", childList); return grp; } void KateViewManager::restoreSplitter(const KConfigBase *configBase, const QString &group, QSplitter *parent, const QString &viewConfGrp) { KConfigGroup config(configBase, group); parent->setOrientation((Qt::Orientation)config.readEntry("Orientation", int(Qt::Horizontal))); - QStringList children = config.readEntry("Children", QStringList()); - for (QStringList::Iterator it = children.begin(); it != children.end(); ++it) { + const QStringList children = config.readEntry("Children", QStringList()); + for (const auto& str : children) { // for a viewspace, create it and open all documents therein. - if ((*it).startsWith(viewConfGrp + QStringLiteral("-ViewSpace"))) { + if (str.startsWith(viewConfGrp + QStringLiteral("-ViewSpace"))) { KateViewSpace *vs = new KateViewSpace(this, nullptr); m_viewSpaceList.append(vs); // make active so that the view created in restoreConfig has this // new view space as parent. setActiveSpace(vs); parent->addWidget(vs); - vs->restoreConfig(this, configBase, *it); + vs->restoreConfig(this, configBase, str); vs->show(); } else { - // for a splitter, recurse. - restoreSplitter(configBase, *it, new QSplitter(parent), viewConfGrp); + // for a splitter, recurse + auto newContainer = new QSplitter(parent); + + // we don't allow full collapse, see bug 366014 + newContainer->setChildrenCollapsible(false); + + restoreSplitter(configBase, str, newContainer, viewConfGrp); } } // set sizes parent->setSizes(config.readEntry("Sizes", QList())); parent->show(); } void KateViewManager::moveSplitter(Qt::Key key, int repeats) { if (repeats < 1) { return; } KateViewSpace *vs = activeViewSpace(); if (!vs) { return; } QSplitter *currentSplitter = qobject_cast(vs->parentWidget()); if (!currentSplitter) { return; } if (currentSplitter->count() == 1) { return; } int move = 4 * repeats; // try to use font height in pixel to move splitter { KTextEditor::Attribute::Ptr attrib(vs->currentView()->defaultStyleAttribute(KTextEditor::dsNormal)); QFontMetrics fm(attrib->font()); move = fm.height() * repeats; } QWidget *currentWidget = (QWidget *)vs; bool foundSplitter = false; // find correct splitter to be moved while (currentSplitter && currentSplitter->count() != 1) { if (currentSplitter->orientation() == Qt::Horizontal && (key == Qt::Key_Right || key == Qt::Key_Left)) { foundSplitter = true; } if (currentSplitter->orientation() == Qt::Vertical && (key == Qt::Key_Up || key == Qt::Key_Down)) { foundSplitter = true; } // if the views within the current splitter can be resized, resize them if (foundSplitter) { QList currentSizes = currentSplitter->sizes(); int index = currentSplitter->indexOf(currentWidget); if ((index == 0 && (key == Qt::Key_Left || key == Qt::Key_Up)) || (index == 1 && (key == Qt::Key_Right || key == Qt::Key_Down))) { currentSizes[index] -= move; } if ((index == 0 && (key == Qt::Key_Right || key == Qt::Key_Down)) || (index == 1 && (key == Qt::Key_Left || key == Qt::Key_Up))) { currentSizes[index] += move; } if (index == 0 && (key == Qt::Key_Right || key == Qt::Key_Down)) { currentSizes[index + 1] -= move; } if (index == 0 && (key == Qt::Key_Left || key == Qt::Key_Up)) { currentSizes[index + 1] += move; } if (index == 1 && (key == Qt::Key_Right || key == Qt::Key_Down)) { currentSizes[index - 1] += move; } if (index == 1 && (key == Qt::Key_Left || key == Qt::Key_Up)) { currentSizes[index - 1] -= move; } currentSplitter->setSizes(currentSizes); break; } currentWidget = (QWidget *)currentSplitter; // the parent of the current splitter will become the current splitter currentSplitter = qobject_cast(currentSplitter->parentWidget()); } } diff --git a/kate/kateviewmanager.h b/kate/kateviewmanager.h index ea0e27c03..955453754 100644 --- a/kate/kateviewmanager.h +++ b/kate/kateviewmanager.h @@ -1,351 +1,349 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2001 Anders Lund This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KATE_VIEWMANAGER_H__ #define __KATE_VIEWMANAGER_H__ #include "katedocmanager.h" #include #include #include #include -#include - namespace KActivities { class ResourceInstance; } namespace KTextEditor { class View; class Document; } class KateDocumentInfo; class KConfigGroup; class KConfigBase; class KateMainWindow; class KateViewSpace; class KateViewManager : public QSplitter { Q_OBJECT public: KateViewManager(QWidget *parentW, KateMainWindow *parent); ~KateViewManager() override; private: /** * create all actions needed for the view manager */ void setupActions(); void updateViewSpaceActions(); public: /* This will save the splitter configuration */ void saveViewConfiguration(KConfigGroup &group); /* restore it */ void restoreViewConfiguration(const KConfigGroup &group); KTextEditor::Document *openUrl(const QUrl &url, const QString &encoding, bool activate = true, bool isTempFile = false, const KateDocumentInfo &docInfo = KateDocumentInfo()); KTextEditor::Document *openUrls(const QList &url, const QString &encoding, bool isTempFile = false, const KateDocumentInfo &docInfo = KateDocumentInfo()); KTextEditor::View *openUrlWithView(const QUrl &url, const QString &encoding); public Q_SLOTS: void openUrl(const QUrl &url); public: void closeView(KTextEditor::View *view); KateMainWindow *mainWindow(); private Q_SLOTS: void activateView(KTextEditor::View *view); void activateSpace(KTextEditor::View *v); public Q_SLOTS: void slotDocumentNew(); void slotDocumentOpen(); void slotDocumentClose(); void slotDocumentClose(KTextEditor::Document *document); void setActiveSpace(KateViewSpace *vs); void setActiveView(KTextEditor::View *view); void activateNextView(); void activatePrevView(); Q_SIGNALS: void viewChanged(KTextEditor::View *); void viewCreated(KTextEditor::View *); public: /** * create and activate a new view for doc, if doc == 0, then * create a new document. * Can return NULL. */ KTextEditor::View *createView(KTextEditor::Document *doc = nullptr, KateViewSpace *vs = nullptr); private: bool deleteView(KTextEditor::View *view); void moveViewtoSplit(KTextEditor::View *view); void moveViewtoStack(KTextEditor::View *view); /* Save the configuration of a single splitter. * If child splitters are found, it calls it self with those as the argument. * If a viewspace child is found, it is asked to save its filelist. */ QString saveSplitterConfig(QSplitter *s, KConfigBase *config, const QString &viewConfGrp); /** Restore a single splitter. * This is all the work is done for @see saveSplitterConfig() */ void restoreSplitter(const KConfigBase *config, const QString &group, QSplitter *parent, const QString &viewConfGrp); void removeViewSpace(KateViewSpace *viewspace); public: KTextEditor::View *activeView(); KateViewSpace *activeViewSpace(); private Q_SLOTS: void slotViewChanged(); void documentCreated(KTextEditor::Document *doc); void documentWillBeDeleted(KTextEditor::Document *doc); void documentSavedOrUploaded(KTextEditor::Document *document, bool saveAs); /** * This signal is emitted before the documents batch is going to be deleted * * note that the batch can be interrupted in the middle and only some * of the documents may be actually deleted. See documentsDeleted() signal. * * @param documents documents we want to delete, may not be deleted */ void aboutToDeleteDocuments(const QList &documents); /** * This signal is emitted after the documents batch was deleted * * This is the batch closing signal for aboutToDeleteDocuments * @param documents the documents that weren't deleted after all */ void documentsDeleted(const QList &documents); public Q_SLOTS: /** * Splits a KateViewSpace into two in the following steps: * 1. create a QSplitter in the parent of the KateViewSpace to be split * 2. move the to-be-split KateViewSpace to the new splitter * 3. create new KateViewSpace and added to the new splitter * 4. create KateView to populate the new viewspace. * 5. The new KateView is made the active one, because createView() does that. * If no viewspace is provided, the result of activeViewSpace() is used. * The orientation of the new splitter is determined by the value of o. * Note: horizontal splitter means vertically aligned views. */ void splitViewSpace(KateViewSpace *vs = nullptr, Qt::Orientation o = Qt::Horizontal); /** * Close the view space that contains the given view. If no view was * given, then the active view space will be closed instead. */ void closeViewSpace(KTextEditor::View *view = nullptr); /** * @returns true of the two given views share the same view space. */ bool viewsInSameViewSpace(KTextEditor::View *view1, KTextEditor::View *view2); /** * activate view for given document * @param doc document to activate view for */ KTextEditor::View *activateView(KTextEditor::Document *doc); /** Splits the active viewspace horizontally */ void slotSplitViewSpaceHoriz() { splitViewSpace(nullptr, Qt::Vertical); } /** Splits the active viewspace vertically */ void slotSplitViewSpaceVert() { splitViewSpace(); } /** moves the splitter according to the key that has been pressed */ void moveSplitter(Qt::Key key, int repeats = 1); /** moves the splitter to the right */ void moveSplitterRight() { moveSplitter(Qt::Key_Right); } /** moves the splitter to the left */ void moveSplitterLeft() { moveSplitter(Qt::Key_Left); } /** moves the splitter up */ void moveSplitterUp() { moveSplitter(Qt::Key_Up); } /** moves the splitter down */ void moveSplitterDown() { moveSplitter(Qt::Key_Down); } /** closes the current view space. */ void slotCloseCurrentViewSpace() { closeViewSpace(); } /** closes every view but the active one */ void slotCloseOtherViews(); /** hide every view but the active one */ void slotHideOtherViews(bool hideOthers); void reactivateActiveView(); /** * Toogle the orientation of current split view */ void toggleSplitterOrientation(); /** * Get a list of all views. * @return all views */ QList views() const { return m_views.keys(); } /** * get views in lru order * @return views in lru order */ QList sortedViews() const { QMap sortedViews; QHashIterator i(m_views); while (i.hasNext()) { i.next(); sortedViews[i.value().lruAge] = i.key(); } return sortedViews.values(); } private: KateMainWindow *m_mainWindow; bool m_init; QAction *m_splitViewVert; QAction *m_splitViewHoriz; QAction *m_closeView; QAction *m_closeOtherViews; QAction *m_toggleSplitterOrientation; QAction *m_hideOtherViews; QAction *goNext; QAction *goPrev; QList m_viewSpaceList; bool m_blockViewCreationAndActivation; bool m_activeViewRunning; int m_splitterIndex; // used during saving splitter config. /** * View meta data */ class ViewData { public: /** * Default constructor */ ViewData() : active(false) , lruAge(0) , activityResource(Q_NULLPTR) { } /** * view active? */ bool active; /** * lru age of the view * important: smallest age ==> latest used view */ qint64 lruAge; /** * activity resource for the view */ KActivities::ResourceInstance *activityResource; }; /** * central storage of all views known in the view manager * maps the view to meta data */ QHash m_views; /** * current minimal age */ qint64 m_minAge; /** * the view that is ATM merged to the xml gui factory */ QPointer m_guiMergedView; /** * last url of open file dialog, used if current document has no valid url */ QUrl m_lastOpenDialogUrl; }; #endif diff --git a/kate/katewaiter.cpp b/kate/katewaiter.cpp new file mode 100644 index 000000000..d7fcdab56 --- /dev/null +++ b/kate/katewaiter.cpp @@ -0,0 +1,47 @@ +/* This file is part of the KDE project + Copyright (C) 2001 Christoph Cullmann + Copyright (C) 2001 Joseph Wenninger + Copyright (C) 2001, 2005 Anders Lund + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "katewaiter.h" + +KateWaiter::KateWaiter(const QString &service, const QStringList &tokens) + : QObject(QCoreApplication::instance()) + , m_tokens(tokens) + , m_watcher(service, QDBusConnection::sessionBus()) +{ + connect(&m_watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &KateWaiter::serviceOwnerChanged); +} + +void KateWaiter::exiting() +{ + QCoreApplication::instance()->quit(); +} + +void KateWaiter::documentClosed(const QString &token) +{ + m_tokens.removeAll(token); + if (m_tokens.count() == 0) { + QCoreApplication::instance()->quit(); + } +} + +void KateWaiter::serviceOwnerChanged(const QString &, const QString &, const QString &) +{ + QCoreApplication::instance()->quit(); +} \ No newline at end of file diff --git a/kate/katewaiter.h b/kate/katewaiter.h index fe2f8a590..8aa59d718 100644 --- a/kate/katewaiter.h +++ b/kate/katewaiter.h @@ -1,67 +1,49 @@ /* This file is part of the KDE libraries Copyright (C) 2005 Christoph Cullmann Copyright (C) 2002, 2003 Joseph Wenninger This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KATE_WAITER_H__ #define __KATE_WAITER_H__ #include #include #include #include #include class KateWaiter : public QObject { Q_OBJECT public: - KateWaiter(const QString &service, const QStringList &tokens) - : QObject(QCoreApplication::instance()) - , m_tokens(tokens) - , m_watcher(service, QDBusConnection::sessionBus()) - { - connect(&m_watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &KateWaiter::serviceOwnerChanged); - } + KateWaiter(const QString &service, const QStringList &tokens); public Q_SLOTS: - void exiting() - { - QCoreApplication::instance()->quit(); - } + void exiting(); - void documentClosed(const QString &token) - { - m_tokens.removeAll(token); - if (m_tokens.count() == 0) { - QCoreApplication::instance()->quit(); - } - } + void documentClosed(const QString &token); - void serviceOwnerChanged(const QString &, const QString &, const QString &) - { - QCoreApplication::instance()->quit(); - } + void serviceOwnerChanged(const QString &, const QString &, const QString &); private: QStringList m_tokens; QDBusServiceWatcher m_watcher; }; #endif diff --git a/kate/session/katesession.h b/kate/session/katesession.h index 80f13ebc3..c568afd18 100644 --- a/kate/session/katesession.h +++ b/kate/session/katesession.h @@ -1,136 +1,137 @@ /* This file is part of the KDE project * * Copyright (C) 2005 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef __KATE_SESSION_H__ #define __KATE_SESSION_H__ +#include "katetests_export.h" + #include #include #include -#include "kateprivate_export.h" class KConfig; class KATE_TESTS_EXPORT KateSession : public QSharedData { public: /** * Define a Shared-Pointer type */ typedef QExplicitlySharedDataPointer Ptr; public: ~KateSession(); /** * session name * @return name for this session */ const QString &name() const { return m_name; } /** * session config * on first access, will create the config object, delete will be done automagic * return 0 if we have no file to read config from atm * @return correct KConfig, never null * @note never delete configRead(), because the return value might be * KSharedConfig::openConfig(). Only delete the member variables directly. */ KConfig *config(); /** * count of documents in this session * @return documents count */ unsigned int documents() const { return m_documents; } /** * update \p number of opened documents in session */ void setDocuments(const unsigned int number); /** * @return true if this is anonymous/new session */ bool isAnonymous() const { return m_anonymous; } /** * @return path to session file */ const QString &file() const; /** * returns last save time of this session */ const QDateTime ×tamp() const { return m_timestamp; } /** * Factories */ public: static KateSession::Ptr create(const QString &file, const QString &name); static KateSession::Ptr createFrom(const KateSession::Ptr &session, const QString &file, const QString &name); static KateSession::Ptr createAnonymous(const QString &file); static KateSession::Ptr createAnonymousFrom(const KateSession::Ptr &session, const QString &file); static bool compareByName(const KateSession::Ptr &s1, const KateSession::Ptr &s2); static bool compareByTimeDesc(const KateSession::Ptr &s1, const KateSession::Ptr &s2); private: friend class KateSessionManager; friend class KateSessionTest; /** * set session name */ void setName(const QString &name); /** * set's new session file to @filename */ void setFile(const QString &filename); /** * create a session from given @file * @param file configuration file * @param name name of this session * @param anonymous anonymous flag * @param config if specified, the session will copy configuration from the KConfig instead of opening the file */ KateSession(const QString &file, const QString &name, const bool anonymous, const KConfig *config = nullptr); private: QString m_name; QString m_file; bool m_anonymous; unsigned int m_documents; KConfig *m_config; QDateTime m_timestamp; }; #endif diff --git a/kate/session/katesessionmanagedialog.cpp b/kate/session/katesessionmanagedialog.cpp index 7677177c5..089a21d08 100644 --- a/kate/session/katesessionmanagedialog.cpp +++ b/kate/session/katesessionmanagedialog.cpp @@ -1,506 +1,508 @@ /* This file is part of the KDE project * * Copyright (C) 2005 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katesessionmanagedialog.h" #include "kateapp.h" #include "katesessionchooseritem.h" #include "katesessionmanager.h" #include #include #include #include #include #include #include KateSessionManageDialog::KateSessionManageDialog(QWidget *parent) : QDialog(parent) { setupUi(this); setWindowTitle(i18n("Manage Sessions")); m_dontAskCheckBox->hide(); m_sessionList->installEventFilter(this); connect(m_sessionList, &QTreeWidget::currentItemChanged, this, &KateSessionManageDialog::selectionChanged); connect(m_sessionList, &QTreeWidget::itemDoubleClicked, this, &KateSessionManageDialog::openSession); m_sessionList->header()->moveSection(0, 1); // Re-order columns to "Files, Sessions" m_filterBox->installEventFilter(this); connect(m_filterBox, &QLineEdit::textChanged, this, &KateSessionManageDialog::filterChanged); connect(m_sortButton, &QPushButton::clicked, this, &KateSessionManageDialog::changeSortOrder); connect(m_newButton, &QPushButton::clicked, this, &KateSessionManageDialog::openNewSession); KGuiItem::assign(m_openButton, KStandardGuiItem::open()); m_openButton->setDefault(true); connect(m_openButton, &QPushButton::clicked, this, &KateSessionManageDialog::openSession); connect(m_templateButton, &QPushButton::clicked, this, &KateSessionManageDialog::openSessionAsTemplate); connect(m_copyButton, &QPushButton::clicked, this, &KateSessionManageDialog::copySession); connect(m_renameButton, &QPushButton::clicked, this, &KateSessionManageDialog::editBegin); connect(m_deleteButton, &QPushButton::clicked, this, &KateSessionManageDialog::updateDeleteList); KGuiItem::assign(m_closeButton, KStandardGuiItem::close()); connect(m_closeButton, &QPushButton::clicked, this, &KateSessionManageDialog::closeDialog); connect(KateApp::self()->sessionManager(), &KateSessionManager::sessionListChanged, this, &KateSessionManageDialog::updateSessionList); changeSortOrder(); // Set order to SortAlphabetical, set button text and fill session list } KateSessionManageDialog::KateSessionManageDialog(QWidget *parent, const QString &lastSession) : KateSessionManageDialog(parent) { setWindowTitle(i18n("Session Chooser")); m_dontAskCheckBox->show(); m_chooserMode = true; connect(m_dontAskCheckBox, &QCheckBox::toggled, this, &KateSessionManageDialog::dontAskToggled); m_prefferedSession = lastSession; changeSortOrder(); // Set order to SortChronological } KateSessionManageDialog::~KateSessionManageDialog() {} void KateSessionManageDialog::dontAskToggled() { m_templateButton->setEnabled(!m_dontAskCheckBox->isChecked()); } void KateSessionManageDialog::changeSortOrder() { switch (m_sortOrder) { case SortAlphabetical: m_sortOrder = SortChronological; m_sortButton->setText(i18n("Sort Alphabetical")); //m_sortButton->setIcon(QIcon::fromTheme(QStringLiteral("FIXME"))); break; case SortChronological: m_sortOrder = SortAlphabetical; m_sortButton->setText(i18n("Sort Last Used")); //m_sortButton->setIcon(QIcon::fromTheme(QStringLiteral("FIXME"))); break; } updateSessionList(); } void KateSessionManageDialog::filterChanged() { static QPointer delay; if (!delay) { delay = new QTimer(this); // Should be auto cleard by Qt when we die delay->setSingleShot(true); delay->setInterval(400); connect(delay, &QTimer::timeout, this, &KateSessionManageDialog::updateSessionList); } delay->start(); } void KateSessionManageDialog::done(int result) { for (auto session : qAsConst(m_deleteList)) { KateApp::self()->sessionManager()->deleteSession(session); } m_deleteList.clear(); // May not needed, but anyway if (ResultQuit == result) { QDialog::done(0); return; } if (m_chooserMode && m_dontAskCheckBox->isChecked()) { // write back our nice boolean :) KConfigGroup generalConfig(KSharedConfig::openConfig(), QStringLiteral("General")); switch (result) { case ResultOpen: generalConfig.writeEntry("Startup Session", "last"); break; case ResultNew: generalConfig.writeEntry("Startup Session", "new"); break; default: break; } generalConfig.sync(); } QDialog::done(1); } void KateSessionManageDialog::selectionChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous) { Q_UNUSED(previous); if (m_editByUser) { editDone(); // Field was left unchanged, no need to apply return; } if (!current) { m_openButton->setEnabled(false); m_templateButton->setEnabled(false); m_copyButton->setEnabled(false); m_renameButton->setEnabled(false); m_deleteButton->setEnabled(false); return; } const KateSession::Ptr activeSession = KateApp::self()->sessionManager()->activeSession(); const bool notActiveSession = !KateApp::self()->sessionManager()->sessionIsActive(currentSelectedSession()->name()); m_deleteButton->setEnabled(notActiveSession); if (m_deleteList.contains(currentSelectedSession())) { m_deleteButton->setText(i18n("Restore")); m_openButton->setEnabled(false); m_templateButton->setEnabled(false); m_copyButton->setEnabled(true); // Looks a little strange but is OK m_renameButton->setEnabled(false); } else { KGuiItem::assign(m_deleteButton, KStandardGuiItem::del()); m_openButton->setEnabled(currentSelectedSession() != activeSession); m_templateButton->setEnabled(true); m_copyButton->setEnabled(true); m_renameButton->setEnabled(true); } } void KateSessionManageDialog::disableButtons() { m_openButton->setEnabled(false); m_newButton->setEnabled(false); m_templateButton->setEnabled(false); m_dontAskCheckBox->setEnabled(false); m_copyButton->setEnabled(false); m_renameButton->setEnabled(false); m_deleteButton->setEnabled(false); m_closeButton->setEnabled(false); m_sortButton->setEnabled(false); m_filterBox->setEnabled(false); } void KateSessionManageDialog::editBegin() { if (m_editByUser) { return; } KateSessionChooserItem *item = currentSessionItem(); if (!item) { return; } disableButtons(); item->setFlags(item->flags() | Qt::ItemIsEditable); m_sessionList->clearSelection(); m_sessionList->editItem(item, 0); // Always apply changes user did, like Dolphin connect(m_sessionList, &QTreeWidget::itemChanged, this, &KateSessionManageDialog::editApply); connect(m_sessionList->itemWidget(item, 0), &QObject::destroyed, this, &KateSessionManageDialog::editApply); m_editByUser = item; // Do it last to block eventFilter() actions until we are ready } void KateSessionManageDialog::editDone() { m_editByUser = nullptr; disconnect(m_sessionList, &QTreeWidget::itemChanged, this, &KateSessionManageDialog::editApply); updateSessionList(); m_newButton->setEnabled(true); m_dontAskCheckBox->setEnabled(true); m_closeButton->setEnabled(true); m_sortButton->setEnabled(true); m_filterBox->setEnabled(true); m_sessionList->setFocus(); } void KateSessionManageDialog::editApply() { if (!m_editByUser) { return; } KateApp::self()->sessionManager()->renameSession(m_editByUser->session, m_editByUser->text(0)); editDone(); } void KateSessionManageDialog::copySession() { KateSessionChooserItem *item = currentSessionItem(); if (!item) { return; } m_prefferedSession = KateApp::self()->sessionManager()->copySession(item->session); m_sessionList->setFocus(); // Only needed when user abort } void KateSessionManageDialog::openSession() { KateSessionChooserItem *item = currentSessionItem(); if (!item) { return; } hide(); - KateApp::self()->sessionManager()->activateSession(item->session); - done(ResultOpen); + + // this might fail, e.g. if session is in use, then e.g. end kate, bug 390740 + const bool success = KateApp::self()->sessionManager()->activateSession(item->session); + done(success ? ResultOpen : ResultQuit); } void KateSessionManageDialog::openSessionAsTemplate() { KateSessionChooserItem *item = currentSessionItem(); if (!item) { return; } hide(); KateSessionManager *sm = KateApp::self()->sessionManager(); KateSession::Ptr ns = KateSession::createAnonymousFrom(item->session, sm->anonymousSessionFile()); sm->activateSession(ns); done(ResultOpen); } void KateSessionManageDialog::openNewSession() { hide(); KateApp::self()->sessionManager()->sessionNew(); done(ResultNew); } void KateSessionManageDialog::updateDeleteList() { KateSessionChooserItem *item = currentSessionItem(); if (!item) { return; } const KateSession::Ptr session = item->session; if (m_deleteList.contains(session)) { m_deleteList.remove(session); item->setForeground(0, QBrush(KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color())); item->setIcon(0, QIcon()); item->setToolTip(0, QString()); } else { m_deleteList.insert(session); markItemAsToBeDeleted(item); } // To ease multiple deletions, move the selection QTreeWidgetItem *newItem = m_sessionList->itemBelow(item) ? m_sessionList->itemBelow(item) : m_sessionList->topLevelItem(0); m_sessionList->setCurrentItem(newItem); m_sessionList->setFocus(); } void KateSessionManageDialog::markItemAsToBeDeleted(QTreeWidgetItem *item) { item->setForeground(0, QBrush(KColorScheme(QPalette::Active).foreground(KColorScheme::InactiveText).color())); item->setIcon(0, QIcon::fromTheme(QStringLiteral("emblem-warning"))); item->setToolTip(0, i18n("Session will be deleted on dialog close")); } void KateSessionManageDialog::closeDialog() { done(ResultQuit); } void KateSessionManageDialog::updateSessionList() { if (m_editByUser) { // Don't crash accidentally an ongoing edit return; } KateSession::Ptr currSelSession = currentSelectedSession(); KateSession::Ptr activeSession = KateApp::self()->sessionManager()->activeSession(); m_sessionList->clear(); KateSessionList slist = KateApp::self()->sessionManager()->sessionList(); switch (m_sortOrder) { case SortAlphabetical: std::sort (slist.begin(), slist.end(), KateSession::compareByName); break; case SortChronological: std::sort (slist.begin(), slist.end(), KateSession::compareByTimeDesc); break; } KateSessionChooserItem *prefferedItem = nullptr; KateSessionChooserItem *currSessionItem = nullptr; KateSessionChooserItem *activeSessionItem= nullptr; for (const KateSession::Ptr &session : qAsConst(slist)) { if (!m_filterBox->text().isEmpty()) { if (!session->name().contains(m_filterBox->text(), Qt::CaseInsensitive)) { continue; } } KateSessionChooserItem *item = new KateSessionChooserItem(m_sessionList, session); if (session == currSelSession) { currSessionItem = item; } else if (session == activeSession) { activeSessionItem = item; } else if (session->name() == m_prefferedSession) { prefferedItem = item; m_prefferedSession.clear(); } if (m_deleteList.contains(session)) { markItemAsToBeDeleted(item); } } m_sessionList->resizeColumnToContents(1); // Minimize "Files" column if (!prefferedItem) { prefferedItem = currSessionItem ? currSessionItem : activeSessionItem; } if (prefferedItem) { m_sessionList->setCurrentItem(prefferedItem); m_sessionList->scrollToItem(prefferedItem); } else if (m_sessionList->topLevelItemCount() > 0) { m_sessionList->setCurrentItem(m_sessionList->topLevelItem(0)); } if (m_filterBox->hasFocus()){ return; } if (m_sessionList->topLevelItemCount() == 0) { m_newButton->setFocus(); } else { m_sessionList->setFocus(); } } KateSessionChooserItem *KateSessionManageDialog::currentSessionItem() const { return static_cast(m_sessionList->currentItem()); } KateSession::Ptr KateSessionManageDialog::currentSelectedSession() const { KateSessionChooserItem *item = currentSessionItem(); if (!item) { return KateSession::Ptr(); } return item->session; } bool KateSessionManageDialog::eventFilter(QObject *object, QEvent *event) { QKeyEvent *ke = static_cast(event); if (object == m_sessionList) { if (!m_editByUser) { // No need for further action return false; } if (event->type() == QEvent::KeyPress) { switch (ke->key()) { // Avoid to apply changes with untypical keys/don't left edit field this way case Qt::Key_Up : case Qt::Key_Down : case Qt::Key_PageUp : case Qt::Key_PageDown : return true; default: break; } } else if (event->type() == QEvent::KeyRelease) { switch (ke->key()) { case Qt::Key_Escape : editDone(); // Abort edit break; case Qt::Key_Return : editApply(); break; default: break; } } } else if (object == m_filterBox) { // Catch Return key to avoid to finish the dialog if (event->type() == QEvent::KeyPress && (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) { updateSessionList(); m_sessionList->setFocus(); return true; } } return false; } void KateSessionManageDialog::closeEvent(QCloseEvent *event) { Q_UNUSED(event); if (m_editByUser) { // We must catch closeEvent here due to connected signal of QLineEdit::destroyed->editApply()->crash! editDone(); // editApply() don't work, m_editByUser->text(0) will not updated from QLineEdit } } diff --git a/kate/session/katesessionmanager.cpp b/kate/session/katesessionmanager.cpp index 89b878155..55751f65e 100644 --- a/kate/session/katesessionmanager.cpp +++ b/kate/session/katesessionmanager.cpp @@ -1,639 +1,651 @@ /* This file is part of the KDE project * * Copyright (C) 2005 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ -#include "config.h" - #include "katesessionmanager.h" #include "katesessionmanagedialog.h" #include "kateapp.h" #include "katepluginmanager.h" #include "katerunninginstanceinfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef Q_OS_WIN #include #endif //BEGIN KateSessionManager KateSessionManager::KateSessionManager(QObject *parent, const QString &sessionsDir) : QObject(parent) { if (sessionsDir.isEmpty()) { m_sessionsDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kate/sessions"); } else { m_sessionsDir = sessionsDir; } // create dir if needed QDir().mkpath(m_sessionsDir); m_dirWatch = new KDirWatch(this); m_dirWatch->addDir(m_sessionsDir); connect(m_dirWatch, &KDirWatch::dirty, this, &KateSessionManager::updateSessionList); updateSessionList(); } KateSessionManager::~KateSessionManager() { delete m_dirWatch; } void KateSessionManager::updateSessionList() { QStringList list; // Let's get a list of all session we have atm QDir dir(m_sessionsDir, QStringLiteral("*.katesession"), QDir::Time); for (unsigned int i = 0; i < dir.count(); ++i) { QString name = dir[i]; name.chop(12); // .katesession list << QUrl::fromPercentEncoding(name.toLatin1()); } // write jump list actions to disk in the kate.desktop file updateJumpListActions(list); bool changed = false; // Add new sessions to our list for (const QString session : qAsConst(list)) { if (!m_sessions.contains(session)) { const QString file = sessionFileForName(session); m_sessions.insert(session, KateSession::create(file, session)); changed = true; } } // Remove gone sessions from our list for (const QString session : m_sessions.keys()) { if ((list.indexOf(session) < 0) && (m_sessions.value(session) != activeSession())) { m_sessions.remove(session); changed = true; } } if (changed) { emit sessionListChanged(); } } bool KateSessionManager::activateSession(KateSession::Ptr session, const bool closeAndSaveLast, const bool loadNew) { if (activeSession() == session) { return true; } if (!session->isAnonymous()) { //check if the requested session is already open in another instance KateRunningInstanceMap instances; if (!fillinRunningKateAppInstances(&instances)) { KMessageBox::error(nullptr, i18n("Internal error: there is more than one instance open for a given session.")); return false; } if (instances.contains(session->name())) { if (KMessageBox::questionYesNo(nullptr, i18n("Session '%1' is already opened in another kate instance, change there instead of reopening?", session->name()), QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("katesessionmanager_switch_instance")) == KMessageBox::Yes) { instances[session->name()]->dbus_if->call(QStringLiteral("activate")); cleanupRunningKateAppInstanceMap(&instances); return false; } } cleanupRunningKateAppInstanceMap(&instances); } // try to close and save last session if (closeAndSaveLast) { if (KateApp::self()->activeKateMainWindow()) { if (!KateApp::self()->activeKateMainWindow()->queryClose_internal()) { return true; } } // save last session or not? saveActiveSession(); // really close last KateApp::self()->documentManager()->closeAllDocuments(); } // set the new session m_activeSession = session; // there is one case in which we don't want the restoration and that is // when restoring session from session manager. // In that case the restore is handled by the caller if (loadNew) { loadSession(session); } emit sessionChanged(); return true; } void KateSessionManager::loadSession(const KateSession::Ptr &session) const { // open the new session KSharedConfigPtr sharedConfig = KSharedConfig::openConfig(); KConfig *sc = session->config(); const bool loadDocs = !session->isAnonymous(); // do not load docs for new sessions // if we have no session config object, try to load the default // (anonymous/unnamed sessions) // load plugin config + plugins KateApp::self()->pluginManager()->loadConfig(sc); if (loadDocs) { KateApp::self()->documentManager()->restoreDocumentList(sc); } // window config KConfigGroup c(sharedConfig, "General"); - if (c.readEntry("Restore Window Configuration", true)) { - KConfig *cfg = sc; - bool delete_cfg = false; - // a new, named session, read settings of the default session. - if (! sc->hasGroup("Open MainWindows")) { - delete_cfg = true; - cfg = new KConfig(anonymousSessionFile(), KConfig::SimpleConfig); - } + KConfig *cfg = sc; + bool delete_cfg = false; + // a new, named session, read settings of the default session. + if (! sc->hasGroup("Open MainWindows")) { + delete_cfg = true; + cfg = new KConfig(anonymousSessionFile(), KConfig::SimpleConfig); + } + if (c.readEntry("Restore Window Configuration", true)) { int wCount = cfg->group("Open MainWindows").readEntry("Count", 1); for (int i = 0; i < wCount; ++i) { if (i >= KateApp::self()->mainWindowsCount()) { KateApp::self()->newMainWindow(cfg, QStringLiteral("MainWindow%1").arg(i)); } else { KateApp::self()->mainWindow(i)->readProperties(KConfigGroup(cfg, QStringLiteral("MainWindow%1").arg(i))); } KateApp::self()->mainWindow(i)->restoreWindowConfig(KConfigGroup(cfg, QStringLiteral("MainWindow%1 Settings").arg(i))); } - if (delete_cfg) { - delete cfg; - } - // remove mainwindows we need no longer... if (wCount > 0) { while (wCount < KateApp::self()->mainWindowsCount()) { delete KateApp::self()->mainWindow(KateApp::self()->mainWindowsCount() - 1); } } + } else { + // load recent files for all existing windows, see bug 408499 + for (int i = 0; i < KateApp::self()->mainWindowsCount(); ++i) { + KateApp::self()->mainWindow(i)->loadOpenRecent(cfg); + } + } + + // ensure we have at least one window, always! load recent files for it, too, see bug 408499 + if (KateApp::self()->mainWindowsCount() == 0) { + auto w = KateApp::self()->newMainWindow(); + w->loadOpenRecent(cfg); + } + + if (delete_cfg) { + delete cfg; } + + // we shall always have some existing windows here! + Q_ASSERT(KateApp::self()->mainWindowsCount() > 0); } bool KateSessionManager::activateSession(const QString &name, const bool closeAndSaveLast, const bool loadNew) { return activateSession(giveSession(name), closeAndSaveLast, loadNew); } bool KateSessionManager::activateAnonymousSession() { return activateSession(QString(), false); } KateSession::Ptr KateSessionManager::giveSession(const QString &name) { if (name.isEmpty()) { return KateSession::createAnonymous(anonymousSessionFile()); } if (m_sessions.contains(name)) { return m_sessions.value(name); } KateSession::Ptr s = KateSession::create(sessionFileForName(name), name); saveSessionTo(s->config()); m_sessions[name] = s; // Due to this add to m_sessions will updateSessionList() no signal emit, // but it's importand to add. Otherwise could it be happen that m_activeSession // is not part of m_sessions but a double emit sessionListChanged(); return s; } bool KateSessionManager::deleteSession(KateSession::Ptr session) { if (sessionIsActive(session->name())) { return false; } QFile::remove(session->file()); m_sessions.remove(session->name()); // Due to this remove from m_sessions will updateSessionList() no signal emit, // but this way is there no delay between deletion and information emit sessionListChanged(); return true; } QString KateSessionManager::copySession(const KateSession::Ptr &session, const QString &newName) { const QString name = askForNewSessionName(session, newName); if (name.isEmpty()) { return name; } const QString newFile = sessionFileForName(name); KateSession::Ptr ns = KateSession::createFrom(session, newFile, name); ns->config()->sync(); return name; } QString KateSessionManager::renameSession(KateSession::Ptr session, const QString &newName) { const QString name = askForNewSessionName(session, newName); if (name.isEmpty()) { return name; } const QString newFile = sessionFileForName(name); session->config()->sync(); const QUrl srcUrl = QUrl::fromLocalFile(session->file()); const QUrl dstUrl = QUrl::fromLocalFile(newFile); KIO::CopyJob *job = KIO::move(srcUrl, dstUrl, KIO::HideProgressInfo); if (!job->exec()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("The session could not be renamed to \"%1\". Failed to write to \"%2\"", newName, newFile), i18n("Session Renaming")); return QString(); } m_sessions[newName] = m_sessions.take(session->name()); session->setName(newName); session->setFile(newFile); session->config()->sync(); // updateSessionList() will this edit not notice, so force signal emit sessionListChanged(); if (session == activeSession()) { emit sessionChanged(); } return name; } void KateSessionManager::saveSessionTo(KConfig *sc) const { // Clear the session file to avoid to accumulate outdated entries for (auto group : sc->groupList()) { sc->deleteGroup(group); } // save plugin configs and which plugins to load KateApp::self()->pluginManager()->writeConfig(sc); // save document configs + which documents to load KateApp::self()->documentManager()->saveDocumentList(sc); sc->group("Open MainWindows").writeEntry("Count", KateApp::self()->mainWindowsCount()); // save config for all windows around ;) bool saveWindowConfig = KConfigGroup(KSharedConfig::openConfig(), "General").readEntry("Restore Window Configuration", true); for (int i = 0; i < KateApp::self()->mainWindowsCount(); ++i) { KConfigGroup cg(sc, QStringLiteral("MainWindow%1").arg(i)); + // saveProperties() handles saving the "open recent" files list KateApp::self()->mainWindow(i)->saveProperties(cg); if (saveWindowConfig) { KateApp::self()->mainWindow(i)->saveWindowConfig(KConfigGroup(sc, QStringLiteral("MainWindow%1 Settings").arg(i))); } } sc->sync(); /** * try to sync file to disk */ QFile fileToSync(sc->name()); if (fileToSync.open(QIODevice::ReadOnly)) { #ifndef Q_OS_WIN // ensure that the file is written to disk #ifdef HAVE_FDATASYNC fdatasync(fileToSync.handle()); #else fsync(fileToSync.handle()); #endif #endif } } bool KateSessionManager::saveActiveSession(bool rememberAsLast) { if (!activeSession()) { return false; } KConfig *sc = activeSession()->config(); saveSessionTo(sc); if (rememberAsLast && !activeSession()->isAnonymous()) { KSharedConfigPtr c = KSharedConfig::openConfig(); c->group("General").writeEntry("Last Session", activeSession()->name()); c->sync(); } return true; } bool KateSessionManager::chooseSession() { const KConfigGroup c(KSharedConfig::openConfig(), "General"); // get last used session, default to default session const QString lastSession(c.readEntry("Last Session", QString())); const QString sesStart(c.readEntry("Startup Session", "manual")); // uhh, just open last used session, show no chooser if (sesStart == QStringLiteral("last")) { - activateSession(lastSession, false); - return true; + return activateSession(lastSession, false); } // start with empty new session or in case no sessions exist if (sesStart == QStringLiteral("new") || sessionList().size() == 0) { - activateAnonymousSession(); - return true; + return activateAnonymousSession(); } + // else: ask the user return QScopedPointer(new KateSessionManageDialog(nullptr, lastSession))->exec(); } void KateSessionManager::sessionNew() { activateSession(giveSession(QString())); } void KateSessionManager::sessionSave() { if (activeSession() && activeSession()->isAnonymous()) { sessionSaveAs(); } else { saveActiveSession(); } } void KateSessionManager::sessionSaveAs() { const QString newName = askForNewSessionName(activeSession()); if (newName.isEmpty()) { return; } activeSession()->config()->sync(); KateSession::Ptr ns = KateSession::createFrom(activeSession(), sessionFileForName(newName), newName); m_activeSession = ns; saveActiveSession(); emit sessionChanged(); } QString KateSessionManager::askForNewSessionName(KateSession::Ptr session, const QString &newName) { if (session->name() == newName && !session->isAnonymous()) { return QString(); } const QString messagePrompt = i18n("Session name:"); const KLocalizedString messageExist = ki18n("There is already an existing session with your chosen name: %1\n" "Please choose a different one."); const QString messageEmpty = i18n("To save a session, you must specify a name."); QString messageTotal = messagePrompt; QString name = newName; while (true) { QString preset = name; if (name.isEmpty()) { preset = suggestNewSessionName(session->name()); messageTotal = messageEmpty + QStringLiteral("\n\n") + messagePrompt; } else if (QFile::exists(sessionFileForName(name))) { preset = suggestNewSessionName(name); if (preset.isEmpty()) { // Very unlikely, but as fall back we keep users input preset = name; } messageTotal = messageExist.subs(name).toString() + QStringLiteral("\n\n") + messagePrompt; } else { return name; } QInputDialog dlg(KateApp::self()->activeKateMainWindow()); dlg.setInputMode(QInputDialog::TextInput); if (session->isAnonymous()) { dlg.setWindowTitle(i18n("Specify a name for this session")); } else { dlg.setWindowTitle(i18n("Specify a new name for session: %1", session->name())); } dlg.setLabelText(messageTotal); dlg.setTextValue(preset); dlg.resize(900,100); // FIXME Calc somehow a proper size bool ok = dlg.exec(); name = dlg.textValue(); if (!ok) { return QString(); } } } QString KateSessionManager::suggestNewSessionName(const QString &target) { if (target.isEmpty()) { // Here could also a default name set or the current session name used return QString(); } const QString mask = QStringLiteral("%1 (%2)"); QString name; for (int i = 2; i < 1000000; i++) { // Should be enough to get an unique name name = mask.arg(target).arg(i); if (!QFile::exists(sessionFileForName(name))) { return name; } } return QString(); } void KateSessionManager::sessionManage() { QScopedPointer(new KateSessionManageDialog(KateApp::self()->activeKateMainWindow()))->exec(); } bool KateSessionManager::sessionIsActive(const QString &session) { // Try to avoid unneed action if (activeSession() && activeSession()->name() == session) { return true; } QDBusConnectionInterface *i = QDBusConnection::sessionBus().interface(); if (!i) { return false; } // look up all running kate instances and there sessions QDBusReply servicesReply = i->registeredServiceNames(); QStringList services; if (servicesReply.isValid()) { services = servicesReply.value(); } for (const QString &s : qAsConst(services)) { if (!s.startsWith(QStringLiteral("org.kde.kate-"))) { continue; } KateRunningInstanceInfo rii(s); if (rii.valid && rii.sessionName == session) { return true; } } return false; } QString KateSessionManager::anonymousSessionFile() const { const QString file = m_sessionsDir + QStringLiteral("/../anonymous.katesession"); return QDir().cleanPath(file); } QString KateSessionManager::sessionFileForName(const QString &name) const { Q_ASSERT(!name.isEmpty()); const QString sname = QString::fromLatin1(QUrl::toPercentEncoding(name, QByteArray(), QByteArray("."))); return m_sessionsDir + QStringLiteral("/") + sname + QStringLiteral(".katesession"); } KateSessionList KateSessionManager::sessionList() { return m_sessions.values(); } void KateSessionManager::updateJumpListActions(const QStringList &sessionList) { #if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) KService::Ptr service = KService::serviceByStorageId(qApp->desktopFileName()); if (!service) { return; } QScopedPointer df(new KDesktopFile(service->entryPath())); QStringList newActions = df->readActions(); // try to keep existing custom actions intact, only remove our "Session" actions and add them back later newActions.erase(std::remove_if(newActions.begin(), newActions.end(), [](const QString &action) { return action.startsWith(QLatin1String("Session ")); }), newActions.end()); // Limit the number of list entries we like to offer const int maxEntryCount = std::min(sessionList.count(), 10); // sessionList is ordered by time, but we like it alphabetical to avoid even more a needed update QStringList sessionSubList = sessionList.mid(0, maxEntryCount); sessionSubList.sort(); // we compute the new group names in advance so we can tell whether we changed something // and avoid touching the desktop file leading to an expensive ksycoca recreation QStringList sessionActions; sessionActions.reserve(maxEntryCount); for (int i = 0; i < maxEntryCount; ++i) { sessionActions << QStringLiteral("Session %1").arg(QString::fromLatin1(QCryptographicHash::hash(sessionSubList.at(i).toUtf8() , QCryptographicHash::Md5).toHex())); } newActions += sessionActions; // nothing to do if (df->readActions() == newActions) { return; } const QString &localPath = service->locateLocal(); if (service->entryPath() != localPath) { df.reset(df->copyTo(localPath)); } // remove all Session action groups first to not leave behind any cruft for (const QString &action : df->readActions()) { if (action.startsWith(QLatin1String("Session "))) { // TODO is there no deleteGroup(KConfigGroup)? df->deleteGroup(df->actionGroup(action).name()); } } for (int i = 0; i < maxEntryCount; ++i) { const QString &action = sessionActions.at(i); // is a transform of sessionSubList, so count and order is identical const QString &session = sessionSubList.at(i); KConfigGroup grp = df->actionGroup(action); grp.writeEntry(QStringLiteral("Name"), session); grp.writeEntry(QStringLiteral("Exec"), QStringLiteral("kate -s %1").arg(KShell::quoteArg(session))); // TODO proper executable name? } df->desktopGroup().writeXdgListEntry("Actions", newActions); #endif } //END KateSessionManager diff --git a/kate/session/katesessionsaction.h b/kate/session/katesessionsaction.h index aaa449ec9..03f01e87d 100644 --- a/kate/session/katesessionsaction.h +++ b/kate/session/katesessionsaction.h @@ -1,50 +1,50 @@ /* This file is part of the KDE project * * Copyright (C) 2005 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef __KATE_SESSIONS_ACTION_H__ #define __KATE_SESSIONS_ACTION_H__ -#include "kateprivate_export.h" +#include "katetests_export.h" #include class KateSessionManager; class KATE_TESTS_EXPORT KateSessionsAction : public KActionMenu { Q_OBJECT public: KateSessionsAction(const QString &text, QObject *parent, KateSessionManager *manager = nullptr); ~KateSessionsAction() override { } public Q_SLOTS: void slotAboutToShow(); void openSession(QAction *action); void slotSessionChanged(); private: friend class KateSessionsActionTest; // tfuj QActionGroup *sessionsGroup; KateSessionManager *m_manager; }; #endif diff --git a/kwrite/CMakeLists.txt b/kwrite/CMakeLists.txt index f95836403..898ccd993 100644 --- a/kwrite/CMakeLists.txt +++ b/kwrite/CMakeLists.txt @@ -1,72 +1,88 @@ -# KWrite -project (kwrite) +add_executable(kwrite "") -# Load the frameworks we need -find_package(KF5 REQUIRED COMPONENTS DBusAddons) +configure_file(config.h.in config.h) +target_include_directories(kwrite PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) # config.h -# collect icons -set(KWRITE_ICONS_PNG - ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-apps-kwrite.png - ${CMAKE_CURRENT_SOURCE_DIR}/icons/22-apps-kwrite.png - ${CMAKE_CURRENT_SOURCE_DIR}/icons/32-apps-kwrite.png - ${CMAKE_CURRENT_SOURCE_DIR}/icons/48-apps-kwrite.png - ${CMAKE_CURRENT_SOURCE_DIR}/icons/64-apps-kwrite.png - ${CMAKE_CURRENT_SOURCE_DIR}/icons/128-apps-kwrite.png +find_package( + KF5 + QUIET + REQUIRED + COMPONENTS + TextEditor + DBusAddons + Crash + OPTIONAL_COMPONENTS + Activities ) -set(KWRITE_ICONS_SVG -${CMAKE_CURRENT_SOURCE_DIR}/icons/sc-apps-kwrite.svgz +target_link_libraries( + kwrite + PUBLIC + KF5::TextEditor + KF5::DBusAddons + KF5::Crash ) -# collect the sources -set (KWRITE_APPLICATION_SRCS main.cpp kwrite.cpp kwriteapplication.cpp) -qt5_add_resources(KWRITE_APPLICATION_SRCS data/kwrite.qrc) - -# add icons to application sources, to have them bundled -ecm_add_app_icon(KWRITE_APPLICATION_SRCS ICONS ${KWRITE_ICONS_PNG}) +if(KF5Activities_FOUND) + target_link_libraries(kwrite PUBLIC KF5::Activities) +endif() -# build KWrite application -add_executable(kwrite ${KWRITE_APPLICATION_SRCS}) -target_link_libraries(kwrite -PUBLIC - KF5::TextEditor - KF5::I18n - KF5::DBusAddons - KF5::Crash +set(ICONS_PNG + ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-apps-kwrite.png + ${CMAKE_CURRENT_SOURCE_DIR}/icons/22-apps-kwrite.png + ${CMAKE_CURRENT_SOURCE_DIR}/icons/32-apps-kwrite.png + ${CMAKE_CURRENT_SOURCE_DIR}/icons/48-apps-kwrite.png + ${CMAKE_CURRENT_SOURCE_DIR}/icons/64-apps-kwrite.png + ${CMAKE_CURRENT_SOURCE_DIR}/icons/128-apps-kwrite.png ) -if(KF5Activities_FOUND) - target_link_libraries(kwrite - PUBLIC - KF5::Activities) -endif() +set(ICONS_SVG ${CMAKE_CURRENT_SOURCE_DIR}/icons/sc-apps-kwrite.svgz) + +# Add icon files to the application's source files to have CMake bundle them in the executable. +ecm_add_app_icon(ICONS_SOURCES ICONS ${ICONS_PNG}) +target_sources(kwrite PRIVATE ${ICONS_SOURCES}) -# own plist magic for mac os +target_sources( + kwrite + PRIVATE + data/kwrite.qrc + kwrite.cpp + kwriteapplication.cpp + main.cpp +) + +# See https://cmake.org/cmake/help/v3.15/prop_tgt/MACOSX_BUNDLE_INFO_PLIST.html if(APPLE) - # own plist template - set_target_properties (kwrite PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/data/MacOSXBundleInfo.plist.in) + set_property( + TARGET kwrite + PROPERTY MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/data/MacOSXBundleInfo.plist.in + ) - # the MacOSX bundle display name property (CFBundleDisplayName) is not currently supported by cmake, - # so has to be set for all targets in this cmake file - set(MACOSX_BUNDLE_DISPLAY_NAME KWrite) - set_target_properties(kwrite PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.KWrite") - set_target_properties(kwrite PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KWrite") - set_target_properties(kwrite PROPERTIES MACOSX_BUNDLE_DISPLAY_NAME "KWrite") - set_target_properties(kwrite PROPERTIES MACOSX_BUNDLE_INFO_STRING "KWrite - Text Editor") - set_target_properties(kwrite PROPERTIES MACOSX_BUNDLE_LONG_VERSION_STRING "KWrite ${KDE_APPLICATIONS_VERSION}") - set_target_properties(kwrite PROPERTIES MACOSX_BUNDLE_SHORT_VERSION_STRING "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}") - set_target_properties(kwrite PROPERTIES MACOSX_BUNDLE_BUNDLE_VERSION "${KDE_APPLICATIONS_VERSION}") - set_target_properties(kwrite PROPERTIES MACOSX_BUNDLE_COPYRIGHT "2000-2016 The KWrite Authors") + # These are substituted by CMake into plist.in. + set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.KWrite") + set(MACOSX_BUNDLE_BUNDLE_NAME "KWrite") + set(MACOSX_BUNDLE_DISPLAY_NAME "KWrite") + set(MACOSX_BUNDLE_INFO_STRING "KWrite - Text Editor") + set(MACOSX_BUNDLE_LONG_VERSION_STRING "KWrite ${KDE_APPLICATIONS_VERSION}") + set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}") + set(MACOSX_BUNDLE_BUNDLE_VERSION "${KDE_APPLICATIONS_VERSION}") + set(MACOSX_BUNDLE_COPYRIGHT "2000-2016 The KWrite Authors") endif() -# install executable install(TARGETS kwrite ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) -# desktop file -install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/data/org.kde.kwrite.desktop DESTINATION ${XDG_APPS_INSTALL_DIR}) +install( + FILES ${CMAKE_CURRENT_SOURCE_DIR}/data/org.kde.kwrite.desktop + DESTINATION ${XDG_APPS_INSTALL_DIR} +) -# appdata -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/data/org.kde.kwrite.appdata.xml DESTINATION ${CMAKE_INSTALL_METAINFODIR}) +install( + FILES ${CMAKE_CURRENT_SOURCE_DIR}/data/org.kde.kwrite.appdata.xml + DESTINATION ${CMAKE_INSTALL_METAINFODIR} +) -# install icons -ecm_install_icons(ICONS ${KWRITE_ICONS_PNG} ${KWRITE_ICONS_SVG} DESTINATION ${ICON_INSTALL_DIR} THEME hicolor) +ecm_install_icons( + ICONS ${ICONS_PNG} ${ICONS_SVG} + DESTINATION ${ICON_INSTALL_DIR} + THEME hicolor +) diff --git a/kwrite/config.h.in b/kwrite/config.h.in new file mode 100644 index 000000000..e644f1ab5 --- /dev/null +++ b/kwrite/config.h.in @@ -0,0 +1,10 @@ +#ifndef KWRITE_CONFIG_H +#define KWRITE_CONFIG_H + +/* config.h. Generated by cmake from config.h.cmake */ + +#define KWRITE_VERSION "${KDE_APPLICATIONS_VERSION}" + +#cmakedefine KF5Activities_FOUND + +#endif diff --git a/kwrite/kwrite.cpp b/kwrite/kwrite.cpp index 651e86cff..0c9591a9c 100644 --- a/kwrite/kwrite.cpp +++ b/kwrite/kwrite.cpp @@ -1,529 +1,528 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2001 Anders Lund This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kwrite.h" +#include "kwriteapplication.h" +#include "config.h" + #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include - -#ifdef KActivities_FOUND +#ifdef KF5Activities_FOUND #include #endif #include #include #include #include #include #include #include #include #include #include -#include "kwriteapplication.h" - KWrite::KWrite(KTextEditor::Document *doc, KWriteApplication *app) : m_view(nullptr) , m_recentFiles(nullptr) , m_paShowPath(nullptr) , m_paShowMenuBar(nullptr) , m_paShowStatusBar(nullptr) , m_activityResource(nullptr) , m_app(app) , m_mainWindow(this) { if (!doc) { doc = KTextEditor::Editor::instance()->createDocument(nullptr); // enable the modified on disk warning dialogs if any if (qobject_cast(doc)) { qobject_cast(doc)->setModifiedOnDiskWarning(true); } m_app->addDocument(doc); } m_view = doc->createView(this); setCentralWidget(m_view); setupActions(); // signals for the statusbar connect(m_view->document(), &KTextEditor::Document::modifiedChanged, this, &KWrite::modifiedChanged); connect(m_view->document(), &KTextEditor::Document::documentNameChanged, this, &KWrite::documentNameChanged); connect(m_view->document(), &KTextEditor::Document::readWriteChanged, this, &KWrite::documentNameChanged); connect(m_view->document(), &KTextEditor::Document::documentUrlChanged, this, &KWrite::urlChanged); setAcceptDrops(true); connect(m_view, SIGNAL(dropEventPass(QDropEvent*)), this, SLOT(slotDropEvent(QDropEvent*))); setXMLFile(QStringLiteral("kwriteui.rc")); createShellGUI(true); guiFactory()->addClient(m_view); // FIXME: make sure the config dir exists, any idea how to do it more cleanly? QDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation)).mkpath(QStringLiteral(".")); // call it as last thing, must be sure everything is already set up ;) setAutoSaveSettings(); readConfig(); documentNameChanged(); show(); // give view focus m_view->setFocus(Qt::OtherFocusReason); /** * handle mac os x like file open request via event filter */ qApp->installEventFilter(this); } KWrite::~KWrite() { m_app->removeWindow(this); guiFactory()->removeClient(m_view); KTextEditor::Document *doc = m_view->document(); delete m_view; // kill document, if last view is closed if (doc->views().isEmpty()) { m_app->removeDocument(doc); delete doc; } KSharedConfig::openConfig()->sync(); } QSize KWrite::sizeHint () const { /** * have some useful size hint, else we have mini windows per default */ return (QSize(640, 480).expandedTo(minimumSizeHint())); } void KWrite::setupActions() { m_closeAction = actionCollection()->addAction(KStandardAction::Close, QStringLiteral("file_close"), this, SLOT(slotFlush())); m_closeAction->setIcon(QIcon::fromTheme(QStringLiteral("document-close"))); m_closeAction->setWhatsThis(i18n("Use this command to close the current document")); m_closeAction->setDisabled(true); // setup File menu actionCollection()->addAction(KStandardAction::New, QStringLiteral("file_new"), this, SLOT(slotNew())) ->setWhatsThis(i18n("Use this command to create a new document")); actionCollection()->addAction(KStandardAction::Open, QStringLiteral("file_open"), this, SLOT(slotOpen())) ->setWhatsThis(i18n("Use this command to open an existing document for editing")); m_recentFiles = KStandardAction::openRecent(this, SLOT(slotOpen(QUrl)), this); actionCollection()->addAction(m_recentFiles->objectName(), m_recentFiles); m_recentFiles->setWhatsThis(i18n("This lists files which you have opened recently, and allows you to easily open them again.")); QAction *a = actionCollection()->addAction(QStringLiteral("view_new_view")); a->setIcon(QIcon::fromTheme(QStringLiteral("window-new"))); a->setText(i18n("&New Window")); connect(a, &QAction::triggered, this, &KWrite::newView); a->setWhatsThis(i18n("Create another view containing the current document")); actionCollection()->addAction(KStandardAction::Quit, this, SLOT(close())) ->setWhatsThis(i18n("Close the current document view")); // setup Settings menu setStandardToolBarMenuEnabled(true); m_paShowMenuBar = KStandardAction::showMenubar(this, SLOT(toggleMenuBar()), actionCollection()); m_paShowStatusBar = KStandardAction::showStatusbar(this, SLOT(toggleStatusBar()), this); actionCollection()->addAction(m_paShowStatusBar->objectName(), m_paShowStatusBar); m_paShowStatusBar->setWhatsThis(i18n("Use this command to show or hide the view's statusbar")); m_paShowPath = new KToggleAction(i18n("Sho&w Path in Titlebar"), this); actionCollection()->addAction(QStringLiteral("set_showPath"), m_paShowPath); connect(m_paShowPath, &QAction::triggered, this, &KWrite::documentNameChanged); m_paShowPath->setWhatsThis(i18n("Show the complete document path in the window caption")); a = actionCollection()->addAction(KStandardAction::KeyBindings, this, SLOT(editKeys())); a->setWhatsThis(i18n("Configure the application's keyboard shortcut assignments.")); a = actionCollection()->addAction(KStandardAction::ConfigureToolbars, QStringLiteral("options_configure_toolbars"), this, SLOT(editToolbars())); a->setWhatsThis(i18n("Configure which items should appear in the toolbar(s).")); a = actionCollection()->addAction(QStringLiteral("help_about_editor")); a->setText(i18n("&About Editor Component")); connect(a, &QAction::triggered, this, &KWrite::aboutEditor); } // load on url void KWrite::loadURL(const QUrl &url) { m_view->document()->openUrl(url); -#ifdef KActivities_FOUND +#ifdef KF5Activities_FOUND if (!m_activityResource) { m_activityResource = new KActivities::ResourceInstance(winId(), this); } m_activityResource->setUri(m_view->document()->url()); #endif m_closeAction->setEnabled(true); } // is closing the window wanted by user ? bool KWrite::queryClose() { if (m_view->document()->views().count() > 1) { return true; } if (m_view->document()->queryClose()) { writeConfig(); return true; } return false; } void KWrite::slotFlush() { if (m_view->document()->closeUrl()) { m_closeAction->setDisabled(true); } } void KWrite::modifiedChanged() { documentNameChanged(); m_closeAction->setEnabled(true); } void KWrite::slotNew() { m_app->newWindow(); } void KWrite::slotOpen() { const QList urls = QFileDialog::getOpenFileUrls(this, i18n("Open File"), m_view->document()->url()); Q_FOREACH(QUrl url, urls) { slotOpen(url); } } void KWrite::slotOpen(const QUrl &url) { if (url.isEmpty()) { return; } if (m_view->document()->isModified() || !m_view->document()->url().isEmpty()) { KWrite *t = m_app->newWindow(); t->loadURL(url); } else { loadURL(url); } } void KWrite::urlChanged() { if (! m_view->document()->url().isEmpty()) { m_recentFiles->addUrl(m_view->document()->url()); } // update caption documentNameChanged(); } void KWrite::newView() { m_app->newWindow(m_view->document()); } void KWrite::toggleMenuBar(bool showMessage) { if (m_paShowMenuBar->isChecked()) { menuBar()->show(); removeMenuBarActionFromContextMenu(); } else { if (showMessage) { const QString accel = m_paShowMenuBar->shortcut().toString(); KMessageBox::information(this, i18n("This will hide the menu bar completely." " You can show it again by typing %1.", accel), i18n("Hide menu bar"), QStringLiteral("HideMenuBarWarning")); } menuBar()->hide(); addMenuBarActionToContextMenu(); } } void KWrite::addMenuBarActionToContextMenu() { m_view->contextMenu()->addAction(m_paShowMenuBar); } void KWrite::removeMenuBarActionFromContextMenu() { m_view->contextMenu()->removeAction(m_paShowMenuBar); } void KWrite::toggleStatusBar() { m_view->setStatusBarEnabled(m_paShowStatusBar->isChecked()); } void KWrite::editKeys() { KShortcutsDialog dlg(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this); dlg.addCollection(actionCollection()); if (m_view) { dlg.addCollection(m_view->actionCollection()); } dlg.configure(); } void KWrite::editToolbars() { KConfigGroup cfg = KSharedConfig::openConfig()->group("MainWindow"); saveMainWindowSettings(cfg); KEditToolBar dlg(guiFactory(), this); connect(&dlg, &KEditToolBar::newToolBarConfig, this, &KWrite::slotNewToolbarConfig); dlg.exec(); } void KWrite::slotNewToolbarConfig() { applyMainWindowSettings(KSharedConfig::openConfig()->group("MainWindow")); } void KWrite::dragEnterEvent(QDragEnterEvent *event) { const QList uriList = event->mimeData()->urls(); event->setAccepted(! uriList.isEmpty()); } void KWrite::dropEvent(QDropEvent *event) { slotDropEvent(event); } void KWrite::slotDropEvent(QDropEvent *event) { const QList textlist = event->mimeData()->urls(); foreach(const QUrl & url, textlist) slotOpen(url); } void KWrite::slotEnableActions(bool enable) { QList actions = actionCollection()->actions(); QList::ConstIterator it = actions.constBegin(); QList::ConstIterator end = actions.constEnd(); for (; it != end; ++it) { (*it)->setEnabled(enable); } actions = m_view->actionCollection()->actions(); it = actions.constBegin(); end = actions.constEnd(); for (; it != end; ++it) { (*it)->setEnabled(enable); } } //common config void KWrite::readConfig(KSharedConfigPtr config) { KConfigGroup cfg(config, "General Options"); m_paShowMenuBar->setChecked(cfg.readEntry("ShowMenuBar", true)); m_paShowStatusBar->setChecked(cfg.readEntry("ShowStatusBar", true)); m_paShowPath->setChecked(cfg.readEntry("ShowPath", false)); m_recentFiles->loadEntries(config->group("Recent Files")); // update visibility of menu bar and status bar toggleMenuBar(false); m_view->setStatusBarEnabled(m_paShowStatusBar->isChecked()); } void KWrite::writeConfig(KSharedConfigPtr config) { KConfigGroup generalOptions(config, "General Options"); generalOptions.writeEntry("ShowMenuBar", m_paShowMenuBar->isChecked()); generalOptions.writeEntry("ShowStatusBar", m_paShowStatusBar->isChecked()); generalOptions.writeEntry("ShowPath", m_paShowPath->isChecked()); m_recentFiles->saveEntries(KConfigGroup(config, "Recent Files")); config->sync(); } //config file void KWrite::readConfig() { readConfig(KSharedConfig::openConfig()); } void KWrite::writeConfig() { writeConfig(KSharedConfig::openConfig()); } // session management void KWrite::restore(KConfig *config, int n) { readPropertiesInternal(config, n); } void KWrite::readProperties(const KConfigGroup &config) { readConfig(); m_view->readSessionConfig(KConfigGroup(&config, QStringLiteral("General Options"))); } void KWrite::saveProperties(KConfigGroup &config) { writeConfig(); config.writeEntry("DocumentNumber", m_app->documents().indexOf(m_view->document()) + 1); KConfigGroup cg(&config, QStringLiteral("General Options")); m_view->writeSessionConfig(cg); } void KWrite::saveGlobalProperties(KConfig *config) //save documents { m_app->saveProperties(config); } void KWrite::aboutEditor() { KAboutApplicationDialog dlg(KTextEditor::Editor::instance()->aboutData(), this); dlg.exec(); } void KWrite::documentNameChanged() { QString readOnlyCaption; if (!m_view->document()->isReadWrite()) { readOnlyCaption = i18n(" [read only]"); } if (m_view->document()->url().isEmpty()) { setCaption(i18n("Untitled") + readOnlyCaption + QStringLiteral(" [*]"), m_view->document()->isModified()); return; } QString c; if (m_paShowPath->isChecked()) { c = m_view->document()->url().toString(QUrl::PreferLocalFile); const QString homePath = QDir::homePath(); if (c.startsWith(homePath)) { c = QStringLiteral("~") + c.right(c.length() - homePath.length()); } //File name shouldn't be too long - Maciek if (c.length() > 64) { c = QStringLiteral("...") + c.right(64); } } else { c = m_view->document()->url().fileName(); //File name shouldn't be too long - Maciek if (c.length() > 64) { c = c.left(64) + QStringLiteral("..."); } } setCaption(c + readOnlyCaption + QStringLiteral(" [*]"), m_view->document()->isModified()); } bool KWrite::eventFilter(QObject *obj, QEvent *event) { /** * handle mac os like file open */ if (event->type() == QEvent::FileOpen) { /** * try to open and activate the new document, like we would do for stuff * opened via file dialog */ QFileOpenEvent *foe = static_cast(event); slotOpen(foe->url()); return true; } /** * else: pass over to default implementation */ return KParts::MainWindow::eventFilter(obj, event); } QList KWrite::views() { QList list; list.append(m_view); return list; } KTextEditor::View *KWrite::activateView(KTextEditor::Document *document) { if (m_view->document() == document) { return m_view; } return nullptr; } diff --git a/kwrite/kwrite.h b/kwrite/kwrite.h index b7aece7e7..46c4c4b02 100644 --- a/kwrite/kwrite.h +++ b/kwrite/kwrite.h @@ -1,147 +1,145 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2001 Anders Lund This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KWRITE_MAIN_H #define KWRITE_MAIN_H #include #include #include #include #include #include -#include - class QLabel; namespace KActivities { class ResourceInstance; } class KToggleAction; class KRecentFilesAction; class KSqueezedTextLabel; class KWriteApplication; class KWrite : public KParts::MainWindow { Q_OBJECT public: KWrite(KTextEditor::Document * = nullptr, KWriteApplication *app = nullptr); ~KWrite() override; void loadURL(const QUrl &url); private: void setupActions(); void addMenuBarActionToContextMenu(); void removeMenuBarActionFromContextMenu(); bool queryClose() override; void dragEnterEvent(QDragEnterEvent *) override; void dropEvent(QDropEvent *) override; public Q_SLOTS: void slotNew(); void slotFlush(); void slotOpen(); void slotOpen(const QUrl &url); void newView(); void toggleStatusBar(); void toggleMenuBar(bool showMessage = true); void editKeys(); void editToolbars(); void aboutEditor(); void modifiedChanged(); private Q_SLOTS: void slotNewToolbarConfig(); public Q_SLOTS: void slotDropEvent(QDropEvent *); void slotEnableActions(bool enable); /** * adds a changed URL to the recent files */ void urlChanged(); /** * Overwrite size hint for better default window sizes * @return size hint */ QSize sizeHint () const override; //config file functions public: void readConfig(KSharedConfigPtr); void writeConfig(KSharedConfigPtr); void readConfig(); void writeConfig(); //session management public: void restore(KConfig *, int); public: KTextEditor::MainWindow *mainWindow() { return &m_mainWindow; } public Q_SLOTS: QWidget *window() { return this; } QList views(); KTextEditor::View *activeView() { return m_view; } KTextEditor::View *activateView(KTextEditor::Document *document); private: void readProperties(const KConfigGroup &) override; void saveProperties(KConfigGroup &) override; void saveGlobalProperties(KConfig *) override; private: KTextEditor::View *m_view; KRecentFilesAction *m_recentFiles; KToggleAction *m_paShowPath; KToggleAction *m_paShowMenuBar; KToggleAction *m_paShowStatusBar; QAction *m_closeAction; KActivities::ResourceInstance *m_activityResource; KWriteApplication *m_app; KTextEditor::MainWindow m_mainWindow; public Q_SLOTS: void documentNameChanged(); protected: /** * Event filter for QApplication to handle mac os like file open */ bool eventFilter(QObject *obj, QEvent *event) override; }; #endif diff --git a/kwrite/main.cpp b/kwrite/main.cpp index 97287139a..599eeb5c6 100644 --- a/kwrite/main.cpp +++ b/kwrite/main.cpp @@ -1,314 +1,316 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2001 Anders Lund This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +#include "config.h" + #include "kwrite.h" #include "kwriteapplication.h" #include #include #include #include #include // for KAboutData::setDesktopFileName() #include #include #include #include #if KCrash_VERSION >= QT_VERSION_CHECK(5, 15, 0) #include #endif // KCrash >= 5.15 #include #include #include #include #include #include #include "../urlinfo.h" #ifndef Q_OS_WIN #include #endif #include extern "C" Q_DECL_EXPORT int main(int argc, char **argv) { #ifndef Q_OS_WIN // Prohibit using sudo or kdesu (but allow using the root user directly) if (getuid() == 0) { if (!qEnvironmentVariableIsEmpty("SUDO_USER")) { std::cout << "Executing KWrite with sudo is not possible due to unfixable security vulnerabilities." << std::endl; return EXIT_FAILURE; } else if (!qEnvironmentVariableIsEmpty("KDESU_USER")) { std::cout << "Executing KWrite with kdesu is not possible due to unfixable security vulnerabilities." << std::endl; return EXIT_FAILURE; } } #endif /** * Create application first * Enforce application name even if the executable is renamed */ QApplication app(argc, argv); app.setApplicationName(QStringLiteral("kwrite")); /** * enable high dpi support */ app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); /** * Enable crash handling through KCrash. */ #if KCrash_VERSION >= QT_VERSION_CHECK(5, 15, 0) KCrash::initialize(); #endif /** * Connect application with translation catalogs */ KLocalizedString::setApplicationDomain("kwrite"); /** * then use i18n and co */ KAboutData aboutData(QStringLiteral("kwrite"), i18n("KWrite"), - QStringLiteral(KATE_VERSION), + QStringLiteral(KWRITE_VERSION), i18n("KWrite - Text Editor"), KAboutLicense::LGPL_V2, i18n("(c) 2000-2019 The Kate Authors"), QString(), QStringLiteral("https://kate-editor.org")); /** * right dbus prefix == org.kde. */ aboutData.setOrganizationDomain(QByteArray("kde.org")); /** * desktop file association to make application icon work (e.g. in Wayland window decoration) */ #if KCOREADDONS_VERSION >= QT_VERSION_CHECK(5, 16, 0) aboutData.setDesktopFileName(QStringLiteral("org.kde.kwrite")); #endif aboutData.addAuthor(i18n("Christoph Cullmann"), i18n("Maintainer"), QStringLiteral("cullmann@kde.org"), QStringLiteral("https://cullmann.io")); aboutData.addAuthor(i18n("Dominik Haumann"), i18n("Core Developer"), QStringLiteral("dhaumann@kde.org")); aboutData.addAuthor(i18n("Anders Lund"), i18n("Core Developer"), QStringLiteral("anders@alweb.dk"), QStringLiteral("http://www.alweb.dk")); aboutData.addAuthor(i18n("Joseph Wenninger"), i18n("Core Developer"), QStringLiteral("jowenn@kde.org"), QStringLiteral("http://stud3.tuwien.ac.at/~e9925371")); aboutData.addAuthor(i18n("Hamish Rodda"), i18n("Core Developer"), QStringLiteral("rodda@kde.org")); aboutData.addAuthor(i18n("Waldo Bastian"), i18n("The cool buffersystem"), QStringLiteral("bastian@kde.org")); aboutData.addAuthor(i18n("Charles Samuels"), i18n("The Editing Commands"), QStringLiteral("charles@kde.org")); aboutData.addAuthor(i18n("Matt Newell"), i18nc("Credit text for someone that did testing and some other similar things", "Testing, ..."), QStringLiteral("newellm@proaxis.com")); aboutData.addAuthor(i18n("Michael Bartl"), i18n("Former Core Developer"), QStringLiteral("michael.bartl1@chello.at")); aboutData.addAuthor(i18n("Michael McCallum"), i18n("Core Developer"), QStringLiteral("gholam@xtra.co.nz")); aboutData.addAuthor(i18n("Jochen Wilhemly"), i18n("KWrite Author"), QStringLiteral("digisnap@cs.tu-berlin.de")); aboutData.addAuthor(i18n("Michael Koch"), i18n("KWrite port to KParts"), QStringLiteral("koch@kde.org")); aboutData.addAuthor(i18n("Christian Gebauer"), QString(), QStringLiteral("gebauer@kde.org")); aboutData.addAuthor(i18n("Simon Hausmann"), QString(), QStringLiteral("hausmann@kde.org")); aboutData.addAuthor(i18n("Glen Parker"), i18n("KWrite Undo History, Kspell integration"), QStringLiteral("glenebob@nwlink.com")); aboutData.addAuthor(i18n("Scott Manson"), i18n("KWrite XML Syntax highlighting support"), QStringLiteral("sdmanson@alltel.net")); aboutData.addAuthor(i18n("John Firebaugh"), i18n("Patches and more"), QStringLiteral("jfirebaugh@kde.org")); aboutData.addAuthor(i18n("Gerald Senarclens de Grancy"), i18n("QA and Scripting"), QStringLiteral("oss@senarclens.eu"), QStringLiteral("http://find-santa.eu/")); aboutData.addCredit(i18n("Matteo Merli"), i18n("Highlighting for RPM Spec-Files, Perl, Diff and more"), QStringLiteral("merlim@libero.it")); aboutData.addCredit(i18n("Rocky Scaletta"), i18n("Highlighting for VHDL"), QStringLiteral("rocky@purdue.edu")); aboutData.addCredit(i18n("Yury Lebedev"), i18n("Highlighting for SQL")); aboutData.addCredit(i18n("Chris Ross"), i18n("Highlighting for Ferite")); aboutData.addCredit(i18n("Nick Roux"), i18n("Highlighting for ILERPG")); aboutData.addCredit(i18n("Carsten Niehaus"), i18n("Highlighting for LaTeX")); aboutData.addCredit(i18n("Per Wigren"), i18n("Highlighting for Makefiles, Python")); aboutData.addCredit(i18n("Jan Fritz"), i18n("Highlighting for Python")); aboutData.addCredit(i18n("Daniel Naber")); aboutData.addCredit(i18n("Roland Pabel"), i18n("Highlighting for Scheme")); aboutData.addCredit(i18n("Cristi Dumitrescu"), i18n("PHP Keyword/Datatype list")); aboutData.addCredit(i18n("Carsten Pfeiffer"), i18nc("Credit text for someone that helped a lot", "Very nice help")); aboutData.addCredit(i18n("All people who have contributed and I have forgotten to mention")); /** * bugzilla */ aboutData.setProductName(QByteArray("kate/kwrite")); /** * set and register app about data */ KAboutData::setApplicationData(aboutData); /** * set the program icon */ QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("accessories-text-editor"), app.windowIcon())); /** * Create command line parser and feed it with known options */ QCommandLineParser parser; aboutData.setupCommandLine(&parser); // -e/--encoding option const QCommandLineOption useEncoding(QStringList() << QStringLiteral("e") << QStringLiteral("encoding"), i18n("Set encoding for the file to open."), i18n("encoding")); parser.addOption(useEncoding); // -l/--line option const QCommandLineOption gotoLine(QStringList() << QStringLiteral("l") << QStringLiteral("line"), i18n("Navigate to this line."), i18n("line")); parser.addOption(gotoLine); // -c/--column option const QCommandLineOption gotoColumn(QStringList() << QStringLiteral("c") << QStringLiteral("column"), i18n("Navigate to this column."), i18n("column")); parser.addOption(gotoColumn); // -i/--stdin option const QCommandLineOption readStdIn(QStringList() << QStringLiteral("i") << QStringLiteral("stdin"), i18n("Read the contents of stdin.")); parser.addOption(readStdIn); // --tempfile option const QCommandLineOption tempfile(QStringList() << QStringLiteral("tempfile"), i18n("The files/URLs opened by the application will be deleted after use")); parser.addOption(tempfile); // urls to open parser.addPositionalArgument(QStringLiteral("urls"), i18n("Documents to open."), i18n("[urls...]")); /** * do the command line parsing */ parser.process(app); /** * handle standard options */ aboutData.processCommandLine(&parser); KWriteApplication kapp; if (app.isSessionRestored()) { kapp.restore(); } else { bool nav = false; int line = 0, column = 0; QTextCodec *codec = parser.isSet(QStringLiteral("encoding")) ? QTextCodec::codecForName(parser.value(QStringLiteral("encoding")).toLocal8Bit()) : nullptr; if (parser.isSet(QStringLiteral("line"))) { line = parser.value(QStringLiteral("line")).toInt() - 1; nav = true; } if (parser.isSet(QStringLiteral("column"))) { column = parser.value(QStringLiteral("column")).toInt() - 1; nav = true; } if (parser.positionalArguments().count() == 0) { KWrite *t = kapp.newWindow(); if (parser.isSet(QStringLiteral("stdin"))) { QTextStream input(stdin, QIODevice::ReadOnly); // set chosen codec if (codec) { input.setCodec(codec); } QString line; QString text; do { line = input.readLine(); text.append(line + QLatin1Char('\n')); } while (!line.isNull()); KTextEditor::Document *doc = t->activeView()->document(); if (doc) { // remember codec in document, e.g. to show the right one if (codec) { doc->setEncoding(QString::fromLatin1(codec->name())); } doc->setText(text); } } if (nav && t->activeView()) { t->activeView()->setCursorPosition(KTextEditor::Cursor(line, column)); } } else { int docs_opened = 0; Q_FOREACH(const QString positionalArgument, parser.positionalArguments()) { UrlInfo info(positionalArgument); if (nav) { info.cursor = KTextEditor::Cursor(line, column); } // this file is no local dir, open it, else warn bool noDir = !info.url.isLocalFile() || !QFileInfo(info.url.toLocalFile()).isDir(); if (noDir) { ++docs_opened; KWrite *t = kapp.newWindow(); if (codec) { t->activeView()->document()->setEncoding(QString::fromLatin1(codec->name())); } t->loadURL(info.url); if (info.cursor.isValid()) { t->activeView()->setCursorPosition(info.cursor); } else if (info.url.hasQuery()) { QUrlQuery q(info.url); QString lineStr = q.queryItemValue(QStringLiteral("line")); QString columnStr = q.queryItemValue(QStringLiteral("column")); line = lineStr.toInt(); if (line > 0) line--; column = columnStr.toInt(); if (column > 0) column--; t->activeView()->setCursorPosition(KTextEditor::Cursor(line, column)); } } else { KMessageBox::sorry(nullptr, i18n("The file '%1' could not be opened: it is not a normal file, it is a folder.", info.url.toString())); } } if (!docs_opened) { ::exit(1); // see http://bugs.kde.org/show_bug.cgi?id=124708 } } } // no window there, uh, ohh, for example borked session config !!! // create at least one !! if (kapp.noWindows()) { kapp.newWindow(); } /** * finally register this kwrite instance for dbus, don't die if no dbus is around! */ const KDBusService dbusService(KDBusService::Multiple | KDBusService::NoExitOnFailure); /** * Run the event loop */ return app.exec(); }

    foo test bar X *