├── .gitignore ├── LICENSE ├── README.md ├── Ruby.json.in ├── Ruby.png ├── Ruby.qrc ├── RubyConstants.h ├── RubyPlugin.cpp ├── RubyPlugin.h ├── configure.rb ├── editor ├── RubyAmbiguousMethodAssistProvider.cpp ├── RubyAmbiguousMethodAssistProvider.h ├── RubyAutoCompleter.cpp ├── RubyAutoCompleter.h ├── RubyBlockState.h ├── RubyCodeModel.cpp ├── RubyCodeModel.h ├── RubyCodeStylePreferencesFactory.cpp ├── RubyCodeStylePreferencesFactory.h ├── RubyCompletionAssist.cpp ├── RubyCompletionAssist.h ├── RubyEditor.cpp ├── RubyEditor.h ├── RubyEditorDocument.cpp ├── RubyEditorDocument.h ├── RubyEditorFactory.cpp ├── RubyEditorFactory.h ├── RubyEditorWidget.cpp ├── RubyEditorWidget.h ├── RubyHighlighter.cpp ├── RubyHighlighter.h ├── RubyIndenter.cpp ├── RubyIndenter.h ├── RubyQuickFixAssistProvider.cpp ├── RubyQuickFixAssistProvider.h ├── RubyQuickFixes.cpp ├── RubyQuickFixes.h ├── RubyRubocopHighlighter.cpp ├── RubyRubocopHighlighter.h ├── RubyScanner.cpp ├── RubyScanner.h ├── RubySymbol.h ├── RubySymbolFilter.cpp ├── RubySymbolFilter.h ├── ScannerTest.cpp ├── SourceCodeStream.h └── rubocop.rb ├── projectmanager ├── RubyProject.cpp ├── RubyProject.h ├── RubyProjectWizard.cpp ├── RubyProjectWizard.h ├── RubyRunConfiguration.cpp └── RubyRunConfiguration.h ├── ruby-project.qbs ├── ruby.pro └── ruby.qbs /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /debug 3 | /release 4 | /.moc/ 5 | /.obj/ 6 | /.rcc/ 7 | /.qmake.stash 8 | Makefile* 9 | Ruby.json 10 | *.user* 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code Climate](https://codeclimate.com/github/hugopl/RubyCreator/badges/gpa.svg)](https://codeclimate.com/github/hugopl/RubyCreator) 2 | 3 | # ⚠️ Notice 4 | 5 | This repository is archived, I'm not working on this anymore, for an updated fork see https://github.com/NickLion/RubyCreator 6 | 7 | So Long, and Thanks for All the Fish! 🐬 8 | 9 | # RubyCreator 10 | 11 | Plugin to add Ruby language support to QtCreator IDE. 12 | 13 | More info can be found at: http://hugopl.github.io/RubyCreator/ 14 | 15 | ## How to install 16 | 17 | Packages are available only for Arch Linux in AUR, for anything different you will need to clone the repository and compile it yourself. 18 | 19 | # Note About branches 20 | 21 | `master` branch should work with the `master` branch of QtCreator, it may not compile since QtCreator changes their API very often. 22 | 23 | There is usually one, sometimes two, branches named with a version number like 4.9.x, etc. These version numbers should match the QtCreator version they are supposed to work. 24 | 25 | ~As I use this plugin on my every day work~ (now I'm using/developing [Tijolo](https://github.com/hugopl/tijolo)), I do the development on a version branch that matches the current QtCreator version packaged by ArchLinux. 26 | 27 | When Archlinux upgrades the QtCreator package minor version, I just create a version tag and another version branch to match the new version. 28 | 29 | ## How to compile 30 | 31 | **You need Qt5!!** 32 | 33 | If you want to try QtCreator but don't want to have a custom QtCreator compiled just to do that, follow these instructions: 34 | 35 | * ./configure.rb 36 | * cd build && make 37 | 38 | At the end of the build you will see an error about lack of permissions to move the plugin library to /usr/..., move it by yourself and it's done. 39 | 40 | If you intent to contribute with RubyCreator or already write plugins for QtCreator you probably already have a custom build of QtCreator installed in 41 | a sandbox somewhere in your system, so just call qmake passing QTC_SOURCE and QTC_BUILD variables. 42 | -------------------------------------------------------------------------------- /Ruby.json.in: -------------------------------------------------------------------------------- 1 | { 2 | \"Name\" : \"Ruby\", 3 | \"Version\" : \"$$QTCREATOR_VERSION\", 4 | \"CompatVersion\" : \"$$QTCREATOR_COMPAT_VERSION\", 5 | \"Vendor\" : \"hugopl\", 6 | \"Copyright\" : \"(C) 2014-2015 Hugo Parente Lima\", 7 | \"License\" : [ \"Commercial Usage\", 8 | \"\", 9 | \"Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt Commercial License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and Digia.\", 10 | \"\", 11 | \"GNU Lesser General Public License Usage\", 12 | \"\", 13 | \"Alternatively, this plugin may be used under the terms of the GNU Lesser General Public License version 2.1 or version 3 as published by the Free Software Foundation. Please review the following information to ensure the GNU Lesser General Public License requirements will be met: https://www.gnu.org/licenses/lgpl.html and http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.\" 14 | ], 15 | \"Category\" : \"Other Languages\", 16 | \"Description\" : \"Ruby language support.\", 17 | \"Url\" : \"http://hugopl.github.io/RubyCreator/\", 18 | $$dependencyList, 19 | 20 | \"Mimetypes\" : [ 21 | \"\", 22 | \"\", 23 | \"\", 24 | \"\", 25 | \"Ruby project file\", 26 | \"\", 27 | \"\", 28 | \"\", 29 | \"\", 30 | \"Ruby source file\", 31 | \"\", 32 | \"\", 33 | \"\", 34 | \"\", 35 | \"\", 36 | \"\", 37 | \"\", 38 | \"\", 39 | \"\", 40 | \"\" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /Ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugopl/RubyCreator/1940bd551d6d7d671dbbe2efdefb16d71a076cbf/Ruby.png -------------------------------------------------------------------------------- /Ruby.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ruby.png 4 | editor/rubocop.rb 5 | 6 | 7 | -------------------------------------------------------------------------------- /RubyConstants.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_Constants_h 2 | #define Ruby_Constants_h 3 | 4 | namespace Ruby { 5 | namespace Constants { 6 | 7 | const char EditorId[] = "Ruby.RubyEditor"; 8 | const char ProjectId[] = "Ruby.Project"; 9 | const char RubyIcon[] = ":/rubysupport/Ruby.png"; 10 | const char SettingsId[] = "Ruby.Settings"; 11 | const char SnippetGroupId[] = "Ruby.Snippets"; 12 | const char EditorDisplayName[] = "Ruby Editor"; 13 | const char LangRuby[] = "RUBY"; 14 | const char RubyProjectTask[] = "RubyProject.task"; 15 | 16 | const char MimeType[] = "application/x-ruby"; 17 | const char ProjectMimeType[] = "text/x-rubycreator-project"; 18 | 19 | } 20 | } 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /RubyPlugin.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyPlugin.h" 2 | 3 | #include "RubyConstants.h" 4 | 5 | #include "editor/RubyCodeModel.h" 6 | #include "editor/RubyCodeStylePreferencesFactory.h" 7 | #include "editor/RubyEditorFactory.h" 8 | #include "editor/RubyHighlighter.h" 9 | #include "editor/RubyQuickFixAssistProvider.h" 10 | #include "editor/RubyQuickFixes.h" 11 | #include "editor/RubySymbolFilter.h" 12 | #include "editor/RubyCompletionAssist.h" 13 | #include "editor/RubyRubocopHighlighter.h" 14 | #include "projectmanager/RubyProject.h" 15 | #include "projectmanager/RubyProjectWizard.h" 16 | #include "projectmanager/RubyRunConfiguration.h" 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | namespace Ruby { 28 | 29 | class PluginPrivate 30 | { 31 | public: 32 | EditorFactory editorFactory; 33 | RunConfigurationFactory runConfigFactory; 34 | SymbolFilter curDocumentMethodsSymbolFilter; 35 | SymbolFilter methodsSymbolFilter; 36 | SymbolFilter classesSymbolFilter; 37 | ProjectWizard projectWizard; 38 | CompletionAssistProvider completionAssistProvider; 39 | QuickFixAssistProvider m_quickFixProvider; 40 | RubocopHighlighter highlighter; 41 | 42 | PluginPrivate() : 43 | curDocumentMethodsSymbolFilter([](const QString &file) { 44 | return CodeModel::instance()->methodsIn(file); 45 | }, "Ruby Methods in Current Document", '.'), 46 | methodsSymbolFilter([](const QString &) { 47 | return CodeModel::instance()->allMethods(); 48 | }, "Ruby methods", 'm'), 49 | classesSymbolFilter([](const QString &) { 50 | return CodeModel::instance()->allClasses(); 51 | }, "Ruby classes", 'c') 52 | {} 53 | }; 54 | 55 | Plugin *Plugin::m_instance = nullptr; 56 | 57 | Plugin::Plugin() 58 | { 59 | m_instance = this; 60 | } 61 | 62 | Plugin::~Plugin() 63 | { 64 | TextEditor::TextEditorSettings::unregisterCodeStyle(Constants::SettingsId); 65 | TextEditor::TextEditorSettings::unregisterCodeStylePool(Constants::SettingsId); 66 | TextEditor::TextEditorSettings::unregisterCodeStyleFactory(Constants::SettingsId); 67 | 68 | m_instance = nullptr; 69 | delete d; 70 | } 71 | 72 | Plugin *Plugin::instance() 73 | { 74 | return m_instance; 75 | } 76 | 77 | bool Plugin::initialize(const QStringList &, QString *errorString) 78 | { 79 | Q_UNUSED(errorString) 80 | 81 | initializeToolsSettings(); 82 | 83 | d = new PluginPrivate; 84 | ProjectExplorer::ProjectManager::registerProjectType(Constants::ProjectMimeType); 85 | 86 | Core::IWizardFactory::registerFactoryCreator([]() { 87 | return QList() << new ProjectWizard; 88 | }); 89 | 90 | registerQuickFixes(this); 91 | TextEditor::SnippetProvider::registerGroup(Constants::SnippetGroupId, 92 | tr("Ruby", "SnippetProvider"), 93 | &EditorFactory::decorateEditor); 94 | 95 | return true; 96 | } 97 | 98 | void Plugin::extensionsInitialized() 99 | { 100 | } 101 | 102 | QuickFixAssistProvider *Plugin::quickFixProvider() 103 | { 104 | return &d->m_quickFixProvider; 105 | } 106 | 107 | void Plugin::initializeToolsSettings() 108 | { 109 | // code style factory 110 | auto factory = new CodeStylePreferencesFactory; 111 | TextEditor::TextEditorSettings::registerCodeStyleFactory(factory); 112 | 113 | // code style pool 114 | auto pool = new TextEditor::CodeStylePool(factory, this); 115 | TextEditor::TextEditorSettings::registerCodeStylePool(Constants::SettingsId, pool); 116 | 117 | // global code style settings 118 | auto globalCodeStyle = new TextEditor::SimpleCodeStylePreferences(this); 119 | globalCodeStyle->setDelegatingPool(pool); 120 | globalCodeStyle->setDisplayName(tr("Global", "Settings")); 121 | globalCodeStyle->setId("RubyGlobal"); 122 | pool->addCodeStyle(globalCodeStyle); 123 | TextEditor::TextEditorSettings::registerCodeStyle(Constants::SettingsId, globalCodeStyle); 124 | 125 | // built-in settings 126 | // Ruby style 127 | auto rubyCodeStyle = new TextEditor::SimpleCodeStylePreferences; 128 | rubyCodeStyle->setId("ruby"); 129 | rubyCodeStyle->setDisplayName(tr("RubyCreator")); 130 | rubyCodeStyle->setReadOnly(true); 131 | TextEditor::TabSettings tabSettings; 132 | tabSettings.m_tabPolicy = TextEditor::TabSettings::SpacesOnlyTabPolicy; 133 | tabSettings.m_tabSize = 2; 134 | tabSettings.m_indentSize = 2; 135 | tabSettings.m_continuationAlignBehavior = TextEditor::TabSettings::ContinuationAlignWithIndent; 136 | rubyCodeStyle->setTabSettings(tabSettings); 137 | pool->addCodeStyle(rubyCodeStyle); 138 | 139 | // default delegate for global preferences 140 | globalCodeStyle->setCurrentDelegate(rubyCodeStyle); 141 | 142 | pool->loadCustomCodeStyles(); 143 | 144 | // load global settings (after built-in settings are added to the pool) 145 | globalCodeStyle->fromSettings(Constants::SettingsId, Core::ICore::settings()); 146 | 147 | // mimetypes to be handled 148 | TextEditor::TextEditorSettings::registerMimeTypeForLanguageId(Constants::MimeType, Constants::SettingsId); 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /RubyPlugin.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_Plugin_h 2 | #define Ruby_Plugin_h 3 | 4 | #include 5 | #include "editor/RubyQuickFixAssistProvider.h" 6 | 7 | namespace Ruby { 8 | 9 | class PluginPrivate; 10 | 11 | class Plugin : public ExtensionSystem::IPlugin 12 | { 13 | Q_OBJECT 14 | Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Ruby.json") 15 | 16 | public: 17 | Plugin(); 18 | ~Plugin(); 19 | 20 | static Plugin *instance(); 21 | 22 | virtual bool initialize(const QStringList &arguments, QString *errorString) override; 23 | virtual void extensionsInitialized() override; 24 | QuickFixAssistProvider* quickFixProvider(); 25 | 26 | private: 27 | void initializeToolsSettings(); 28 | 29 | static Plugin* m_instance; 30 | PluginPrivate *d = nullptr; 31 | 32 | #ifdef WITH_TESTS 33 | private slots: 34 | void cleanupTestCase(); 35 | 36 | void test_context(); 37 | void test_indentIf(); 38 | void test_indentBraces(); 39 | void test_lineCount(); 40 | void test_ifs(); 41 | void test_scanner(); 42 | void test_scanner_data(); 43 | #endif 44 | }; 45 | 46 | } 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /configure.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | BUILD_DIR = 'build' 4 | 5 | def read_qtc_version 6 | output = `qtcreator -version 2>&1` 7 | found = output =~ /Qt\ Creator (\d\.\d+\.\d+)/ 8 | abort 'Sorry, can not find the QtCreator version.' unless found 9 | $1 10 | rescue Errno::ENOENT 11 | abort 'Install QtCreator and/or have it on your system PATH first.' 12 | end 13 | 14 | def get_qtc_sources version 15 | puts 'Downloading QtCreator sources... (using curl)' 16 | 17 | short_version = version.gsub(/\.\d+$/, '') 18 | Dir.chdir(BUILD_DIR) do 19 | url = "http://download.qt-project.org/official_releases/qtcreator/#{short_version}/#{version}/qt-creator-opensource-src-#{version}.tar.gz" 20 | if system("curl -L -O #{url}") 21 | abort 'Failed to unpack QtCreator sources.' unless system("tar -xf qt-creator-opensource-src-#{version}.tar.gz") 22 | end 23 | end 24 | end 25 | 26 | puts < 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | namespace Ruby { 21 | 22 | class AmbigousMethodProposalItem : public TextEditor::AssistProposalItem 23 | { 24 | public: 25 | AmbigousMethodProposalItem(const Symbol &symbol, bool inNextSplit) 26 | : m_symbol(symbol) 27 | , m_inNextSplit(inNextSplit) 28 | { 29 | QString text = symbol.context; 30 | if (!text.isEmpty()) 31 | text += "::"; 32 | text += symbol.name + QString::fromLatin1(" [line %1]").arg(symbol.line); 33 | setText(text); 34 | setDetail(*symbol.file); 35 | } 36 | 37 | void apply(TextEditor::TextDocumentManipulatorInterface&, int) const override 38 | { 39 | Core::EditorManager::OpenEditorFlags flags = Core::EditorManager::NoFlags; 40 | if (m_inNextSplit) 41 | flags |= Core::EditorManager::OpenInOtherSplit; 42 | Core::EditorManager::openEditorAt(*m_symbol.file, 43 | m_symbol.line, 44 | m_symbol.column, 45 | Utils::Id(Constants::EditorId), 46 | flags); 47 | } 48 | private: 49 | Symbol m_symbol; 50 | bool m_inNextSplit; 51 | }; 52 | 53 | class AmbigousMethodAssistProcessor : public TextEditor::IAssistProcessor { 54 | public: 55 | AmbigousMethodAssistProcessor(const QList &symbols, int cursorPosition, int inNextSplit) 56 | : m_symbols(symbols) 57 | , m_cursorPosition(cursorPosition) 58 | , m_inNextSplit(inNextSplit) 59 | { 60 | } 61 | 62 | TextEditor::IAssistProposal *perform(const TextEditor::AssistInterface *interface) override 63 | { 64 | delete interface; 65 | 66 | QList proposals; 67 | for (const Symbol &symbol : m_symbols) 68 | proposals << new AmbigousMethodProposalItem(symbol, m_inNextSplit); 69 | auto proposal = new TextEditor::GenericProposal(m_cursorPosition, proposals); 70 | proposal->setFragile(true); 71 | return proposal; 72 | } 73 | 74 | const QList m_symbols; 75 | int m_cursorPosition; 76 | bool m_inNextSplit; 77 | }; 78 | 79 | TextEditor::IAssistProvider::RunType AmbigousMethodAssistProvider::runType() const 80 | { 81 | return TextEditor::IAssistProvider::AsynchronousWithThread; 82 | } 83 | 84 | TextEditor::IAssistProcessor *AmbigousMethodAssistProvider::createProcessor() const 85 | { 86 | return new AmbigousMethodAssistProcessor(m_symbols, m_cursorPosition, m_inNexSplit); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /editor/RubyAmbiguousMethodAssistProvider.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_AmbigousMethodAssistProvider_h 2 | #define Ruby_AmbigousMethodAssistProvider_h 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "RubySymbol.h" 9 | 10 | namespace Ruby { 11 | 12 | class AmbigousMethodAssistProvider : public TextEditor::IAssistProvider 13 | { 14 | public: 15 | RunType runType() const override; 16 | TextEditor::IAssistProcessor *createProcessor() const override; 17 | 18 | void setSymbols(const QList &symbols) { m_symbols = symbols; } 19 | void setCursorPosition(int cursorPosition) { m_cursorPosition = cursorPosition; } 20 | void setInNextSplit(bool value) { m_inNexSplit = value; } 21 | private: 22 | QList m_symbols; 23 | int m_cursorPosition; 24 | bool m_inNexSplit; 25 | }; 26 | 27 | } 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /editor/RubyAutoCompleter.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyAutoCompleter.h" 2 | #include "RubyIndenter.h" 3 | 4 | #include 5 | #include 6 | 7 | namespace Ruby { 8 | 9 | bool AutoCompleter::contextAllowsAutoQuotes(const QTextCursor &cursor, const QString &textToInsert) const 10 | { 11 | if (isInComment(cursor)) 12 | return false; 13 | 14 | QChar ch; 15 | 16 | if (!textToInsert.isEmpty()) 17 | ch = textToInsert.at(0); 18 | 19 | switch (ch.unicode()) { 20 | case '"': 21 | case '\'': 22 | return true; 23 | 24 | default: 25 | return false; 26 | } 27 | } 28 | 29 | bool AutoCompleter::contextAllowsAutoBrackets(const QTextCursor &cursor, const QString &textToInsert) const 30 | { 31 | if (isInComment(cursor)) 32 | return false; 33 | 34 | QChar ch; 35 | 36 | if (!textToInsert.isEmpty()) 37 | ch = textToInsert.at(0); 38 | 39 | switch (ch.unicode()) { 40 | case '(': 41 | case '[': 42 | case '{': 43 | case ')': 44 | case ']': 45 | case '}': 46 | return true; 47 | 48 | default: 49 | return false; 50 | } 51 | } 52 | 53 | QString AutoCompleter::insertMatchingBrace(const QTextCursor &, const QString &text, QChar la, bool skipChars, int *skippedChars) const 54 | { 55 | if (text.length() != 1) 56 | return QString(); 57 | 58 | const QChar ch = text.at(0); 59 | switch (ch.unicode()) { 60 | case '(': 61 | return QStringLiteral(")"); 62 | 63 | case '[': 64 | return QStringLiteral("]"); 65 | 66 | case '{': 67 | return QStringLiteral("}"); 68 | 69 | case ')': 70 | case ']': 71 | case '}': 72 | case ';': 73 | if (skipChars && la == ch) 74 | ++*skippedChars; 75 | break; 76 | 77 | default: 78 | break; 79 | } // end of switch 80 | 81 | return QString(); 82 | } 83 | 84 | QString AutoCompleter::insertMatchingQuote(const QTextCursor &, const QString &text, QChar la, bool skipChars, int *skippedChars) const 85 | { 86 | if (text.length() != 1) 87 | return QString(); 88 | 89 | const QChar ch = text.at(0); 90 | switch (ch.unicode()) { 91 | case '\'': 92 | if (la != ch) 93 | return QString(ch); 94 | if (skipChars) 95 | ++*skippedChars; 96 | break; 97 | 98 | case '"': 99 | if (la != ch) 100 | return QString(ch); 101 | if (skipChars) 102 | ++*skippedChars; 103 | break; 104 | 105 | case ';': 106 | if (skipChars && la == ch) 107 | ++*skippedChars; 108 | break; 109 | 110 | default: 111 | break; 112 | } // end of switch 113 | 114 | return QString(); 115 | } 116 | 117 | bool AutoCompleter::isInComment(const QTextCursor &cursor) const 118 | { 119 | const int hashIndex = cursor.block().text().indexOf('#'); 120 | return (hashIndex != -1 && hashIndex >= cursor.columnNumber()); 121 | } 122 | 123 | int AutoCompleter::paragraphSeparatorAboutToBeInserted(QTextCursor &cursor) 124 | { 125 | QTextBlock block = cursor.block(); 126 | const QString text = block.text().trimmed(); 127 | if (text == "end" 128 | || text == "else" 129 | || text.startsWith("elsif") 130 | || text.startsWith("rescue") 131 | || text == "ensure") { 132 | Indenter indenter(const_cast(block.document())); 133 | indenter.indentBlock(block, QChar(), tabSettings()); 134 | } 135 | 136 | return 0; 137 | // This implementation is buggy 138 | #if 0 139 | const QString textFromCursor = text.mid(cursor.positionInBlock()).trimmed(); 140 | if (!textFromCursor.isEmpty()) 141 | return 0; 142 | 143 | if (Language::symbolDefinition.indexIn(text) == -1 144 | && Language::startOfBlock.indexIn(text) == -1) { 145 | return 0; 146 | } 147 | 148 | int spaces = 0; 149 | for (const QChar c : text) { 150 | if (!c.isSpace()) 151 | break; 152 | spaces++; 153 | } 154 | QString indent = text.left(spaces); 155 | 156 | QString line; 157 | QTextBlock nextBlock = block.next(); 158 | while (nextBlock.isValid()) { 159 | line = nextBlock.text(); 160 | if (Language::endKeyword.indexIn(line) != -1 && line.startsWith(indent)) 161 | return 0; 162 | if (!line.trimmed().isEmpty()) 163 | break; 164 | nextBlock = nextBlock.next(); 165 | } 166 | 167 | int pos = cursor.position(); 168 | cursor.insertBlock(); 169 | cursor.insertText("end"); 170 | cursor.setPosition(pos); 171 | 172 | return 1; 173 | #endif 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /editor/RubyAutoCompleter.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_AutoCompleter_h 2 | #define Ruby_AutoCompleter_h 3 | 4 | #include 5 | 6 | namespace TextEditor { 7 | class TabSettings; 8 | } 9 | 10 | namespace Ruby { 11 | 12 | class AutoCompleter : public TextEditor::AutoCompleter 13 | { 14 | public: 15 | bool contextAllowsAutoBrackets(const QTextCursor &cursor, const QString &textToInsert) const override; 16 | bool contextAllowsAutoQuotes(const QTextCursor &cursor, const QString &textToInsert) const override; 17 | 18 | QString insertMatchingBrace(const QTextCursor &cursor, const QString &text, QChar la, bool skipChars, int *skippedChars) const override; 19 | QString insertMatchingQuote(const QTextCursor &cursor, const QString &text, QChar la, bool skipChars, int *skippedChars) const override; 20 | bool isInComment(const QTextCursor &cursor) const override; 21 | 22 | int paragraphSeparatorAboutToBeInserted(QTextCursor &cursor) override; 23 | 24 | }; 25 | 26 | } 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /editor/RubyBlockState.h: -------------------------------------------------------------------------------- 1 | #ifndef RubyBlockState_h 2 | #define RubyBlockState_h 3 | 4 | // We store weird things in QTextBlock user state integer, I don't remember why we do this way, but we do. 5 | // So this macros are to try to at least unify the way we manage the block state. 6 | // Why 20? Because scanner uses 2 bits to store the state code plus 16 to store a unicode char to know 7 | // the character used to close the string/regexp/etc... i.e. a mess 8 | #define RUBY_BLOCK_IDENT(state) (state >> 20) 9 | #define RUBY_BLOCK_SCANNER_STATE(state) (state & 0xfffff) 10 | #define RUBY_BLOCK_PACK(indent, state) ((indent << 20) | state) 11 | #endif 12 | -------------------------------------------------------------------------------- /editor/RubyCodeModel.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyCodeModel.h" 2 | #include "RubyScanner.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace Ruby { 11 | 12 | static bool isRubyFile(const QString& fileName) 13 | { 14 | if (fileName.endsWith(".rb") || fileName.endsWith(".rake")) 15 | return true; 16 | QFile file(fileName); 17 | if (file.open(QFile::ReadOnly)) { 18 | QByteArray line = file.readLine(); 19 | return line.startsWith("#!") && line.indexOf("ruby"); // Stupid heuristic that should work 99% of times :-) 20 | } 21 | return false; 22 | } 23 | 24 | class CodeModel::Data 25 | { 26 | Q_DISABLE_COPY(Data) 27 | 28 | public: 29 | Data(const QString &fileName = QString()) : fileName(fileName) {} 30 | 31 | void clear() 32 | { 33 | methods.clear(); 34 | identifiers.clear(); 35 | constants.clear(); 36 | classes.clear(); 37 | constantsDelc.clear(); 38 | symbols.clear(); 39 | } 40 | 41 | QDateTime lastUpdate; 42 | QString fileName; 43 | 44 | QList methods; 45 | QList classes; 46 | QList constantsDelc; 47 | QSet identifiers; 48 | QSet constants; 49 | QSet symbols; 50 | }; 51 | 52 | CodeModel::CodeModel() 53 | { 54 | } 55 | 56 | CodeModel::~CodeModel() 57 | { 58 | qDeleteAll(m_model); 59 | } 60 | 61 | CodeModel *CodeModel::instance() 62 | { 63 | static CodeModel model; 64 | return &model; 65 | } 66 | 67 | void CodeModel::removeSymbolsFrom(const QString &file) 68 | { 69 | delete m_model[file]; 70 | m_model.remove(file); 71 | } 72 | 73 | void CodeModel::addFile(const QString &file) 74 | { 75 | if (!isRubyFile(file)) 76 | return; 77 | 78 | QFileInfo info(file); 79 | Data *&data = m_model[file]; 80 | if (!data) 81 | data = new Data(file); 82 | 83 | if (!data->lastUpdate.isNull() && data->lastUpdate > info.lastModified()) 84 | return; 85 | 86 | QFile fp(file); 87 | if (!fp.open(QFile::ReadOnly)) 88 | return; 89 | updateFile(file, QString::fromUtf8(fp.readAll())); 90 | } 91 | 92 | static Symbol createSymbol(const QString *fileName, const QString &contents, Scanner &scanner, Token token) 93 | { 94 | Symbol sym(fileName); 95 | sym.name = contents.mid(token.position, token.length); 96 | sym.line = scanner.currentLine(); 97 | sym.column = scanner.currentColumn(token); 98 | sym.context = scanner.contextName(); 99 | return sym; 100 | } 101 | 102 | // This does not support multiline symbols defined by %i. 103 | static void parseRubySymbol(const QString &contents, Token token, QSet& symbols) 104 | { 105 | if (contents[token.position] == ':') { 106 | symbols << contents.mid(token.position, token.length); 107 | } else if (contents[token.position + token.length - 1] == ':') { 108 | QString symbol = contents.mid(token.position, token.length - 1); 109 | symbol.prepend(':'); 110 | symbols << symbol; 111 | } else { 112 | QStringRef symbolsToSplit; 113 | if (contents[token.position] == '%') { 114 | if (token.length < 4 || contents[token.position + 1] != 'i') 115 | return; 116 | 117 | QChar endDelimiter = translateDelimiter(contents[token.position + 2]); 118 | int start = token.position + 3; 119 | QStringRef contentsRef(&contents, start, token.length - 3); 120 | int end = contentsRef.indexOf(endDelimiter); 121 | symbolsToSplit = end < 0 ? contentsRef : contentsRef.left(end); 122 | } else { 123 | // Work 90% of the time... but not for the last item in a multiline symbol declaration. 124 | symbolsToSplit = QStringRef(&contents, token.position, token.length); 125 | } 126 | 127 | // To be able to use QStringRef everywhere we split things by spaces instead of by the regexp /\s+/ 128 | // But who cares for the ones using TABS!? :-) 129 | QVector result = symbolsToSplit.split(' '); 130 | for (QStringRef item : result) 131 | symbols << item.toString().prepend(':'); 132 | } 133 | } 134 | 135 | void addMethodParameter(Symbol& method, const QString& parameter) 136 | { 137 | QString& name = method.name; 138 | const QChar end = name[name.length() -1]; 139 | 140 | if (end == ')') { 141 | name.chop(1); 142 | name.append(", "); 143 | } else { 144 | name.append('('); 145 | } 146 | name.append(parameter); 147 | name.append(')'); 148 | } 149 | 150 | void CodeModel::updateFile(const QString &fileName, const QString &contents) 151 | { 152 | if (fileName.isEmpty()) 153 | return; 154 | 155 | Data *&data = m_model[fileName]; 156 | if (!data) 157 | data = new Data(fileName); 158 | data->clear(); 159 | 160 | Scanner scanner(&contents); 161 | scanner.enableContextRecognition(); 162 | 163 | const QString *fileNamePtr = &data->fileName; 164 | 165 | Token token; 166 | Token previousToken; 167 | while ((token = scanner.read()).kind != Token::EndOfBlock) { 168 | switch (token.kind) { 169 | case Token::Method: 170 | data->methods << createSymbol(fileNamePtr, contents, scanner, token); 171 | break; 172 | case Token::Parameter: 173 | addMethodParameter(data->methods.last(), contents.mid(token.position, token.length)); 174 | break; 175 | case Token::Identifier: 176 | data->identifiers << contents.mid(token.position, token.length); 177 | break; 178 | case Token::Constant: 179 | if (previousToken.kind == Token::KeywordClass) 180 | data->classes << createSymbol(fileNamePtr, contents, scanner, token); 181 | data->constants << contents.mid(token.position, token.length); 182 | break; 183 | case Token::Symbol: 184 | case Token::SymbolHashKey: 185 | parseRubySymbol(contents, token, data->symbols); 186 | break; 187 | case Token::OperatorAssign: 188 | if (previousToken.kind == Token::Constant) 189 | data->constantsDelc << createSymbol(fileNamePtr, contents, scanner, previousToken); 190 | break; 191 | default: 192 | break; 193 | } 194 | if (token.kind != Token::Whitespace) 195 | previousToken = token; 196 | } 197 | 198 | data->lastUpdate = QDateTime::currentDateTime(); 199 | } 200 | 201 | QList CodeModel::methodsIn(const QString &file) const 202 | { 203 | Data *data = m_model[file]; 204 | return data ? data->methods : QList(); 205 | } 206 | 207 | QSet CodeModel::identifiersIn(const QString &file) const 208 | { 209 | Data *data = m_model[file]; 210 | return data ? data->identifiers : QSet(); 211 | } 212 | 213 | QSet CodeModel::constantsIn(const QString &file) const 214 | { 215 | Data *data = m_model[file]; 216 | return data ? data->constants : QSet(); 217 | } 218 | 219 | QSet CodeModel::symbolsIn(const QString &file) const 220 | { 221 | Data *data = m_model[file]; 222 | return data ? data->symbols : QSet(); 223 | } 224 | 225 | QList CodeModel::allMethods() const 226 | { 227 | QList result; 228 | for (const Data *data : m_model) 229 | result << data->methods; 230 | return result; 231 | } 232 | 233 | QList CodeModel::allClasses() const 234 | { 235 | QList result; 236 | for (const Data *data : m_model) 237 | result << data->classes; 238 | return result; 239 | } 240 | 241 | QList CodeModel::allMethodsNamed(const QString &name) const 242 | { 243 | QList result; 244 | const int nameLength = name.length(); 245 | // FIXME: Replace this linear brute force approach 246 | for (const Data *data : m_model) { 247 | for (const Symbol &symbol : data->methods) { 248 | const QString &symbolName = symbol.name; 249 | if (symbolName.startsWith(name)) { 250 | if (symbolName.length() > nameLength && symbolName[nameLength] != '(') 251 | continue; 252 | result << symbol; 253 | } 254 | } 255 | } 256 | return result; 257 | } 258 | 259 | QList CodeModel::allClassesAndConstantsNamed(const QString &name) const 260 | { 261 | QList result; 262 | // FIXME: Replace this linear brute force approach 263 | for (const Data *data : m_model) { 264 | for (const Symbol &symbol : data->classes) { 265 | if (symbol.name == name) 266 | result << symbol; 267 | } 268 | } 269 | 270 | // constants are less important, keep them at the bottom. 271 | for (const Data *data : m_model) { 272 | for (const Symbol &symbol : data->constantsDelc) { 273 | if (symbol.name == name) 274 | result << symbol; 275 | } 276 | } 277 | 278 | return result; 279 | } 280 | 281 | } 282 | -------------------------------------------------------------------------------- /editor/RubyCodeModel.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_CodeModel_h 2 | #define Ruby_CodeModel_h 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "RubySymbol.h" 11 | 12 | namespace Ruby { 13 | 14 | class CodeModel : QObject 15 | { 16 | Q_OBJECT 17 | 18 | Q_DISABLE_COPY(CodeModel) 19 | 20 | public: 21 | CodeModel(); 22 | ~CodeModel(); 23 | 24 | static CodeModel *instance(); 25 | void removeSymbolsFrom(const QString &file); 26 | void addFile(const QString &file); 27 | // pass a QIODevice because the file may not be saved on file system. 28 | void updateFile(const QString &fileName, const QString &contents); 29 | 30 | QList methodsIn(const QString &file) const; 31 | QSet identifiersIn(const QString &file) const; 32 | QSet constantsIn(const QString &file) const; 33 | QSet symbolsIn(const QString &file) const; 34 | QList allMethods() const; 35 | QList allClasses() const; 36 | QList allMethodsNamed(const QString &name) const; 37 | QList allClassesAndConstantsNamed(const QString &name) const; 38 | 39 | private: 40 | 41 | class Data; 42 | QHash m_model; 43 | }; 44 | 45 | } 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /editor/RubyCodeStylePreferencesFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyCodeStylePreferencesFactory.h" 2 | #include "../RubyConstants.h" 3 | #include "RubyIndenter.h" 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | 12 | namespace Ruby { 13 | 14 | Utils::Id CodeStylePreferencesFactory::languageId() 15 | { 16 | return Constants::SettingsId; 17 | } 18 | 19 | QString CodeStylePreferencesFactory::displayName() 20 | { 21 | return QStringLiteral("Ruby"); 22 | } 23 | 24 | TextEditor::ICodeStylePreferences *CodeStylePreferencesFactory::createCodeStyle() const 25 | { 26 | return new TextEditor::SimpleCodeStylePreferences(); 27 | } 28 | 29 | QWidget *CodeStylePreferencesFactory::createEditor(TextEditor::ICodeStylePreferences*, QWidget *parent) const 30 | { 31 | return new QLabel(tr("There's no configuration widget yet, sorry."), parent); 32 | } 33 | 34 | TextEditor::Indenter *CodeStylePreferencesFactory::createIndenter(QTextDocument *doc) const 35 | { 36 | return new Indenter(doc); 37 | } 38 | 39 | QString CodeStylePreferencesFactory::snippetProviderGroupId() const 40 | { 41 | return Constants::SnippetGroupId; 42 | } 43 | 44 | QString CodeStylePreferencesFactory::previewText() const 45 | { 46 | return QStringLiteral( 47 | "module Foo\n" 48 | " class Bar\n" 49 | " attr_reader :something\n" 50 | "\n" 51 | " def foo(arg1, arg2)\n" 52 | " arg1.each do |i|\n" 53 | " bar(i + 2) if arg2 =~ /^fo+/\n" 54 | " end\n" 55 | " end\n" 56 | " end\n" 57 | "end\n"); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /editor/RubyCodeStylePreferencesFactory.h: -------------------------------------------------------------------------------- 1 | #ifndef RubyCodeStylePreferencesFactory_h 2 | #define RubyCodeStylePreferencesFactory_h 3 | 4 | #include 5 | 6 | namespace Ruby { 7 | 8 | class CodeStylePreferencesFactory : public TextEditor::ICodeStylePreferencesFactory 9 | { 10 | Q_DECLARE_TR_FUNCTIONS(CodeStylePreferencesFactory) 11 | public: 12 | Utils::Id languageId() override; 13 | QString displayName() override; 14 | TextEditor::ICodeStylePreferences *createCodeStyle() const override; 15 | QWidget *createEditor(TextEditor::ICodeStylePreferences*, QWidget *parent) const override; 16 | TextEditor::Indenter *createIndenter(QTextDocument *doc) const override; 17 | QString snippetProviderGroupId() const override; 18 | QString previewText() const override; 19 | 20 | }; 21 | 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /editor/RubyCompletionAssist.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyCompletionAssist.h" 2 | #include "../RubyConstants.h" 3 | #include "RubyCodeModel.h" 4 | #include "RubyScanner.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | namespace Ruby { 16 | 17 | enum KindOfCompletion { 18 | MayBeAIdentifier, 19 | MayBeAMethod, 20 | MayBeConstant, 21 | MayBeASymbol, 22 | MaybeNothing 23 | }; 24 | 25 | static KindOfCompletion kindOfCompletion(QTextDocument *document, int &startPosition) 26 | { 27 | QChar ch; 28 | bool mayBeAConstant = false; 29 | do { 30 | ch = document->characterAt(--startPosition); 31 | 32 | if (ch == '.') { 33 | startPosition++; 34 | return MayBeAMethod; 35 | } 36 | if (ch == ':') { 37 | QChar lastChar = document->characterAt(startPosition - 1); 38 | if (lastChar.isLetterOrNumber() || lastChar == ':') 39 | return MaybeNothing; 40 | return MayBeASymbol; 41 | } 42 | if (ch.isUpper()) 43 | mayBeAConstant = true; 44 | } while (ch.isLetterOrNumber() || ch == '_'); 45 | 46 | startPosition++; 47 | return mayBeAConstant ? MayBeConstant : MayBeAIdentifier; 48 | } 49 | 50 | TextEditor::IAssistProcessor *CompletionAssistProvider::createProcessor() const 51 | { 52 | return new CompletionAssistProcessor; 53 | } 54 | 55 | bool CompletionAssistProvider::isActivationCharSequence(const QString &sequence) const 56 | { 57 | return sequence.at(0) == '.' || sequence.at(0) == ':'; 58 | } 59 | 60 | static const QString &nameFor(const QString &s) 61 | { 62 | return s; 63 | } 64 | 65 | static const QString &nameFor(const Symbol &s) 66 | { 67 | return s.name; 68 | } 69 | 70 | template 71 | static void addProposalFromSet(QList &proposals, 72 | const T &container, const QString &myTyping, 73 | const QIcon &icon, int order = 0) 74 | { 75 | for (const auto &item : container) { 76 | const QString &name = nameFor(item); 77 | if (myTyping == name) 78 | continue; 79 | 80 | auto proposal = new TextEditor::AssistProposalItem; 81 | 82 | int indexOfParenthesis = name.indexOf('('); 83 | if (indexOfParenthesis != -1) { 84 | proposal->setText(name.mid(0, indexOfParenthesis)); 85 | proposal->setDetail(name); 86 | } else { 87 | proposal->setText(name); 88 | } 89 | 90 | proposal->setIcon(icon); 91 | proposal->setOrder(order); 92 | proposals << proposal; 93 | } 94 | } 95 | 96 | CompletionAssistProcessor::CompletionAssistProcessor() 97 | : m_methodIcon(":/codemodel/images/func.png") 98 | , m_identifierIcon(":/codemodel/images/var.png") 99 | , m_constantIcon(":/codemodel/images/macro.png") 100 | , m_symbolIcon(m_identifierIcon) 101 | , m_snippetCollector(Constants::SnippetGroupId, 102 | QIcon(":/texteditor/images/snippet.png")) 103 | { 104 | 105 | } 106 | 107 | TextEditor::IAssistProposal *CompletionAssistProcessor::perform(const TextEditor::AssistInterface *interface) 108 | { 109 | if (interface->reason() == TextEditor::IdleEditor) 110 | return 0; 111 | 112 | int startPosition = interface->position(); 113 | 114 | // FIXME: We should check the block status in case of multi-line tokens 115 | QTextBlock block = interface->textDocument()->findBlock(startPosition); 116 | int linePosition = startPosition - block.position(); 117 | const QString line = interface->textDocument()->findBlock(startPosition).text(); 118 | 119 | switch(Scanner::tokenAt(&line, linePosition).kind) { 120 | case Token::Comment: 121 | case Token::String: 122 | case Token::Backtick: 123 | case Token::Regexp: 124 | return 0; 125 | default: 126 | break; 127 | } 128 | 129 | KindOfCompletion kind = kindOfCompletion(interface->textDocument(), startPosition); 130 | CodeModel *cm = CodeModel::instance(); 131 | 132 | QString myTyping = interface->textAt(startPosition, interface->position() - startPosition); 133 | const QString fileName = interface->filePath().fileName(); 134 | 135 | QList proposals; 136 | 137 | switch (kind) { 138 | case MayBeAMethod: 139 | case MayBeAIdentifier: 140 | addProposalFromSet(proposals, cm->methodsIn(fileName), myTyping, m_methodIcon, 1); 141 | addProposalFromSet(proposals, cm->identifiersIn(fileName), myTyping, m_identifierIcon); 142 | break; 143 | case MayBeConstant: 144 | addProposalFromSet(proposals, cm->constantsIn(fileName), myTyping, m_constantIcon); 145 | break; 146 | case MayBeASymbol: 147 | addProposalFromSet(proposals, cm->symbolsIn(fileName), myTyping, m_symbolIcon); 148 | break; 149 | default: 150 | break; 151 | } 152 | proposals += m_snippetCollector.collect(); 153 | 154 | if (proposals.empty()) { 155 | return 0; 156 | } 157 | TextEditor::GenericProposalModelPtr model(new TextEditor::GenericProposalModel); 158 | model->loadContent(proposals); 159 | TextEditor::IAssistProposal *proposal = new TextEditor::GenericProposal(startPosition, model); 160 | return proposal; 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /editor/RubyCompletionAssist.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_CompletionAssist_h 2 | #define Ruby_CompletionAssist_h 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | namespace Ruby { 11 | 12 | class CompletionAssistProvider : public TextEditor::CompletionAssistProvider 13 | { 14 | public: 15 | TextEditor::IAssistProcessor *createProcessor() const override; 16 | 17 | int activationCharSequenceLength() const override { return 1; } 18 | bool isActivationCharSequence(const QString &sequence) const override; 19 | }; 20 | 21 | class CompletionAssistProcessor : public TextEditor::IAssistProcessor 22 | { 23 | public: 24 | CompletionAssistProcessor(); 25 | TextEditor::IAssistProposal *perform(const TextEditor::AssistInterface *interface) override; 26 | private: 27 | // TODO: Share this icons with all instances 28 | QIcon m_methodIcon; 29 | QIcon m_identifierIcon; 30 | QIcon m_constantIcon; 31 | QIcon m_symbolIcon; 32 | TextEditor::SnippetAssistCollector m_snippetCollector; 33 | }; 34 | 35 | } 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /editor/RubyEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyEditor.h" 2 | 3 | #include "../RubyConstants.h" 4 | 5 | namespace Ruby { 6 | 7 | Editor::Editor() 8 | { 9 | addContext(Constants::LangRuby); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /editor/RubyEditor.h: -------------------------------------------------------------------------------- 1 | #ifndef RubyEditor_h 2 | #define RubyEditor_h 3 | 4 | #include 5 | 6 | namespace Ruby { 7 | 8 | class EditorWidget; 9 | 10 | class Editor : public TextEditor::BaseTextEditor 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | Editor(); 16 | }; 17 | 18 | } 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /editor/RubyEditorDocument.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyEditorDocument.h" 2 | #include "../RubyConstants.h" 3 | #include "../RubyPlugin.h" 4 | 5 | namespace Ruby 6 | { 7 | 8 | EditorDocument::EditorDocument() 9 | { 10 | setId(Constants::EditorId); 11 | } 12 | 13 | TextEditor::IAssistProvider *EditorDocument::quickFixAssistProvider() const 14 | { 15 | return Plugin::instance()->quickFixProvider(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /editor/RubyEditorDocument.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_EditorDocument_h 2 | #define Ruby_EditorDocument_h 3 | 4 | #include 5 | 6 | namespace Ruby { 7 | 8 | class EditorDocument : public TextEditor::TextDocument 9 | { 10 | public: 11 | explicit EditorDocument(); 12 | 13 | TextEditor::IAssistProvider *quickFixAssistProvider() const override; 14 | }; 15 | 16 | } 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /editor/RubyEditorFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyAutoCompleter.h" 2 | #include "RubyCompletionAssist.h" 3 | #include "RubyEditorDocument.h" 4 | #include "RubyEditor.h" 5 | #include "RubyEditorFactory.h" 6 | #include "RubyHighlighter.h" 7 | #include "RubyIndenter.h" 8 | 9 | #include "../RubyConstants.h" 10 | #include "RubyEditorWidget.h" 11 | 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | namespace Ruby { 18 | 19 | EditorFactory::EditorFactory() 20 | { 21 | setId(Constants::EditorId); 22 | setDisplayName(qApp->translate("OpenWith::Editors", Constants::EditorDisplayName)); 23 | addMimeType(Constants::MimeType); 24 | addMimeType(Constants::ProjectMimeType); 25 | 26 | setDocumentCreator([]() { return new EditorDocument; }); 27 | setIndenterCreator([](QTextDocument *doc) { return new Indenter(doc); }); 28 | setEditorWidgetCreator([]() { return new EditorWidget; }); 29 | setEditorCreator([]() { return new Editor; }); 30 | setAutoCompleterCreator([]() { return new AutoCompleter; }); 31 | setCompletionAssistProvider(new CompletionAssistProvider); 32 | setSyntaxHighlighterCreator([]() { return new Highlighter; }); 33 | setCommentDefinition(Utils::CommentDefinition::HashStyle); 34 | setParenthesesMatchingEnabled(true); 35 | setCodeFoldingSupported(true); 36 | setMarksVisible(true); 37 | 38 | setEditorActionHandlers(TextEditor::TextEditorActionHandler::Format 39 | | TextEditor::TextEditorActionHandler::UnCommentSelection 40 | | TextEditor::TextEditorActionHandler::UnCollapseAll 41 | | TextEditor::TextEditorActionHandler::FollowSymbolUnderCursor); 42 | } 43 | 44 | void EditorFactory::decorateEditor(TextEditor::TextEditorWidget *editor) 45 | { 46 | if (TextEditor::TextDocument *document = editor->textDocument()) { 47 | document->setSyntaxHighlighter(new Highlighter); 48 | document->setIndenter(new Indenter(document->document())); 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /editor/RubyEditorFactory.h: -------------------------------------------------------------------------------- 1 | #ifndef RubyEditorFactory_h 2 | #define RubyEditorFactory_h 3 | 4 | #include 5 | 6 | namespace Ruby { 7 | 8 | class EditorFactory : public TextEditor::TextEditorFactory 9 | { 10 | Q_OBJECT 11 | 12 | public: 13 | EditorFactory(); 14 | static void decorateEditor(TextEditor::TextEditorWidget *editor); 15 | }; 16 | 17 | } 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /editor/RubyEditorWidget.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyEditorWidget.h" 2 | 3 | #include "RubyAmbiguousMethodAssistProvider.h" 4 | #include "RubyAutoCompleter.h" 5 | #include "RubyCodeModel.h" 6 | #include "../RubyConstants.h" 7 | #include "RubyHighlighter.h" 8 | #include "RubyIndenter.h" 9 | #include "RubyRubocopHighlighter.h" 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | using Utils::Link; 18 | 19 | namespace Ruby { 20 | 21 | const int CODEMODEL_UPDATE_INTERVAL = 150; 22 | const int RUBOCOP_UPDATE_INTERVAL = 300; 23 | 24 | EditorWidget::EditorWidget() 25 | : m_wordRegex("[\\w!\\?]+") 26 | , m_ambigousMethodAssistProvider(new AmbigousMethodAssistProvider) 27 | { 28 | setLanguageSettingsId(Constants::SettingsId); 29 | 30 | m_commentDefinition.multiLineStart.clear(); 31 | m_commentDefinition.multiLineEnd.clear(); 32 | m_commentDefinition.singleLine = '#'; 33 | 34 | m_updateCodeModelTimer.setSingleShot(true); 35 | m_updateCodeModelTimer.setInterval(CODEMODEL_UPDATE_INTERVAL); 36 | connect(&m_updateCodeModelTimer, &QTimer::timeout, this, [this] { 37 | if (m_codeModelUpdatePending) 38 | updateCodeModel(); 39 | }); 40 | 41 | m_updateRubocopTimer.setSingleShot(true); 42 | m_updateRubocopTimer.setInterval(RUBOCOP_UPDATE_INTERVAL); 43 | connect(&m_updateRubocopTimer, &QTimer::timeout, this, [this] { 44 | if (m_rubocopUpdatePending) 45 | updateRubocop(); 46 | }); 47 | 48 | CodeModel::instance(); 49 | } 50 | 51 | EditorWidget::~EditorWidget() 52 | { 53 | delete m_ambigousMethodAssistProvider; 54 | } 55 | 56 | void EditorWidget::findLinkAt(const QTextCursor &cursor, 57 | Utils::ProcessLinkCallback &&processLinkCallback, 58 | bool resolveTarget, 59 | bool inNextSplit) 60 | { 61 | Q_UNUSED(resolveTarget) 62 | QString text = cursor.block().text(); 63 | if (text.isEmpty()) { 64 | processLinkCallback(Utils::Link()); 65 | return; 66 | } 67 | 68 | QString word; 69 | int cursorPos = cursor.positionInBlock(); 70 | int pos = 0; 71 | for (;;) { 72 | QRegularExpressionMatch match = m_wordRegex.match(text, pos + word.length()); 73 | if (!match.hasMatch()) { 74 | processLinkCallback(Utils::Link()); 75 | return; 76 | } 77 | 78 | word = match.captured(); 79 | pos = match.capturedStart(); 80 | if (pos <= cursorPos && (pos + word.length()) >= cursorPos) 81 | break; 82 | } 83 | 84 | if (word.isEmpty() || word[0].isDigit()) { 85 | processLinkCallback(Utils::Link()); 86 | return; 87 | } 88 | 89 | CodeModel* codeModel = CodeModel::instance(); 90 | 91 | const QList symbols = word[0].isUpper() ? codeModel->allClassesAndConstantsNamed(word) : codeModel->allMethodsNamed(word); 92 | if (symbols.empty()) { 93 | processLinkCallback(Utils::Link()); 94 | return; 95 | } 96 | 97 | Link link; 98 | link.linkTextStart = cursor.position() + (pos - cursorPos); 99 | link.linkTextEnd = link.linkTextStart + word.length(); 100 | 101 | if (symbols.count() > 1) { 102 | m_ambigousMethodAssistProvider->setSymbols(symbols); 103 | m_ambigousMethodAssistProvider->setCursorPosition(cursor.position()); 104 | m_ambigousMethodAssistProvider->setInNextSplit(inNextSplit); 105 | 106 | invokeAssist(TextEditor::FollowSymbol, m_ambigousMethodAssistProvider); 107 | processLinkCallback(link); 108 | return; 109 | } 110 | 111 | link.targetLine = symbols.last().line; 112 | link.targetColumn = symbols.last().column; 113 | link.targetFileName = *symbols.last().file; 114 | 115 | processLinkCallback(link); 116 | } 117 | 118 | void EditorWidget::unCommentSelection() 119 | { 120 | Utils::unCommentSelection(this, m_commentDefinition); 121 | } 122 | 123 | void EditorWidget::aboutToOpen(const QString &fileName, const QString &realFileName) 124 | { 125 | Q_UNUSED(fileName) 126 | m_filePathDueToMaybeABug = realFileName; 127 | } 128 | 129 | void EditorWidget::scheduleCodeModelUpdate() 130 | { 131 | m_codeModelUpdatePending = m_updateCodeModelTimer.isActive(); 132 | if (m_codeModelUpdatePending) 133 | return; 134 | 135 | m_codeModelUpdatePending = false; 136 | updateCodeModel(); 137 | m_updateCodeModelTimer.start(); 138 | } 139 | 140 | void EditorWidget::updateCodeModel() 141 | { 142 | const QString textData = textDocument()->plainText(); 143 | CodeModel::instance()->updateFile(textDocument()->filePath().toString(), textData); 144 | } 145 | 146 | void EditorWidget::scheduleRubocopUpdate() 147 | { 148 | m_rubocopUpdatePending = m_updateRubocopTimer.isActive(); 149 | if (m_rubocopUpdatePending) 150 | return; 151 | 152 | m_rubocopUpdatePending = false; 153 | updateRubocop(); 154 | m_updateRubocopTimer.start(); 155 | } 156 | 157 | void EditorWidget::updateRubocop() 158 | { 159 | if (!RubocopHighlighter::instance()->run(textDocument(), m_filePathDueToMaybeABug)) { 160 | m_rubocopUpdatePending = true; 161 | m_updateRubocopTimer.start(); 162 | } 163 | } 164 | 165 | void EditorWidget::finalizeInitialization() 166 | { 167 | connect(document(), &QTextDocument::contentsChanged, this, &EditorWidget::scheduleCodeModelUpdate); 168 | connect(document(), &QTextDocument::contentsChanged, this, &EditorWidget::scheduleRubocopUpdate); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /editor/RubyEditorWidget.h: -------------------------------------------------------------------------------- 1 | #ifndef RubyEditorWidget_h 2 | #define RubyEditorWidget_h 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | namespace Ruby { 11 | 12 | class AmbigousMethodAssistProvider; 13 | 14 | class EditorWidget : public TextEditor::TextEditorWidget 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | EditorWidget(); 20 | ~EditorWidget(); 21 | 22 | void findLinkAt(const QTextCursor &cursor, 23 | Utils::ProcessLinkCallback &&processLinkCallback, 24 | bool resolveTarget = true, 25 | bool inNextSplit = false) override; 26 | void unCommentSelection() override; 27 | 28 | void aboutToOpen(const QString &fileName, const QString &realFileName) override; 29 | 30 | protected: 31 | void finalizeInitialization() override; 32 | 33 | private: 34 | void scheduleCodeModelUpdate(); 35 | 36 | void scheduleRubocopUpdate(); 37 | void updateCodeModel(); 38 | void updateRubocop(); 39 | 40 | private: 41 | QRegularExpression m_wordRegex; 42 | Utils::CommentDefinition m_commentDefinition; 43 | QTimer m_updateCodeModelTimer; 44 | bool m_codeModelUpdatePending = false; 45 | 46 | QTimer m_updateRubocopTimer; 47 | bool m_rubocopUpdatePending = false; 48 | 49 | QString m_filePathDueToMaybeABug; 50 | 51 | AmbigousMethodAssistProvider *m_ambigousMethodAssistProvider; 52 | }; 53 | 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /editor/RubyHighlighter.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyHighlighter.h" 2 | #include "RubyScanner.h" 3 | #include "RubyBlockState.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace Ruby { 12 | 13 | QVector Highlighter::m_formats; 14 | 15 | static void initFormats(QVector &formats) 16 | { 17 | formats.resize(Token::EndOfBlock); 18 | 19 | QTextCharFormat &keywordFormat = formats[Token::Keyword]; 20 | keywordFormat.setFontWeight(75); 21 | const Token::Kind keywordTokens[] = { 22 | Token::KeywordDef, 23 | Token::KeywordClass, 24 | Token::KeywordModule, 25 | Token::KeywordFlowControl, 26 | Token::KeywordLoop, 27 | Token::KeywordBlockStarter, 28 | Token::KeywordEnd, 29 | Token::KeywordElseElsIfRescueEnsure 30 | }; 31 | for (unsigned tk = 0; tk < sizeof(keywordTokens) / sizeof(*keywordTokens); ++tk) 32 | formats[keywordTokens[tk]] = keywordFormat; 33 | 34 | 35 | formats[Token::KeywordVisibility].setForeground(QColor(0, 0, 255)); 36 | formats[Token::KeywordVisibility].setFontWeight(75); 37 | formats[Token::KeywordSelf].setForeground(QColor(68, 85, 136)); 38 | formats[Token::KeywordSelf].setFontWeight(75); 39 | formats[Token::String].setForeground(QColor(208, 16, 64)); 40 | formats[Token::InStringCode].setForeground(QColor(0, 110, 40)); 41 | formats[Token::Backtick] = formats[Token::String]; 42 | formats[Token::Comment].setForeground(QColor(153, 153, 136)); 43 | formats[Token::Constant].setForeground(QColor(0, 128, 128)); 44 | formats[Token::Global].setForeground(QColor(0, 128, 128)); 45 | formats[Token::Regexp].setForeground(QColor(0, 153, 38)); 46 | formats[Token::ClassField].setForeground(QColor(0, 128, 128)); 47 | formats[Token::Number].setForeground(QColor(0, 153, 153)); 48 | formats[Token::Symbol].setForeground(QColor(153, 0, 115)); 49 | formats[Token::SymbolHashKey].setForeground(QColor(70, 0, 115)); 50 | formats[Token::Method].setForeground(QColor(153, 0, 0)); 51 | formats[Token::Method].setFontWeight(75); 52 | formats[Token::Parameter].setFontItalic(true); 53 | formats[Token::Parameter].setForeground(QColor(0, 134, 179)); 54 | } 55 | 56 | Highlighter::Highlighter(QTextDocument *parent) 57 | : TextEditor::SyntaxHighlighter(parent) 58 | { 59 | if (m_formats.empty()) 60 | initFormats(m_formats); 61 | } 62 | 63 | void Highlighter::highlightBlock(const QString &text) 64 | { 65 | int initialState = previousBlockState(); 66 | if (initialState == -1) 67 | initialState = 0; 68 | setCurrentBlockState(highlightLine(text, initialState)); 69 | } 70 | 71 | int Highlighter::highlightLine(const QString &text, int state) 72 | { 73 | m_currentBlockParentheses.clear(); 74 | 75 | Scanner scanner(&text); 76 | scanner.setState(RUBY_BLOCK_SCANNER_STATE(state)); 77 | 78 | static QString openParenthesis = "([{"; 79 | static QString closeParenthesis = ")]}"; 80 | 81 | Token token; 82 | while ((token = scanner.read()).kind != Token::EndOfBlock) { 83 | setFormat(token.position, token.length, formatForToken(token)); 84 | if (token.isParenthesisLike()) { 85 | QChar ch = text[token.position]; 86 | if (openParenthesis.contains(ch)) 87 | m_currentBlockParentheses << Parenthesis(Parenthesis::Opened, ch, token.position); 88 | else if (closeParenthesis.contains(ch)) 89 | m_currentBlockParentheses << Parenthesis(Parenthesis::Closed, ch, token.position); 90 | } 91 | } 92 | 93 | int indentLevel = RUBY_BLOCK_IDENT(state); 94 | int nextIndentLevel = indentLevel + scanner.indentVariation(); 95 | if (scanner.didBlockInterrupt()) 96 | indentLevel--; 97 | 98 | if (nextIndentLevel < 0) 99 | nextIndentLevel = 0; 100 | 101 | TextEditor::TextDocumentLayout::setFoldingIndent(currentBlock(), indentLevel); 102 | TextEditor::TextDocumentLayout::setParentheses(currentBlock(), m_currentBlockParentheses); 103 | return RUBY_BLOCK_PACK(nextIndentLevel, scanner.state()); 104 | } 105 | 106 | QTextCharFormat Highlighter::formatForToken(const Token &token) 107 | { 108 | Q_ASSERT(token.kind < m_formats.size()); 109 | return m_formats[token.kind]; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /editor/RubyHighlighter.h: -------------------------------------------------------------------------------- 1 | #ifndef RubyHighliter_h 2 | #define RubyHighliter_h 3 | 4 | #include 5 | #include 6 | #include "RubyScanner.h" 7 | 8 | namespace Ruby { 9 | 10 | class Highlighter : public TextEditor::SyntaxHighlighter 11 | { 12 | public: 13 | Highlighter(QTextDocument *parent = nullptr); 14 | protected: 15 | virtual void highlightBlock(const QString &text) override; 16 | private: 17 | int highlightLine(const QString &text, int state); 18 | QTextCharFormat formatForToken(const Token &); 19 | 20 | static QVector m_formats; 21 | 22 | typedef TextEditor::Parenthesis Parenthesis; 23 | typedef TextEditor::Parentheses Parentheses; 24 | 25 | Parentheses m_currentBlockParentheses; 26 | }; 27 | 28 | } 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /editor/RubyIndenter.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyIndenter.h" 2 | #include "RubyScanner.h" 3 | #include "RubyBlockState.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace Ruby { 10 | 11 | static bool didBlockStart(const QTextBlock &block) 12 | { 13 | QString text = block.text(); 14 | Scanner scanner(&text); 15 | scanner.readLine(); 16 | return scanner.indentVariation() > 0 || scanner.didBlockInterrupt(); 17 | } 18 | 19 | Indenter::Indenter(QTextDocument *doc) : TextEditor::TextIndenter(doc) 20 | { 21 | } 22 | 23 | void Indenter::indentBlock(const QTextBlock &block, 24 | const QChar &, 25 | const TextEditor::TabSettings &settings, 26 | int) 27 | { 28 | int indent; 29 | 30 | QTextBlock previous = block.previous(); 31 | // Previous line ends on comma, ignore everything and follow the indent 32 | if (previous.text().endsWith(',')) { 33 | indent = previous.text().indexOf(QRegularExpression("\\S")) / settings.m_indentSize; 34 | } else { 35 | // Use the stored indent plus some bizarre heuristics that even myself remember how it works. 36 | indent = RUBY_BLOCK_IDENT(block.userState()); 37 | if (indent < 0) { 38 | while (indent == -1 && previous.isValid()) { 39 | indent = RUBY_BLOCK_IDENT(previous.userState()); 40 | previous = previous.previous(); 41 | } 42 | } 43 | 44 | if (didBlockStart(block) && indent > 0) 45 | indent--; 46 | } 47 | 48 | settings.indentLine(block, indent * settings.m_indentSize); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /editor/RubyIndenter.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_Indenter_h 2 | #define Ruby_Indenter_h 3 | 4 | #include 5 | #include 6 | 7 | namespace Ruby { 8 | 9 | class Indenter : public TextEditor::TextIndenter 10 | { 11 | public: 12 | explicit Indenter(QTextDocument *doc); 13 | bool isElectricCharacter(const QChar &) const override { return false; } 14 | void indentBlock(const QTextBlock &block, 15 | const QChar &, 16 | const TextEditor::TabSettings &settings, 17 | int cursorPositionInEditor = -1) override; 18 | }; 19 | 20 | } 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /editor/RubyQuickFixAssistProvider.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyQuickFixAssistProvider.h" 2 | 3 | #include "RubyQuickFixes.h" 4 | #include "../RubyConstants.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace Ruby { 13 | 14 | QuickFixAssistProvider::QuickFixAssistProvider() 15 | { 16 | } 17 | 18 | TextEditor::IAssistProvider::RunType QuickFixAssistProvider::runType() const 19 | { 20 | return TextEditor::IAssistProvider::Synchronous; 21 | } 22 | 23 | class RubyQuickFixAssistProcessor : public TextEditor::IAssistProcessor 24 | { 25 | TextEditor::IAssistProposal *perform(const TextEditor::AssistInterface *interface) override 26 | { 27 | QSharedPointer assistInterface(interface); 28 | 29 | TextEditor::QuickFixOperations quickFixes; 30 | 31 | for (QuickFixFactory *factory : QuickFixFactory::quickFixFactories()) 32 | factory->match(assistInterface, quickFixes); 33 | 34 | return TextEditor::GenericProposal::createProposal(interface, quickFixes); 35 | } 36 | }; 37 | 38 | TextEditor::IAssistProcessor *QuickFixAssistProvider::createProcessor() const 39 | { 40 | return new RubyQuickFixAssistProcessor; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /editor/RubyQuickFixAssistProvider.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_QuickFixAssistProvider_h 2 | #define Ruby_QuickFixAssistProvider_h 3 | 4 | #include 5 | 6 | namespace Ruby { 7 | 8 | class QuickFixAssistProvider : public TextEditor::IAssistProvider 9 | { 10 | public: 11 | QuickFixAssistProvider(); 12 | IAssistProvider::RunType runType() const override; 13 | TextEditor::IAssistProcessor *createProcessor() const override; 14 | }; 15 | 16 | } 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /editor/RubyQuickFixes.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyQuickFixes.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "RubyScanner.h" 7 | 8 | namespace Ruby { 9 | 10 | static QList g_quickFixFactories; 11 | 12 | QuickFixFactory::QuickFixFactory() 13 | { 14 | g_quickFixFactories.append(this); 15 | } 16 | 17 | QuickFixFactory::~QuickFixFactory() 18 | { 19 | g_quickFixFactories.removeOne(this); 20 | } 21 | 22 | const QList &QuickFixFactory::quickFixFactories() 23 | { 24 | return g_quickFixFactories; 25 | } 26 | 27 | void registerQuickFixes(ExtensionSystem::IPlugin *plugIn) 28 | { 29 | auto ssq = new SwitchStringQuotes; 30 | ssq->setParent(plugIn); 31 | } 32 | 33 | void SwitchStringQuotes::match(const TextEditor::QuickFixInterface &interface, 34 | TextEditor::QuickFixOperations &result) 35 | { 36 | QTextBlock block = interface->textDocument()->findBlock(interface->position()); 37 | QString line = block.text(); 38 | int userCursorPosition = interface->position(); 39 | int position = userCursorPosition - block.position(); 40 | Token token = Scanner::tokenAt(&line, position); 41 | 42 | if (token.kind != Ruby::Token::String) 43 | return; 44 | 45 | SwitchStringQuotesOp* operation = new SwitchStringQuotesOp(block, token, userCursorPosition); 46 | QString description; 47 | if (line[token.position] == '"') 48 | description = tr("Convert to single quotes"); 49 | else 50 | description = tr("Convert to double quotes"); 51 | operation->setDescription(description); 52 | 53 | result << operation; 54 | } 55 | 56 | SwitchStringQuotesOp::SwitchStringQuotesOp(QTextBlock &block, const Token &token, int userCursorPosition) 57 | : m_block(block), m_token(token), m_userCursorPosition(userCursorPosition) 58 | { 59 | } 60 | 61 | void SwitchStringQuotesOp::perform() 62 | { 63 | QString string = m_block.text().mid(m_token.position, m_token.length); 64 | 65 | QString oldQuote = "\""; 66 | QString newQuote = "'"; 67 | if (string[0] != '"') 68 | std::swap(oldQuote, newQuote); 69 | 70 | string.replace("\\" + oldQuote, oldQuote); 71 | string.replace(newQuote, "\\" + newQuote); 72 | string[0] = newQuote[0]; 73 | string[string.length() - 1] = newQuote[0]; 74 | 75 | QTextCursor cursor(m_block); 76 | cursor.beginEditBlock(); 77 | cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_token.position); 78 | cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m_token.length); 79 | cursor.removeSelectedText(); 80 | cursor.insertText(string); 81 | cursor.endEditBlock(); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /editor/RubyQuickFixes.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_QuickFixes_h 2 | #define Ruby_QuickFixes_h 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "RubyScanner.h" 10 | 11 | namespace Ruby { 12 | 13 | void registerQuickFixes(ExtensionSystem::IPlugin *plugIn); 14 | 15 | class QuickFixFactory : public QObject 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | QuickFixFactory(); 21 | ~QuickFixFactory(); 22 | virtual void match(const TextEditor::QuickFixInterface &interface, 23 | TextEditor::QuickFixOperations &result) = 0; 24 | 25 | static const QList &quickFixFactories(); 26 | }; 27 | 28 | class SwitchStringQuotes : public QuickFixFactory { 29 | public: 30 | void match(const TextEditor::QuickFixInterface &interface, 31 | TextEditor::QuickFixOperations &result) override; 32 | }; 33 | 34 | class SwitchStringQuotesOp : public TextEditor::QuickFixOperation { 35 | public: 36 | SwitchStringQuotesOp(QTextBlock &block, const Token &token, int userCursorPosition); 37 | void perform() override; 38 | private: 39 | QTextBlock m_block; 40 | Token m_token; 41 | int m_userCursorPosition; 42 | }; 43 | } 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /editor/RubyRubocopHighlighter.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyRubocopHighlighter.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | Q_LOGGING_CATEGORY(log, "qtc.ruby.rubocop"); 16 | 17 | namespace Ruby { 18 | 19 | static RubocopHighlighter *theInstance = nullptr; 20 | 21 | class RubocopFuture : public QFutureInterface, public QObject 22 | { 23 | public: 24 | RubocopFuture(const Offenses &offenses) 25 | { 26 | reportResults(offenses); 27 | } 28 | }; 29 | 30 | class TextMark : public TextEditor::TextMark 31 | { 32 | public: 33 | static Utils::Theme::Color colorForSeverity(int severity) 34 | { 35 | switch (severity) 36 | { 37 | case 1: return Utils::Theme::ProjectExplorer_TaskWarn_TextMarkColor; 38 | case 2: return Utils::Theme::ProjectExplorer_TaskError_TextMarkColor; 39 | default: return Utils::Theme::TextColorNormal; 40 | } 41 | } 42 | TextMark(const Utils::FilePath &fileName, int line, int severity, const QString &text) 43 | : TextEditor::TextMark(fileName, line, "Rubocop") 44 | { 45 | setColor(colorForSeverity(severity)); 46 | setPriority(TextEditor::TextMark::Priority(severity)); 47 | setLineAnnotation(text); 48 | QString tooltip; 49 | const int lastBracket = text.lastIndexOf('['); 50 | if (lastBracket == -1) { 51 | tooltip = text; 52 | } else { 53 | QStringRef issue = text.midRef(lastBracket + 1, text.size() - lastBracket - 2); 54 | tooltip = text.left(lastBracket) 55 | + "[" + issue + "]"; 56 | } 57 | setToolTip(tooltip); 58 | } 59 | }; 60 | 61 | RubocopHighlighter::RubocopHighlighter() 62 | { 63 | theInstance = this; 64 | QTextCharFormat format; 65 | format.setUnderlineColor(Qt::darkGreen); 66 | format.setUnderlineStyle(QTextCharFormat::WaveUnderline); 67 | m_extraFormats[0] = format; 68 | format.setUnderlineColor(Qt::darkYellow); 69 | m_extraFormats[1] = format; 70 | format.setUnderlineColor(Qt::red); 71 | m_extraFormats[2] = format; 72 | } 73 | 74 | RubocopHighlighter::~RubocopHighlighter() 75 | { 76 | if (!m_rubocop) 77 | return; 78 | m_rubocop->closeWriteChannel(); 79 | m_rubocop->waitForFinished(3000); 80 | delete m_rubocop; 81 | } 82 | 83 | RubocopHighlighter *RubocopHighlighter::instance() 84 | { 85 | return theInstance; 86 | } 87 | 88 | // return false if we are busy, true if everything is ok (or rubocop wasn't found) 89 | bool RubocopHighlighter::run(TextEditor::TextDocument *document, const QString &fileNameTip) 90 | { 91 | if (!m_rubocop) 92 | initRubocopProcess(); 93 | if (m_busy || m_rubocop->state() == QProcess::Starting) 94 | return false; 95 | if (!m_rubocopFound) 96 | return true; 97 | 98 | m_busy = true; 99 | m_startRevision = document->document()->revision(); 100 | 101 | m_timer.start(); 102 | m_document = document; 103 | 104 | const QString filePath = document->filePath().isEmpty() ? fileNameTip 105 | : document->filePath().toString(); 106 | m_rubocop->write(filePath.toUtf8()); 107 | m_rubocop->write("\n"); 108 | QByteArray data = document->plainText().toUtf8(); 109 | m_rubocop->write(data.constData(), data.length() + 1); 110 | return true; 111 | } 112 | 113 | void RubocopHighlighter::initRubocopProcess() 114 | { 115 | if (m_rubocopScript.open()) { 116 | QFile script(":/rubysupport/rubocop.rb"); 117 | script.open(QFile::ReadOnly); 118 | m_rubocopScript.write(script.readAll()); 119 | m_rubocopScript.close(); 120 | } 121 | 122 | m_rubocop = new QProcess; 123 | QObject::connect(m_rubocop, QOverload::of(&QProcess::finished), 124 | [this](int status) { 125 | if (status) { 126 | Core::MessageManager::instance()->write( 127 | QString::fromUtf8(m_rubocop->readAllStandardError().trimmed()), 128 | Core::MessageManager::ModeSwitch); 129 | m_rubocopFound = false; 130 | } 131 | }); 132 | 133 | QObject::connect(m_rubocop, &QProcess::readyReadStandardOutput, [&]() { 134 | m_outputBuffer.append(QString::fromUtf8(m_rubocop->readAllStandardOutput())); 135 | if (m_outputBuffer.endsWith("--\n")) 136 | finishRuboCopHighlight(); 137 | }); 138 | 139 | m_rubocop->start("ruby", {m_rubocopScript.fileName()}, QIODevice::ReadWrite | QIODevice::Text); 140 | } 141 | 142 | void RubocopHighlighter::finishRuboCopHighlight() 143 | { 144 | QTextDocument *doc = m_document ? m_document->document() : nullptr; 145 | if (!doc || m_startRevision != doc->revision()) { 146 | m_busy = false; 147 | return; 148 | } 149 | 150 | Offenses offenses = processRubocopOutput(); 151 | const Utils::FilePath filePath = m_document->filePath(); 152 | for (Diagnostic &diag : m_diagnostics[filePath]) { 153 | diag.textMark = std::make_shared( 154 | filePath, diag.line, diag.severity, diag.message); 155 | m_document->addMark(diag.textMark.get()); 156 | } 157 | RubocopFuture rubocopFuture(offenses); 158 | TextEditor::SemanticHighlighter::incrementalApplyExtraAdditionalFormats(m_document->syntaxHighlighter(), 159 | rubocopFuture.future(), 0, 160 | offenses.count(), m_extraFormats); 161 | TextEditor::SemanticHighlighter::clearExtraAdditionalFormatsUntilEnd(m_document->syntaxHighlighter(), 162 | rubocopFuture.future()); 163 | m_busy = false; 164 | 165 | qCDebug(log) << "rubocop in" << m_timer.elapsed() << "ms," << offenses.count() << "offenses found."; 166 | } 167 | 168 | static int kindOfSeverity(const QStringRef &severity) 169 | { 170 | switch (severity.at(0).toLatin1()) { 171 | case 'C': return 0; // green 172 | case 'W': return 1; // yellow 173 | default: return 2; // red 174 | } 175 | } 176 | 177 | Offenses RubocopHighlighter::processRubocopOutput() 178 | { 179 | Offenses result; 180 | Diagnostics &diag = m_diagnostics[m_document->filePath()] = Diagnostics(); 181 | 182 | const QVector lines = m_outputBuffer.splitRef('\n'); 183 | for (const QStringRef &line : lines) { 184 | if (line == "--") 185 | break; 186 | QVector fields = line.split(':'); 187 | if (fields.size() < 5) 188 | continue; 189 | int kind = kindOfSeverity(fields[0]); 190 | int lineN = fields[1].toInt(); 191 | int column = fields[2].toInt(); 192 | int length = fields[3].toInt(); 193 | result << TextEditor::HighlightingResult(uint(lineN), uint(column), uint(length), kind); 194 | 195 | int messagePos = fields[5].position() + 1; 196 | QStringRef message(line.string(), messagePos, line.position() + line.length() - messagePos); 197 | diag.push_back(Diagnostic{lineN, kind, message.toString(), nullptr}); 198 | } 199 | m_outputBuffer.clear(); 200 | 201 | return result; 202 | } 203 | 204 | Ruby::Range RubocopHighlighter::lineColumnLengthToRange(int line, int column, int length) 205 | { 206 | const QTextBlock block = m_document->document()->findBlockByLineNumber(line - 1); 207 | const int pos = block.position() + column; 208 | return Range(line, pos, length); 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /editor/RubyRubocopHighlighter.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_RubocopHighlighter_h 2 | #define Ruby_RubocopHighlighter_h 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | 18 | QT_FORWARD_DECLARE_CLASS(QProcess) 19 | 20 | namespace TextEditor { class TextDocument; } 21 | 22 | namespace Ruby { 23 | 24 | class TextMark; 25 | 26 | class Range { 27 | public: 28 | int line = 0; 29 | int pos = 0; 30 | int length = 0; 31 | 32 | Range() = default; 33 | Range(int pos, int length) : pos(pos), length(length) { } 34 | Range(int line, int pos, int length) : line(line), pos(pos), length(length) { } 35 | 36 | // Not really equal, since the length attribute is ignored. 37 | bool operator==(const Range &other) const { 38 | const int value = other.pos; 39 | return value >= pos && value < (pos + length); 40 | } 41 | 42 | bool operator<(const Range &other) const { 43 | const int value = other.pos; 44 | return pos < value && (pos + length) < value; 45 | } 46 | }; 47 | 48 | typedef TextEditor::HighlightingResult Offense; 49 | typedef QVector Offenses; 50 | 51 | struct Diagnostic 52 | { 53 | int line; 54 | int severity; 55 | QString message; 56 | std::shared_ptr textMark; 57 | }; 58 | 59 | using Diagnostics = std::vector; 60 | 61 | class RubocopHighlighter : public QObject 62 | { 63 | Q_OBJECT 64 | public: 65 | RubocopHighlighter(); 66 | ~RubocopHighlighter(); 67 | 68 | static RubocopHighlighter *instance(); 69 | 70 | bool run(TextEditor::TextDocument *document, const QString &fileNameTip); 71 | 72 | private: 73 | bool m_rubocopFound = true; 74 | bool m_busy = false; 75 | QProcess *m_rubocop = nullptr; 76 | QTemporaryFile m_rubocopScript; 77 | QString m_outputBuffer; 78 | 79 | int m_startRevision = 0; 80 | QPointer m_document; 81 | QHash m_extraFormats; 82 | 83 | QHash m_diagnostics; 84 | std::vector m_textMarks; 85 | 86 | QElapsedTimer m_timer; 87 | 88 | void initRubocopProcess(); 89 | void finishRuboCopHighlight(); 90 | Offenses processRubocopOutput(); 91 | 92 | Range lineColumnLengthToRange(int line, int column, int length); 93 | }; 94 | } 95 | 96 | #endif 97 | -------------------------------------------------------------------------------- /editor/RubyScanner.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyScanner.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | namespace Ruby { 11 | 12 | static const char *const RUBY_KEYWORDS[] = { 13 | "BEGIN", 14 | "END", 15 | "__ENCODING__", 16 | "__END__", 17 | "__FILE__", 18 | "__LINE__", 19 | "alias", 20 | "and", 21 | "break", 22 | "defined?", 23 | "false", 24 | "in", 25 | "next", 26 | "nil", 27 | "not", 28 | "or", 29 | "raise", 30 | "redo", 31 | "require", 32 | "retry", 33 | "return", 34 | "self", 35 | "super", 36 | "then", 37 | "true", 38 | "undef", 39 | "when", 40 | "yield" 41 | }; 42 | 43 | static const int N_KEYWORDS = std::extent::value; 44 | 45 | QChar translateDelimiter(QChar ch) 46 | { 47 | switch (ch.toLatin1()) { 48 | case '(': return ')'; 49 | case '[': return ']'; 50 | case '{': return '}'; 51 | case '<': return '>'; 52 | case ')': return '('; 53 | case ']': return '['; 54 | case '}': return '{'; 55 | case '>': return '<'; 56 | default: return ch; 57 | } 58 | } 59 | 60 | static bool delimiterHasPair(QChar ch) 61 | { 62 | switch (ch.toLatin1()) { 63 | case '(': 64 | case '[': 65 | case '{': 66 | case '<': 67 | case ')': 68 | case ']': 69 | case '}': 70 | case '>': 71 | return true; 72 | default: 73 | return false; 74 | } 75 | } 76 | 77 | #define SELF_DOT_PATTERN "(16_(2_)?18_(2_)?)?" 78 | #define METHOD_PATTERN "15_2_" SELF_DOT_PATTERN 79 | #define CLASS_MODULE_PATTERN "(19|20)_2_" SELF_DOT_PATTERN 80 | // if ;if a=if 81 | #define FLOWCTL_SHOULD_INC_INDENT "^(2_)?21_|26_(2_)?21_|25_(2_)?21_" 82 | // Version without 21_ at end, used on readIdentifier 83 | #define FLOWCTL_SHOULD_INC_INDENT2 "^(2_)?" "|26_(2_)?" "|25_(2_)?" 84 | #define INDENT_INC "(" CLASS_MODULE_PATTERN "|" METHOD_PATTERN "|" FLOWCTL_SHOULD_INC_INDENT "|22_|23_|28_|30_)" 85 | 86 | static const QRegularExpression m_methodPattern(METHOD_PATTERN "$"); 87 | // METHOD ( & parameter, & 88 | static const QRegularExpression m_parameterPattern( 89 | METHOD_PATTERN "8_(2_)?(3_)?((2_)?(3_)?(2_)?9_(2_)?(17_)?(2_)?(3_)?(2_)?)*$"); 90 | static const QRegularExpression m_contextPattern(CLASS_MODULE_PATTERN "$"); 91 | static const QRegularExpression m_controlFlowShouldIncIndentPattern("(" FLOWCTL_SHOULD_INC_INDENT2 ")$"); 92 | 93 | static bool isLineFeed(QChar ch) 94 | { 95 | return ch == '\n'; 96 | } 97 | 98 | Scanner::Scanner(const QString *text) 99 | : m_src(text) 100 | { 101 | } 102 | 103 | void Scanner::enableContextRecognition() 104 | { 105 | m_hasContextRecognition = true; 106 | } 107 | 108 | void Scanner::setState(int state) 109 | { 110 | m_state = state; 111 | } 112 | 113 | int Scanner::state() const 114 | { 115 | return m_state; 116 | } 117 | 118 | Token Scanner::read() 119 | { 120 | m_src.setAnchor(); 121 | if (m_src.isEnd()) 122 | return Token(Token::EndOfBlock, m_src.anchor(), 0); 123 | 124 | State state; 125 | QChar saved; 126 | parseState(state, saved); 127 | switch (state) { 128 | case State_String: 129 | case State_Regexp: 130 | case State_Symbols: 131 | return readStringLiteral(saved, state); 132 | default: 133 | return onDefaultState(); 134 | } 135 | } 136 | 137 | void Scanner::readLine() 138 | { 139 | Token token; 140 | while ((token = read()).kind != Token::EndOfBlock); 141 | } 142 | 143 | Token Scanner::tokenAt(const QString* line, int position) 144 | { 145 | Scanner scanner(line); 146 | Token token; 147 | while ((token = scanner.read()).kind != Token::EndOfBlock) { 148 | if (position > token.position && (token.position + token.length) >= position) 149 | break; 150 | } 151 | return token; 152 | } 153 | 154 | QString Scanner::contextName() const 155 | { 156 | return m_context.join("::"); 157 | } 158 | 159 | int Scanner::indentVariation() const 160 | { 161 | static const QRegularExpression indent(INDENT_INC); 162 | static const QRegularExpression unindent("24_|29_|31_"); 163 | int delta = 0; 164 | 165 | int offset = -1; 166 | while ((offset = m_tokenSequence.indexOf(indent, offset + 1)) != -1) 167 | delta++; 168 | offset = -1; 169 | while ((offset = m_tokenSequence.indexOf(unindent, offset + 1)) != -1) 170 | delta--; 171 | 172 | return delta; 173 | } 174 | 175 | bool Scanner::didBlockInterrupt() 176 | { 177 | static const QRegularExpression regex("27_"); 178 | return regex.match(m_tokenSequence).hasMatch(); 179 | } 180 | 181 | Token Scanner::onDefaultState() 182 | { 183 | QChar first = m_src.peek(); 184 | m_src.move(); 185 | 186 | // Ignore new lines 187 | bool hasNewLine = false; 188 | while (isLineFeed(first)) { 189 | hasNewLine = true; 190 | m_line++; 191 | m_lineStartOffset = m_src.position(); 192 | first = m_src.peek(); 193 | m_src.setAnchor(); 194 | m_src.move(); 195 | } 196 | if (hasNewLine) 197 | m_tokenSequence.clear(); 198 | 199 | Token token; 200 | 201 | if (first.isDigit()) { 202 | token = readFloatNumber(); 203 | } else if (first == '\'' || first == '\"' || first == '`') { 204 | token = readStringLiteral(first, State_String); 205 | } else if (m_methodPattern.match(m_tokenSequence).hasMatch()) { 206 | token = readMethodDefinition(); 207 | } else if (first == '$' && (m_src.peek() == '`' || m_src.peek() == '\'')) { 208 | m_src.move(); 209 | token = Token(Token::String, m_src.anchor(), m_src.length()); 210 | } else if (first.isLetter() || first == '_' || first == '@' 211 | || first == '$' || (first == ':' && m_src.peek() != ':')) { 212 | token = readIdentifier(); 213 | } else if (first.isDigit()) { 214 | token = readNumber(); 215 | } else if (first == '#') { 216 | token = readComment(); 217 | } else if (first == '/') { 218 | token = readRegexp(); 219 | } else if (first.isSpace()) { 220 | token = readWhiteSpace(); 221 | } else if (first == ',') { 222 | token = Token(Token::OperatorComma, m_src.anchor(), m_src.length()); 223 | } else if (first == '.') { 224 | token = Token(Token::OperatorDot, m_src.anchor(), m_src.length()); 225 | } else if (first == '=' && m_src.peek() != '=') { 226 | token = Token(Token::OperatorAssign, m_src.anchor(), m_src.length()); 227 | } else if (first == ';') { 228 | token = Token(Token::OperatorSemiColon, m_src.anchor(), m_src.length()); 229 | } else if (first == '%') { 230 | token = readPercentageNotation(); 231 | } else if (first == '{') { 232 | token = Token(Token::OpenBraces, m_src.anchor(), m_src.length()); 233 | } else if (first == '}') { 234 | token = Token(Token::CloseBraces, m_src.anchor(), m_src.length()); 235 | } else if (first == '[') { 236 | token = Token(Token::OpenBrackets, m_src.anchor(), m_src.length()); 237 | } else if (first == ']') { 238 | token = Token(Token::CloseBrackets, m_src.anchor(), m_src.length()); 239 | // For historic reasons, ( and ) are the Operator token, this will 240 | // be changed soon. 241 | } else if (first == '(' || first == ')') { 242 | token = Token(Token::Operator, m_src.anchor(), m_src.length()); 243 | } else { 244 | token = readOperator(first); 245 | } 246 | 247 | m_tokenSequence += QString::number(token.kind); 248 | m_tokenSequence += '_'; 249 | 250 | return token; 251 | } 252 | 253 | static Token::Kind tokenKindFor(QChar ch, Scanner::State state) 254 | { 255 | if (state == Scanner::State_Regexp) 256 | return Token::Regexp; 257 | else if (state == Scanner::State_Symbols) 258 | return Token::Symbol; 259 | 260 | switch(ch.toLatin1()) { 261 | case '`': 262 | return Token::Backtick; 263 | case '\'': 264 | case '"': 265 | default: 266 | return Token::String; 267 | } 268 | } 269 | 270 | Token Scanner::readStringLiteral(QChar quoteChar, Scanner::State state) 271 | { 272 | QChar ch = m_src.peek(); 273 | 274 | if (ch == '#' && m_src.peek(1) == '{') { 275 | if (m_src.length()) { 276 | saveState(state, quoteChar); 277 | return Token(tokenKindFor(quoteChar, state), m_src.anchor(), m_src.length()); 278 | } 279 | return readInStringToken(); 280 | } 281 | 282 | if (isLineFeed(ch)) { 283 | m_src.move(); 284 | ch = m_src.peek(); 285 | m_src.setAnchor(); 286 | m_line++; 287 | } 288 | 289 | QChar startQuoteChar = translateDelimiter(quoteChar); 290 | bool quoteCharHasPair = delimiterHasPair(quoteChar); 291 | int bracketCount = 0; 292 | 293 | forever { 294 | ch = m_src.peek(); 295 | if (isLineFeed(ch) || ch.isNull()) { 296 | saveState(state, quoteChar); 297 | break; 298 | } 299 | 300 | if (ch == quoteChar && bracketCount == 0) 301 | break; 302 | 303 | // handles %r{{}} 304 | if (quoteCharHasPair) { 305 | if (ch == startQuoteChar) { 306 | bracketCount++; 307 | m_src.move(); 308 | continue; 309 | } else if (ch == quoteChar) { 310 | bracketCount--; 311 | m_src.move(); 312 | continue; 313 | } 314 | } 315 | 316 | if (ch == '\\') { 317 | m_src.move(); 318 | ch = m_src.peek(); 319 | m_src.move(); 320 | if (isLineFeed(ch) || ch.isNull()) { 321 | saveState(state, quoteChar); 322 | break; 323 | } 324 | } else if (quoteChar != '\'' && ch == '#' && m_src.peek(1) == '{') { 325 | saveState(state, quoteChar); 326 | break; 327 | } else if (isLineFeed(ch) || ch.isNull()) { 328 | saveState(state, quoteChar); 329 | break; 330 | } else { 331 | m_src.move(); 332 | } 333 | } 334 | 335 | if (ch == quoteChar) { 336 | m_src.move(); 337 | if (state == State_Regexp) 338 | consumeRegexpModifiers(); 339 | clearState(); 340 | } 341 | 342 | return Token(tokenKindFor(quoteChar, state), m_src.anchor(), m_src.length()); 343 | } 344 | 345 | Token Scanner::readInStringToken() 346 | { 347 | m_src.move(); 348 | m_src.move(); 349 | QChar ch = m_src.peek(); 350 | while (ch != '}' && !ch.isNull()) { 351 | m_src.move(); 352 | ch = m_src.peek(); 353 | } 354 | m_src.move(); 355 | return Token(Token::InStringCode, m_src.anchor(), m_src.length()); 356 | } 357 | 358 | Token Scanner::readRegexp() 359 | { 360 | const QChar slash = '/'; 361 | const QChar backSlash = '\\'; 362 | QChar ch = m_src.peek(); 363 | while (ch != slash && !ch.isNull()) { 364 | m_src.move(); 365 | ch = m_src.peek(); 366 | if (ch == backSlash) { 367 | m_src.move(); 368 | m_src.move(); 369 | ch = m_src.peek(); 370 | } 371 | } 372 | m_src.move(); 373 | consumeRegexpModifiers(); 374 | 375 | return Token(Token::Regexp, m_src.anchor(), m_src.length()); 376 | } 377 | 378 | void Scanner::consumeRegexpModifiers() 379 | { 380 | QChar ch = m_src.peek(); 381 | while (ch.isLetter() && ch.isLower()) { 382 | m_src.move(); 383 | ch = m_src.peek(); 384 | } 385 | } 386 | 387 | /** 388 | reads identifier and classifies it 389 | */ 390 | Token Scanner::readIdentifier() 391 | { 392 | QChar ch = m_src.peek(); 393 | while (ch.isLetterOrNumber() || ch == '_' || ch == '?' || ch == '!') { 394 | m_src.move(); 395 | ch = m_src.peek(); 396 | } 397 | QStringRef value = m_src.value(); 398 | 399 | Token::Kind kind = Token::Identifier; 400 | if (m_src.peek() == ':' && m_src.peek(1) != ':') { 401 | m_src.move(); 402 | kind = Token::SymbolHashKey; 403 | } else if (value.at(0) == '@') { 404 | kind = Token::ClassField; 405 | } else if (value.length() > 1 && value.at(0) == ':') { 406 | kind = Token::Symbol; 407 | } else if (value.at(0) == '$') { 408 | kind = Token::Global; 409 | } else if (value.at(0).isUpper()) { 410 | kind = Token::Constant; 411 | if (m_hasContextRecognition && m_contextPattern.match(m_tokenSequence).hasMatch()) { 412 | m_context << value.toString(); 413 | m_contextDepths << m_indentDepth; 414 | } 415 | // TODO: Use gperf for this keywords hash 416 | } else if (value == "end") { 417 | kind = Token::KeywordEnd; 418 | m_indentDepth--; 419 | if (!m_contextDepths.empty() && m_indentDepth < m_contextDepths.last()) { 420 | m_context.pop_back(); 421 | m_contextDepths.pop_back(); 422 | } 423 | } else if (value == "self") { 424 | kind = Token::KeywordSelf; 425 | } else if (value == "def") { 426 | kind = Token::KeywordDef; 427 | m_indentDepth++; 428 | } else if (value == "module") { 429 | kind = Token::KeywordModule; 430 | m_indentDepth++; 431 | } else if (value == "class") { 432 | kind = Token::KeywordClass; 433 | m_indentDepth++; 434 | } else if (value == "if" || value == "unless") { 435 | kind = Token::KeywordFlowControl; 436 | if (m_controlFlowShouldIncIndentPattern.match(m_tokenSequence).hasMatch()) 437 | m_indentDepth++; 438 | } else if (value == "while" || value == "until") { 439 | kind = Token::KeywordLoop; 440 | m_indentDepth++; 441 | } else if (value == "do" || value == "begin" || value == "case") { 442 | kind = Token::KeywordBlockStarter; 443 | m_indentDepth++; 444 | } else if (value == "else" 445 | || value == "elsif" 446 | || value == "ensure" 447 | || value == "rescue") { 448 | kind = Token::KeywordElseElsIfRescueEnsure; 449 | } else if (value == "protected" || value == "private" || value == "public") { 450 | kind = Token::KeywordVisibility; 451 | } else if (std::find(&RUBY_KEYWORDS[0], &RUBY_KEYWORDS[N_KEYWORDS], value.toUtf8()) != &RUBY_KEYWORDS[N_KEYWORDS]) { 452 | kind = Token::Keyword; 453 | } else if (m_methodPattern.match(m_tokenSequence).hasMatch()) { 454 | QChar ch = m_src.peek(); 455 | while (!ch.isNull() && !ch.isSpace() && ch != '(' && ch != '#') { 456 | m_src.move(); 457 | ch = m_src.peek(); 458 | } 459 | kind = Token::Method; 460 | } else if (m_parameterPattern.match(m_tokenSequence).hasMatch()) { 461 | kind = Token::Parameter; 462 | } 463 | 464 | return Token(kind, m_src.anchor(), m_src.length()); 465 | } 466 | 467 | inline static bool isHexDigit(QChar ch) 468 | { 469 | return (ch.isDigit() 470 | || (ch >= 'a' && ch <= 'f') 471 | || (ch >= 'A' && ch <= 'F')); 472 | } 473 | 474 | inline static bool isOctalDigit(QChar ch) 475 | { 476 | return (ch.isDigit() && ch != '8' && ch != '9'); 477 | } 478 | 479 | inline static bool isBinaryDigit(QChar ch) 480 | { 481 | return (ch == '0' || ch == '1'); 482 | } 483 | 484 | inline static bool isValidIntegerSuffix(QChar ch) 485 | { 486 | return (ch == 'l' || ch == 'L'); 487 | } 488 | 489 | Token Scanner::readNumber() 490 | { 491 | if (!m_src.isEnd()) { 492 | QChar ch = m_src.peek(); 493 | if (ch.toLower() == 'b') { 494 | m_src.move(); 495 | while (isBinaryDigit(m_src.peek())) 496 | m_src.move(); 497 | } else if (ch.toLower() == 'o') { 498 | m_src.move(); 499 | while (isOctalDigit(m_src.peek())) 500 | m_src.move(); 501 | } else if (ch.toLower() == 'x') { 502 | m_src.move(); 503 | while (isHexDigit(m_src.peek())) 504 | m_src.move(); 505 | } else { // either integer or float number 506 | return readFloatNumber(); 507 | } 508 | if (isValidIntegerSuffix(m_src.peek())) 509 | m_src.move(); 510 | } 511 | return Token(Token::Number, m_src.anchor(), m_src.length()); 512 | } 513 | 514 | Token Scanner::readFloatNumber() 515 | { 516 | enum 517 | { 518 | State_INTEGER, 519 | State_FRACTION, 520 | State_EXPONENT 521 | } state; 522 | state = State_INTEGER; 523 | bool hadFraction = false; 524 | 525 | for (;;) { 526 | QChar ch = m_src.peek(); 527 | if (ch.isNull()) 528 | break; 529 | 530 | if (state == State_INTEGER) { 531 | if (ch == '.' && m_src.peek(1).isDigit() && !hadFraction) { 532 | m_src.move(); 533 | hadFraction = true; 534 | state = State_FRACTION; 535 | } else if (!ch.isDigit()) { 536 | break; 537 | } 538 | } else if (state == State_FRACTION) { 539 | if (ch == 'e' || ch == 'E') { 540 | QChar next = m_src.peek(1); 541 | QChar next2 = m_src.peek(2); 542 | bool isExp = next.isDigit() 543 | || (((next == '-') || (next == '+')) && next2.isDigit()); 544 | if (isExp) { 545 | m_src.move(); 546 | state = State_EXPONENT; 547 | } else { 548 | break; 549 | } 550 | } else if (!ch.isDigit()) { 551 | break; 552 | } 553 | } else if (!ch.isDigit()) { 554 | break; 555 | } 556 | m_src.move(); 557 | } 558 | 559 | return Token(Token::Number, m_src.anchor(), m_src.length()); 560 | } 561 | 562 | /** 563 | reads single-line ruby comment, started with "#" 564 | */ 565 | Token Scanner::readComment() 566 | { 567 | QChar ch = m_src.peek(); 568 | while (!isLineFeed(ch) && !ch.isNull()) { 569 | m_src.move(); 570 | ch = m_src.peek(); 571 | } 572 | return Token(Token::Comment, m_src.anchor(), m_src.length()); 573 | } 574 | 575 | /** 576 | reads whitespace 577 | */ 578 | Token Scanner::readWhiteSpace() 579 | { 580 | QChar chr = m_src.peek(); 581 | while (chr.isSpace() && !isLineFeed(chr)) { 582 | m_src.move(); 583 | chr = m_src.peek(); 584 | } 585 | return Token(Token::Whitespace, m_src.anchor(), m_src.length()); 586 | } 587 | 588 | /** 589 | reads punctuation symbols, excluding some special 590 | */ 591 | Token Scanner::readOperator(QChar first) 592 | { 593 | static const QString operators = "<=>+-/*%!"; 594 | static const QString colon = ":"; 595 | const QString &acceptedChars = first == ':' ? colon : operators; 596 | QChar ch = m_src.peek(); 597 | 598 | while (acceptedChars.contains(ch)) { 599 | m_src.move(); 600 | ch = m_src.peek(); 601 | } 602 | return Token(Token::Operator, m_src.anchor(), m_src.length()); 603 | } 604 | 605 | Token Scanner::readPercentageNotation() 606 | { 607 | QChar ch = m_src.peek(); 608 | if (ch.isSpace() || ch.isDigit()) 609 | return Token(Token::Operator, m_src.anchor(), m_src.length()); 610 | 611 | State state = State_String; 612 | if (ch.isLetter()) { 613 | if (ch == 'r') 614 | state = State_Regexp; 615 | if (ch == 'i') 616 | state = State_Symbols; 617 | m_src.move(); 618 | } 619 | QChar delimiter = translateDelimiter(m_src.peek()); 620 | m_src.move(); 621 | return readStringLiteral(delimiter, state); 622 | } 623 | 624 | Token Scanner::readMethodDefinition() 625 | { 626 | consumeUntil(".(#", "!?"); 627 | QStringRef value = m_src.value(); 628 | if (value == "self") 629 | return Token(Token::KeywordSelf, m_src.anchor(), m_src.length()); 630 | 631 | if (!std::strchr("!?", m_src.peek(-1).toLatin1())) 632 | consumeUntil("(#", "!?"); 633 | return Token(Token::Method, m_src.anchor(), m_src.length()); 634 | } 635 | 636 | void Scanner::consumeUntil(const char* stopAt, const char* stopAfter) 637 | { 638 | QChar ch = m_src.peek(); 639 | while (!ch.isNull() && !ch.isSpace() && !std::strchr(stopAt, ch.toLatin1())) { 640 | m_src.move(); 641 | ch = m_src.peek(); 642 | if (stopAfter && !ch.isNull() && std::strchr(stopAfter, ch.toLatin1())) { 643 | m_src.move(); 644 | break; 645 | } 646 | } 647 | } 648 | 649 | void Scanner::clearState() 650 | { 651 | m_state = 0; 652 | } 653 | 654 | void Scanner::saveState(State state, QChar savedData) 655 | { 656 | m_state = (state << 16) | static_cast(savedData.unicode()); 657 | } 658 | 659 | void Scanner::parseState(State &state, QChar &savedData) const 660 | { 661 | state = static_cast((m_state >> 16) & 0xf); 662 | savedData = m_state & 0xffff; 663 | } 664 | } 665 | -------------------------------------------------------------------------------- /editor/RubyScanner.h: -------------------------------------------------------------------------------- 1 | #ifndef RubyScanner_h 2 | #define RubyScanner_h 3 | 4 | #include "SourceCodeStream.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace Ruby { 11 | 12 | QChar translateDelimiter(QChar ch); 13 | 14 | class Token 15 | { 16 | public: 17 | enum Kind 18 | { 19 | // Don't change these numbers until I find a better way to keep track of then in 20 | // the regexes used against m_tokenSequence 21 | Number = 0, 22 | String = 1, 23 | Whitespace = 2, 24 | Operator = 3, 25 | Comment = 4, 26 | Identifier = 5, 27 | Regexp = 6, 28 | Symbol = 7, 29 | Method = 8, 30 | Parameter = 9, 31 | ClassField = 11, 32 | Constant = 12, 33 | Global = 13, 34 | Keyword = 14, 35 | KeywordDef = 15, 36 | KeywordSelf = 16, 37 | OperatorComma = 17, 38 | OperatorDot = 18, 39 | KeywordClass = 19, 40 | KeywordModule = 20, 41 | KeywordFlowControl = 21, 42 | KeywordLoop = 22, 43 | KeywordBlockStarter = 23, 44 | KeywordEnd = 24, 45 | OperatorAssign = 25, 46 | OperatorSemiColon = 26, 47 | KeywordElseElsIfRescueEnsure = 27, 48 | // If you change this order, see isParenthesisLike() method. 49 | OpenBraces = 28, 50 | CloseBraces = 29, 51 | OpenBrackets = 30, 52 | CloseBrackets = 31, 53 | 54 | Backtick, 55 | InStringCode, 56 | KeywordVisibility, 57 | SymbolHashKey, 58 | EndOfBlock 59 | }; 60 | 61 | Token(Kind _kind = EndOfBlock, int _position = 0, int _length = 0) : 62 | kind(_kind), 63 | position(_position), 64 | length(_length) 65 | {} 66 | 67 | Kind kind; 68 | int position; 69 | int length; 70 | 71 | bool isParenthesisLike() const 72 | { 73 | return kind == Operator || (kind >= OpenBraces && kind <= CloseBrackets); 74 | } 75 | }; 76 | 77 | class Scanner 78 | { 79 | Q_DISABLE_COPY(Scanner) 80 | 81 | public: 82 | enum State { 83 | State_Default, 84 | State_String, 85 | State_Regexp, 86 | State_Symbols 87 | }; 88 | 89 | Scanner(const QString *text); 90 | void enableContextRecognition(); 91 | 92 | void setState(int state); 93 | int state() const; 94 | Token read(); 95 | void readLine(); 96 | 97 | static Token tokenAt(const QString* line, int position); 98 | 99 | QString contextName() const; 100 | int currentLine() const { return m_line; } 101 | int currentColumn(const Token &token) const { return token.position - m_lineStartOffset; } 102 | 103 | int indentVariation() const; 104 | // current line has a else, elsif, rescue or ensure. 105 | bool didBlockInterrupt(); 106 | private: 107 | Token onDefaultState(); 108 | 109 | Token readStringLiteral(QChar quoteChar, State stateRestored); 110 | Token readInStringToken(); 111 | Token readRegexp(); 112 | Token readIdentifier(); 113 | Token readNumber(); 114 | Token readFloatNumber(); 115 | Token readComment(); 116 | Token readDoxygenComment(); 117 | Token readWhiteSpace(); 118 | Token readOperator(QChar first); 119 | Token readPercentageNotation(); 120 | Token readMethodDefinition(); 121 | 122 | void consumeUntil(const char* stopAt, const char* stopAfter = nullptr); 123 | void consumeRegexpModifiers(); 124 | 125 | void clearState(); 126 | void saveState(State state, QChar savedData); 127 | void parseState(State &state, QChar &savedData) const; 128 | 129 | SourceCodeStream m_src; 130 | int m_state = 0; 131 | bool m_hasContextRecognition = false; 132 | 133 | QString m_tokenSequence; 134 | 135 | QStringList m_context; 136 | int m_line = 1; 137 | int m_lineStartOffset = 0; 138 | 139 | QList m_contextDepths; 140 | int m_indentDepth = 0; 141 | }; 142 | 143 | } 144 | 145 | #endif 146 | -------------------------------------------------------------------------------- /editor/RubySymbol.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_Symbol_h 2 | #define Ruby_Symbol_h 3 | 4 | #include 5 | #include 6 | 7 | namespace Ruby { 8 | 9 | struct Symbol 10 | { 11 | Symbol(const QString *file = nullptr) : file(file) { } 12 | const QString *file; 13 | QString name; 14 | QString context; 15 | int line; 16 | int column; 17 | }; 18 | 19 | } 20 | 21 | Q_DECLARE_METATYPE(Ruby::Symbol) 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /editor/RubySymbolFilter.cpp: -------------------------------------------------------------------------------- 1 | #include "RubySymbolFilter.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace Ruby { 8 | 9 | SymbolFilter::SymbolFilter(SymbolProvider provider, const char *description, QChar shortcut) 10 | : m_icon(":/codemodel/images/func.png") 11 | , m_symbolProvider(provider) 12 | { 13 | setId(description); 14 | setDisplayName(tr(description)); 15 | setShortcutString(shortcut); 16 | setIncludedByDefault(true); 17 | 18 | connect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged, 19 | this, &SymbolFilter::onCurrentEditorChanged); 20 | } 21 | 22 | QList SymbolFilter::matchesFor(QFutureInterface &, const QString &entry) 23 | { 24 | QList list; 25 | QStringMatcher matcher(entry, Qt::CaseInsensitive); 26 | 27 | const auto symbols = m_symbolProvider(m_fileName.toString()); 28 | for (const Symbol &symbol : symbols) { 29 | if (matcher.indexIn(symbol.name) != -1) { 30 | list << Core::LocatorFilterEntry(this, symbol.name, qVariantFromValue(symbol), m_icon); 31 | list.last().extraInfo = symbol.context; 32 | } 33 | } 34 | return list; 35 | } 36 | 37 | void SymbolFilter::accept(Core::LocatorFilterEntry selection, 38 | QString *newText, int *selectionStart, int *selectionLength) const 39 | { 40 | Q_UNUSED(newText) 41 | Q_UNUSED(selectionStart) 42 | Q_UNUSED(selectionLength) 43 | Symbol symbol = selection.internalData.value(); 44 | Core::EditorManager::openEditorAt(*symbol.file, symbol.line, symbol.column); 45 | } 46 | 47 | void SymbolFilter::refresh(QFutureInterface &) 48 | { 49 | } 50 | 51 | void SymbolFilter::onCurrentEditorChanged(Core::IEditor *editor) 52 | { 53 | if (editor) 54 | m_fileName = editor->document()->filePath(); 55 | else 56 | m_fileName.clear(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /editor/RubySymbolFilter.h: -------------------------------------------------------------------------------- 1 | #ifndef RubySymbolFilter_h 2 | #define RubySymbolFilter_h 3 | 4 | #include "RubySymbol.h" 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | 12 | namespace Core { class IEditor; } 13 | 14 | namespace Ruby { 15 | 16 | typedef std::function(const QString &)> SymbolProvider; 17 | 18 | class SymbolFilter : public Core::ILocatorFilter 19 | { 20 | Q_OBJECT 21 | public: 22 | SymbolFilter(SymbolProvider provider, const char *description, QChar shortcut); 23 | 24 | QList matchesFor(QFutureInterface &future, const QString &entry) override; 25 | void accept(Core::LocatorFilterEntry selection, 26 | QString *newText, int *selectionStart, int *selectionLength) const override; 27 | void refresh(QFutureInterface &future) override; 28 | 29 | private: 30 | void onCurrentEditorChanged(Core::IEditor *editor); 31 | 32 | private: 33 | QIcon m_icon; 34 | Utils::FilePath m_fileName; 35 | SymbolProvider m_symbolProvider; 36 | }; 37 | 38 | } 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /editor/ScannerTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../RubyPlugin.h" 2 | #include "../editor/RubyScanner.h" 3 | 4 | #include 5 | #include 6 | 7 | typedef QVector Tokens; 8 | 9 | struct TestData 10 | { 11 | TestData() = default; 12 | TestData(const QByteArray &input, const Tokens &tokens) : 13 | input(input), tokens(tokens) 14 | {} 15 | 16 | const QByteArray input; 17 | const Tokens tokens; 18 | 19 | }; 20 | 21 | Q_DECLARE_METATYPE(TestData); 22 | 23 | namespace Ruby { 24 | 25 | static Scanner *m_scanner = nullptr; 26 | 27 | #define CASE_STR(x) case Token::x: str = #x; break 28 | QDebug &operator<<(QDebug &s, Token::Kind t) 29 | { 30 | const char *str = ""; 31 | switch (t) { 32 | CASE_STR(Number); 33 | CASE_STR(String); 34 | CASE_STR(Whitespace); 35 | CASE_STR(Operator); 36 | CASE_STR(Comment); 37 | CASE_STR(Identifier); 38 | CASE_STR(Regexp); 39 | CASE_STR(Symbol); 40 | CASE_STR(SymbolHashKey); 41 | CASE_STR(Method); 42 | CASE_STR(Parameter); 43 | CASE_STR(ClassField); 44 | CASE_STR(Constant); 45 | CASE_STR(Global); 46 | CASE_STR(Keyword); 47 | CASE_STR(KeywordDef); 48 | CASE_STR(KeywordSelf); 49 | CASE_STR(OperatorComma); 50 | CASE_STR(OperatorDot); 51 | CASE_STR(KeywordClass); 52 | CASE_STR(KeywordModule); 53 | CASE_STR(KeywordFlowControl); 54 | CASE_STR(KeywordLoop); 55 | CASE_STR(KeywordBlockStarter); 56 | CASE_STR(KeywordEnd); 57 | CASE_STR(OperatorAssign); 58 | CASE_STR(OperatorSemiColon); 59 | CASE_STR(KeywordElseElsIfRescueEnsure); 60 | CASE_STR(OpenBraces); 61 | CASE_STR(CloseBraces); 62 | CASE_STR(OpenBrackets); 63 | CASE_STR(CloseBrackets); 64 | 65 | CASE_STR(Backtick); 66 | CASE_STR(InStringCode); 67 | CASE_STR(KeywordVisibility); 68 | case Token::EndOfBlock: str = "EOB"; break; 69 | // No default. Let the compiler warn when new values are added. 70 | } 71 | return s << str; 72 | } 73 | 74 | static Tokens tokenize(const QByteArray &code, bool debug = false) 75 | { 76 | if (m_scanner) 77 | delete m_scanner; 78 | QString strCode = QString::fromUtf8(code); 79 | m_scanner = new Scanner(&strCode); 80 | m_scanner->enableContextRecognition(); 81 | 82 | QVector tokens; 83 | Token token; 84 | while ((token = m_scanner->read()).kind != Token::EndOfBlock) { 85 | tokens << token.kind; 86 | if (debug) 87 | qDebug() << "> " << token.kind << code.mid(token.position, token.length); 88 | } 89 | return tokens; 90 | } 91 | 92 | void Plugin::cleanupTestCase() 93 | { 94 | delete m_scanner; 95 | m_scanner = nullptr; 96 | } 97 | 98 | void Plugin::test_context() 99 | { 100 | Tokens expectedTokens = { Token::KeywordClass, Token::Whitespace, Token::Constant }; 101 | QCOMPARE(tokenize("class Foo"), expectedTokens); 102 | QCOMPARE(m_scanner->contextName(), QStringLiteral("Foo")); 103 | 104 | expectedTokens = { Token::KeywordModule, Token::Whitespace, Token::Constant }; 105 | QCOMPARE(tokenize("module Bar"), expectedTokens); 106 | QCOMPARE(m_scanner->contextName(), QStringLiteral("Bar")); 107 | 108 | tokenize("module Foo\n" 109 | " class Bar"); 110 | QCOMPARE(m_scanner->contextName(), QStringLiteral("Foo::Bar")); 111 | 112 | tokenize("module FooBar\n" 113 | " class Klass\n" 114 | "end"); 115 | QCOMPARE(m_scanner->contextName(), QStringLiteral("FooBar")); 116 | 117 | tokenize("module Foo\n" 118 | " class Klass\n" 119 | " def foo\n" 120 | " a = 2 if b" 121 | " end" 122 | " end"); 123 | QCOMPARE(m_scanner->contextName(), QStringLiteral("Foo")); 124 | } 125 | 126 | void Plugin::test_indentIf() 127 | { 128 | tokenize("if foo;end"); 129 | QCOMPARE(m_scanner->indentVariation(), 0); 130 | tokenize("if foo"); 131 | QCOMPARE(m_scanner->indentVariation(), 1); 132 | tokenize("a = 2 if foo"); 133 | QCOMPARE(m_scanner->indentVariation(), 0); 134 | tokenize("a = 2;if foo"); 135 | QCOMPARE(m_scanner->indentVariation(), 1); 136 | // ugliest code style ever 137 | tokenize("a = if foo"); 138 | QCOMPARE(m_scanner->indentVariation(), 1); 139 | 140 | // Weird code can show folding mark, but I don't care about show weird code 141 | // like "if foo; bar; end; if bleh" this will not be folded or indented correctly 142 | } 143 | 144 | void Plugin::test_indentBraces() 145 | { 146 | tokenize("foo.bar {|x|"); 147 | QCOMPARE(m_scanner->indentVariation(), 1); 148 | 149 | tokenize("foo.bar {|x| }"); 150 | QCOMPARE(m_scanner->indentVariation(), 0); 151 | 152 | tokenize("def foo a = {}"); 153 | QCOMPARE(m_scanner->indentVariation(), 1); 154 | } 155 | 156 | void Plugin::test_lineCount() 157 | { 158 | tokenize("\nif foo\n\nend"); 159 | QCOMPARE(m_scanner->currentLine(), 4); 160 | } 161 | 162 | void Plugin::test_ifs() 163 | { 164 | tokenize("class Foo\n" 165 | " def method\n" 166 | " something do\n" 167 | " if foo\n" 168 | " otherthing\n" 169 | " end\n" 170 | " end\n" 171 | " end"); 172 | QCOMPARE(m_scanner->contextName(), QStringLiteral("Foo")); 173 | } 174 | 175 | void Plugin::test_scanner() 176 | { 177 | QFETCH(TestData, td); 178 | QCOMPARE(tokenize(td.input), td.tokens); 179 | } 180 | 181 | void Plugin::test_scanner_data() 182 | { 183 | QTest::addColumn("td"); 184 | 185 | QTest::newRow("symbol on array") 186 | << TestData("foo[:bar]", { 187 | Token::Identifier, 188 | Token::OpenBrackets, Token::Symbol, Token::CloseBrackets }); 189 | QTest::newRow("method def - param after comma") 190 | << TestData("def foo bar, tender", { 191 | Token::KeywordDef, Token::Whitespace, Token::Method, Token::Whitespace, 192 | Token::Parameter, Token::OperatorComma, Token::Whitespace, Token::Parameter }); 193 | QTest::newRow("method def - param after WS and comma") 194 | << TestData("def foo bar , tender", { 195 | Token::KeywordDef, Token::Whitespace, Token::Method, Token::Whitespace, 196 | Token::Parameter, Token::Whitespace, Token::OperatorComma, 197 | Token::Whitespace, Token::Parameter }); 198 | QTest::newRow("method def - self with param") 199 | << TestData("def self.foo bar", { 200 | Token::KeywordDef, Token::Whitespace, Token::KeywordSelf, 201 | Token::OperatorDot, Token::Method, Token::Whitespace, Token::Parameter }); 202 | QTest::newRow("method def - parentheses") 203 | << TestData("def foo(bar)", { 204 | Token::KeywordDef, Token::Whitespace, Token::Method, Token::Operator, 205 | Token::Parameter, Token::Operator }); 206 | QTest::newRow("method def - param and block") 207 | << TestData("def foo(bar, &tender)", { 208 | Token::KeywordDef, Token::Whitespace, Token::Method, Token::Operator, 209 | Token::Parameter, Token::OperatorComma, Token::Whitespace, 210 | Token::Operator, Token::Parameter, Token::Operator }); 211 | QTest::newRow("method def - block and param, no parens") 212 | << TestData("def foo &bar, tender", { 213 | Token::KeywordDef, Token::Whitespace, Token::Method, Token::Whitespace, 214 | Token::Operator, Token::Parameter, Token::OperatorComma, 215 | Token::Whitespace, Token::Parameter }); 216 | 217 | const Tokens simpleFunc{ Token::KeywordDef, Token::Whitespace, Token::Method }; 218 | QTest::newRow("method excl") << TestData("def foo!", simpleFunc); 219 | QTest::newRow("method question") << TestData("def foo?", simpleFunc); 220 | QTest::newRow("method equals") << TestData("def foo=", simpleFunc); 221 | QTest::newRow("method spaceship") << TestData("def <=>", simpleFunc); 222 | QTest::newRow("method plus-equals") << TestData("def +=", simpleFunc); 223 | QTest::newRow("method with comment") 224 | << TestData("def foo# comment", { 225 | Token::KeywordDef, Token::Whitespace, Token::Method, Token::Comment }); 226 | QTest::newRow("method with param no paren") 227 | << TestData("def foo oi", { 228 | Token::KeywordDef, Token::Whitespace, Token::Method, 229 | Token::Whitespace, Token::Parameter }); 230 | QTest::newRow("method with param with paren") 231 | << TestData("def foo(oi)", { 232 | Token::KeywordDef, Token::Whitespace, Token::Method, Token::Operator, 233 | Token::Parameter, Token::Operator }); 234 | QTest::newRow("method excl with param") 235 | << TestData("def foo!bar", { Token::KeywordDef, Token::Whitespace, Token::Method, Token::Parameter }); 236 | QTest::newRow("namespace is not a symbol") 237 | << TestData("Foo::Bar oi", { 238 | Token::Constant, Token::Operator, Token::Constant, 239 | Token::Whitespace, Token::Identifier }); 240 | QTest::newRow("Backtick") << TestData("`Nice \"backtick\" son`", { Token::Backtick }); 241 | QTest::newRow("Newline in Backtick") 242 | << TestData("`Nice \"backt \\\nick\" son`", { Token::Backtick, Token::Backtick }); 243 | QTest::newRow("Escpae in string") << TestData("\"Nice \\\"escape!\"", { Token::String }); 244 | QTest::newRow("Single-quote string") << TestData("'Nice #{Hello}'", { Token::String }); 245 | QTest::newRow("In string code") 246 | << TestData("\"#{foo}bar\"", { Token::String, Token::InStringCode, Token::String }); 247 | QTest::newRow("In string code in backtick") 248 | << TestData("`Nice #{Hello}`", { Token::Backtick, Token::InStringCode, Token::Backtick }); 249 | QTest::newRow("Percentage") << TestData("%(Hello)", { Token::String }); 250 | QTest::newRow("%w") << TestData("%w/Hello asas/", { Token::String }); 251 | QTest::newRow("Percentage operator") << TestData("%2", { Token::Operator, Token::Number }); 252 | QTest::newRow("%w with function call") 253 | << TestData("%w(a b).length", { Token::String, Token::OperatorDot, Token::Identifier }); 254 | QTest::newRow("Regexp") << TestData("%r{foo/bar}", { Token::Regexp }); 255 | QTest::newRow("Regexp + newline") << TestData("%r{foo\n/bar}x", { Token::Regexp, Token::Regexp }); 256 | QTest::newRow("Brackets 1") << TestData("%r{{}}", { Token::Regexp }); 257 | QTest::newRow("Brackets 2") << TestData("%r<<>>", { Token::Regexp }); 258 | QTest::newRow("Brackets 3") << TestData("%q[[]]", { Token::String }); 259 | QTest::newRow("Brackets 4") << TestData("%w(())", { Token::String }); 260 | QTest::newRow("Brackets 5") << TestData("%q!\\!!", { Token::String }); 261 | QTest::newRow("keyword symbols 1") 262 | << TestData("if: :foo", { Token::SymbolHashKey, Token::Whitespace, Token::Symbol }); 263 | QTest::newRow("keyword symbols 2") 264 | << TestData("a= :if", { Token::Identifier, Token::OperatorAssign, Token::Whitespace, Token::Symbol }); 265 | } 266 | 267 | } // namespace Ruby 268 | -------------------------------------------------------------------------------- /editor/SourceCodeStream.h: -------------------------------------------------------------------------------- 1 | #ifndef SourceCodeStream_h 2 | #define SourceCodeStream_h 3 | 4 | #include 5 | 6 | namespace Ruby { 7 | 8 | class SourceCodeStream 9 | { 10 | public: 11 | SourceCodeStream(const QString *text) 12 | : m_text(text) 13 | , m_textPtr(m_text->data()) 14 | , m_textLength(text->length()) 15 | {} 16 | 17 | inline void setAnchor() 18 | { 19 | m_markedPosition = m_position; 20 | } 21 | 22 | inline void move() 23 | { 24 | ++m_position; 25 | } 26 | 27 | int position() const 28 | { 29 | return m_position; 30 | } 31 | 32 | inline int length() const 33 | { 34 | return m_position - m_markedPosition; 35 | } 36 | 37 | inline int anchor() const 38 | { 39 | return m_markedPosition; 40 | } 41 | 42 | inline bool isEnd() const 43 | { 44 | return m_position >= m_textLength; 45 | } 46 | 47 | inline QChar peek(int offset = 0) const 48 | { 49 | int pos = m_position + offset; 50 | if (pos >= m_textLength) 51 | return QChar(); 52 | return m_textPtr[pos]; 53 | } 54 | 55 | inline QStringRef value() const 56 | { 57 | return QStringRef(m_text, m_markedPosition, length()); 58 | } 59 | 60 | inline QStringRef value(int begin, int length) const 61 | { 62 | return QStringRef(m_text, begin, length); 63 | } 64 | 65 | private: 66 | const QString *m_text; 67 | const QChar *m_textPtr; 68 | const int m_textLength; 69 | int m_position = 0; 70 | int m_markedPosition = 0; 71 | }; 72 | 73 | } 74 | 75 | #endif 76 | -------------------------------------------------------------------------------- /editor/rubocop.rb: -------------------------------------------------------------------------------- 1 | # wraper for rubocop 2 | begin 3 | require 'rubocop' 4 | 5 | $rubocop36 = Gem::Version.new(RuboCop::Version.version) >= Gem::Version.new('0.36.0') 6 | rescue LoadError 7 | abort 'Rubocop not installed or not found, if you use rvm, check if rubocop is installed on your system ruby.' 8 | end 9 | 10 | module RuboCop 11 | class Runner 12 | if $rubocop36 13 | def _inspect_code(code, path) 14 | @formatter_set ||= Formatter::FormatterSet.new 15 | # This would be configurable in the project settings someday 16 | inspect_file(RuboCop::ProcessedSource.new(code, RUBY_VERSION.to_f, path))[0] 17 | end 18 | else 19 | def _inspect_code(code, path) 20 | @formatter_set ||= Formatter::FormatterSet.new 21 | inspect_file(RuboCop::ProcessedSource.new(code, path))[0] 22 | end 23 | end 24 | end 25 | end 26 | 27 | class RouboCop 28 | def initialize 29 | config = RuboCop::ConfigStore.new 30 | options, _ = RuboCop::Options.new.parse([]) 31 | @runner = RuboCop::Runner.new(options, config) 32 | end 33 | 34 | def parse(data) 35 | idx = data.index("\n") 36 | path = data[0, idx] 37 | file = data[(idx + 1)..-2] 38 | file.gsub!("\n", "\r\n") if RUBY_PLATFORM.include?('mingw') 39 | offenses = @runner._inspect_code(file, path) 40 | 41 | offenses.sort! { |a, b| a.location.line <=> b.location.line } 42 | offenses.each do |offense| 43 | location = offense.location 44 | puts "#{offense.severity.code}:#{location.line}:#{location.column + 1}:#{location.length}:#{offense.message.tr("\n", ' ')} [#{offense.cop_name}]" 45 | end 46 | rescue Errno::ENOENT 47 | $stderr.puts $!.message 48 | ensure 49 | puts '--' 50 | $stdout.flush 51 | end 52 | end 53 | 54 | def main 55 | roubocop = RouboCop.new 56 | until $stdin.eof? 57 | data = $stdin.gets("\0") 58 | roubocop.parse(data) 59 | end 60 | end 61 | 62 | main 63 | -------------------------------------------------------------------------------- /projectmanager/RubyProject.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyProject.h" 2 | 3 | #include "../editor/RubyCodeModel.h" 4 | #include "../RubyConstants.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace Ruby { 25 | 26 | class BuildSystem : public ProjectExplorer::BuildSystem 27 | { 28 | public: 29 | explicit BuildSystem(ProjectExplorer::Target *target); 30 | ~BuildSystem(); 31 | 32 | void triggerParsing() final; 33 | 34 | bool supportsAction(ProjectExplorer::Node *context, 35 | ProjectExplorer::ProjectAction action, 36 | const ProjectExplorer::Node *node) const override; 37 | 38 | bool canRenameFile(ProjectExplorer::Node *, const QString &, const QString &) override { return true; } 39 | 40 | ProjectExplorer::RemovedFilesFromProject removeFiles(ProjectExplorer::Node *, 41 | const QStringList &, 42 | QStringList*) override 43 | { return ProjectExplorer::RemovedFilesFromProject::Ok; } 44 | bool deleteFiles(ProjectExplorer::Node *, const QStringList &) override { return true; } 45 | bool renameFile(ProjectExplorer::Node *, const QString &, const QString &) override { return true; } 46 | 47 | private: 48 | QStringList m_rawFileList; 49 | QHash m_rawListEntries; 50 | QSet m_files; 51 | QTimer m_projectScanTimer; 52 | QStringList m_ignoredDirectories; 53 | QElapsedTimer m_lastProjectScan; 54 | QFileSystemWatcher m_fsWatcher; 55 | QFuture m_projectScanFuture; 56 | 57 | void scheduleProjectScan(); 58 | void populateProject(); 59 | void recursiveScanDirectory(const QDir &dir); 60 | bool shouldIgnoreDir(const QString &filePath) const; 61 | void readProjectSettings(const Utils::FilePath &filePath); 62 | }; 63 | 64 | class ProjectNode : public ProjectExplorer::ProjectNode 65 | { 66 | public: 67 | ProjectNode(const Utils::FilePath &path) 68 | : ProjectExplorer::ProjectNode(path) 69 | { 70 | setDisplayName(path.toFileInfo().completeBaseName()); 71 | setAddFileFilter("*.rb"); 72 | } 73 | }; 74 | 75 | /** 76 | * @brief Provides displayName relative to project node 77 | */ 78 | class FileNode : public ProjectExplorer::FileNode 79 | { 80 | public: 81 | FileNode(const Utils::FilePath &filePath) 82 | : ProjectExplorer::FileNode(filePath, ProjectExplorer::FileType::Source) 83 | , m_displayName(filePath.fileName()) 84 | { 85 | } 86 | 87 | QString displayName() const override { return m_displayName; } 88 | private: 89 | QString m_displayName; 90 | }; 91 | 92 | BuildSystem::BuildSystem(ProjectExplorer::Target *target) 93 | : ProjectExplorer::BuildSystem(target) 94 | { 95 | m_projectScanTimer.setSingleShot(true); 96 | connect(&m_projectScanTimer, &QTimer::timeout, this, &BuildSystem::triggerParsing); 97 | connect(target->project(), &Project::projectFileIsDirty, this, &BuildSystem::triggerParsing); 98 | connect(&m_fsWatcher, &QFileSystemWatcher::directoryChanged, this, &BuildSystem::scheduleProjectScan); 99 | QTimer::singleShot(0, this, &BuildSystem::triggerParsing); 100 | readProjectSettings(projectFilePath()); 101 | } 102 | 103 | const int MIN_TIME_BETWEEN_PROJECT_SCANS = 4500; 104 | 105 | Project::Project(const Utils::FilePath &filePath) : 106 | ProjectExplorer::Project(Constants::MimeType, filePath) 107 | { 108 | setId(Constants::ProjectId); 109 | setDisplayName(filePath.toFileInfo().completeBaseName()); 110 | 111 | setNeedsBuildConfigurations(false); 112 | setBuildSystemCreator([](ProjectExplorer::Target *t) { return new BuildSystem(t); }); 113 | } 114 | 115 | BuildSystem::~BuildSystem() 116 | { 117 | if (m_projectScanFuture.isRunning()) { 118 | m_projectScanFuture.cancel(); 119 | m_projectScanFuture.waitForFinished(); 120 | } 121 | } 122 | 123 | void BuildSystem::readProjectSettings(const Utils::FilePath &filePath) 124 | { 125 | QString base = filePath.toFileInfo().absoluteDir().absolutePath(); 126 | base.append("/"); 127 | QSettings settings(filePath.toString(), QSettings::IniFormat); 128 | settings.beginGroup("Config"); 129 | for (const QString &path : settings.value("Ignore").toStringList()) { 130 | if (path.isEmpty()) 131 | continue; 132 | m_ignoredDirectories << base + path; 133 | } 134 | settings.endGroup(); 135 | } 136 | 137 | void BuildSystem::scheduleProjectScan() 138 | { 139 | auto elapsedTime = m_lastProjectScan.elapsed(); 140 | if (elapsedTime < MIN_TIME_BETWEEN_PROJECT_SCANS) { 141 | if (!m_projectScanTimer.isActive()) { 142 | m_projectScanTimer.setInterval(MIN_TIME_BETWEEN_PROJECT_SCANS - elapsedTime); 143 | m_projectScanTimer.start(); 144 | } 145 | } else { 146 | triggerParsing(); 147 | } 148 | } 149 | 150 | void BuildSystem::populateProject() 151 | { 152 | m_lastProjectScan.start(); 153 | QSet oldFiles(m_files); 154 | m_files.clear(); 155 | const QDir baseDir(projectDirectory().toString()); 156 | recursiveScanDirectory(baseDir); 157 | if (m_projectScanFuture.isCanceled()) 158 | return; 159 | 160 | const auto removedFiles = oldFiles - m_files; 161 | const auto addedFiles = m_files - oldFiles; 162 | 163 | for (const QString &file : removedFiles) 164 | CodeModel::instance()->removeSymbolsFrom(file); 165 | for (const QString &file : addedFiles) 166 | CodeModel::instance()->addFile(file); 167 | } 168 | 169 | void BuildSystem::triggerParsing() 170 | { 171 | if (isParsing()) { 172 | m_projectScanTimer.setInterval(MIN_TIME_BETWEEN_PROJECT_SCANS); 173 | m_projectScanTimer.start(); 174 | return; 175 | } 176 | m_projectScanFuture = QtConcurrent::run(this, &BuildSystem::populateProject); 177 | auto *watcher = new QFutureWatcher(); 178 | watcher->setFuture(m_projectScanFuture); 179 | Core::ProgressManager::instance()->addTask(m_projectScanFuture, tr("Parsing Ruby Files"), Constants::RubyProjectTask); 180 | connect(watcher, &QFutureWatcher::finished, 181 | this, [this, watcher] { 182 | BuildSystem::ParseGuard guard(guardParsingRun()); 183 | const QDir baseDir(projectDirectory().toString()); 184 | QList appTargets; 185 | auto newRoot = std::make_unique(projectDirectory()); 186 | for (const QString &f : qAsConst(m_files)) { 187 | const Utils::FilePath filePath = Utils::FilePath::fromString(f); 188 | 189 | newRoot->addNestedNode(std::make_unique(filePath)); 190 | if (f.endsWith(".rubyproject")) 191 | continue; 192 | ProjectExplorer::BuildTargetInfo bti; 193 | bti.buildKey = f; 194 | bti.targetFilePath = filePath; 195 | bti.projectFilePath = projectFilePath(); 196 | appTargets.append(bti); 197 | } 198 | setRootProjectNode(std::move(newRoot)); 199 | 200 | setApplicationTargets(appTargets); 201 | 202 | guard.markAsSuccess(); 203 | 204 | emitBuildSystemUpdated(); 205 | watcher->deleteLater(); 206 | }); 207 | } 208 | 209 | void BuildSystem::recursiveScanDirectory(const QDir &dir) 210 | { 211 | if (m_projectScanFuture.isCanceled()) 212 | return; 213 | QRegularExpression projectFilePattern(".*\\.rubyproject(?:\\.user)?$"); 214 | const auto files = dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot 215 | | QDir::NoSymLinks | QDir::CaseSensitive | QDir::Hidden); 216 | for (const QFileInfo &info : files) { 217 | if (m_projectScanFuture.isCanceled()) 218 | return; 219 | if (info.isDir() && !info.isHidden() && !shouldIgnoreDir(info.filePath())) 220 | recursiveScanDirectory(QDir(info.filePath())); 221 | else if (!projectFilePattern.match(info.fileName()).hasMatch()) 222 | m_files << info.filePath(); 223 | } 224 | m_fsWatcher.addPath(dir.absolutePath()); 225 | } 226 | 227 | bool BuildSystem::shouldIgnoreDir(const QString &filePath) const 228 | { 229 | for (const QString& path : m_ignoredDirectories) 230 | if (filePath.startsWith(path)) 231 | return true; 232 | return false; 233 | } 234 | 235 | bool BuildSystem::supportsAction(ProjectExplorer::Node *, 236 | ProjectExplorer::ProjectAction action, 237 | const ProjectExplorer::Node *) const 238 | { 239 | switch (action) { 240 | case ProjectExplorer::ProjectAction::AddNewFile: 241 | case ProjectExplorer::ProjectAction::EraseFile: 242 | case ProjectExplorer::ProjectAction::Rename: 243 | return true; 244 | default: 245 | return false; 246 | } 247 | } 248 | 249 | Project::RestoreResult Project::fromMap(const QVariantMap &map, QString *errorMessage) 250 | { 251 | RestoreResult res = ProjectExplorer::Project::fromMap(map, errorMessage); 252 | if (res == RestoreResult::Ok) 253 | addTargetForDefaultKit(); 254 | 255 | return res; 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /projectmanager/RubyProject.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_Project 2 | #define Ruby_Project 3 | 4 | #include 5 | 6 | namespace TextEditor { class TextDocument; } 7 | 8 | namespace Ruby { 9 | 10 | class Project : public ProjectExplorer::Project 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit Project(const Utils::FilePath &fileName); 16 | 17 | private: 18 | RestoreResult fromMap(const QVariantMap &map, QString *errorMessage) override; 19 | bool needsConfiguration() const final { return false; } 20 | }; 21 | 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /projectmanager/RubyProjectWizard.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyProjectWizard.h" 2 | #include "../RubyConstants.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace Ruby { 12 | 13 | ProjectWizard::ProjectWizard() 14 | { 15 | setSupportedProjectTypes({ Constants::ProjectId }); 16 | 17 | setDisplayName(tr("Import Existing Ruby Project")); 18 | setId("Z.Ruby"); 19 | setDescription(tr("Imports existing Ruby projects.")); 20 | setCategory(ProjectExplorer::Constants::IMPORT_WIZARD_CATEGORY); 21 | setDisplayCategory(ProjectExplorer::Constants::IMPORT_WIZARD_CATEGORY_DISPLAY); 22 | 23 | setIcon(QIcon(Constants::RubyIcon)); 24 | } 25 | 26 | Core::BaseFileWizard *ProjectWizard::create(QWidget *parent, const Core::WizardDialogParameters ¶meters) const 27 | { 28 | Core::BaseFileWizard *wizard = new Core::BaseFileWizard(this, parameters.extraValues(), parent); 29 | wizard->setWindowTitle(displayName()); 30 | 31 | Utils::FileWizardPage *page = new Utils::FileWizardPage; 32 | page->setPath(parameters.defaultPath()); 33 | wizard->addPage(page); 34 | 35 | const auto pages = wizard->extensionPages(); 36 | for (QWizardPage *p : pages) 37 | wizard->addPage(p); 38 | 39 | return wizard; 40 | } 41 | 42 | Core::GeneratedFiles ProjectWizard::generateFiles(const QWizard *widget, QString *) const 43 | { 44 | const Core::BaseFileWizard *wizard = qobject_cast(widget); 45 | Utils::FileWizardPage *page = wizard->find(); 46 | const QString projectPath = page->path(); 47 | const QDir dir(projectPath); 48 | const QString projectName = page->fileName(); 49 | 50 | Core::GeneratedFile projectFile(QFileInfo(dir, projectName + ".rubyproject").absoluteFilePath()); 51 | projectFile.setContents("[Config]\nIgnore=node_modules,public,tmp\n"); 52 | projectFile.setAttributes(Core::GeneratedFile::OpenProjectAttribute); 53 | 54 | return Core::GeneratedFiles() << projectFile; 55 | } 56 | 57 | bool ProjectWizard::postGenerateFiles(const QWizard*, const Core::GeneratedFiles &files, QString *errorMessage) const 58 | { 59 | return ProjectExplorer::CustomProjectWizard::postGenerateOpen(files, errorMessage); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /projectmanager/RubyProjectWizard.h: -------------------------------------------------------------------------------- 1 | #ifndef Ruby_ProjectWizard_h 2 | #define Ruby_ProjectWizard_h 3 | 4 | #include 5 | 6 | namespace Ruby { 7 | 8 | class ProjectWizard : public Core::BaseFileWizardFactory 9 | { 10 | Q_OBJECT 11 | public: 12 | ProjectWizard(); 13 | protected: 14 | Core::BaseFileWizard *create(QWidget *parent, const Core::WizardDialogParameters &wizardDialogParameters) const override; 15 | Core::GeneratedFiles generateFiles(const QWizard *widget, QString *) const override; 16 | bool postGenerateFiles(const QWizard *, const Core::GeneratedFiles &files, QString *errorMessage) const override; 17 | }; 18 | 19 | } 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /projectmanager/RubyRunConfiguration.cpp: -------------------------------------------------------------------------------- 1 | #include "RubyRunConfiguration.h" 2 | #include "RubyProject.h" 3 | #include "../RubyConstants.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | using namespace ProjectExplorer; 17 | using namespace Utils; 18 | 19 | namespace Ruby { 20 | 21 | RunConfiguration::RunConfiguration(Target *target, const Utils::Id &id) 22 | : ProjectExplorer::RunConfiguration(target, id) 23 | { 24 | addAspect(target); 25 | addAspect(); 26 | addAspect(); 27 | addAspect(); 28 | addAspect(); 29 | } 30 | 31 | Runnable RunConfiguration::runnable() const 32 | { 33 | Runnable result; 34 | result.executable = aspect()->executable(); 35 | result.commandLineArguments = aspect()->arguments(macroExpander()); 36 | result.workingDirectory = aspect()->workingDirectory(macroExpander()).toString(); 37 | result.environment = aspect()->environment(); 38 | return result; 39 | } 40 | 41 | RunConfigurationFactory::RunConfigurationFactory() 42 | { 43 | registerRunConfiguration("Ruby.RunConfiguration."); 44 | addSupportedProjectType(Constants::ProjectId); 45 | } 46 | 47 | } // namespace Ruby 48 | -------------------------------------------------------------------------------- /projectmanager/RubyRunConfiguration.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBYRUNCONFIGURATION_H 2 | #define RUBYRUNCONFIGURATION_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace Ruby { 9 | 10 | class RunConfiguration : public ProjectExplorer::RunConfiguration 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit RunConfiguration(ProjectExplorer::Target *target, const Utils::Id &id); 16 | 17 | ProjectExplorer::Runnable runnable() const override; 18 | }; 19 | 20 | class RunConfigurationFactory : public ProjectExplorer::RunConfigurationFactory 21 | { 22 | public: 23 | RunConfigurationFactory(); 24 | }; 25 | 26 | } // namespace Ruby 27 | 28 | #endif // RUBYRUNCONFIGURATION_H 29 | -------------------------------------------------------------------------------- /ruby-project.qbs: -------------------------------------------------------------------------------- 1 | import qbs 1.0 2 | 3 | Project { 4 | references: "ruby.qbs" 5 | } 6 | -------------------------------------------------------------------------------- /ruby.pro: -------------------------------------------------------------------------------- 1 | isEmpty(QTC_SOURCE):error(QTC_SOURCE must be set) 2 | isEmpty(QTC_BUILD):error(QTC_BUILD must be set) 3 | IDE_BUILD_TREE=$$QTC_BUILD 4 | QTC_PLUGIN_NAME = Ruby 5 | QTC_PLUGIN_DEPENDS = coreplugin texteditor projectexplorer 6 | include($$QTC_SOURCE/src/qtcreatorplugin.pri) 7 | 8 | CONFIG += qjson 9 | 10 | SOURCES += RubyPlugin.cpp \ 11 | editor/RubyAmbiguousMethodAssistProvider.cpp \ 12 | editor/RubyAutoCompleter.cpp \ 13 | editor/RubyCodeModel.cpp \ 14 | editor/RubyCodeStylePreferencesFactory.cpp \ 15 | editor/RubyCompletionAssist.cpp \ 16 | editor/RubyEditor.cpp \ 17 | editor/RubyEditorDocument.cpp \ 18 | editor/RubyEditorFactory.cpp \ 19 | editor/RubyEditorWidget.cpp \ 20 | editor/RubyHighlighter.cpp \ 21 | editor/RubyIndenter.cpp \ 22 | editor/RubyQuickFixAssistProvider.cpp \ 23 | editor/RubyQuickFixes.cpp \ 24 | editor/RubyRubocopHighlighter.cpp \ 25 | editor/RubyScanner.cpp \ 26 | editor/RubySymbolFilter.cpp \ 27 | projectmanager/RubyProject.cpp \ 28 | projectmanager/RubyProjectWizard.cpp \ 29 | projectmanager/RubyRunConfiguration.cpp 30 | 31 | equals(TEST, 1) { 32 | SOURCES += editor/ScannerTest.cpp 33 | } 34 | 35 | HEADERS += RubyPlugin.h \ 36 | RubyConstants.h \ 37 | editor/RubyAmbiguousMethodAssistProvider.h \ 38 | editor/RubyAutoCompleter.h \ 39 | editor/RubyBlockState.h \ 40 | editor/RubyCodeModel.h \ 41 | editor/RubyCodeStylePreferencesFactory.h \ 42 | editor/RubyCompletionAssist.h \ 43 | editor/RubyEditor.h \ 44 | editor/RubyEditorDocument.h \ 45 | editor/RubyEditorFactory.h \ 46 | editor/RubyEditorWidget.h \ 47 | editor/RubyHighlighter.h \ 48 | editor/RubyIndenter.h \ 49 | editor/RubyQuickFixAssistProvider.h \ 50 | editor/RubyQuickFixes.h \ 51 | editor/RubyRubocopHighlighter.h \ 52 | editor/RubyScanner.h \ 53 | editor/RubySymbol.h \ 54 | editor/RubySymbolFilter.h \ 55 | editor/SourceCodeStream.h \ 56 | projectmanager/RubyProject.h \ 57 | projectmanager/RubyProjectWizard.h \ 58 | projectmanager/RubyRunConfiguration.h 59 | 60 | RESOURCES += \ 61 | Ruby.qrc 62 | 63 | OTHER_FILES += \ 64 | Readme.md Ruby.json.in 65 | -------------------------------------------------------------------------------- /ruby.qbs: -------------------------------------------------------------------------------- 1 | import qbs 1.0 2 | import QtcPlugin 3 | 4 | QtcPlugin { 5 | name: "Ruby" 6 | 7 | Depends { name: "Qt.widgets" } 8 | Depends { name: "Utils" } 9 | 10 | Depends { name: "Core" } 11 | Depends { name: "TextEditor" } 12 | Depends { name: "ProjectExplorer" } 13 | 14 | Group { 15 | name: "General" 16 | files: [ 17 | "RubyPlugin.cpp", "RubyPlugin.h", 18 | "RubyConstants.h", 19 | "Ruby.qrc", 20 | "README.md", 21 | ] 22 | } 23 | 24 | Group { 25 | name: "Editor" 26 | prefix: "editor/" 27 | files: [ 28 | "RubyAmbiguousMethodAssistProvider.cpp", "RubyAmbiguousMethodAssistProvider.h", 29 | "RubyAutoCompleter.cpp", "RubyAutoCompleter.h", 30 | "RubyBlockState.h", 31 | "RubyCodeModel.cpp", "RubyCodeModel.h", 32 | "RubyCodeStylePreferencesFactory.cpp", "RubyCodeStylePreferencesFactory.h", 33 | "RubyCompletionAssist.cpp", "RubyCompletionAssist.h", 34 | "RubyEditor.cpp", 35 | "RubyEditorFactory.cpp", "RubyEditorFactory.h", 36 | "RubyEditor.h", 37 | "RubyEditorWidget.cpp", "RubyEditorWidget.h", 38 | "RubyHighlighter.cpp", "RubyHighlighter.h", 39 | "RubyIndenter.cpp", "RubyIndenter.h", 40 | "RubyQuickFixAssistProvider.cpp", "RubyQuickFixAssistProvider.h", 41 | "RubyQuickFixes.cpp", "RubyQuickFixes.h", 42 | "RubyRubocopHighlighter.cpp", "RubyRubocopHighlighter.h", 43 | "RubyScanner.cpp", "RubyScanner.h", 44 | "RubySnippetProvider.h", 45 | "RubySymbolFilter.cpp", "RubySymbolFilter.h", 46 | "RubySymbol.h", 47 | "SourceCodeStream.h", 48 | ] 49 | } 50 | 51 | Group { 52 | name: "Project Manager" 53 | prefix: "projectmanager/" 54 | files: [ 55 | "RubyProject.cpp", "RubyProject.h", 56 | "RubyProjectManager.cpp", "RubyProjectManager.h", 57 | "RubyProjectWizard.cpp", "RubyProjectWizard.h", 58 | "RubyRunConfiguration.cpp", "RubyRunConfiguration.h", 59 | ] 60 | } 61 | 62 | Group { 63 | name: "Tests" 64 | condition: qtc.testsEnabled 65 | files: ["editor/ScannerTest.cpp"] 66 | } 67 | } 68 | --------------------------------------------------------------------------------