diff --git a/Makefile.am b/Makefile.am index 0ed08b315..fbda3c3cb 100644 --- a/Makefile.am +++ b/Makefile.am @@ -109,6 +109,10 @@ if ENABLE_LIPSUM SUBDIRS += lipsum endif +if ENABLE_LSP +SUBDIRS += lsp +endif + if ENABLE_MARKDOWN SUBDIRS += markdown endif diff --git a/README b/README index a55656c8f..575064517 100644 --- a/README +++ b/README @@ -71,6 +71,7 @@ Available plugins are: * ``latex`` -- the LaTeX plugin * ``lineoperations`` -- simple line functions that can be applied to an open file * ``lipsum`` -- the Lipsum plugin +* ``lsp`` -- the LSP plugin * ``markdown`` -- the Markdown plugin * ``overview``-- the overview plugin * ``pairtaghighlighter`` -- the PairTagHighlighter plugin diff --git a/build/geany-plugins.nsi b/build/geany-plugins.nsi index cee98b3f2..1684fd36f 100644 --- a/build/geany-plugins.nsi +++ b/build/geany-plugins.nsi @@ -196,6 +196,7 @@ Section Uninstall Delete "$INSTDIR\lib\geany\keyrecord.dll" Delete "$INSTDIR\lib\geany\lipsum.dll" Delete "$INSTDIR\lib\geany\lineoperations.dll" + Delete "$INSTDIR\lib\geany\lsp.dll" Delete "$INSTDIR\lib\geany\overview.dll" Delete "$INSTDIR\lib\geany\pairtaghighlighter.dll" Delete "$INSTDIR\lib\geany\pohelper.dll" diff --git a/build/lsp.m4 b/build/lsp.m4 new file mode 100644 index 000000000..fa3f66ef2 --- /dev/null +++ b/build/lsp.m4 @@ -0,0 +1,11 @@ +AC_DEFUN([GP_CHECK_LSP], +[ + GP_ARG_DISABLE([LSP], [auto]) + GP_COMMIT_PLUGIN_STATUS([LSP]) + AC_CONFIG_FILES([ + lsp/Makefile + lsp/deps/Makefile + lsp/src/Makefile + lsp/data/Makefile + ]) +]) diff --git a/configure.ac b/configure.ac index 38fafcc95..bc400837c 100644 --- a/configure.ac +++ b/configure.ac @@ -55,6 +55,7 @@ GP_CHECK_GITCHANGEBAR GP_CHECK_KEYRECORD GP_CHECK_LINEOPERATIONS GP_CHECK_LIPSUM +GP_CHECK_LSP GP_CHECK_MARKDOWN GP_CHECK_OVERVIEW GP_CHECK_PAIRTAGHIGHLIGHTER diff --git a/lsp/AUTHORS b/lsp/AUTHORS new file mode 100644 index 000000000..eb925ec9a --- /dev/null +++ b/lsp/AUTHORS @@ -0,0 +1 @@ +Jiří Techet diff --git a/lsp/COPYING b/lsp/COPYING new file mode 100644 index 000000000..8c4c849e2 --- /dev/null +++ b/lsp/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/lsp/ChangeLog b/lsp/ChangeLog new file mode 100644 index 000000000..e69de29bb diff --git a/lsp/Makefile.am b/lsp/Makefile.am new file mode 100644 index 000000000..d04a7a9b4 --- /dev/null +++ b/lsp/Makefile.am @@ -0,0 +1,4 @@ +include $(top_srcdir)/build/vars.auxfiles.mk + +SUBDIRS = deps src data +plugin = lsp diff --git a/lsp/NEWS b/lsp/NEWS new file mode 100644 index 000000000..e69de29bb diff --git a/lsp/README b/lsp/README new file mode 100644 index 000000000..d3e42782a --- /dev/null +++ b/lsp/README @@ -0,0 +1,261 @@ +=== +LSP +=== + +.. contents:: + +About +===== + +LSP Client is a language server protocol client plugin that allows to run multiple +language servers for various programming languages, making their functionality +accessible to Geany. + +Configuration +============= + +The plugin does not come bundled with any language server; these must +be installed independently of the plugin. For installation and configuration +instructions, please refer to the documentation of the specific servers you plan +to use, as some may have specific requirements. Note that many language servers, +such as ``clangd``, ``pylsp``, and ``gopls``, are often packaged by Linux +distributions, making them easy to install and use. + +You can configure servers and other settings using the User configuration file, +accessible from:: + + Tools->LSP Client->User configuration + +This file provides extensive information about all the settings options, so be +sure to refer to it for more details. The default configuration file comes with +pre-configured values for several language servers; other servers have to be +added manually. + +By default, the LSP plugin is disabled unless explicitly enabled for a +project under + +:: + + Project->Properties->LSP Client + +This behavior can be controlled by the first three configuration options in +the `[all]` section of the configuration file. + +Language servers are started lazily, meaning they only launch when you switch +a tab to a file with a filetype that has a corresponding LSP server configured. +After the initial handshake between the client and server, you can check the +result under + +:: + + Tools->LSP Client->Server Initialize Responses + +This file also provides information about the capabilities offered by the server; +for more details, refer to: + +https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ + +In addition to the User configuration file, you can also create a per-project +configuration file (which can also be shared by multiple projects). This file +can be configured under the + +:: + + Project->Properties->LSP Client + +tab. + +Furthermore, the plugin offers a range of customizable keybindings, which can be +configured from:: + + Edit->Preferences->Keybindings->LSP Client + +Usage +===== + +This section provides an overview of the individual LSP features supported by +the plugin, along with guidance on how to use them. You can enable or disable +each feature in the configuration file, where you can also customize certain +aspects of their behavior. + +Please note that not all language servers support every feature. For more +information on the specific features supported by a language server, consult +the server's documentation. + +Autocompletion +-------------- + +Autocompletion works similarly to Geany's autocompletion feature. You can +configure keybindings triggering the autocompletion popup. + +Function signagure +------------------ + +When you type an open brace after a function name, the plugin displays the +function's signature in a popup window similarly to Geany's behavior. + +Diagnostic messages +------------------- + +LSP diagnostic messages typically include error messages or warnings from +compilers, as well as messages from linters. These messages are highlighted in +the code; the exact style of highlighting can be configured to suit your +preferences. When you hover over the highlighted part with your mouse cursor, +a popup window appears, providing additional details about the issue. + +Some servers offer auto-fixes of certain issues. For instance, the ``clangd`` +server displays ``fix available`` next to the issue in the hover popup window. +To perform the auto-fix, right-click the line with the issue and select the +corresponding option from the Commands submenu. + +Code lenses +----------- + +Code lenses are executable commands that are specific to a particular piece of +code. As Geany's Scintilla component limitations prevent these commands +from being clickable and executable directly in the editor, they are accessible +through the Commands submenu of the context menu, similarly to diagnostic +messages. + +Semantic token type highlighting +-------------------------------- + +Language servers that provide semantic token support can be used to highlight +types, such as class names, in the code. You can customize various aspects of +how the results are visualized in the editor through the configuration file. + +Hover popup +----------- + +The language server can be configured to display a popup window with detailed +information about the symbol under the mouse cursor. However, as this feature +can be slightly annoying, it is disabled by default. Alternatively, you can +access this feature through a keybinding. + +Symbol tree +----------- + +The LSP symbol tree tab in the sidebar, separate from Geany's Symbols tab, +shows document symbols in a similar manner to the Geany's symbol tree feature. + +Go to symbol definition/declaration +----------------------------------- + +Similarly to Geany, you can navigate to the symbol definition/declaration +by control-clicking it in the document or by using the corresponding keybinding. +This feature is also available from the context menu. + +Go to type definition +--------------------- + +This feature enables quick navigation to the definition of the type associated +with the symbol under the cursor, such as the type of a variable. You can also +access this feature from the context menu. + +Swap header/source +------------------ + +This is a non-standard clangd extension allowing quick swapping between a +source file and the corresponding header. This feature is not supported by +any other language server. + +Find references +--------------- + +This feature finds all references of the symbol under the cursor in the project. +This feature is also accessible from the context menu. + +Find implementations +-------------------- + +This feature allows you to locate all classes that implement the interface under +the cursor. + +Navigation to document/project symbols, files, and line numbers +--------------------------------------------------------------- + +The plugin provides a simple, VSCode-style panel for navigating your project. +The + +:: + + Tools->LSP Client->Go to Anywhere + +command offers four types of navigation options: + +- Open files by typing their name directly in the entry +- Navigate to symbols in the current document by prefixing the query with ``@`` +- Navigate to symbols across the entire project by prefixing the query with ``#`` +- Jump to a specific line in the current document by prefixing the query with ``:`` + +The other related queries in the LSP Client menu (also accessible via a keybinding) +simply pre-fill the prefix for you, but otherwise function identically. + +Code formatting +--------------- + +The code formatting feature allows you to format either the entire document or +a selected portion of code, depending on the LSP server's support for this +functionality. You can access this feature from the context menu. + +Identical symbol highlighting +----------------------------- + +When you click on a symbol in the document, this feature highlights all its +occurrences in the document. You can customize the highlighting style to your +preference by configuring it in the configuration file. + +Document symbol renaming +------------------------ + +This feature leverages the identical symbol highlighting described above to +select all symbol occurrences, create multiple cursors at their positions in the +document, and rename them simultaneously as you type. You can also access this +feature from the context menu. + +Project-wide renaming +--------------------- + +After selecting Rename in Project from the context menu or the plugin menu, +you can rename all symbols in the project. + +**Warning:** This feature has a potential to modify many files and language +servers may not be completely reliable when performing the rename so be very +cautious when using it. The plugin does not perform any additional +checks and does not show any preview of the changes so it is best to use this +feature only after committing all modified files so you can +easily revert to a working state if needed. + +License +======= + +Geany LSP Client is distributed under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 of the +License, or (at your option) any later version. A copy of this license +can be found in the file COPYING included with the source code of this +program. + +Downloads +========= + +Geany LSP Client can be downloaded from the development repository available +at https://github.com/techee/geany-lsp/. In addition, it is also distributed +as part of the combined Geany Plugins release. For more information and +downloads, please visit https://plugins.geany.org/geany-plugins/ + +Development Code +================ + +Get the code from:: + + git clone https://github.com/techee/geany-lsp.git + +Ideas, questions, patches and bug reports +========================================= + +Please direct all questions, bug reports and patches to the development +repository at https://github.com/techee/geany-lsp/ and open the corresponding +bug report or pull request there. + +2023-2024 by Jiří Techet +techet(at)gmail(dot)com diff --git a/lsp/data/Makefile.am b/lsp/data/Makefile.am new file mode 100644 index 000000000..45b493940 --- /dev/null +++ b/lsp/data/Makefile.am @@ -0,0 +1,5 @@ +include $(top_srcdir)/build/vars.docs.mk + +plugin = lsp + +dist_plugindata_DATA = lsp.conf diff --git a/lsp/data/lsp.conf b/lsp/data/lsp.conf new file mode 100644 index 000000000..67486d94b --- /dev/null +++ b/lsp/data/lsp.conf @@ -0,0 +1,471 @@ +# The global configuration file, accessible through +# Tools->LSP Client->Global Configuration, defines default values for various +# configuration options. +# +# When accessing the user configuration file using +# Tools->LSP Client->User Configuration, a copy of the global configuration file +# is created in user's configuration directory in which users can override +# various settings from the global configuration file. +# +# In addition, it is possible to create a project-wide configuration file used +# for a single project or shared by multiple projects whose path can be +# specified under Project->Properties->LSP Client->Configuration file. When such +# a file is defined and the project is configured to use the project +# configuration, this configuration file overrides the global configuration +# instead of the user configuration file. +# +# Each configuration file may contain the [all] section which contains settings +# common for all language servers, and several filetype-specific sections +# containing settings specific for the given filetype. The names of these +# sections are identical to the names defined in +# Tools->Configuration Files->filetype_extensions.conf; for instance [Python] +# for the Python programming language (case sensitive). Most of the options can +# appear both in the [all] section and the filetype-specific sections except for +# a few that make only sense in the filetype-specific section, like e.g. the +# 'cmd' option containing the command that starts the server. Any option defined +# in the filetype-specific section overrides identically named option defined in +# the [all] section for that particular language server. +# +# Not all LSP features are supported by all LSP servers. To learn more about +# what features are supported by the particular server you are trying to use, +# you can check the result of the initial client-server handshake by going to +# Tools->LSP Client->Server Initialize Responses (the server must be running +# to see any result). +# +# All servers are automatically restarted when any of the configuration files +# changes. Servers are started lazily after a tab is switched to a document for +# which a server is configured (this means that restart typically means shutdown +# of all servers after which they are started as needed). + + +[all] +# Defines whether the plugin should be enabled automatically for new or existing +# projects (that have not yet been configured to use LSP). This option +# is only valid in the [all] section +enable_by_default=false +# Defines whether the server should be used when no project is open. Servers +# may not work correctly without a project because most of them need to know +# the path to the project directory which corresponds to the path defined under +# Project->Properties->Base path. This option can be partially overridden +# by project_root_marker_patterns, see below +use_without_project=false +# Defines whether the server should be used for files whose path is not within +# the project directory. This option can be partially overridden by +# project_root_marker_patterns, see below +use_outside_project_dir=false +# A semicolon-separated list of glob patterns of files that are typically stored +# inside the root directory of the project. Language servers supporting +# changeNotifications of workspaceFolders (these two values should appear inside +# the server initialize response) can use these marker files to detect root +# project directories of open files. Starting from the open file directory, +# the plugin goes up in the directory structure and tests whether a file +# matching project_root_marker_patterns exists - if it does, such a directory +# is considered to be the root directory of the project. This allows to +# detect projects without any Geany project open and allows the plugin to work +# on multiple projects simultaneously. Typically, the pattern contains +# files/directories such as .git, configure.ac, go.mod, etc. If a pattern is +# found, use_without_project and use_outside_project_dir are ignored +project_root_marker_patterns= +# In additon to standard identifier characters a-zA-Z0-9_, this configuration +# option allows adding extra characters that can appear inside identifiers. +# For instance $- will add '$' and '-' to the above set. +extra_identifier_characters= +# Some servers require that the initialization options configuration is sent +# using workspace/didChangeConfiguration instead of the initialize request. +# This option does this right after the initialize request completes. +send_did_change_configuration=false + +# The number of keybindings that can be assigned to LSP code action commands. +# This option is valid only within the [all] section and changing the value +# requires either the plugin reload or Geany restart +command_keybinding_num=5 +# When the keybinding Command 1 is invoked, it checks whether the +# command_1_regex matches any of the items from the Commands submenu of the +# context menu. The first matched entry is executed. For convenience, regex +# matches are case insensitive. Up to command_keybinding_num command keybindings +# can be specified this way +command_1_regex= +# Regex specifying which of the commands present in the context menu under the +# Commands submenu will be automatically performed on save. If multiple entries +# match, all of them will be performed. For convenience, regex matches are case +# insensitive +command_on_save_regex= + +# When the filetype-specific 'rpc_log' settings is defined, this option +# specifies whether the log should contain all details including method +# parameters, or just the method name and type of the communication +rpc_log_full=false +# Show server's stderr in Geany's stderr (when started from terminal) +show_server_stderr=false + +# Whether LSP should be used for autocompletion +autocomplete_enable=true +# Servers return the label that can be shown in the autocompletion popup for +# individual autocompletion entries, or it is possible to use just the text that +# gets inserted when selecting the entry. See also autocomplete_use_snippets. +autocomplete_use_label=true +# Whether snippets should be shown in the autocompletion list. Snippet support +# is only partial so things may not work as expected. When snippets are enabled, +# it is recommended to use autocomplete_use_label=true, otherwise snippet +# tab stops and other snippet information is shown in the autocompletion popup. +autocomplete_use_snippets=false +# Maximum number of autocompletion entries shown in the popup window (including +# those that will only get visible after scrolling the contents) +autocomplete_window_max_entries=20 +# Maximum number of autocompletion entries shown without scrolling (defining the +# actual height of the popup) +autocomplete_window_max_displayed=8 +# The maximum width of the autocompletion popup in displayed characters +autocomplete_window_max_width=60 +# Whether to automatically apply additional edits defined for the autocompletion +# entry. These are typically imports of the modules where the inserted symbol is +# defined +autocomplete_apply_additional_edits=false +# Semicolon separated list of character sequences which can trigger +# autocompletion. Normally, the server defines these but this option can be used +# to further restrict the list only to some sequences if the server-provided +# value does not work well (e.g. when server's trigger chars for autocompletion +# clash with signature trigger chars - Rust server for instance uses '(' for +# both and omitting '(' in autocomplete sequences shows signature instead) +autocomplete_trigger_sequences= +# Semicolon separated list of words that make the autocompletion popup hide. +# This is useful for instance for languages like Pascal that use keywords +# 'begin' and 'end' which are typically followed by a newline where typing enter +# after these words might select some unwanted word from the autocompletion list. +autocomplete_hide_after_words= + +# Whether LSP should be used to display diagnostic messages. Typically these are +# compiler errors or warnings +diagnostics_enable=true +# Semicolon-separated glob patterns specifying files for which diagnostic +# messages are not shown. Useful when the server has a problem with some files +diagnostics_disable_for= +# For the statusbar issue number indicator, consider only issues of the +# configured severity or highler. Valid values are: 1 (error), 2 (warning), +# 3 (info), 4 (hint). E.g. setting this value to 2 will show issue number +# for errors and warnings only. +diagnostics_statusbar_severity=2 +# Defines the style of error diagnostics - visual style such as underline, and +# its color. Empty value means that diagnostic messages of the given severity +# are not displayed. +# The first number is the "indicator index" of Scintilla - each style should +# have a unique value from 8 to 31. Can be changed when the value clashes with +# some other plugin. +# The remaining values correspond to +# SCI_INDICSETFORE; SCI_INDICSETALPHA; SCI_INDICSETOUTLINEALPHA; SCI_INDICSETSTYLE +# - see Scintilla documentation for more information +diagnostics_error_style=13;#ff3030;70;255;1 +# Defines the style of warning diagnostics +diagnostics_warning_style=14;#ee00ee;70;255;1 +# Defines the style of information diagnostics +diagnostics_info_style=15;#909090;70;255;14 +# Defines the style of hint diagnostics +diagnostics_hint_style=16;#909090;70;255;14 + +# Whether LSP should be used to show a popup with details when hovering over +# symbols. +hover_enable=false +# Maximum number of lines of the popup window +hover_popup_max_lines=20 +# Maximum number of paragraphs shown in the popup window +hover_popup_max_paragraphs=1000 + +# Whether LSP should be used to show function parameter signatures e.g. after +# typing '(' +signature_enable=true + +# Whether LSP should be used for going to symbol definition/declaration +goto_enable=true + +# Whether LSP should be used for displaying symbols in the sidebar (in a tab +# separate from normal Geany symbols) +document_symbols_enable=true +# The label used for the LSP symbols tab. When left empty, the tab is not +# displayed. This option is only valid in the [all] section +document_symbols_tab_label=LSP Symbols + +# Whether LSP should be used for highlighting semantic tokens in the editor, +# such as types. Most servers don't support this feature so disabled by default. +semantic_tokens_enable=false +# Always perform "full" semantic token request instead of using "delta" +# requests. Can be used when servers don't support delta tokens correctly +semantic_tokens_force_full=false +# Semicolon-separated list of semantic tokens that should be highlighted as +# types. For valid values, see +# https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens +semantic_tokens_types=type;class;enum;interface;struct +# When not empty, Scintilla indicators are used for highlighting semantic +# token types. See diagnostics_error_style for more details about the valid +# values. Note that because of Scintilla limitations, this value cannot be bold +#semantic_tokens_type_style=17;#00007f;255;255;17 +semantic_tokens_type_style= +# When not using semantic_tokens_type_style, this indicates the index in the +# Scintilla lexer used for custom keywords. This feature is supported only by +# some lexers like C/C++ and derived lexers. When using this method, the +# value can be bold but all the occurrences of the given word in the document +# is highlighted regardless of the context in which it is used +semantic_tokens_lexer_kw_index=3 + +# Whether LSP should be used for highlighting all other uses of a variable under +# cursor. +highlighting_enable=true +# Indicator style used for highlighting - see diagnostics_error_style +highlighting_style=18;#b0b0b0;90;255;8 + +# Whether LSP should be used for "code lenses" showing commands available +# for the given line. After right-clicking the line with the code lens, these +# commands are available in the Commands submenu. +code_lens_enable=true +# Defines the foreground and the background color of the code lens indicator. +code_lens_style=#000000;#ffffa0 + +# JSON file containing formatting options defined in +# https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#formattingOptions +# e.g. { "tabSize": 4, "insertSpaces": false }. Supported only by some language +# servers. +#formatting_options_file=/home/some_user/my_formatting_config_file.json +formatting_options_file= +# Like above but with the JSON placed directly into the value. When both +# formatting_options_file and formatting_options are specified, the JSON from +# formatting_options_file is used +formatting_options={ "tabSize": 4, "insertSpaces": false } +# Defines whether the LSP code-formatting feature should be auto-performed +# on every save +format_on_save=false + +# Show progress bar for work in progress server operations. Can be disabled +# when servers do not correctly terminate progress notifications. +progress_bar_enable=true + + +# This is a dummy language server configuration describing the available +# language-specific options. Most of the configuration options from the [all] +# section can be used here as well. +# For an extensive list of various servers and their configurations, check +# https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md +# While the configuration options names of neovim differ from Geany, the +# general concepts are similar and applicable here. +[DummyLanguage] +# The command (including parameters and possibly also the full path) used to +# start the LSP server. Instead of starting a new server, it is also possible to +# reuse other language's server using the 'use' option - see the C++ +# configuration +cmd=srvcmd +# The server can be started with additional environment variables (such as foo +# with the value bar, and foo1 with the value bar1 like in the example below). +env=foo=bar;foo1=bar1 +# File containing initialization options of the server. The server is +# automatically restarted when this file is modified from within Geany +initialization_options_file=/home/some_user/init_options.json +# Like above but with the JSON placed directly into the value. When both +# initialization_options_file and formatting_options are specified, the JSON +# from initialization_options_file is used +initialization_options={"compilationDatabasePath": "/home/some_user/"} +# When defined, performs logging to the specified file where all RPC +# communication between the client plugin and the server will be stored (can +# also be 'stdout' or 'stderr') +rpc_log=stdout +# Additional files and their mappings to LSP language IDs for which the server +# is used as well. The Nth item in the list is always a LSP language ID and the +# (N+1)th item is a glob pattern defining files for which the language ID is +# used +lang_id_mappings=dummylanguage;*.dummy + + +[C] +# By default, clangd expects compile_commands.json inside the 'build' directory +# of your project. You can create it using either 'meson setup build' if your +# project uses meson or e.g. using: +# mkdir build; bear --output build/compile_commands.json -- make +# if your project uses some other build tool (you need to install the bear tool +# first). The compile_commands.json file has to be manually regenerated when +# the build is modified in any way, such as a file is added/removed. +cmd=clangd +autocomplete_use_label=false +semantic_tokens_enable=true +#initialization_options={"compilationDatabasePath": "/home/some_user/my_project/my_builddir"} +formatting_options={ "tabSize": 4, "insertSpaces": false } +command_1_regex=Apply fix:.* +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[C++] +# Don't start a new server but reuse the server defined for some other language +# instead (C server used for C++ in this case) +use=C + + +[Go] +cmd=gopls +autocomplete_apply_additional_edits=true +lang_id_mappings=go.mod;go.mod;go.sum;go.sum;gotmpl;*tmpl +semantic_tokens_enable=true +semantic_tokens_type_style=17;#00007f;255;255;17 +format_on_save=true +command_on_save_regex=Organize Imports +progress_bar_enable=false +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[Haskell] +cmd=haskell-language-server-wrapper --lsp +# Full semantic tokens work but are kind of useless as Scintilla already +# highlights types +semantic_tokens_enable=false +#semantic_tokens_force_full=true +#initialization_options={"plugin": {"semanticTokens": {"config": { }, "globalOn": true}}} +#semantic_tokens_type_style=17;#00007f;255;255;17 +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[Java] +cmd=jdtls +autocomplete_use_label=false +#semantic_tokens_enable=true +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[JSON] +cmd=vscode-json-languageserver --stdio +#initialization_options={"provideFormatter": true, "json": { "schemas": [ { "fileMatch": [ "*.json"], "url": "file:///home/parallels/schema.json"}]}} +send_did_change_configuration=true +autocomplete_use_snippets=true +use_without_project=true +use_outside_project_dir=true +#formatting_options={ "tabSize": 4, "insertSpaces": true } +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[LaTeX] +cmd=texlab +autocomplete_use_snippets=true +extra_identifier_characters=: +use_without_project=true +use_outside_project_dir=true +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[Lua] +cmd=lua-language-server +autocomplete_use_label=false +autocomplete_hide_after_words=do;then;true;false;end;else +use_outside_project_dir=true +use_without_project=true +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[Nix] +cmd=nil +use_without_project=true +use_outside_project_dir=true +extra_identifier_characters=-' +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[PHP] +# Note: Server returns highlighting indices off by 1 and highlighting doesn't work +cmd=phpactor.phar language-server +autocomplete_trigger_sequences=:;>;$;[;@;';\;\\ +extra_identifier_characters=$ +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[Python] +# pip install pyright (or: pipx install pyright) +cmd=pyright-langserver --stdio +# alternatively pylsp +#cmd=pylsp +use_outside_project_dir=true +use_without_project=true +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[Ruby] +cmd=solargraph stdio +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[Rust] +cmd=rust-analyzer +semantic_tokens_enable=true +autocomplete_trigger_sequences=:;.;' +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[Sh] +cmd=bash-language-server start +autocomplete_use_snippets=true +use_outside_project_dir=true +use_without_project=true +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[TypeScript] +cmd=typescript-language-server --stdio +semantic_tokens_enable=true +use_outside_project_dir=true +use_without_project=true +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[Javascript] +use=TypeScript + + +[XML] +cmd=lemminx +autocomplete_use_snippets=true +diagnostics_statusbar_severity=4 +use_without_project=true +use_outside_project_dir=true +# see https://github.com/eclipse/lemminx/blob/main/docs/Configuration.md +#initialization_options_file=/home/some_user/init_options.json +#formatting_options={ "tabSize": 4, "insertSpaces": true } +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +[YAML] +cmd=yaml-language-server --stdio +#initialization_options={"yaml": {"schemas": { "/home/parallels/schema.json": "*"}}} +use_without_project=true +use_outside_project_dir=true +#formatting_options={ "tabSize": 4, "insertSpaces": true } +#rpc_log=stdout +#rpc_log_full=true +#show_server_stderr=true + + +# TODO: help needed! Only the above defined language servers have been tested +# (lightly). If you know some other working language server or find a problem +# with the settings above, please open an issue report or a pull request +# on Github. diff --git a/lsp/deps/Makefile.am b/lsp/deps/Makefile.am new file mode 100644 index 000000000..6b7ee4f94 --- /dev/null +++ b/lsp/deps/Makefile.am @@ -0,0 +1,74 @@ +include $(top_srcdir)/build/vars.build.mk + +noinst_LTLIBRARIES = libjsonrpc.la + +json_glib_srcs = \ + json-glib/json-array.c \ + json-glib/json-builder.c \ + json-glib/json-builder.h \ + json-glib/json-debug.c \ + json-glib/json-debug.h \ + json-glib/json-enum-types.c \ + json-glib/json-enum-types.h \ + json-glib/json-gboxed.c \ + json-glib/json-generator.c \ + json-glib/json-generator.h \ + json-glib/json-glib.h \ + json-glib/json-gobject.c \ + json-glib/json-gobject.h \ + json-glib/json-gobject-private.h \ + json-glib/json-gvariant.c \ + json-glib/json-gvariant.h \ + json-glib/json-node.c \ + json-glib/json-object.c \ + json-glib/json-parser.c \ + json-glib/json-parser.h \ + json-glib/json-path.c \ + json-glib/json-path.h \ + json-glib/json-reader.c \ + json-glib/json-reader.h \ + json-glib/json-scanner.c \ + json-glib/json-scanner.h \ + json-glib/json-serializable.c \ + json-glib/json-types.h \ + json-glib/json-types-private.h \ + json-glib/json-utils.c \ + json-glib/json-utils.h \ + json-glib/json-value.c \ + json-glib/json-version.h \ + json-glib/json-version-macros.h + +jsonrpc_glib_srcs = \ + jsonrpc-glib/jsonrpc-client.c \ + jsonrpc-glib/jsonrpc-client.h \ + jsonrpc-glib/jsonrpc-glib.h \ + jsonrpc-glib/jsonrpc-input-stream.c \ + jsonrpc-glib/jsonrpc-input-stream.h \ + jsonrpc-glib/jsonrpc-input-stream-private.h \ + jsonrpc-glib/jsonrpc-marshalers.c \ + jsonrpc-glib/jsonrpc-marshalers.h \ + jsonrpc-glib/jsonrpc-message.c \ + jsonrpc-glib/jsonrpc-message.h \ + jsonrpc-glib/jsonrpc-output-stream.c \ + jsonrpc-glib/jsonrpc-output-stream.h \ + jsonrpc-glib/jsonrpc-server.c \ + jsonrpc-glib/jsonrpc-server.h \ + jsonrpc-glib/jsonrpc-version.h \ + jsonrpc-glib/jsonrpc-version-macros.h + +libjsonrpc_la_SOURCES = \ + $(json_glib_srcs) \ + $(jsonrpc_glib_srcs) + +libjsonrpc_la_CPPFLAGS = $(AM_CPPFLAGS) \ + -DG_LOG_DOMAIN=\"LSP\" \ + -DJSON_COMPILATION \ + -DJSONRPC_GLIB_COMPILATION \ + -I$(top_srcdir)/lsp/deps/json-glib \ + -I$(top_srcdir)/lsp/deps/jsonrpc-glib +libjsonrpc_la_CFLAGS = $(AM_CFLAGS) + +# do not enable cppcheck for json-glib or jsonrpc-glib as it generates lots of +# false positives and it's not "our" code anyway +# +# include $(top_srcdir)/build/cppcheck.mk diff --git a/lsp/deps/json-glib/json-array.c b/lsp/deps/json-glib/json-array.c new file mode 100644 index 000000000..200cd92a6 --- /dev/null +++ b/lsp/deps/json-glib/json-array.c @@ -0,0 +1,838 @@ +/* json-array.c - JSON array implementation + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +#include "config.h" + +#include "json-types-private.h" + +/** + * JsonArray: + * + * `JsonArray` is the representation of the array type inside JSON. + * + * A `JsonArray` contains [struct@Json.Node] elements, which may contain + * fundamental types, other arrays or objects. + * + * Since arrays can be arbitrarily big, copying them can be expensive; for + * this reason, they are reference counted. You can control the lifetime of + * a `JsonArray` using [method@Json.Array.ref] and [method@Json.Array.unref]. + * + * To append an element, use [method@Json.Array.add_element]. + * + * To extract an element at a given index, use [method@Json.Array.get_element]. + * + * To retrieve the entire array in list form, use [method@Json.Array.get_elements]. + * + * To retrieve the length of the array, use [method@Json.Array.get_length]. + */ + +G_DEFINE_BOXED_TYPE (JsonArray, json_array, json_array_ref, json_array_unref); + +/** + * json_array_new: (constructor) + * + * Creates a new array. + * + * Return value: (transfer full): the newly created array + */ +JsonArray * +json_array_new (void) +{ + JsonArray *array; + + array = g_slice_new0 (JsonArray); + + array->ref_count = 1; + array->elements = g_ptr_array_new (); + + return array; +} + +/** + * json_array_sized_new: (constructor) + * @n_elements: number of slots to pre-allocate + * + * Creates a new array with `n_elements` slots already allocated. + * + * Return value: (transfer full): the newly created array + */ +JsonArray * +json_array_sized_new (guint n_elements) +{ + JsonArray *array; + + array = g_slice_new0 (JsonArray); + + array->ref_count = 1; + array->elements = g_ptr_array_sized_new (n_elements); + + return array; +} + +/** + * json_array_ref: + * @array: the array to reference + * + * Acquires a reference on the given array. + * + * Return value: (transfer none): the passed array, with the reference count + * increased by one + */ +JsonArray * +json_array_ref (JsonArray *array) +{ + g_return_val_if_fail (array != NULL, NULL); + g_return_val_if_fail (array->ref_count > 0, NULL); + + array->ref_count++; + + return array; +} + +/** + * json_array_unref: + * @array: the array to unreference + * + * Releases a reference on the given array. + * + * If the reference count reaches zero, the array is destroyed and all + * its allocated resources are freed. + */ +void +json_array_unref (JsonArray *array) +{ + g_return_if_fail (array != NULL); + g_return_if_fail (array->ref_count > 0); + + if (--array->ref_count == 0) + { + guint i; + + for (i = 0; i < array->elements->len; i++) + json_node_unref (g_ptr_array_index (array->elements, i)); + + g_ptr_array_free (array->elements, TRUE); + array->elements = NULL; + + g_slice_free (JsonArray, array); + } +} + +/** + * json_array_seal: + * @array: the array to seal + * + * Seals the given array, making it immutable to further changes. + * + * This function will recursively seal all elements in the array too. + * + * If the `array` is already immutable, this is a no-op. + * + * Since: 1.2 + */ +void +json_array_seal (JsonArray *array) +{ + guint i; + + g_return_if_fail (array != NULL); + g_return_if_fail (array->ref_count > 0); + + if (array->immutable) + return; + + /* Propagate to all members. */ + for (i = 0; i < array->elements->len; i++) + json_node_seal (g_ptr_array_index (array->elements, i)); + + array->immutable_hash = json_array_hash (array); + array->immutable = TRUE; +} + +/** + * json_array_is_immutable: + * @array: a JSON array + * + * Check whether the given `array` has been marked as immutable by calling + * [method@Json.Array.seal] on it. + * + * Since: 1.2 + * Returns: %TRUE if the array is immutable + */ +gboolean +json_array_is_immutable (JsonArray *array) +{ + g_return_val_if_fail (array != NULL, FALSE); + g_return_val_if_fail (array->ref_count > 0, FALSE); + + return array->immutable; +} + +/** + * json_array_get_elements: + * @array: a JSON array + * + * Retrieves all the elements of an array as a list of nodes. + * + * Return value: (element-type JsonNode) (transfer container) (nullable): the elements + * of the array + */ +GList * +json_array_get_elements (JsonArray *array) +{ + GList *retval; + guint i; + + g_return_val_if_fail (array != NULL, NULL); + + retval = NULL; + for (i = 0; i < array->elements->len; i++) + retval = g_list_prepend (retval, + g_ptr_array_index (array->elements, i)); + + return g_list_reverse (retval); +} + +/** + * json_array_dup_element: + * @array: a JSON array + * @index_: the index of the element to retrieve + * + * Retrieves a copy of the element at the given position in the array. + * + * Return value: (transfer full): a copy of the element at the given position + * + * Since: 0.6 + */ +JsonNode * +json_array_dup_element (JsonArray *array, + guint index_) +{ + JsonNode *retval; + + g_return_val_if_fail (array != NULL, NULL); + g_return_val_if_fail (index_ < array->elements->len, NULL); + + retval = json_array_get_element (array, index_); + if (!retval) + return NULL; + + return json_node_copy (retval); +} + +/** + * json_array_get_element: + * @array: a JSON array + * @index_: the index of the element to retrieve + * + * Retrieves the element at the given position in the array. + * + * Return value: (transfer none): the element at the given position + */ +JsonNode * +json_array_get_element (JsonArray *array, + guint index_) +{ + g_return_val_if_fail (array != NULL, NULL); + g_return_val_if_fail (index_ < array->elements->len, NULL); + + return g_ptr_array_index (array->elements, index_); +} + +/** + * json_array_get_int_element: + * @array: a JSON array + * @index_: the index of the element to retrieve + * + * Conveniently retrieves the integer value of the element at the given + * position inside an array. + * + * See also: [method@Json.Array.get_element], [method@Json.Node.get_int] + * + * Return value: the integer value + * + * Since: 0.8 + */ +gint64 +json_array_get_int_element (JsonArray *array, + guint index_) +{ + JsonNode *node; + + g_return_val_if_fail (array != NULL, 0); + g_return_val_if_fail (index_ < array->elements->len, 0); + + node = g_ptr_array_index (array->elements, index_); + g_return_val_if_fail (node != NULL, 0); + g_return_val_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE, 0); + + return json_node_get_int (node); +} + +/** + * json_array_get_double_element: + * @array: a JSON array + * @index_: the index of the element to retrieve + * + * Conveniently retrieves the floating point value of the element at + * the given position inside an array. + * + * See also: [method@Json.Array.get_element], [method@Json.Node.get_double] + * + * Return value: the floating point value + * + * Since: 0.8 + */ +gdouble +json_array_get_double_element (JsonArray *array, + guint index_) +{ + JsonNode *node; + + g_return_val_if_fail (array != NULL, 0.0); + g_return_val_if_fail (index_ < array->elements->len, 0.0); + + node = g_ptr_array_index (array->elements, index_); + g_return_val_if_fail (node != NULL, 0.0); + g_return_val_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE, 0.0); + + return json_node_get_double (node); +} + +/** + * json_array_get_boolean_element: + * @array: a JSON array + * @index_: the index of the element to retrieve + * + * Conveniently retrieves the boolean value of the element at the given + * position inside an array. + * + * See also: [method@Json.Array.get_element], [method@Json.Node.get_boolean] + * + * Return value: the boolean value + * + * Since: 0.8 + */ +gboolean +json_array_get_boolean_element (JsonArray *array, + guint index_) +{ + JsonNode *node; + + g_return_val_if_fail (array != NULL, FALSE); + g_return_val_if_fail (index_ < array->elements->len, FALSE); + + node = g_ptr_array_index (array->elements, index_); + g_return_val_if_fail (node != NULL, FALSE); + g_return_val_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE, FALSE); + + return json_node_get_boolean (node); +} + +/** + * json_array_get_string_element: + * @array: a JSON array + * @index_: the index of the element to retrieve + * + * Conveniently retrieves the string value of the element at the given + * position inside an array. + * + * See also: [method@Json.Array.get_element], [method@Json.Node.get_string] + * + * Return value: (transfer none): the string value + * + * Since: 0.8 + */ +const gchar * +json_array_get_string_element (JsonArray *array, + guint index_) +{ + JsonNode *node; + + g_return_val_if_fail (array != NULL, NULL); + g_return_val_if_fail (index_ < array->elements->len, NULL); + + node = g_ptr_array_index (array->elements, index_); + g_return_val_if_fail (node != NULL, NULL); + g_return_val_if_fail (JSON_NODE_HOLDS_VALUE (node) || JSON_NODE_HOLDS_NULL (node), NULL); + + if (JSON_NODE_HOLDS_NULL (node)) + return NULL; + + return json_node_get_string (node); +} + +/** + * json_array_get_null_element: + * @array: a JSON array + * @index_: the index of the element to retrieve + * + * Conveniently checks whether the element at the given position inside the + * array contains a `null` value. + * + * See also: [method@Json.Array.get_element], [method@Json.Node.is_null] + * + * Return value: `TRUE` if the element is `null` + * + * Since: 0.8 + */ +gboolean +json_array_get_null_element (JsonArray *array, + guint index_) +{ + JsonNode *node; + + g_return_val_if_fail (array != NULL, FALSE); + g_return_val_if_fail (index_ < array->elements->len, FALSE); + + node = g_ptr_array_index (array->elements, index_); + g_return_val_if_fail (node != NULL, FALSE); + + if (JSON_NODE_HOLDS_NULL (node)) + return TRUE; + + if (JSON_NODE_HOLDS_ARRAY (node)) + return json_node_get_array (node) == NULL; + + if (JSON_NODE_HOLDS_OBJECT (node)) + return json_node_get_object (node) == NULL; + + return FALSE; +} + +/** + * json_array_get_array_element: + * @array: a JSON array + * @index_: the index of the element to retrieve + * + * Conveniently retrieves the array at the given position inside an array. + * + * See also: [method@Json.Array.get_element], [method@Json.Node.get_array] + * + * Return value: (transfer none): the array + * + * Since: 0.8 + */ +JsonArray * +json_array_get_array_element (JsonArray *array, + guint index_) +{ + JsonNode *node; + + g_return_val_if_fail (array != NULL, NULL); + g_return_val_if_fail (index_ < array->elements->len, NULL); + + node = g_ptr_array_index (array->elements, index_); + g_return_val_if_fail (node != NULL, NULL); + g_return_val_if_fail (JSON_NODE_HOLDS_ARRAY (node) || JSON_NODE_HOLDS_NULL (node), NULL); + + if (JSON_NODE_HOLDS_NULL (node)) + return NULL; + + return json_node_get_array (node); +} + +/** + * json_array_get_object_element: + * @array: a JSON array + * @index_: the index of the element to retrieve + * + * Conveniently retrieves the object at the given position inside an array. + * + * See also: [method@Json.Array.get_element], [method@Json.Node.get_object] + * + * Return value: (transfer none): the object + * + * Since: 0.8 + */ +JsonObject * +json_array_get_object_element (JsonArray *array, + guint index_) +{ + JsonNode *node; + + g_return_val_if_fail (array != NULL, NULL); + g_return_val_if_fail (index_ < array->elements->len, NULL); + + node = g_ptr_array_index (array->elements, index_); + g_return_val_if_fail (node != NULL, NULL); + g_return_val_if_fail (JSON_NODE_HOLDS_OBJECT (node) || JSON_NODE_HOLDS_NULL (node), NULL); + + if (JSON_NODE_HOLDS_NULL (node)) + return NULL; + + return json_node_get_object (node); +} + +/** + * json_array_get_length: + * @array: a JSON array + * + * Retrieves the length of the given array + * + * Return value: the length of the array + */ +guint +json_array_get_length (JsonArray *array) +{ + g_return_val_if_fail (array != NULL, 0); + + return array->elements->len; +} + +/** + * json_array_add_element: + * @array: a JSON array + * @node: (transfer full): the element to add + * + * Appends the given `node` inside an array. + */ +void +json_array_add_element (JsonArray *array, + JsonNode *node) +{ + g_return_if_fail (array != NULL); + g_return_if_fail (node != NULL); + + g_ptr_array_add (array->elements, node); +} + +/** + * json_array_add_int_element: + * @array: a JSON array + * @value: the integer value to add + * + * Conveniently adds the given integer value into an array. + * + * See also: [method@Json.Array.add_element], [method@Json.Node.set_int] + * + * Since: 0.8 + */ +void +json_array_add_int_element (JsonArray *array, + gint64 value) +{ + g_return_if_fail (array != NULL); + + json_array_add_element (array, json_node_init_int (json_node_alloc (), value)); +} + +/** + * json_array_add_double_element: + * @array: a JSON array + * @value: the floating point value to add + * + * Conveniently adds the given floating point value into an array. + * + * See also: [method@Json.Array.add_element], [method@Json.Node.set_double] + * + * Since: 0.8 + */ +void +json_array_add_double_element (JsonArray *array, + gdouble value) +{ + g_return_if_fail (array != NULL); + + json_array_add_element (array, json_node_init_double (json_node_alloc (), value)); +} + +/** + * json_array_add_boolean_element: + * @array: a JSON array + * @value: the boolean value to add + * + * Conveniently adds the given boolean value into an array. + * + * See also: [method@Json.Array.add_element], [method@Json.Node.set_boolean] + * + * Since: 0.8 + */ +void +json_array_add_boolean_element (JsonArray *array, + gboolean value) +{ + g_return_if_fail (array != NULL); + + json_array_add_element (array, json_node_init_boolean (json_node_alloc (), value)); +} + +/** + * json_array_add_string_element: + * @array: a JSON array + * @value: the string value to add + * + * Conveniently adds the given string value into an array. + * + * See also: [method@Json.Array.add_element], [method@Json.Node.set_string] + * + * Since: 0.8 + */ +void +json_array_add_string_element (JsonArray *array, + const gchar *value) +{ + JsonNode *node; + + g_return_if_fail (array != NULL); + + node = json_node_alloc (); + + if (value != NULL) + json_node_init_string (node, value); + else + json_node_init_null (node); + + json_array_add_element (array, node); +} + +/** + * json_array_add_null_element: + * @array: a JSON array + * + * Conveniently adds a `null` element into an array + * + * See also: [method@Json.Array.add_element], `JSON_NODE_NULL` + * + * Since: 0.8 + */ +void +json_array_add_null_element (JsonArray *array) +{ + g_return_if_fail (array != NULL); + + json_array_add_element (array, json_node_init_null (json_node_alloc ())); +} + +/** + * json_array_add_array_element: + * @array: a JSON array + * @value: (nullable) (transfer full): the array to add + * + * Conveniently adds an array element into an array. + * + * If `value` is `NULL`, a `null` element will be added instead. + * + * See also: [method@Json.Array.add_element], [method@Json.Node.take_array] + * + * Since: 0.8 + */ +void +json_array_add_array_element (JsonArray *array, + JsonArray *value) +{ + JsonNode *node; + + g_return_if_fail (array != NULL); + + node = json_node_alloc (); + + if (value != NULL) + { + json_node_init_array (node, value); + json_array_unref (value); + } + else + json_node_init_null (node); + + json_array_add_element (array, node); +} + +/** + * json_array_add_object_element: + * @array: a JSON array + * @value: (transfer full) (nullable): the object to add + * + * Conveniently adds an object into an array. + * + * If `value` is `NULL`, a `null` element will be added instead. + * + * See also: [method@Json.Array.add_element], [method@Json.Node.take_object] + * + * Since: 0.8 + */ +void +json_array_add_object_element (JsonArray *array, + JsonObject *value) +{ + JsonNode *node; + + g_return_if_fail (array != NULL); + + node = json_node_alloc (); + + if (value != NULL) + { + json_node_init_object (node, value); + json_object_unref (value); + } + else + json_node_init_null (node); + + json_array_add_element (array, node); +} + +/** + * json_array_remove_element: + * @array: a JSON array + * @index_: the position of the element to be removed + * + * Removes the element at the given position inside an array. + * + * This function will release the reference held on the element. + */ +void +json_array_remove_element (JsonArray *array, + guint index_) +{ + g_return_if_fail (array != NULL); + g_return_if_fail (index_ < array->elements->len); + + json_node_unref (g_ptr_array_remove_index (array->elements, index_)); +} + +/** + * json_array_foreach_element: + * @array: a JSON array + * @func: (scope call): the function to be called on each element + * @data: (closure): data to be passed to the function + * + * Iterates over all elements of an array, and calls a function on + * each one of them. + * + * It is safe to change the value of an element of the array while + * iterating over it, but it is not safe to add or remove elements + * from the array. + * + * Since: 0.8 + */ +void +json_array_foreach_element (JsonArray *array, + JsonArrayForeach func, + gpointer data) +{ + g_return_if_fail (array != NULL); + g_return_if_fail (func != NULL); + + for (guint i = 0; i < array->elements->len; i++) + { + JsonNode *element_node; + + element_node = g_ptr_array_index (array->elements, i); + + (* func) (array, i, element_node, data); + } +} + +/** + * json_array_hash: + * @key: (type JsonArray) (not nullable): a JSON array to hash + * + * Calculates a hash value for the given `key`. + * + * The hash is calculated over the array and all its elements, recursively. + * + * If the array is immutable, this is a fast operation; otherwise, it scales + * proportionally with the length of the array. + * + * Returns: hash value for the key + * Since: 1.2 + */ +guint +json_array_hash (gconstpointer key) +{ + JsonArray *array; /* unowned */ + guint hash = 0; + guint i; + + g_return_val_if_fail (key != NULL, 0); + + array = (JsonArray *) key; + + /* If the array is immutable, we can use the calculated hash. */ + if (array->immutable) + return array->immutable_hash; + + /* Otherwise, calculate the hash. */ + for (i = 0; i < array->elements->len; i++) + { + JsonNode *node = g_ptr_array_index (array->elements, i); + hash ^= (i ^ json_node_hash (node)); + } + + return hash; +} + +/** + * json_array_equal: + * @a: (type JsonArray) (not nullable): a JSON array + * @b: (type JsonArray) (not nullable): another JSON array + * + * Check whether two arrays are equal. + * + * Equality is defined as: + * + * - the array have the same number of elements + * - the values of elements in corresponding positions are equal + * + * Returns: `TRUE` if the arrays are equal, and `FALSE` otherwise + * Since: 1.2 + */ +gboolean +json_array_equal (gconstpointer a, + gconstpointer b) +{ + JsonArray *array_a, *array_b; /* unowned */ + guint length_a, length_b, i; + + g_return_val_if_fail (a != NULL, FALSE); + g_return_val_if_fail (b != NULL, FALSE); + + array_a = (JsonArray *) a; + array_b = (JsonArray *) b; + + /* Identity comparison. */ + if (array_a == array_b) + return TRUE; + + /* Check lengths. */ + length_a = json_array_get_length (array_a); + length_b = json_array_get_length (array_b); + + if (length_a != length_b) + return FALSE; + + /* Check elements. */ + for (i = 0; i < length_a; i++) + { + JsonNode *child_a, *child_b; /* unowned */ + + child_a = json_array_get_element (array_a, i); + child_b = json_array_get_element (array_b, i); + + if (!json_node_equal (child_a, child_b)) + return FALSE; + } + + return TRUE; +} diff --git a/lsp/deps/json-glib/json-builder.c b/lsp/deps/json-glib/json-builder.c new file mode 100644 index 000000000..bc1164656 --- /dev/null +++ b/lsp/deps/json-glib/json-builder.c @@ -0,0 +1,845 @@ +/* json-generator.c - JSON tree builder + * + * This file is part of JSON-GLib + * Copyright (C) 2010 Luca Bruno + * Copyright (C) 2015 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Luca Bruno + * Philip Withnall + */ + +/** + * JsonBuilder: + * + * `JsonBuilder` provides an object for generating a JSON tree. + * + * The root of the JSON tree can be either a [struct@Json.Object] or a [struct@Json.Array]. + * Thus the first call must necessarily be either + * [method@Json.Builder.begin_object] or [method@Json.Builder.begin_array]. + * + * For convenience to language bindings, most `JsonBuilder` method return the + * instance, making it easy to chain function calls. + * + * ## Using `JsonBuilder` + * + * ```c + * g_autoptr(JsonBuilder) builder = json_builder_new (); + * + * json_builder_begin_object (builder); + * + * json_builder_set_member_name (builder, "url"); + * json_builder_add_string_value (builder, "http://www.gnome.org/img/flash/two-thirty.png"); + * + * json_builder_set_member_name (builder, "size"); + * json_builder_begin_array (builder); + * json_builder_add_int_value (builder, 652); + * json_builder_add_int_value (builder, 242); + * json_builder_end_array (builder); + * + * json_builder_end_object (builder); + * + * g_autoptr(JsonNode) root = json_builder_get_root (builder); + * + * g_autoptr(JsonGenerator) gen = json_generator_new (); + * json_generator_set_root (gen, root); + * g_autofree char *str = json_generator_to_data (gen, NULL); + * + * // str now contains the following JSON data + * // { "url" : "http://www.gnome.org/img/flash/two-thirty.png", "size" : [ 652, 242 ] } + * ``` + */ + +#include "config.h" + +#include +#include + +#include "json-types-private.h" + +#include "json-builder.h" + +struct _JsonBuilderPrivate +{ + GQueue *stack; + JsonNode *root; + gboolean immutable; +}; + +enum +{ + PROP_IMMUTABLE = 1, + PROP_LAST +}; + +static GParamSpec *builder_props[PROP_LAST] = { NULL, }; + +typedef enum +{ + JSON_BUILDER_MODE_OBJECT, + JSON_BUILDER_MODE_ARRAY, + JSON_BUILDER_MODE_MEMBER +} JsonBuilderMode; + +typedef struct +{ + JsonBuilderMode mode; + + union + { + JsonObject *object; + JsonArray *array; + } data; + gchar *member_name; +} JsonBuilderState; + +static void +json_builder_state_free (JsonBuilderState *state) +{ + if (G_LIKELY (state)) + { + switch (state->mode) + { + case JSON_BUILDER_MODE_OBJECT: + case JSON_BUILDER_MODE_MEMBER: + json_object_unref (state->data.object); + g_free (state->member_name); + state->data.object = NULL; + state->member_name = NULL; + break; + + case JSON_BUILDER_MODE_ARRAY: + json_array_unref (state->data.array); + state->data.array = NULL; + break; + + default: + g_assert_not_reached (); + } + + g_slice_free (JsonBuilderState, state); + } +} + +G_DEFINE_TYPE_WITH_PRIVATE (JsonBuilder, json_builder, G_TYPE_OBJECT) + +static void +json_builder_free_all_state (JsonBuilder *builder) +{ + JsonBuilderState *state; + + while (!g_queue_is_empty (builder->priv->stack)) + { + state = g_queue_pop_head (builder->priv->stack); + json_builder_state_free (state); + } + + if (builder->priv->root) + { + json_node_unref (builder->priv->root); + builder->priv->root = NULL; + } +} + +static void +json_builder_finalize (GObject *gobject) +{ + JsonBuilderPrivate *priv = json_builder_get_instance_private ((JsonBuilder *) gobject); + + json_builder_free_all_state (JSON_BUILDER (gobject)); + + g_queue_free (priv->stack); + priv->stack = NULL; + + G_OBJECT_CLASS (json_builder_parent_class)->finalize (gobject); +} + +static void +json_builder_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + JsonBuilderPrivate *priv = JSON_BUILDER (gobject)->priv; + + switch (prop_id) + { + case PROP_IMMUTABLE: + /* Construct-only. */ + priv->immutable = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +json_builder_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + JsonBuilderPrivate *priv = JSON_BUILDER (gobject)->priv; + + switch (prop_id) + { + case PROP_IMMUTABLE: + g_value_set_boolean (value, priv->immutable); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +json_builder_class_init (JsonBuilderClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + /** + * JsonBuilder:immutable: + * + * Whether the tree should be immutable when created. + * + * Making the output immutable on creation avoids the expense + * of traversing it to make it immutable later. + * + * Since: 1.2 + */ + builder_props[PROP_IMMUTABLE] = + g_param_spec_boolean ("immutable", + "Immutable Output", + "Whether the builder output is immutable.", + FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + + gobject_class->set_property = json_builder_set_property; + gobject_class->get_property = json_builder_get_property; + gobject_class->finalize = json_builder_finalize; + + g_object_class_install_properties (gobject_class, PROP_LAST, builder_props); +} + +static void +json_builder_init (JsonBuilder *builder) +{ + JsonBuilderPrivate *priv = json_builder_get_instance_private (builder); + + builder->priv = priv; + + priv->stack = g_queue_new (); + priv->root = NULL; +} + +static inline JsonBuilderMode +json_builder_current_mode (JsonBuilder *builder) +{ + JsonBuilderState *state = g_queue_peek_head (builder->priv->stack); + return state->mode; +} + +static inline gboolean +json_builder_is_valid_add_mode (JsonBuilder *builder) +{ + JsonBuilderMode mode = json_builder_current_mode (builder); + return mode == JSON_BUILDER_MODE_MEMBER || mode == JSON_BUILDER_MODE_ARRAY; +} + +/** + * json_builder_new: + * + * Creates a new `JsonBuilder`. + * + * You can use this object to generate a JSON tree and obtain the root node. + * + * Return value: the newly created builder instance + */ +JsonBuilder * +json_builder_new (void) +{ + return g_object_new (JSON_TYPE_BUILDER, NULL); +} + +/** + * json_builder_new_immutable: (constructor) + * + * Creates a new, immutable `JsonBuilder` instance. + * + * It is equivalent to setting the [property@Json.Builder:immutable] property + * set to `TRUE` at construction time. + * + * Since: 1.2 + * Returns: (transfer full): the newly create builder instance + */ +JsonBuilder * +json_builder_new_immutable (void) +{ + return g_object_new (JSON_TYPE_BUILDER, "immutable", TRUE, NULL); +} + +/** + * json_builder_get_root: + * @builder: a builder + * + * Returns the root of the currently constructed tree. + * + * if the build is incomplete (ie: if there are any opened objects, or any + * open object members and array elements) then this function will return + * `NULL`. + * + * Return value: (nullable) (transfer full): the root node + */ +JsonNode * +json_builder_get_root (JsonBuilder *builder) +{ + JsonNode *root = NULL; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + + if (builder->priv->root) + root = json_node_copy (builder->priv->root); + + /* Sanity check. */ + g_assert (!builder->priv->immutable || + root == NULL || + json_node_is_immutable (root)); + + return root; +} + +/** + * json_builder_reset: + * @builder: a builder + * + * Resets the state of the builder back to its initial state. + */ +void +json_builder_reset (JsonBuilder *builder) +{ + g_return_if_fail (JSON_IS_BUILDER (builder)); + + json_builder_free_all_state (builder); +} + +/** + * json_builder_begin_object: + * @builder: a builder + * + * Opens an object inside the given builder. + * + * You can add a new member to the object by using [method@Json.Builder.set_member_name], + * followed by [method@Json.Builder.add_value]. + * + * Once you added all members to the object, you must call [method@Json.Builder.end_object] + * to close the object. + * + * If the builder is in an inconsistent state, this function will return `NULL`. + * + * Return value: (nullable) (transfer none): the builder instance + */ +JsonBuilder * +json_builder_begin_object (JsonBuilder *builder) +{ + JsonObject *object; + JsonBuilderState *state; + JsonBuilderState *cur_state; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + g_return_val_if_fail (builder->priv->root == NULL, NULL); + g_return_val_if_fail (g_queue_is_empty (builder->priv->stack) || json_builder_is_valid_add_mode (builder), NULL); + + object = json_object_new (); + cur_state = g_queue_peek_head (builder->priv->stack); + if (cur_state) + { + switch (cur_state->mode) + { + case JSON_BUILDER_MODE_ARRAY: + json_array_add_object_element (cur_state->data.array, json_object_ref (object)); + break; + + case JSON_BUILDER_MODE_MEMBER: + json_object_set_object_member (cur_state->data.object, cur_state->member_name, json_object_ref (object)); + g_free (cur_state->member_name); + cur_state->member_name = NULL; + cur_state->mode = JSON_BUILDER_MODE_OBJECT; + break; + + default: + g_assert_not_reached (); + } + } + + state = g_slice_new (JsonBuilderState); + state->data.object = object; + state->member_name = NULL; + state->mode = JSON_BUILDER_MODE_OBJECT; + g_queue_push_head (builder->priv->stack, state); + + return builder; +} + +/** + * json_builder_end_object: + * @builder: a builder + * + * Closes the object inside the given builder that was opened by the most + * recent call to [method@Json.Builder.begin_object]. + * + * This function cannot be called after [method@Json.Builder.set_member_name]. + * + * Return value: (nullable) (transfer none): the builder instance + */ +JsonBuilder * +json_builder_end_object (JsonBuilder *builder) +{ + JsonBuilderState *state; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + g_return_val_if_fail (!g_queue_is_empty (builder->priv->stack), NULL); + g_return_val_if_fail (json_builder_current_mode (builder) == JSON_BUILDER_MODE_OBJECT, NULL); + + state = g_queue_pop_head (builder->priv->stack); + + if (builder->priv->immutable) + json_object_seal (state->data.object); + + if (g_queue_is_empty (builder->priv->stack)) + { + builder->priv->root = json_node_new (JSON_NODE_OBJECT); + json_node_take_object (builder->priv->root, json_object_ref (state->data.object)); + + if (builder->priv->immutable) + json_node_seal (builder->priv->root); + } + + json_builder_state_free (state); + + return builder; +} + +/** + * json_builder_begin_array: + * @builder: a builder + * + * Opens an array inside the given builder. + * + * You can add a new element to the array by using [method@Json.Builder.add_value]. + * + * Once you added all elements to the array, you must call + * [method@Json.Builder.end_array] to close the array. + * + * Return value: (nullable) (transfer none): the builder instance + */ +JsonBuilder * +json_builder_begin_array (JsonBuilder *builder) +{ + JsonArray *array; + JsonBuilderState *state; + JsonBuilderState *cur_state; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + g_return_val_if_fail (builder->priv->root == NULL, NULL); + g_return_val_if_fail (g_queue_is_empty (builder->priv->stack) || json_builder_is_valid_add_mode (builder), NULL); + + array = json_array_new (); + cur_state = g_queue_peek_head (builder->priv->stack); + if (cur_state) + { + switch (cur_state->mode) + { + case JSON_BUILDER_MODE_ARRAY: + json_array_add_array_element (cur_state->data.array, json_array_ref (array)); + break; + + case JSON_BUILDER_MODE_MEMBER: + json_object_set_array_member (cur_state->data.object, cur_state->member_name, json_array_ref (array)); + g_free (cur_state->member_name); + cur_state->member_name = NULL; + cur_state->mode = JSON_BUILDER_MODE_OBJECT; + break; + + default: + g_assert_not_reached (); + } + } + + state = g_slice_new (JsonBuilderState); + state->data.array = array; + state->mode = JSON_BUILDER_MODE_ARRAY; + g_queue_push_head (builder->priv->stack, state); + + return builder; +} + +/** + * json_builder_end_array: + * @builder: a builder + * + * Closes the array inside the given builder that was opened by the most + * recent call to [method@Json.Builder.begin_array]. + * + * This function cannot be called after [method@Json.Builder.set_member_name]. + * + * Return value: (nullable) (transfer none): the builder instance + */ +JsonBuilder * +json_builder_end_array (JsonBuilder *builder) +{ + JsonBuilderState *state; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + g_return_val_if_fail (!g_queue_is_empty (builder->priv->stack), NULL); + g_return_val_if_fail (json_builder_current_mode (builder) == JSON_BUILDER_MODE_ARRAY, NULL); + + state = g_queue_pop_head (builder->priv->stack); + + if (builder->priv->immutable) + json_array_seal (state->data.array); + + if (g_queue_is_empty (builder->priv->stack)) + { + builder->priv->root = json_node_new (JSON_NODE_ARRAY); + json_node_take_array (builder->priv->root, json_array_ref (state->data.array)); + + if (builder->priv->immutable) + json_node_seal (builder->priv->root); + } + + json_builder_state_free (state); + + return builder; +} + +/** + * json_builder_set_member_name: + * @builder: a builder + * @member_name: the name of the member + * + * Sets the name of the member in an object. + * + * This function must be followed by of these functions: + * + * - [method@Json.Builder.add_value], to add a scalar value to the member + * - [method@Json.Builder.begin_object], to add an object to the member + * - [method@Json.Builder.begin_array], to add an array to the member + * + * This function can only be called within an open object. + * + * Return value: (nullable) (transfer none): the builder instance + */ +JsonBuilder * +json_builder_set_member_name (JsonBuilder *builder, + const gchar *member_name) +{ + JsonBuilderState *state; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + g_return_val_if_fail (member_name != NULL, NULL); + g_return_val_if_fail (!g_queue_is_empty (builder->priv->stack), NULL); + g_return_val_if_fail (json_builder_current_mode (builder) == JSON_BUILDER_MODE_OBJECT, NULL); + + state = g_queue_peek_head (builder->priv->stack); + state->member_name = g_strdup (member_name); + state->mode = JSON_BUILDER_MODE_MEMBER; + + return builder; +} + +/** + * json_builder_add_value: + * @builder: a builder + * @node: (transfer full): the value of the member or element + * + * Adds a value to the currently open object member or array. + * + * If called after [method@Json.Builder.set_member_name], sets the given node + * as the value of the current member in the open object; otherwise, the node + * is appended to the elements of the open array. + * + * The builder will take ownership of the node. + * + * Return value: (nullable) (transfer none): the builder instance + */ +JsonBuilder * +json_builder_add_value (JsonBuilder *builder, + JsonNode *node) +{ + JsonBuilderState *state; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + g_return_val_if_fail (node != NULL, NULL); + g_return_val_if_fail (!g_queue_is_empty (builder->priv->stack), NULL); + g_return_val_if_fail (json_builder_is_valid_add_mode (builder), NULL); + + state = g_queue_peek_head (builder->priv->stack); + + if (builder->priv->immutable) + json_node_seal (node); + + switch (state->mode) + { + case JSON_BUILDER_MODE_MEMBER: + json_object_set_member (state->data.object, state->member_name, node); + g_free (state->member_name); + state->member_name = NULL; + state->mode = JSON_BUILDER_MODE_OBJECT; + break; + + case JSON_BUILDER_MODE_ARRAY: + json_array_add_element (state->data.array, node); + break; + + default: + g_assert_not_reached (); + } + + return builder; +} + +/** + * json_builder_add_int_value: + * @builder: a builder + * @value: the value of the member or element + * + * Adds an integer value to the currently open object member or array. + * + * If called after [method@Json.Builder.set_member_name], sets the given value + * as the value of the current member in the open object; otherwise, the value + * is appended to the elements of the open array. + * + * See also: [method@Json.Builder.add_value] + * + * Return value: (nullable) (transfer none): the builder instance + */ +JsonBuilder * +json_builder_add_int_value (JsonBuilder *builder, + gint64 value) +{ + JsonBuilderState *state; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + g_return_val_if_fail (!g_queue_is_empty (builder->priv->stack), NULL); + g_return_val_if_fail (json_builder_is_valid_add_mode (builder), NULL); + + state = g_queue_peek_head (builder->priv->stack); + switch (state->mode) + { + case JSON_BUILDER_MODE_MEMBER: + json_object_set_int_member (state->data.object, state->member_name, value); + g_free (state->member_name); + state->member_name = NULL; + state->mode = JSON_BUILDER_MODE_OBJECT; + break; + + case JSON_BUILDER_MODE_ARRAY: + json_array_add_int_element (state->data.array, value); + break; + + default: + g_assert_not_reached (); + } + + return builder; +} + +/** + * json_builder_add_double_value: + * @builder: a builder + * @value: the value of the member or element + * + * Adds a floating point value to the currently open object member or array. + * + * If called after [method@Json.Builder.set_member_name], sets the given value + * as the value of the current member in the open object; otherwise, the value + * is appended to the elements of the open array. + * + * See also: [method@Json.Builder.add_value] + * + * Return value: (nullable) (transfer none): the builder instance + */ +JsonBuilder * +json_builder_add_double_value (JsonBuilder *builder, + gdouble value) +{ + JsonBuilderState *state; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + g_return_val_if_fail (!g_queue_is_empty (builder->priv->stack), NULL); + g_return_val_if_fail (json_builder_is_valid_add_mode (builder), NULL); + + state = g_queue_peek_head (builder->priv->stack); + + switch (state->mode) + { + case JSON_BUILDER_MODE_MEMBER: + json_object_set_double_member (state->data.object, state->member_name, value); + g_free (state->member_name); + state->member_name = NULL; + state->mode = JSON_BUILDER_MODE_OBJECT; + break; + + case JSON_BUILDER_MODE_ARRAY: + json_array_add_double_element (state->data.array, value); + break; + + default: + g_assert_not_reached (); + } + + return builder; +} + +/** + * json_builder_add_boolean_value: + * @builder: a builder + * @value: the value of the member or element + * + * Adds a boolean value to the currently open object member or array. + * + * If called after [method@Json.Builder.set_member_name], sets the given value + * as the value of the current member in the open object; otherwise, the value + * is appended to the elements of the open array. + * + * See also: [method@Json.Builder.add_value] + * + * Return value: (nullable) (transfer none): the builder instance + */ +JsonBuilder * +json_builder_add_boolean_value (JsonBuilder *builder, + gboolean value) +{ + JsonBuilderState *state; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + g_return_val_if_fail (!g_queue_is_empty (builder->priv->stack), NULL); + g_return_val_if_fail (json_builder_is_valid_add_mode (builder), NULL); + + state = g_queue_peek_head (builder->priv->stack); + + switch (state->mode) + { + case JSON_BUILDER_MODE_MEMBER: + json_object_set_boolean_member (state->data.object, state->member_name, value); + g_free (state->member_name); + state->member_name = NULL; + state->mode = JSON_BUILDER_MODE_OBJECT; + break; + + case JSON_BUILDER_MODE_ARRAY: + json_array_add_boolean_element (state->data.array, value); + break; + + default: + g_assert_not_reached (); + } + + return builder; +} + +/** + * json_builder_add_string_value: + * @builder: a builder + * @value: the value of the member or element + * + * Adds a string value to the currently open object member or array. + * + * If called after [method@Json.Builder.set_member_name], sets the given value + * as the value of the current member in the open object; otherwise, the value + * is appended to the elements of the open array. + * + * See also: [method@Json.Builder.add_value] + * + * Return value: (nullable) (transfer none): the builder instance + */ +JsonBuilder * +json_builder_add_string_value (JsonBuilder *builder, + const gchar *value) +{ + JsonBuilderState *state; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + g_return_val_if_fail (!g_queue_is_empty (builder->priv->stack), NULL); + g_return_val_if_fail (json_builder_is_valid_add_mode (builder), NULL); + + state = g_queue_peek_head (builder->priv->stack); + + switch (state->mode) + { + case JSON_BUILDER_MODE_MEMBER: + json_object_set_string_member (state->data.object, state->member_name, value); + g_free (state->member_name); + state->member_name = NULL; + state->mode = JSON_BUILDER_MODE_OBJECT; + break; + + case JSON_BUILDER_MODE_ARRAY: + json_array_add_string_element (state->data.array, value); + break; + + default: + g_assert_not_reached (); + } + + return builder; +} + +/** + * json_builder_add_null_value: + * @builder: a builder + * + * Adds a null value to the currently open object member or array. + * + * If called after [method@Json.Builder.set_member_name], sets the given value + * as the value of the current member in the open object; otherwise, the value + * is appended to the elements of the open array. + * + * See also: [method@Json.Builder.add_value] + * + * Return value: (nullable) (transfer none): the builder instance + */ +JsonBuilder * +json_builder_add_null_value (JsonBuilder *builder) +{ + JsonBuilderState *state; + + g_return_val_if_fail (JSON_IS_BUILDER (builder), NULL); + g_return_val_if_fail (!g_queue_is_empty (builder->priv->stack), NULL); + g_return_val_if_fail (json_builder_is_valid_add_mode (builder), NULL); + + state = g_queue_peek_head (builder->priv->stack); + + switch (state->mode) + { + case JSON_BUILDER_MODE_MEMBER: + json_object_set_null_member (state->data.object, state->member_name); + g_free (state->member_name); + state->member_name = NULL; + state->mode = JSON_BUILDER_MODE_OBJECT; + break; + + case JSON_BUILDER_MODE_ARRAY: + json_array_add_null_element (state->data.array); + break; + + default: + g_assert_not_reached (); + } + + return builder; +} diff --git a/lsp/deps/json-glib/json-builder.h b/lsp/deps/json-glib/json-builder.h new file mode 100644 index 000000000..99f3e61f2 --- /dev/null +++ b/lsp/deps/json-glib/json-builder.h @@ -0,0 +1,108 @@ +/* json-builder.h: JSON tree builder + * + * This file is part of JSON-GLib + * Copyright (C) 2010 Luca Bruno + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Luca Bruno + */ + +#pragma once + +#if !defined(__JSON_GLIB_INSIDE__) && !defined(JSON_COMPILATION) +#error "Only can be included directly." +#endif + +#include + +G_BEGIN_DECLS + +#define JSON_TYPE_BUILDER (json_builder_get_type ()) +#define JSON_BUILDER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), JSON_TYPE_BUILDER, JsonBuilder)) +#define JSON_IS_BUILDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), JSON_TYPE_BUILDER)) +#define JSON_BUILDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), JSON_TYPE_BUILDER, JsonBuilderClass)) +#define JSON_IS_BUILDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), JSON_TYPE_BUILDER)) +#define JSON_BUILDER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), JSON_TYPE_BUILDER, JsonBuilderClass)) + +typedef struct _JsonBuilder JsonBuilder; +typedef struct _JsonBuilderPrivate JsonBuilderPrivate; +typedef struct _JsonBuilderClass JsonBuilderClass; + +struct _JsonBuilder +{ + /*< private >*/ + GObject parent_instance; + + JsonBuilderPrivate *priv; +}; + +struct _JsonBuilderClass +{ + /*< private >*/ + GObjectClass parent_class; + + /* padding, for future expansion */ + void (* _json_reserved1) (void); + void (* _json_reserved2) (void); +}; + +JSON_AVAILABLE_IN_1_0 +GType json_builder_get_type (void) G_GNUC_CONST; + +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_new (void); +JSON_AVAILABLE_IN_1_2 +JsonBuilder *json_builder_new_immutable (void); +JSON_AVAILABLE_IN_1_0 +JsonNode *json_builder_get_root (JsonBuilder *builder); +JSON_AVAILABLE_IN_1_0 +void json_builder_reset (JsonBuilder *builder); + +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_begin_array (JsonBuilder *builder); +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_end_array (JsonBuilder *builder); +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_begin_object (JsonBuilder *builder); +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_end_object (JsonBuilder *builder); + +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_set_member_name (JsonBuilder *builder, + const gchar *member_name); +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_add_value (JsonBuilder *builder, + JsonNode *node); +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_add_int_value (JsonBuilder *builder, + gint64 value); +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_add_double_value (JsonBuilder *builder, + gdouble value); +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_add_boolean_value (JsonBuilder *builder, + gboolean value); +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_add_string_value (JsonBuilder *builder, + const gchar *value); +JSON_AVAILABLE_IN_1_0 +JsonBuilder *json_builder_add_null_value (JsonBuilder *builder); + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC (JsonBuilder, g_object_unref) +#endif + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-debug.c b/lsp/deps/json-glib/json-debug.c new file mode 100644 index 000000000..b9c3b8fc9 --- /dev/null +++ b/lsp/deps/json-glib/json-debug.c @@ -0,0 +1,38 @@ +#include "config.h" + +#include "json-debug.h" + +static unsigned int json_debug_flags = 0; + +#ifdef JSON_ENABLE_DEBUG +static const GDebugKey json_debug_keys[] = { + { "parser", JSON_DEBUG_PARSER }, + { "gobject", JSON_DEBUG_GOBJECT }, + { "path", JSON_DEBUG_PATH }, + { "node", JSON_DEBUG_NODE }, +}; +#endif /* JSON_ENABLE_DEBUG */ + +JsonDebugFlags +json_get_debug_flags (void) +{ +#ifdef JSON_ENABLE_DEBUG + static gboolean json_debug_flags_set; + const gchar *env_str; + + if (G_LIKELY (json_debug_flags_set)) + return json_debug_flags; + + env_str = g_getenv ("JSON_DEBUG"); + if (env_str != NULL && *env_str != '\0') + { + json_debug_flags |= g_parse_debug_string (env_str, + json_debug_keys, + G_N_ELEMENTS (json_debug_keys)); + } + + json_debug_flags_set = TRUE; +#endif /* JSON_ENABLE_DEBUG */ + + return json_debug_flags; +} diff --git a/lsp/deps/json-glib/json-debug.h b/lsp/deps/json-glib/json-debug.h new file mode 100644 index 000000000..1b3da52e5 --- /dev/null +++ b/lsp/deps/json-glib/json-debug.h @@ -0,0 +1,51 @@ +#ifndef __JSON_DEBUG_H__ +#define __JSON_DEBUG_H__ + +#include + +G_BEGIN_DECLS + +typedef enum { + JSON_DEBUG_PARSER = 1 << 0, + JSON_DEBUG_GOBJECT = 1 << 1, + JSON_DEBUG_PATH = 1 << 2, + JSON_DEBUG_NODE = 1 << 3 +} JsonDebugFlags; + +#define JSON_HAS_DEBUG(flag) (json_get_debug_flags () & JSON_DEBUG_##flag) + +#ifdef JSON_ENABLE_DEBUG + +# ifdef __GNUC__ + +# define JSON_NOTE(type,x,a...) G_STMT_START { \ + if (JSON_HAS_DEBUG (type)) { \ + g_message ("[" #type "] " G_STRLOC ": " x, ##a); \ + } } G_STMT_END + +# else +/* Try the C99 version; unfortunately, this does not allow us to pass + * empty arguments to the macro, which means we have to + * do an intemediate printf. + */ +# define JSON_NOTE(type,...) G_STMT_START { \ + if (JSON_HAS_DEBUG (type)) { \ + gchar * _fmt = g_strdup_printf (__VA_ARGS__); \ + g_message ("[" #type "] " G_STRLOC ": %s",_fmt); \ + g_free (_fmt); \ + } } G_STMT_END + +# endif /* __GNUC__ */ + +#else + +#define JSON_NOTE(type,...) G_STMT_START { } G_STMT_END + +#endif /* JSON_ENABLE_DEBUG */ + +G_GNUC_INTERNAL +JsonDebugFlags json_get_debug_flags (void); + +G_END_DECLS + +#endif /* __JSON_DEBUG_H__ */ diff --git a/lsp/deps/json-glib/json-enum-types.c b/lsp/deps/json-glib/json-enum-types.c new file mode 100644 index 000000000..5aecb7de6 --- /dev/null +++ b/lsp/deps/json-glib/json-enum-types.c @@ -0,0 +1,123 @@ + +/* This file is generated by glib-mkenums, do not modify it. This code is licensed under the same license as the containing project. Note that it links to GLib, so must comply with the LGPL linking clauses. */ + +#ifndef JSON_COMPILATION +#define JSON_COMPILATION +#endif + +#include "config.h" +#include "json-enum-types.h" + +/* enumerations from "json-parser.h" */ +#include "json-parser.h" +GType +json_parser_error_get_type (void) +{ + static gsize g_enum_type_id__volatile = 0; + + if (g_once_init_enter (&g_enum_type_id__volatile)) + { + static const GEnumValue values[] = { + { JSON_PARSER_ERROR_PARSE, "JSON_PARSER_ERROR_PARSE", "parse" }, + { JSON_PARSER_ERROR_TRAILING_COMMA, "JSON_PARSER_ERROR_TRAILING_COMMA", "trailing-comma" }, + { JSON_PARSER_ERROR_MISSING_COMMA, "JSON_PARSER_ERROR_MISSING_COMMA", "missing-comma" }, + { JSON_PARSER_ERROR_MISSING_COLON, "JSON_PARSER_ERROR_MISSING_COLON", "missing-colon" }, + { JSON_PARSER_ERROR_INVALID_BAREWORD, "JSON_PARSER_ERROR_INVALID_BAREWORD", "invalid-bareword" }, + { JSON_PARSER_ERROR_EMPTY_MEMBER_NAME, "JSON_PARSER_ERROR_EMPTY_MEMBER_NAME", "empty-member-name" }, + { JSON_PARSER_ERROR_INVALID_DATA, "JSON_PARSER_ERROR_INVALID_DATA", "invalid-data" }, + { JSON_PARSER_ERROR_UNKNOWN, "JSON_PARSER_ERROR_UNKNOWN", "unknown" }, + { JSON_PARSER_ERROR_NESTING, "JSON_PARSER_ERROR_NESTING", "nesting" }, + { JSON_PARSER_ERROR_INVALID_STRUCTURE, "JSON_PARSER_ERROR_INVALID_STRUCTURE", "invalid-structure" }, + { JSON_PARSER_ERROR_INVALID_ASSIGNMENT, "JSON_PARSER_ERROR_INVALID_ASSIGNMENT", "invalid-assignment" }, + { 0, NULL, NULL } + }; + + GType g_enum_type_id = + g_enum_register_static (g_intern_static_string ("JsonParserError"), values); + + g_once_init_leave (&g_enum_type_id__volatile, g_enum_type_id); + } + + return g_enum_type_id__volatile; +} + +/* enumerations from "json-path.h" */ +#include "json-path.h" +GType +json_path_error_get_type (void) +{ + static gsize g_enum_type_id__volatile = 0; + + if (g_once_init_enter (&g_enum_type_id__volatile)) + { + static const GEnumValue values[] = { + { JSON_PATH_ERROR_INVALID_QUERY, "JSON_PATH_ERROR_INVALID_QUERY", "query" }, + { 0, NULL, NULL } + }; + + GType g_enum_type_id = + g_enum_register_static (g_intern_static_string ("JsonPathError"), values); + + g_once_init_leave (&g_enum_type_id__volatile, g_enum_type_id); + } + + return g_enum_type_id__volatile; +} + +/* enumerations from "json-reader.h" */ +#include "json-reader.h" +GType +json_reader_error_get_type (void) +{ + static gsize g_enum_type_id__volatile = 0; + + if (g_once_init_enter (&g_enum_type_id__volatile)) + { + static const GEnumValue values[] = { + { JSON_READER_ERROR_NO_ARRAY, "JSON_READER_ERROR_NO_ARRAY", "no-array" }, + { JSON_READER_ERROR_INVALID_INDEX, "JSON_READER_ERROR_INVALID_INDEX", "invalid-index" }, + { JSON_READER_ERROR_NO_OBJECT, "JSON_READER_ERROR_NO_OBJECT", "no-object" }, + { JSON_READER_ERROR_INVALID_MEMBER, "JSON_READER_ERROR_INVALID_MEMBER", "invalid-member" }, + { JSON_READER_ERROR_INVALID_NODE, "JSON_READER_ERROR_INVALID_NODE", "invalid-node" }, + { JSON_READER_ERROR_NO_VALUE, "JSON_READER_ERROR_NO_VALUE", "no-value" }, + { JSON_READER_ERROR_INVALID_TYPE, "JSON_READER_ERROR_INVALID_TYPE", "invalid-type" }, + { 0, NULL, NULL } + }; + + GType g_enum_type_id = + g_enum_register_static (g_intern_static_string ("JsonReaderError"), values); + + g_once_init_leave (&g_enum_type_id__volatile, g_enum_type_id); + } + + return g_enum_type_id__volatile; +} + +/* enumerations from "json-types.h" */ +#include "json-types.h" +GType +json_node_type_get_type (void) +{ + static gsize g_enum_type_id__volatile = 0; + + if (g_once_init_enter (&g_enum_type_id__volatile)) + { + static const GEnumValue values[] = { + { JSON_NODE_OBJECT, "JSON_NODE_OBJECT", "object" }, + { JSON_NODE_ARRAY, "JSON_NODE_ARRAY", "array" }, + { JSON_NODE_VALUE, "JSON_NODE_VALUE", "value" }, + { JSON_NODE_NULL, "JSON_NODE_NULL", "null" }, + { 0, NULL, NULL } + }; + + GType g_enum_type_id = + g_enum_register_static (g_intern_static_string ("JsonNodeType"), values); + + g_once_init_leave (&g_enum_type_id__volatile, g_enum_type_id); + } + + return g_enum_type_id__volatile; +} + +/* Generated data ends here */ + diff --git a/lsp/deps/json-glib/json-enum-types.h b/lsp/deps/json-glib/json-enum-types.h new file mode 100644 index 000000000..51e9c31d4 --- /dev/null +++ b/lsp/deps/json-glib/json-enum-types.h @@ -0,0 +1,33 @@ + +/* This file is generated by glib-mkenums, do not modify it. This code is licensed under the same license as the containing project. Note that it links to GLib, so must comply with the LGPL linking clauses. */ + +#pragma once + +#if !defined(__JSON_GLIB_INSIDE__) && !defined(JSON_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include + +G_BEGIN_DECLS +/* enumerations from "json-parser.h" */ +JSON_AVAILABLE_IN_1_0 +GType json_parser_error_get_type (void) G_GNUC_CONST; +#define JSON_TYPE_PARSER_ERROR (json_parser_error_get_type()) +/* enumerations from "json-path.h" */ +JSON_AVAILABLE_IN_1_0 +GType json_path_error_get_type (void) G_GNUC_CONST; +#define JSON_TYPE_PATH_ERROR (json_path_error_get_type()) +/* enumerations from "json-reader.h" */ +JSON_AVAILABLE_IN_1_0 +GType json_reader_error_get_type (void) G_GNUC_CONST; +#define JSON_TYPE_READER_ERROR (json_reader_error_get_type()) +/* enumerations from "json-types.h" */ +JSON_AVAILABLE_IN_1_0 +GType json_node_type_get_type (void) G_GNUC_CONST; +#define JSON_TYPE_NODE_TYPE (json_node_type_get_type()) +G_END_DECLS + +/* Generated data ends here */ + diff --git a/lsp/deps/json-glib/json-gboxed.c b/lsp/deps/json-glib/json-gboxed.c new file mode 100644 index 000000000..8b51288eb --- /dev/null +++ b/lsp/deps/json-glib/json-gboxed.c @@ -0,0 +1,301 @@ +/* json-gboxed.c - JSON GBoxed integration + * + * This file is part of JSON-GLib + * + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Author: + * Emmanuele Bassi + */ + +#include "config.h" + +#include +#include + +#include "json-types-private.h" +#include "json-gobject.h" + +typedef struct _BoxedTransform BoxedTransform; + +struct _BoxedTransform +{ + GType boxed_type; + gint node_type; + + JsonBoxedSerializeFunc serialize; + JsonBoxedDeserializeFunc deserialize; +}; + +G_LOCK_DEFINE_STATIC (boxed_serialize); +static GSList *boxed_serialize = NULL; + +G_LOCK_DEFINE_STATIC (boxed_deserialize); +static GSList *boxed_deserialize = NULL; + +static gint +boxed_transforms_cmp (gconstpointer a, + gconstpointer b) +{ + const BoxedTransform *ta = a; + const BoxedTransform *tb = b; + + return tb->boxed_type - ta->boxed_type; +} + +static gint +boxed_transforms_find (gconstpointer a, + gconstpointer b) +{ + const BoxedTransform *haystack = a; + const BoxedTransform *needle = b; + + if (needle->node_type != -1) + return (haystack->boxed_type == needle->boxed_type && + haystack->node_type == needle->node_type) ? 0 : 1; + else + return (haystack->boxed_type == needle->boxed_type) ? 0 : 1; +} + +static BoxedTransform * +lookup_boxed_transform (GSList *transforms, + GType gboxed_type, + JsonNodeType node_type) +{ + BoxedTransform lookup; + GSList *t; + + lookup.boxed_type = gboxed_type; + lookup.node_type = node_type; + + t = g_slist_find_custom (transforms, &lookup, boxed_transforms_find); + if (t == NULL) + return NULL; + + return t->data; +} + +/** + * json_boxed_register_serialize_func: (skip) + * @gboxed_type: a boxed type + * @node_type: a node type + * @serialize_func: serialization function + * + * Registers a serialization function for a `GBoxed` of type `gboxed_type` + * to a [struct@Json.Node] of type `node_type`. + * + * Since: 0.10 + */ +void +json_boxed_register_serialize_func (GType gboxed_type, + JsonNodeType node_type, + JsonBoxedSerializeFunc serialize_func) +{ + BoxedTransform *t; + + g_return_if_fail (G_TYPE_IS_BOXED (gboxed_type)); + g_return_if_fail (G_TYPE_IS_ABSTRACT (gboxed_type) == FALSE); + + G_LOCK (boxed_serialize); + + t = lookup_boxed_transform (boxed_serialize, gboxed_type, node_type); + if (t == NULL) + { + t = g_slice_new (BoxedTransform); + + t->boxed_type = gboxed_type; + t->node_type = node_type; + t->serialize = serialize_func; + + boxed_serialize = g_slist_insert_sorted (boxed_serialize, t, + boxed_transforms_cmp); + } + else + g_warning ("A serialization function for the boxed type %s into " + "JSON nodes of type %s already exists", + g_type_name (gboxed_type), + json_node_type_get_name (node_type)); + + G_UNLOCK (boxed_serialize); +} + +/** + * json_boxed_register_deserialize_func: (skip) + * @gboxed_type: a boxed type + * @node_type: a node type + * @deserialize_func: deserialization function + * + * Registers a deserialization function for a `GBoxed` of type `gboxed_type` + * from a [struct@Json.Node] of type `node_type`. + * + * Since: 0.10 + */ +void +json_boxed_register_deserialize_func (GType gboxed_type, + JsonNodeType node_type, + JsonBoxedDeserializeFunc deserialize_func) +{ + BoxedTransform *t; + + g_return_if_fail (G_TYPE_IS_BOXED (gboxed_type)); + g_return_if_fail (G_TYPE_IS_ABSTRACT (gboxed_type) == FALSE); + + G_LOCK (boxed_deserialize); + + t = lookup_boxed_transform (boxed_deserialize, gboxed_type, node_type); + if (t == NULL) + { + t = g_slice_new (BoxedTransform); + + t->boxed_type = gboxed_type; + t->node_type = node_type; + t->deserialize = deserialize_func; + + boxed_deserialize = g_slist_insert_sorted (boxed_deserialize, t, + boxed_transforms_cmp); + } + else + g_warning ("A deserialization function for the boxed type %s from " + "JSON nodes of type %s already exists", + g_type_name (gboxed_type), + json_node_type_get_name (node_type)); + + G_UNLOCK (boxed_deserialize); +} + +/** + * json_boxed_can_serialize: + * @gboxed_type: a boxed type + * @node_type: (out) (optional): the node type to which the boxed type + * can be serialized into + * + * Checks whether it is possible to serialize a `GBoxed` of + * type `gboxed_type` into a [struct@Json.Node]. + * + * The type of the node is placed inside `node_type` if the function + * returns `TRUE`, and it's undefined otherwise. + * + * Return value: `TRUE` if the type can be serialized, and `FALSE` otherwise + * + * Since: 0.10 + */ +gboolean +json_boxed_can_serialize (GType gboxed_type, + JsonNodeType *node_type) +{ + BoxedTransform *t; + + g_return_val_if_fail (G_TYPE_IS_BOXED (gboxed_type), FALSE); + g_return_val_if_fail (G_TYPE_IS_ABSTRACT (gboxed_type) == FALSE, FALSE); + + t = lookup_boxed_transform (boxed_serialize, gboxed_type, -1); + if (t != NULL) + { + if (node_type) + *node_type = t->node_type; + + return TRUE; + } + + return FALSE; +} + +/** + * json_boxed_can_deserialize: + * @gboxed_type: a boxed type + * @node_type: a node type + * + * Checks whether it is possible to deserialize a `GBoxed` of + * type `gboxed_type` from a [struct@Json.Node] of type `node_type`. + * + * Return value: `TRUE` if the type can be deserialized, and `FALSE` otherwise + * + * Since: 0.10 + */ +gboolean +json_boxed_can_deserialize (GType gboxed_type, + JsonNodeType node_type) +{ + BoxedTransform *t; + + g_return_val_if_fail (G_TYPE_IS_BOXED (gboxed_type), FALSE); + g_return_val_if_fail (G_TYPE_IS_ABSTRACT (gboxed_type) == FALSE, FALSE); + + t = lookup_boxed_transform (boxed_deserialize, gboxed_type, node_type); + if (t != NULL) + return TRUE; + + return FALSE; +} + +/** + * json_boxed_serialize: + * @gboxed_type: a boxed type + * @boxed: a pointer to a boxed of type `gboxed_type` + * + * Serializes a pointer to a `GBoxed` of the given type into a [struct@Json.Node]. + * + * If the serialization is not possible, this function will return `NULL`. + * + * Return value: (nullable) (transfer full): a node with the serialized boxed type + * + * Since: 0.10 + */ +JsonNode * +json_boxed_serialize (GType gboxed_type, + gconstpointer boxed) +{ + BoxedTransform *t; + + g_return_val_if_fail (G_TYPE_IS_BOXED (gboxed_type), NULL); + g_return_val_if_fail (G_TYPE_IS_ABSTRACT (gboxed_type) == FALSE, NULL); + g_return_val_if_fail (boxed != NULL, NULL); + + t = lookup_boxed_transform (boxed_serialize, gboxed_type, -1); + if (t != NULL && t->serialize != NULL) + return t->serialize (boxed); + + return NULL; +} + +/** + * json_boxed_deserialize: + * @gboxed_type: a boxed type + * @node: a node + * + * Deserializes the given [struct@Json.Node] into a `GBoxed` of the given type. + * + * Return value: (transfer full): the newly allocated boxed data + * + * Since: 0.10 + */ +gpointer +json_boxed_deserialize (GType gboxed_type, + JsonNode *node) +{ + JsonNodeType node_type; + BoxedTransform *t; + + g_return_val_if_fail (G_TYPE_IS_BOXED (gboxed_type), NULL); + g_return_val_if_fail (G_TYPE_IS_ABSTRACT (gboxed_type) == FALSE, NULL); + g_return_val_if_fail (node != NULL, NULL); + + node_type = json_node_get_node_type (node); + + t = lookup_boxed_transform (boxed_deserialize, gboxed_type, node_type); + if (t != NULL && t->deserialize != NULL) + return t->deserialize (node); + + return NULL; +} diff --git a/lsp/deps/json-glib/json-generator.c b/lsp/deps/json-glib/json-generator.c new file mode 100644 index 000000000..eef41a736 --- /dev/null +++ b/lsp/deps/json-glib/json-generator.c @@ -0,0 +1,807 @@ +/* json-generator.c - JSON streams generator + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +/** + * JsonGenerator: + * + * `JsonGenerator` provides an object for generating a JSON data stream + * from a tree of [struct@Json.Node] instances, and put it into a buffer + * or a file. + */ + +#include "config.h" + +#include +#include + +#include "json-types-private.h" + +#include "json-generator.h" + +struct _JsonGeneratorPrivate +{ + JsonNode *root; + + guint indent; + gunichar indent_char; + + guint pretty : 1; +}; + +enum +{ + PROP_0, + + PROP_PRETTY, + PROP_INDENT, + PROP_ROOT, + PROP_INDENT_CHAR, + + PROP_LAST +}; + +static void dump_value (GString *buffer, + JsonNode *node); +static void dump_array (JsonGenerator *generator, + GString *buffer, + gint level, + JsonArray *array); +static void dump_object (JsonGenerator *generator, + GString *buffer, + gint level, + JsonObject *object); + +static GParamSpec *generator_props[PROP_LAST] = { NULL, }; + +G_DEFINE_TYPE_WITH_PRIVATE (JsonGenerator, json_generator, G_TYPE_OBJECT) + +static void +json_strescape (GString *output, + const gchar *str) +{ + const gchar *p; + const gchar *end; + gsize len; + + len = strlen (str); + end = str + len; + + for (p = str; p < end; p++) + { + if (*p == '\\' || *p == '"') + { + g_string_append_c (output, '\\'); + g_string_append_c (output, *p); + } + else if ((*p > 0 && *p < 0x1f) || *p == 0x7f) + { + switch (*p) + { + case '\b': + g_string_append (output, "\\b"); + break; + case '\f': + g_string_append (output, "\\f"); + break; + case '\n': + g_string_append (output, "\\n"); + break; + case '\r': + g_string_append (output, "\\r"); + break; + case '\t': + g_string_append (output, "\\t"); + break; + default: + g_string_append_printf (output, "\\u00%02x", (guint)*p); + break; + } + } + else + { + g_string_append_c (output, *p); + } + } +} + +static void +json_generator_finalize (GObject *gobject) +{ + JsonGeneratorPrivate *priv; + + priv = json_generator_get_instance_private ((JsonGenerator *) gobject); + if (priv->root != NULL) + json_node_unref (priv->root); + + G_OBJECT_CLASS (json_generator_parent_class)->finalize (gobject); +} + +static void +json_generator_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + JsonGenerator *generator = JSON_GENERATOR (gobject); + + switch (prop_id) + { + case PROP_PRETTY: + json_generator_set_pretty (generator, g_value_get_boolean (value)); + break; + + case PROP_INDENT: + json_generator_set_indent (generator, g_value_get_uint (value)); + break; + + case PROP_INDENT_CHAR: + json_generator_set_indent_char (generator, g_value_get_uint (value)); + break; + + case PROP_ROOT: + json_generator_set_root (generator, g_value_get_boxed (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +json_generator_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + JsonGeneratorPrivate *priv = JSON_GENERATOR (gobject)->priv; + + switch (prop_id) + { + case PROP_PRETTY: + g_value_set_boolean (value, priv->pretty); + break; + case PROP_INDENT: + g_value_set_uint (value, priv->indent); + break; + case PROP_INDENT_CHAR: + g_value_set_uint (value, priv->indent_char); + break; + case PROP_ROOT: + g_value_set_boxed (value, priv->root); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +json_generator_class_init (JsonGeneratorClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + /** + * JsonGenerator:pretty: (attributes org.gtk.Property.get=json_generator_get_pretty org.gtk.Property.set=json_generator_set_pretty) + * + * Whether the output should be "pretty-printed", with indentation and + * newlines. + * + * The indentation level can be controlled by using the + * [property@Json.Generator:indent] property. + */ + generator_props[PROP_PRETTY] = + g_param_spec_boolean ("pretty", + "Pretty", + "Pretty-print the output", + FALSE, + G_PARAM_READWRITE); + + /** + * JsonGenerator:indent: (attributes org.gtk.Property.get=json_generator_get_indent org.gtk.Property.set=json_generator_set_indent) + * + * Number of spaces to be used to indent when pretty printing. + */ + generator_props[PROP_INDENT] = + g_param_spec_uint ("indent", + "Indent", + "Number of indentation spaces", + 0, G_MAXUINT, + 2, + G_PARAM_READWRITE); + + /** + * JsonGenerator:root: (attributes org.gtk.Property.get=json_generator_get_root org.gtk.Property.set=json_generator_set_root) + * + * The root node to be used when constructing a JSON data + * stream. + * + * Since: 0.4 + */ + generator_props[PROP_ROOT] = + g_param_spec_boxed ("root", + "Root", + "Root of the JSON data tree", + JSON_TYPE_NODE, + G_PARAM_READWRITE); + + /** + * JsonGenerator:indent-char: (attributes org.gtk.Property.get=json_generator_get_indent_char org.gtk.Property.set=json_generator_set_indent_char) + * + * The character that should be used when indenting in pretty print. + * + * Since: 0.6 + */ + generator_props[PROP_INDENT_CHAR] = + g_param_spec_unichar ("indent-char", + "Indent Char", + "Character that should be used when indenting", + ' ', + G_PARAM_READWRITE); + + gobject_class->set_property = json_generator_set_property; + gobject_class->get_property = json_generator_get_property; + gobject_class->finalize = json_generator_finalize; + g_object_class_install_properties (gobject_class, PROP_LAST, generator_props); +} + +static void +json_generator_init (JsonGenerator *generator) +{ + JsonGeneratorPrivate *priv = json_generator_get_instance_private (generator); + + generator->priv = priv; + + priv->pretty = FALSE; + priv->indent = 2; + priv->indent_char = ' '; +} + +static void +dump_node (JsonGenerator *generator, + GString *buffer, + gint level, + const gchar *name, + JsonNode *node) +{ + JsonGeneratorPrivate *priv = generator->priv; + gboolean pretty = priv->pretty; + guint indent = priv->indent; + + if (pretty) + { + guint i; + + for (i = 0; i < (level * indent); i++) + g_string_append_c (buffer, priv->indent_char); + } + + if (name) + { + g_string_append_c (buffer, '"'); + json_strescape (buffer, name); + g_string_append_c (buffer, '"'); + + if (pretty) + g_string_append (buffer, " : "); + else + g_string_append_c (buffer, ':'); + } + + switch (JSON_NODE_TYPE (node)) + { + case JSON_NODE_NULL: + g_string_append (buffer, "null"); + break; + + case JSON_NODE_VALUE: + dump_value (buffer, node); + break; + + case JSON_NODE_ARRAY: + dump_array (generator, buffer, level, + json_node_get_array (node)); + break; + + case JSON_NODE_OBJECT: + dump_object (generator, buffer, level, + json_node_get_object (node)); + break; + } +} + +static void +dump_value (GString *buffer, + JsonNode *node) +{ + const JsonValue *value; + + value = node->data.value; + + switch (value->type) + { + case JSON_VALUE_INT: + g_string_append_printf (buffer, "%" G_GINT64_FORMAT, json_value_get_int (value)); + break; + + case JSON_VALUE_STRING: + { + g_string_append_c (buffer, '"'); + json_strescape (buffer, json_value_get_string (value)); + g_string_append_c (buffer, '"'); + } + break; + + case JSON_VALUE_DOUBLE: + { + gchar buf[G_ASCII_DTOSTR_BUF_SIZE]; + + g_string_append (buffer, + g_ascii_dtostr (buf, sizeof (buf), + json_value_get_double (value))); + /* ensure doubles don't become ints */ + /* also make sure not to append .0 that results in invalid exponential notation + * since the numbers should be decimal, a hex 'e' or "E" can not be mistaken + */ + if (g_strstr_len (buf, G_ASCII_DTOSTR_BUF_SIZE, ".") == NULL && + g_strstr_len (buf, G_ASCII_DTOSTR_BUF_SIZE, "e") == NULL && + g_strstr_len (buf, G_ASCII_DTOSTR_BUF_SIZE, "E") == NULL) + { + g_string_append (buffer, ".0"); + } + } + break; + + case JSON_VALUE_BOOLEAN: + g_string_append (buffer, json_value_get_boolean (value) ? "true" : "false"); + break; + + case JSON_VALUE_NULL: + g_string_append (buffer, "null"); + break; + + default: + break; + } +} + +static void +dump_array (JsonGenerator *generator, + GString *buffer, + gint level, + JsonArray *array) +{ + JsonGeneratorPrivate *priv = generator->priv; + guint array_len = json_array_get_length (array); + guint i; + gboolean pretty = priv->pretty; + guint indent = priv->indent; + + g_string_append_c (buffer, '['); + + if (array_len == 0) + goto out; + + for (i = 0; i < array_len; i++) + { + JsonNode *cur = json_array_get_element (array, i); + + if (i == 0 && pretty) + g_string_append_c (buffer, '\n'); + + dump_node (generator, buffer, level + 1, NULL, cur); + + if ((i + 1) != array_len) + g_string_append_c (buffer, ','); + + if (pretty) + g_string_append_c (buffer, '\n'); + } + + if (pretty) + { + for (i = 0; i < (level * indent); i++) + g_string_append_c (buffer, priv->indent_char); + } + +out: + g_string_append_c (buffer, ']'); +} + +static void +dump_object (JsonGenerator *generator, + GString *buffer, + gint level, + JsonObject *object) +{ + JsonGeneratorPrivate *priv = generator->priv; + GQueue *members; + GList *l; + gboolean pretty = priv->pretty; + guint indent = priv->indent; + guint i; + + g_string_append_c (buffer, '{'); + + members = json_object_get_members_internal (object); + + for (l = members->head; l != NULL; l = l->next) + { + const gchar *member_name = l->data; + JsonNode *cur = json_object_get_member (object, member_name); + + if (l->prev == NULL && pretty) + g_string_append_c (buffer, '\n'); + + dump_node (generator, buffer, level + 1, member_name, cur); + + if (l->next != NULL) + g_string_append_c (buffer, ','); + + if (pretty) + g_string_append_c (buffer, '\n'); + } + + if (pretty) + { + for (i = 0; i < (level * indent); i++) + g_string_append_c (buffer, priv->indent_char); + } + + g_string_append_c (buffer, '}'); +} + +/** + * json_generator_new: + * + * Creates a new `JsonGenerator`. + * + * You can use this object to generate a JSON data stream starting from a + * data object model composed by [struct@Json.Node]s. + * + * Return value: the newly created generator instance + */ +JsonGenerator * +json_generator_new (void) +{ + return g_object_new (JSON_TYPE_GENERATOR, NULL); +} + +/** + * json_generator_to_gstring: + * @generator: a generator + * @string: a string buffer + * + * Generates a JSON data stream and appends it to the string buffer. + * + * Return value: (transfer none): the passed string, updated with + * the generated JSON data + * + * Since: 1.4 + */ +GString * +json_generator_to_gstring (JsonGenerator *generator, + GString *string) +{ + JsonNode *root; + + g_return_val_if_fail (JSON_IS_GENERATOR (generator), NULL); + g_return_val_if_fail (string != NULL, NULL); + + root = generator->priv->root; + if (root != NULL) + dump_node (generator, string, 0, NULL, root); + + return string; +} + +/** + * json_generator_to_data: + * @generator: a generator + * @length: (out) (optional): return location for the length of the returned + * buffer + * + * Generates a JSON data stream from @generator and returns it as a + * buffer. + * + * Return value: (transfer full): a newly allocated string holding a JSON data stream + */ +gchar * +json_generator_to_data (JsonGenerator *generator, + gsize *length) +{ + GString *string; + + g_return_val_if_fail (JSON_IS_GENERATOR (generator), NULL); + + string = g_string_new (""); + json_generator_to_gstring (generator, string); + + if (length) + *length = string->len; + + return g_string_free (string, FALSE); +} + +/** + * json_generator_to_file: + * @generator: a generator + * @filename: (type filename): the path to the target file + * @error: return location for a #GError, or %NULL + * + * Creates a JSON data stream and puts it inside `filename`, overwriting + * the file's current contents. + * + * This operation is atomic, in the sense that the data is written to a + * temporary file which is then renamed to the given `filename`. + * + * Return value: %TRUE if saving was successful. + */ +gboolean +json_generator_to_file (JsonGenerator *generator, + const gchar *filename, + GError **error) +{ + gchar *buffer; + gsize len; + gboolean retval; + + g_return_val_if_fail (JSON_IS_GENERATOR (generator), FALSE); + g_return_val_if_fail (filename != NULL, FALSE); + + buffer = json_generator_to_data (generator, &len); + retval = g_file_set_contents (filename, buffer, len, error); + g_free (buffer); + + return retval; +} + +/** + * json_generator_to_stream: + * @generator: a generator + * @stream: the output stream used to write the JSON data + * @cancellable: (nullable): a `GCancellable` + * @error: return location for a #GError, or %NULL + * + * Outputs JSON data and writes it (synchronously) to the given stream. + * + * Return value: whether the write operation was successful + * + * Since: 0.12 + */ +gboolean +json_generator_to_stream (JsonGenerator *generator, + GOutputStream *stream, + GCancellable *cancellable, + GError **error) +{ + gboolean retval; + gchar *buffer; + gsize len; + + g_return_val_if_fail (JSON_IS_GENERATOR (generator), FALSE); + g_return_val_if_fail (G_IS_OUTPUT_STREAM (stream), FALSE); + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + buffer = json_generator_to_data (generator, &len); + retval = g_output_stream_write (stream, buffer, len, cancellable, error); + g_free (buffer); + + return retval; +} + +/** + * json_generator_set_root: (attributes org.gtk.Method.set_property=root) + * @generator: a generator + * @node: the root node + * + * Sets the root of the JSON data stream to be serialized by + * the given generator. + * + * The passed `node` is copied by the generator object, so it can be + * safely freed after calling this function. + */ +void +json_generator_set_root (JsonGenerator *generator, + JsonNode *node) +{ + g_return_if_fail (JSON_IS_GENERATOR (generator)); + + if (generator->priv->root == node) + return; + + if (generator->priv->root != NULL) + { + json_node_unref (generator->priv->root); + generator->priv->root = NULL; + } + + if (node != NULL) + generator->priv->root = json_node_copy (node); + + g_object_notify_by_pspec (G_OBJECT (generator), generator_props[PROP_ROOT]); +} + +/** + * json_generator_get_root: (attributes org.gtk.Method.get_property=root) + * @generator: a generator + * + * Retrieves a pointer to the root node set using + * [method@Json.Generator.set_root]. + * + * Return value: (nullable) (transfer none): the root node + * + * Since: 0.14 + */ +JsonNode * +json_generator_get_root (JsonGenerator *generator) +{ + g_return_val_if_fail (JSON_IS_GENERATOR (generator), NULL); + + return generator->priv->root; +} + +/** + * json_generator_set_pretty: (attributes org.gtk.Method.set_property=pretty) + * @generator: a generator + * @is_pretty: whether the generated string should be pretty printed + * + * Sets whether the generated JSON should be pretty printed. + * + * Pretty printing will use indentation character specified in the + * [property@Json.Generator:indent-char] property and the spacing + * specified in the [property@Json.Generator:indent] property. + * + * Since: 0.14 + */ +void +json_generator_set_pretty (JsonGenerator *generator, + gboolean is_pretty) +{ + JsonGeneratorPrivate *priv; + + g_return_if_fail (JSON_IS_GENERATOR (generator)); + + priv = generator->priv; + + is_pretty = !!is_pretty; + + if (priv->pretty != is_pretty) + { + priv->pretty = is_pretty; + + g_object_notify_by_pspec (G_OBJECT (generator), generator_props[PROP_PRETTY]); + } +} + +/** + * json_generator_get_pretty: (attributes org.gtk.Method.get_property=pretty) + * @generator: a generator + * + * Retrieves the value set using [method@Json.Generator.set_pretty]. + * + * Return value: `TRUE` if the generated JSON should be pretty-printed, and + * `FALSE` otherwise + * + * Since: 0.14 + */ +gboolean +json_generator_get_pretty (JsonGenerator *generator) +{ + g_return_val_if_fail (JSON_IS_GENERATOR (generator), FALSE); + + return generator->priv->pretty; +} + +/** + * json_generator_set_indent: (attributes org.gtk.Method.set_property=indent) + * @generator: a generator + * @indent_level: the number of repetitions of the indentation character + * that should be applied when pretty printing + * + * Sets the number of repetitions for each indentation level. + * + * Since: 0.14 + */ +void +json_generator_set_indent (JsonGenerator *generator, + guint indent_level) +{ + JsonGeneratorPrivate *priv; + + g_return_if_fail (JSON_IS_GENERATOR (generator)); + + priv = generator->priv; + + if (priv->indent != indent_level) + { + priv->indent = indent_level; + + g_object_notify_by_pspec (G_OBJECT (generator), generator_props[PROP_INDENT]); + } +} + +/** + * json_generator_get_indent: (attributes org.gtk.Method.get_property=indent) + * @generator: a generator + * + * Retrieves the value set using [method@Json.Generator.set_indent]. + * + * Return value: the number of repetitions per indentation level + * + * Since: 0.14 + */ +guint +json_generator_get_indent (JsonGenerator *generator) +{ + g_return_val_if_fail (JSON_IS_GENERATOR (generator), 0); + + return generator->priv->indent; +} + +/** + * json_generator_set_indent_char: (attributes org.gtk.Method.set_property=indent-char) + * @generator: a generator + * @indent_char: a Unicode character to be used when indenting + * + * Sets the character to be used when indenting. + * + * Since: 0.14 + */ +void +json_generator_set_indent_char (JsonGenerator *generator, + gunichar indent_char) +{ + JsonGeneratorPrivate *priv; + + g_return_if_fail (JSON_IS_GENERATOR (generator)); + + priv = generator->priv; + + if (priv->indent_char != indent_char) + { + priv->indent_char = indent_char; + + g_object_notify_by_pspec (G_OBJECT (generator), generator_props[PROP_INDENT_CHAR]); + } +} + +/** + * json_generator_get_indent_char: (attributes org.gtk.Method.get_property=indent-char) + * @generator: a generator + * + * Retrieves the value set using [method@Json.Generator.set_indent_char]. + * + * Return value: the character to be used when indenting + * + * Since: 0.14 + */ +gunichar +json_generator_get_indent_char (JsonGenerator *generator) +{ + g_return_val_if_fail (JSON_IS_GENERATOR (generator), FALSE); + + return generator->priv->indent_char; +} diff --git a/lsp/deps/json-glib/json-generator.h b/lsp/deps/json-glib/json-generator.h new file mode 100644 index 000000000..c653416d9 --- /dev/null +++ b/lsp/deps/json-glib/json-generator.h @@ -0,0 +1,113 @@ +/* json-generator.h - JSON streams generator + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ +#pragma once + +#if !defined(__JSON_GLIB_INSIDE__) && !defined(JSON_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include + +G_BEGIN_DECLS + +#define JSON_TYPE_GENERATOR (json_generator_get_type ()) +#define JSON_GENERATOR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), JSON_TYPE_GENERATOR, JsonGenerator)) +#define JSON_IS_GENERATOR(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), JSON_TYPE_GENERATOR)) +#define JSON_GENERATOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), JSON_TYPE_GENERATOR, JsonGeneratorClass)) +#define JSON_IS_GENERATOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), JSON_TYPE_GENERATOR)) +#define JSON_GENERATOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), JSON_TYPE_GENERATOR, JsonGeneratorClass)) + +typedef struct _JsonGenerator JsonGenerator; +typedef struct _JsonGeneratorPrivate JsonGeneratorPrivate; +typedef struct _JsonGeneratorClass JsonGeneratorClass; + +struct _JsonGenerator +{ + /*< private >*/ + GObject parent_instance; + + JsonGeneratorPrivate *priv; +}; + +struct _JsonGeneratorClass +{ + /*< private >*/ + GObjectClass parent_class; + + /* padding, for future expansion */ + void (* _json_reserved1) (void); + void (* _json_reserved2) (void); + void (* _json_reserved3) (void); + void (* _json_reserved4) (void); +}; + +JSON_AVAILABLE_IN_1_0 +GType json_generator_get_type (void) G_GNUC_CONST; + +JSON_AVAILABLE_IN_1_0 +JsonGenerator * json_generator_new (void); + +JSON_AVAILABLE_IN_1_0 +void json_generator_set_pretty (JsonGenerator *generator, + gboolean is_pretty); +JSON_AVAILABLE_IN_1_0 +gboolean json_generator_get_pretty (JsonGenerator *generator); +JSON_AVAILABLE_IN_1_0 +void json_generator_set_indent (JsonGenerator *generator, + guint indent_level); +JSON_AVAILABLE_IN_1_0 +guint json_generator_get_indent (JsonGenerator *generator); +JSON_AVAILABLE_IN_1_0 +void json_generator_set_indent_char (JsonGenerator *generator, + gunichar indent_char); +JSON_AVAILABLE_IN_1_0 +gunichar json_generator_get_indent_char (JsonGenerator *generator); +JSON_AVAILABLE_IN_1_0 +void json_generator_set_root (JsonGenerator *generator, + JsonNode *node); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_generator_get_root (JsonGenerator *generator); + +JSON_AVAILABLE_IN_1_4 +GString *json_generator_to_gstring (JsonGenerator *generator, + GString *string); + +JSON_AVAILABLE_IN_1_0 +gchar * json_generator_to_data (JsonGenerator *generator, + gsize *length); +JSON_AVAILABLE_IN_1_0 +gboolean json_generator_to_file (JsonGenerator *generator, + const gchar *filename, + GError **error); +JSON_AVAILABLE_IN_1_0 +gboolean json_generator_to_stream (JsonGenerator *generator, + GOutputStream *stream, + GCancellable *cancellable, + GError **error); + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC (JsonGenerator, g_object_unref) +#endif + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-glib.h b/lsp/deps/json-glib/json-glib.h new file mode 100644 index 000000000..b3d06cb9b --- /dev/null +++ b/lsp/deps/json-glib/json-glib.h @@ -0,0 +1,44 @@ +/* json-glib.h: Main header + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ +#pragma once + +#define __JSON_GLIB_INSIDE__ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#undef __JSON_GLIB_INSIDE__ diff --git a/lsp/deps/json-glib/json-gobject-private.h b/lsp/deps/json-glib/json-gobject-private.h new file mode 100644 index 000000000..0bea5ef49 --- /dev/null +++ b/lsp/deps/json-glib/json-gobject-private.h @@ -0,0 +1,36 @@ +/* json-gobject-private.h - GObject private + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +#pragma once + +#include "json-gobject.h" + +G_BEGIN_DECLS + +JsonNode *json_serialize_pspec (const GValue *real_value, + GParamSpec *pspec); +gboolean json_deserialize_pspec (GValue *value, + GParamSpec *pspec, + JsonNode *node); + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-gobject.c b/lsp/deps/json-glib/json-gobject.c new file mode 100644 index 000000000..f783209e4 --- /dev/null +++ b/lsp/deps/json-glib/json-gobject.c @@ -0,0 +1,1005 @@ +/* json-gobject.c - JSON GObject integration + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Author: + * Emmanuele Bassi + */ + +#include "config.h" + +#include +#include + +#include + +#include "json-types-private.h" +#include "json-gobject-private.h" + +#include "json-debug.h" +#include "json-parser.h" +#include "json-generator.h" + +static gboolean +enum_from_string (GType type, + const gchar *string, + gint *enum_value) +{ + GEnumClass *eclass; + GEnumValue *ev; + gchar *endptr; + gint value; + gboolean retval = TRUE; + + g_return_val_if_fail (G_TYPE_IS_ENUM (type), FALSE); + g_return_val_if_fail (string != NULL, FALSE); + + value = strtoul (string, &endptr, 0); + if (endptr != string) /* parsed a number */ + *enum_value = value; + else + { + eclass = g_type_class_ref (type); + ev = g_enum_get_value_by_name (eclass, string); + if (!ev) + ev = g_enum_get_value_by_nick (eclass, string); + + if (ev) + *enum_value = ev->value; + else + retval = FALSE; + + g_type_class_unref (eclass); + } + + return retval; +} + +static gboolean +flags_from_string (GType type, + const gchar *string, + gint *flags_value) +{ + GFlagsClass *fclass; + gchar *endptr, *prevptr; + guint i, j, ret, value; + gchar *flagstr; + GFlagsValue *fv; + const gchar *flag; + gunichar ch; + gboolean eos; + + g_return_val_if_fail (G_TYPE_IS_FLAGS (type), FALSE); + g_return_val_if_fail (string != 0, FALSE); + + ret = TRUE; + + value = strtoul (string, &endptr, 0); + if (endptr != string) /* parsed a number */ + *flags_value = value; + else + { + fclass = g_type_class_ref (type); + + flagstr = g_strdup (string); + for (value = i = j = 0; ; i++) + { + eos = flagstr[i] == '\0'; + + if (!eos && flagstr[i] != '|') + continue; + + flag = &flagstr[j]; + endptr = &flagstr[i]; + + if (!eos) + { + flagstr[i++] = '\0'; + j = i; + } + + /* trim spaces */ + for (;;) + { + ch = g_utf8_get_char (flag); + if (!g_unichar_isspace (ch)) + break; + flag = g_utf8_next_char (flag); + } + + while (endptr > flag) + { + prevptr = g_utf8_prev_char (endptr); + ch = g_utf8_get_char (prevptr); + if (!g_unichar_isspace (ch)) + break; + endptr = prevptr; + } + + if (endptr > flag) + { + *endptr = '\0'; + fv = g_flags_get_value_by_name (fclass, flag); + + if (!fv) + fv = g_flags_get_value_by_nick (fclass, flag); + + if (fv) + value |= fv->value; + else + { + ret = FALSE; + break; + } + } + + if (eos) + { + *flags_value = value; + break; + } + } + + g_free (flagstr); + + g_type_class_unref (fclass); + } + + return ret; +} + +static GObject * +json_gobject_new (GType gtype, + JsonObject *object) +{ + JsonSerializableIface *iface = NULL; + JsonSerializable *serializable = NULL; + gboolean find_property; + gboolean deserialize_property; + gboolean set_property; + GQueue *members; + GList *l; + GQueue members_left = G_QUEUE_INIT; + guint n_members; + GObjectClass *klass; + GObject *retval; + GArray *construct_values; + GPtrArray *construct_names; + + klass = g_type_class_ref (gtype); + + n_members = json_object_get_size (object); + members = json_object_get_members_internal (object); + + /* first pass: construct-only properties; here we cannot use Serializable + * because we don't have an instance yet; we use the default implementation + * of json_deserialize_pspec() to deserialize known types + * + * FIXME - find a way to allow deserialization for these properties + */ + construct_names = g_ptr_array_sized_new (n_members); + g_ptr_array_set_free_func (construct_names, g_free); + + construct_values = g_array_sized_new (FALSE, FALSE, sizeof (GValue), n_members); + g_array_set_clear_func (construct_values, (GDestroyNotify) g_value_unset); + + + for (l = members->head; l != NULL; l = l->next) + { + const char *member_name = l->data; + GValue value = G_VALUE_INIT; + gboolean res = FALSE; + GParamSpec *pspec; + JsonNode *val; + + pspec = g_object_class_find_property (klass, member_name); + if (!pspec) + goto next_member; + + /* we only apply construct-only properties here */ + if ((pspec->flags & G_PARAM_CONSTRUCT_ONLY) == 0) + goto next_member; + + if (!(pspec->flags & G_PARAM_WRITABLE)) + goto next_member; + + g_value_init (&value, G_PARAM_SPEC_VALUE_TYPE (pspec)); + + val = json_object_get_member (object, member_name); + res = json_deserialize_pspec (&value, pspec, val); + if (!res) + { + g_warning ("Failed to deserialize \"%s\" property of type \"%s\" for an object of type \"%s\"", + pspec->name, G_VALUE_TYPE_NAME (&value), g_type_name (gtype)); + + g_value_unset (&value); + } + else + { + g_ptr_array_add (construct_names, g_strdup (pspec->name)); + g_array_append_val (construct_values, value); + + continue; + } + + next_member: + g_queue_push_tail (&members_left, l->data); + } + + retval = g_object_new_with_properties (gtype, + construct_names->len, + (const char **) construct_names->pdata, + (GValue *) construct_values->data); + + g_ptr_array_unref (construct_names); + g_array_unref (construct_values); + + /* do the Serializable type check once */ + if (g_type_is_a (gtype, JSON_TYPE_SERIALIZABLE)) + { + serializable = JSON_SERIALIZABLE (retval); + iface = JSON_SERIALIZABLE_GET_IFACE (serializable); + find_property = (iface->find_property != NULL); + deserialize_property = (iface->deserialize_property != NULL); + set_property = (iface->set_property != NULL); + } + else + { + find_property = FALSE; + deserialize_property = FALSE; + set_property = FALSE; + } + + g_object_freeze_notify (retval); + + for (l = members_left.head; l != NULL; l = l->next) + { + const gchar *member_name = l->data; + GParamSpec *pspec; + JsonNode *val; + GValue value = { 0, }; + gboolean res = FALSE; + + if (find_property) + pspec = json_serializable_find_property (serializable, member_name); + else + pspec = g_object_class_find_property (klass, member_name); + + if (pspec == NULL) + continue; + + /* we should have dealt with these above */ + if (pspec->flags & G_PARAM_CONSTRUCT_ONLY) + continue; + + if (!(pspec->flags & G_PARAM_WRITABLE)) + continue; + + g_value_init (&value, G_PARAM_SPEC_VALUE_TYPE (pspec)); + + val = json_object_get_member (object, member_name); + + if (deserialize_property) + { + JSON_NOTE (GOBJECT, "Using JsonSerializable for property '%s'", pspec->name); + res = json_serializable_deserialize_property (serializable, + pspec->name, + &value, + pspec, + val); + } + + if (!res) + { + JSON_NOTE (GOBJECT, "Using json_deserialize_pspec for property '%s'", pspec->name); + res = json_deserialize_pspec (&value, pspec, val); + } + + if (res) + { + JSON_NOTE (GOBJECT, "Calling set_property('%s', '%s')", + pspec->name, + g_type_name (G_VALUE_TYPE (&value))); + + if (set_property) + json_serializable_set_property (serializable, pspec, &value); + else + g_object_set_property (retval, pspec->name, &value); + } + else + g_warning ("Failed to deserialize \"%s\" property of type \"%s\" for an object of type \"%s\"", + pspec->name, g_type_name (G_VALUE_TYPE (&value)), g_type_name (gtype)); + + g_value_unset (&value); + } + + g_queue_clear (&members_left); + + g_object_thaw_notify (retval); + + g_type_class_unref (klass); + + return retval; +} + +static JsonObject * +json_gobject_dump (GObject *gobject) +{ + JsonSerializableIface *iface = NULL; + JsonSerializable *serializable = NULL; + gboolean list_properties = FALSE; + gboolean serialize_property = FALSE; + gboolean get_property = FALSE; + JsonObject *object; + GParamSpec **pspecs; + guint n_pspecs, i; + + if (JSON_IS_SERIALIZABLE (gobject)) + { + serializable = JSON_SERIALIZABLE (gobject); + iface = JSON_SERIALIZABLE_GET_IFACE (gobject); + list_properties = (iface->list_properties != NULL); + serialize_property = (iface->serialize_property != NULL); + get_property = (iface->get_property != NULL); + } + + object = json_object_new (); + + if (list_properties) + pspecs = json_serializable_list_properties (serializable, &n_pspecs); + else + pspecs = g_object_class_list_properties (G_OBJECT_GET_CLASS (gobject), &n_pspecs); + + for (i = 0; i < n_pspecs; i++) + { + GParamSpec *pspec = pspecs[i]; + GValue value = { 0, }; + JsonNode *node = NULL; + + /* read only what we can */ + if (!(pspec->flags & G_PARAM_READABLE)) + continue; + + g_value_init (&value, G_PARAM_SPEC_VALUE_TYPE (pspec)); + + if (get_property) + json_serializable_get_property (serializable, pspec, &value); + else + g_object_get_property (gobject, pspec->name, &value); + + /* if there is a serialization vfunc, then it is completely responsible + * for serializing the property, possibly by calling the implementation + * of the default JsonSerializable interface through chaining up + */ + if (serialize_property) + { + node = json_serializable_serialize_property (serializable, + pspec->name, + &value, + pspec); + } + /* skip if the value is the default for the property */ + else if (!g_param_value_defaults (pspec, &value)) + node = json_serialize_pspec (&value, pspec); + + if (node) + json_object_set_member (object, pspec->name, node); + + g_value_unset (&value); + } + + g_free (pspecs); + + return object; +} + +gboolean +json_deserialize_pspec (GValue *value, + GParamSpec *pspec G_GNUC_UNUSED, + JsonNode *node) +{ + GValue node_value = { 0, }; + gboolean retval = FALSE; + + if (G_TYPE_FUNDAMENTAL (G_VALUE_TYPE (value)) == G_TYPE_BOXED) + { + JsonNodeType node_type = json_node_get_node_type (node); + GType boxed_type = G_VALUE_TYPE (value); + + if (json_boxed_can_deserialize (boxed_type, node_type)) + { + gpointer boxed = json_boxed_deserialize (boxed_type, node); + + g_value_take_boxed (value, boxed); + + return TRUE; + } + } + + switch (JSON_NODE_TYPE (node)) + { + case JSON_NODE_OBJECT: + if (g_type_is_a (G_VALUE_TYPE (value), G_TYPE_OBJECT)) + { + GObject *object; + + object = json_gobject_new (G_VALUE_TYPE (value), json_node_get_object (node)); + if (object != NULL) + g_value_take_object (value, object); + else + g_value_set_object (value, NULL); + + retval = TRUE; + } + break; + + case JSON_NODE_ARRAY: + if (G_VALUE_HOLDS (value, G_TYPE_STRV)) + { + JsonArray *array = json_node_get_array (node); + guint i, array_len = json_array_get_length (array); + GPtrArray *str_array = g_ptr_array_sized_new (array_len + 1); + + for (i = 0; i < array_len; i++) + { + JsonNode *val = json_array_get_element (array, i); + + if (JSON_NODE_TYPE (val) != JSON_NODE_VALUE) + continue; + + if (json_node_get_string (val) != NULL) + g_ptr_array_add (str_array, (gpointer) json_node_get_string (val)); + } + + g_ptr_array_add (str_array, NULL); + + g_value_set_boxed (value, str_array->pdata); + + g_ptr_array_free (str_array, TRUE); + + retval = TRUE; + } + break; + + case JSON_NODE_VALUE: + json_node_get_value (node, &node_value); +#if 0 + { + gchar *node_str = g_strdup_value_contents (&node_value); + g_debug ("%s: value type '%s' := node value type '%s' -> '%s'", + G_STRLOC, + g_type_name (G_VALUE_TYPE (value)), + g_type_name (G_VALUE_TYPE (&node_value)), + node_str); + g_free (node_str); + } +#endif + + switch (G_TYPE_FUNDAMENTAL (G_VALUE_TYPE (value))) + { + case G_TYPE_BOOLEAN: + case G_TYPE_INT64: + case G_TYPE_STRING: + if (G_VALUE_HOLDS (&node_value, G_VALUE_TYPE (value))) + { + g_value_copy (&node_value, value); + retval = TRUE; + } + break; + + case G_TYPE_INT: + if (G_VALUE_HOLDS (&node_value, G_TYPE_INT64)) + { + g_value_set_int (value, (gint) g_value_get_int64 (&node_value)); + retval = TRUE; + } + break; + + case G_TYPE_CHAR: + if (G_VALUE_HOLDS (&node_value, G_TYPE_INT64)) + { + g_value_set_schar (value, (gchar) g_value_get_int64 (&node_value)); + retval = TRUE; + } + break; + + case G_TYPE_UINT: + if (G_VALUE_HOLDS (&node_value, G_TYPE_INT64)) + { + g_value_set_uint (value, (guint) g_value_get_int64 (&node_value)); + retval = TRUE; + } + break; + + case G_TYPE_UCHAR: + if (G_VALUE_HOLDS (&node_value, G_TYPE_INT64)) + { + g_value_set_uchar (value, (guchar) g_value_get_int64 (&node_value)); + retval = TRUE; + } + break; + + case G_TYPE_LONG: + if (G_VALUE_HOLDS (&node_value, G_TYPE_INT64)) + { + g_value_set_long (value, (glong) g_value_get_int64 (&node_value)); + retval = TRUE; + } + break; + + case G_TYPE_ULONG: + if (G_VALUE_HOLDS (&node_value, G_TYPE_INT64)) + { + g_value_set_ulong (value, (gulong) g_value_get_int64 (&node_value)); + retval = TRUE; + } + break; + + case G_TYPE_UINT64: + if (G_VALUE_HOLDS (&node_value, G_TYPE_INT64)) + { + g_value_set_uint64 (value, (guint64) g_value_get_int64 (&node_value)); + retval = TRUE; + } + break; + + case G_TYPE_DOUBLE: + + if (G_VALUE_HOLDS (&node_value, G_TYPE_DOUBLE)) + { + g_value_set_double (value, g_value_get_double (&node_value)); + retval = TRUE; + } + else if (G_VALUE_HOLDS (&node_value, G_TYPE_INT64)) + { + g_value_set_double (value, (gdouble) g_value_get_int64 (&node_value)); + retval = TRUE; + } + + break; + + case G_TYPE_FLOAT: + if (G_VALUE_HOLDS (&node_value, G_TYPE_DOUBLE)) + { + g_value_set_float (value, (gfloat) g_value_get_double (&node_value)); + retval = TRUE; + } + else if (G_VALUE_HOLDS (&node_value, G_TYPE_INT64)) + { + g_value_set_float (value, (gfloat) g_value_get_int64 (&node_value)); + retval = TRUE; + } + + break; + + case G_TYPE_ENUM: + { + gint enum_value = 0; + + if (G_VALUE_HOLDS (&node_value, G_TYPE_INT64)) + { + enum_value = g_value_get_int64 (&node_value); + retval = TRUE; + } + else if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING)) + { + retval = enum_from_string (G_VALUE_TYPE (value), + g_value_get_string (&node_value), + &enum_value); + } + + if (retval) + g_value_set_enum (value, enum_value); + } + break; + + case G_TYPE_FLAGS: + { + gint flags_value = 0; + + if (G_VALUE_HOLDS (&node_value, G_TYPE_INT64)) + { + flags_value = g_value_get_int64 (&node_value); + retval = TRUE; + } + else if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING)) + { + retval = flags_from_string (G_VALUE_TYPE (value), + g_value_get_string (&node_value), + &flags_value); + } + + if (retval) + g_value_set_flags (value, flags_value); + } + break; + + default: + retval = FALSE; + break; + } + + g_value_unset (&node_value); + break; + + case JSON_NODE_NULL: + if (G_TYPE_FUNDAMENTAL (G_VALUE_TYPE (value)) == G_TYPE_STRING) + { + g_value_set_string (value, NULL); + retval = TRUE; + } + else if (G_TYPE_FUNDAMENTAL (G_VALUE_TYPE (value)) == G_TYPE_OBJECT) + { + g_value_set_object (value, NULL); + retval = TRUE; + } + else + retval = FALSE; + + break; + } + + return retval; +} + +JsonNode * +json_serialize_pspec (const GValue *real_value, + GParamSpec *pspec G_GNUC_UNUSED) +{ + JsonNode *retval = NULL; + JsonNodeType node_type; + + switch (G_TYPE_FUNDAMENTAL (G_VALUE_TYPE (real_value))) + { + /* JSON native types */ + case G_TYPE_INT64: + retval = json_node_init_int (json_node_alloc (), g_value_get_int64 (real_value)); + break; + + case G_TYPE_BOOLEAN: + retval = json_node_init_boolean (json_node_alloc (), g_value_get_boolean (real_value)); + break; + + case G_TYPE_DOUBLE: + retval = json_node_init_double (json_node_alloc (), g_value_get_double (real_value)); + break; + + case G_TYPE_STRING: + retval = json_node_init_string (json_node_alloc (), g_value_get_string (real_value)); + break; + + /* auto-promoted types */ + case G_TYPE_INT: + retval = json_node_init_int (json_node_alloc (), g_value_get_int (real_value)); + break; + + case G_TYPE_UINT: + retval = json_node_init_int (json_node_alloc (), g_value_get_uint (real_value)); + break; + + case G_TYPE_LONG: + retval = json_node_init_int (json_node_alloc (), g_value_get_long (real_value)); + break; + + case G_TYPE_ULONG: + retval = json_node_init_int (json_node_alloc (), g_value_get_ulong (real_value)); + break; + + case G_TYPE_UINT64: + retval = json_node_init_int (json_node_alloc (), g_value_get_uint64 (real_value)); + break; + + case G_TYPE_FLOAT: + retval = json_node_init_double (json_node_alloc (), g_value_get_float (real_value)); + break; + + case G_TYPE_CHAR: + retval = json_node_alloc (); + json_node_init_int (retval, g_value_get_schar (real_value)); + break; + + case G_TYPE_UCHAR: + retval = json_node_init_int (json_node_alloc (), g_value_get_uchar (real_value)); + break; + + case G_TYPE_ENUM: + retval = json_node_init_int (json_node_alloc (), g_value_get_enum (real_value)); + break; + + case G_TYPE_FLAGS: + retval = json_node_init_int (json_node_alloc (), g_value_get_flags (real_value)); + break; + + /* complex types */ + case G_TYPE_BOXED: + if (G_VALUE_HOLDS (real_value, G_TYPE_STRV)) + { + gchar **strv = g_value_get_boxed (real_value); + gint i, strv_len; + JsonArray *array; + + strv_len = g_strv_length (strv); + array = json_array_sized_new (strv_len); + + for (i = 0; i < strv_len; i++) + { + JsonNode *str = json_node_new (JSON_NODE_VALUE); + + json_node_set_string (str, strv[i]); + json_array_add_element (array, str); + } + + retval = json_node_init_array (json_node_alloc (), array); + json_array_unref (array); + } + else if (json_boxed_can_serialize (G_VALUE_TYPE (real_value), &node_type)) + { + gpointer boxed = g_value_get_boxed (real_value); + + retval = json_boxed_serialize (G_VALUE_TYPE (real_value), boxed); + } + else + g_warning ("Boxed type '%s' is not handled by JSON-GLib", + g_type_name (G_VALUE_TYPE (real_value))); + break; + + case G_TYPE_OBJECT: + { + GObject *object = g_value_get_object (real_value); + + retval = json_node_alloc (); + + if (object != NULL) + { + json_node_init (retval, JSON_NODE_OBJECT); + json_node_take_object (retval, json_gobject_dump (object)); + } + else + json_node_init_null (retval); + } + break; + + case G_TYPE_NONE: + retval = json_node_new (JSON_NODE_NULL); + break; + + default: + g_warning ("Unsupported type `%s'", g_type_name (G_VALUE_TYPE (real_value))); + break; + } + + return retval; +} + +/** + * json_gobject_deserialize: + * @gtype: the type of the object to create + * @node: a node of type `JSON_NODE_OBJECT` describing the + * object instance for the given type + * + * Creates a new `GObject` instance of the given type, and constructs it + * using the members of the object in the given node. + * + * Return value: (transfer full): The newly created instance + * + * Since: 0.10 + */ +GObject * +json_gobject_deserialize (GType gtype, + JsonNode *node) +{ + g_return_val_if_fail (g_type_is_a (gtype, G_TYPE_OBJECT), NULL); + g_return_val_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_OBJECT, NULL); + + return json_gobject_new (gtype, json_node_get_object (node)); +} + +/** + * json_gobject_serialize: + * @gobject: the object to serialize + * + * Creates a JSON tree representing the passed object instance. + * + * Each member of the returned JSON object will map to a property of + * the object type. + * + * The returned JSON tree will be returned as a `JsonNode` with a type + * of `JSON_NODE_OBJECT`. + * + * Return value: (transfer full): the newly created JSON tree + * + * Since: 0.10 + */ +JsonNode * +json_gobject_serialize (GObject *gobject) +{ + JsonNode *retval; + + g_return_val_if_fail (G_IS_OBJECT (gobject), NULL); + + retval = json_node_new (JSON_NODE_OBJECT); + json_node_take_object (retval, json_gobject_dump (gobject)); + + return retval; +} + +/** + * json_construct_gobject: + * @gtype: the type of the object to construct + * @data: a JSON data stream + * @length: length of the data stream (unused) + * @error: return location for a #GError, or %NULL + * + * Deserializes a JSON data stream and creates an instance of the given + * type. + * + * If the given type implements the [iface@Json.Serializable] interface, it + * will be asked to deserialize all the JSON members into their respective + * properties; otherwise, the default implementation will be used to translate + * the compatible JSON native types. + * + * **Note**: the JSON data stream must be an object. + * + * For historical reasons, the `length` argument is unused. The given `data` + * must be a `NUL`-terminated string. + * + * Returns: (transfer full) (nullable): a new object instance of the given + * type + * + * Since: 0.4 + * + * Deprecated: 0.10: Use [func@Json.gobject_from_data] instead + */ +GObject * +json_construct_gobject (GType gtype, + const gchar *data, + gsize length G_GNUC_UNUSED, + GError **error) +{ + return json_gobject_from_data (gtype, data, strlen (data), error); +} + +/** + * json_gobject_from_data: + * @gtype: the type of the object to construct + * @data: a JSON data stream + * @length: length of the data stream, or -1 if it is `NUL`-terminated + * @error: return location for a #GError, or %NULL + * + * Deserializes a JSON data stream and creates an instance of the + * given type. + * + * If the type implements the [iface@Json.Serializable] interface, it will + * be asked to deserialize all the JSON members into their respective properties; + * otherwise, the default implementation will be used to translate the + * compatible JSON native types. + * + * **Note**: the JSON data stream must be an object + * + * Return value: (transfer full) (nullable): a new object instance of the given type + * + * Since: 0.10 + */ +GObject * +json_gobject_from_data (GType gtype, + const gchar *data, + gssize length, + GError **error) +{ + JsonParser *parser; + JsonNode *root; + GError *parse_error; + GObject *retval; + + g_return_val_if_fail (gtype != G_TYPE_INVALID, NULL); + g_return_val_if_fail (data != NULL, NULL); + + if (length < 0) + length = strlen (data); + + parser = json_parser_new (); + + parse_error = NULL; + json_parser_load_from_data (parser, data, length, &parse_error); + if (parse_error) + { + g_propagate_error (error, parse_error); + g_object_unref (parser); + return NULL; + } + + root = json_parser_get_root (parser); + if (root == NULL || JSON_NODE_TYPE (root) != JSON_NODE_OBJECT) + { + g_set_error (error, JSON_PARSER_ERROR, + JSON_PARSER_ERROR_PARSE, + /* translators: the %s is the name of the data structure */ + _("Expecting a JSON object, but the root node is of type “%s”"), + json_node_type_name (root)); + g_object_unref (parser); + return NULL; + } + + retval = json_gobject_deserialize (gtype, root); + + g_object_unref (parser); + + return retval; +} + +/** + * json_serialize_gobject: + * @gobject: the object to serialize + * @length: (out) (optional): return value for the length of the buffer + * + * Serializes a `GObject` instance into a JSON data stream. + * + * If the object implements the [iface@Json.Serializable] interface, it will be + * asked to serizalize all its properties; otherwise, the default + * implementation will be use to translate the compatible types into JSON + * native types. + * + * Return value: (transfer full): a JSON data stream representing the given object + * + * Deprecated: 0.10: Use [func@Json.gobject_to_data] instead + */ +gchar * +json_serialize_gobject (GObject *gobject, + gsize *length) +{ + return json_gobject_to_data (gobject, length); +} + +/** + * json_gobject_to_data: + * @gobject: the object to serialize + * @length: (out) (optional): return value for the length of the buffer + * + * Serializes a `GObject` instance into a JSON data stream, iterating + * recursively over each property. + * + * If the given object implements the [iface@Json.Serializable] interface, + * it will be asked to serialize all its properties; otherwise, the default + * implementation will be use to translate the compatible types into + * JSON native types. + * + * Return value: a JSON data stream representing the given object + * + * Since: 0.10 + */ +gchar * +json_gobject_to_data (GObject *gobject, + gsize *length) +{ + JsonGenerator *gen; + JsonNode *root; + gchar *data; + + g_return_val_if_fail (G_OBJECT (gobject), NULL); + + root = json_gobject_serialize (gobject); + + gen = g_object_new (JSON_TYPE_GENERATOR, + "root", root, + "pretty", TRUE, + "indent", 2, + NULL); + + data = json_generator_to_data (gen, length); + g_object_unref (gen); + + json_node_unref (root); + + return data; +} diff --git a/lsp/deps/json-glib/json-gobject.h b/lsp/deps/json-glib/json-gobject.h new file mode 100644 index 000000000..0f060e1a8 --- /dev/null +++ b/lsp/deps/json-glib/json-gobject.h @@ -0,0 +1,250 @@ +/* json-gobject.h - JSON GObject integration + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define JSON_TYPE_SERIALIZABLE (json_serializable_get_type ()) +#define JSON_SERIALIZABLE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), JSON_TYPE_SERIALIZABLE, JsonSerializable)) +#define JSON_IS_SERIALIZABLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), JSON_TYPE_SERIALIZABLE)) +#define JSON_SERIALIZABLE_GET_IFACE(obj) (G_TYPE_INSTANCE_GET_INTERFACE ((obj), JSON_TYPE_SERIALIZABLE, JsonSerializableIface)) + +typedef struct _JsonSerializable JsonSerializable; /* dummy */ +typedef struct _JsonSerializableIface JsonSerializableIface; + +/** + * JsonSerializableIface: + * @serialize_property: virtual function for serializing an object property + * into JSON + * @deserialize_property: virtual function for deserializing JSON + * into an object property + * @find_property: virtual function for finding a property definition using + * its name + * @list_properties: virtual function for listing the installed property + * definitions + * @set_property: virtual function for setting a property + * @get_property: virtual function for getting a property + * + * Interface that allows serializing and deserializing object instances + * with properties storing complex data types. + * + * The [func@Json.gobject_from_data] and [func@Json.gobject_to_data] + * functions will check if the passed object type implements this interface, + * so it can also be used to override the default property serialization + * sequence. + */ +struct _JsonSerializableIface +{ + /*< private >*/ + GTypeInterface g_iface; + + /*< public >*/ + JsonNode *(* serialize_property) (JsonSerializable *serializable, + const gchar *property_name, + const GValue *value, + GParamSpec *pspec); + gboolean (* deserialize_property) (JsonSerializable *serializable, + const gchar *property_name, + GValue *value, + GParamSpec *pspec, + JsonNode *property_node); + + GParamSpec * (* find_property) (JsonSerializable *serializable, + const char *name); + GParamSpec **(* list_properties) (JsonSerializable *serializable, + guint *n_pspecs); + void (* set_property) (JsonSerializable *serializable, + GParamSpec *pspec, + const GValue *value); + void (* get_property) (JsonSerializable *serializable, + GParamSpec *pspec, + GValue *value); +}; + +JSON_AVAILABLE_IN_1_0 +GType json_serializable_get_type (void) G_GNUC_CONST; + +JSON_AVAILABLE_IN_1_0 +JsonNode *json_serializable_serialize_property (JsonSerializable *serializable, + const gchar *property_name, + const GValue *value, + GParamSpec *pspec); +JSON_AVAILABLE_IN_1_0 +gboolean json_serializable_deserialize_property (JsonSerializable *serializable, + const gchar *property_name, + GValue *value, + GParamSpec *pspec, + JsonNode *property_node); + +JSON_AVAILABLE_IN_1_0 +GParamSpec * json_serializable_find_property (JsonSerializable *serializable, + const char *name); +JSON_AVAILABLE_IN_1_0 +GParamSpec ** json_serializable_list_properties (JsonSerializable *serializable, + guint *n_pspecs); +JSON_AVAILABLE_IN_1_0 +void json_serializable_set_property (JsonSerializable *serializable, + GParamSpec *pspec, + const GValue *value); +JSON_AVAILABLE_IN_1_0 +void json_serializable_get_property (JsonSerializable *serializable, + GParamSpec *pspec, + GValue *value); + +JSON_AVAILABLE_IN_1_0 +JsonNode *json_serializable_default_serialize_property (JsonSerializable *serializable, + const gchar *property_name, + const GValue *value, + GParamSpec *pspec); +JSON_AVAILABLE_IN_1_0 +gboolean json_serializable_default_deserialize_property (JsonSerializable *serializable, + const gchar *property_name, + GValue *value, + GParamSpec *pspec, + JsonNode *property_node); + +/** + * JsonBoxedSerializeFunc: + * @boxed: a boxed data structure + * + * Serializes the passed `GBoxed` and stores it inside a `JsonNode`, for instance: + * + * ```c + * static JsonNode * + * my_point_serialize (gconstpointer boxed) + * { + * const MyPoint *point = boxed; + * + * g_autoptr(JsonBuilder) builder = json_builder_new (); + * + * json_builder_begin_object (builder); + * json_builder_set_member_name (builder, "x"); + * json_builder_add_double_value (builder, point->x); + * json_builder_set_member_name (builder, "y"); + * json_builder_add_double_value (builder, point->y); + * json_builder_end_object (builder); + * + * return json_builder_get_root (builder); + * } + * ``` + * + * Return value: the newly created JSON node tree representing the boxed data + * + * Since: 0.10 + */ +typedef JsonNode *(* JsonBoxedSerializeFunc) (gconstpointer boxed); + +/** + * JsonBoxedDeserializeFunc: + * @node: a node tree representing a boxed data + * + * Deserializes the contents of the passed `JsonNode` into a `GBoxed`, for instance: + * + * ```c + * static gpointer + * my_point_deserialize (JsonNode *node) + * { + * double x = 0.0, y = 0.0; + * + * if (JSON_NODE_HOLDS_ARRAY (node)) + * { + * JsonArray *array = json_node_get_array (node); + * + * if (json_array_get_length (array) == 2) + * { + * x = json_array_get_double_element (array, 0); + * y = json_array_get_double_element (array, 1); + * } + * } + * else if (JSON_NODE_HOLDS_OBJECT (node)) + * { + * JsonObject *obj = json_node_get_object (node); + * + * x = json_object_get_double_member_with_default (obj, "x", 0.0); + * y = json_object_get_double_member_with_default (obj, "y", 0.0); + * } + * + * // my_point_new() is defined elsewhere + * return my_point_new (x, y); + * } + * ``` + * + * Return value: the newly created boxed structure + * + * Since: 0.10 + */ +typedef gpointer (* JsonBoxedDeserializeFunc) (JsonNode *node); + +JSON_AVAILABLE_IN_1_0 +void json_boxed_register_serialize_func (GType gboxed_type, + JsonNodeType node_type, + JsonBoxedSerializeFunc serialize_func); +JSON_AVAILABLE_IN_1_0 +void json_boxed_register_deserialize_func (GType gboxed_type, + JsonNodeType node_type, + JsonBoxedDeserializeFunc deserialize_func); +JSON_AVAILABLE_IN_1_0 +gboolean json_boxed_can_serialize (GType gboxed_type, + JsonNodeType *node_type); +JSON_AVAILABLE_IN_1_0 +gboolean json_boxed_can_deserialize (GType gboxed_type, + JsonNodeType node_type); +JSON_AVAILABLE_IN_1_0 +JsonNode *json_boxed_serialize (GType gboxed_type, + gconstpointer boxed); +JSON_AVAILABLE_IN_1_0 +gpointer json_boxed_deserialize (GType gboxed_type, + JsonNode *node); + +JSON_AVAILABLE_IN_1_0 +JsonNode *json_gobject_serialize (GObject *gobject); +JSON_AVAILABLE_IN_1_0 +GObject * json_gobject_deserialize (GType gtype, + JsonNode *node); + +JSON_AVAILABLE_IN_1_0 +GObject * json_gobject_from_data (GType gtype, + const gchar *data, + gssize length, + GError **error); +JSON_AVAILABLE_IN_1_0 +gchar * json_gobject_to_data (GObject *gobject, + gsize *length); + +JSON_DEPRECATED_IN_1_0_FOR(json_gobject_from_data) +GObject * json_construct_gobject (GType gtype, + const gchar *data, + gsize length, + GError **error); +JSON_DEPRECATED_IN_1_0_FOR(json_gobject_to_data) +gchar * json_serialize_gobject (GObject *gobject, + gsize *length) G_GNUC_MALLOC; + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC (JsonSerializable, g_object_unref) +#endif + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-gvariant.c b/lsp/deps/json-glib/json-gvariant.c new file mode 100644 index 000000000..41aa2121d --- /dev/null +++ b/lsp/deps/json-glib/json-gvariant.c @@ -0,0 +1,1346 @@ +/* json-gvariant.c - JSON GVariant integration + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Author: + * Eduardo Lima Mitev + */ + +#include "config.h" + +#include +#include +#include + +#include + +#include + +#include "json-gvariant.h" + +#include "json-generator.h" +#include "json-parser.h" +#include "json-types-private.h" + +/* custom extension to the GVariantClass enumeration to differentiate + * a single dictionary entry from an array of dictionary entries + */ +#define JSON_G_VARIANT_CLASS_DICTIONARY 'c' + +typedef void (* GVariantForeachFunc) (GVariant *variant_child, + gpointer user_data); + +static GVariant * json_to_gvariant_recurse (JsonNode *json_node, + const gchar **signature, + GError **error); + +/* ========================================================================== */ +/* GVariant to JSON */ +/* ========================================================================== */ + +static void +gvariant_foreach (GVariant *variant, + GVariantForeachFunc func, + gpointer user_data) +{ + GVariantIter iter; + GVariant *variant_child; + + g_variant_iter_init (&iter, variant); + while ((variant_child = g_variant_iter_next_value (&iter)) != NULL) + { + func (variant_child, user_data); + g_variant_unref (variant_child); + } +} + +static void +gvariant_to_json_array_foreach (GVariant *variant_child, + gpointer user_data) +{ + JsonArray *array = user_data; + JsonNode *json_child; + + json_child = json_gvariant_serialize (variant_child); + json_array_add_element (array, json_child); +} + +static JsonNode * +gvariant_to_json_array (GVariant *variant) +{ + JsonArray *array; + JsonNode *json_node; + + array = json_array_new (); + json_node = json_node_new (JSON_NODE_ARRAY); + json_node_set_array (json_node, array); + json_array_unref (array); + + gvariant_foreach (variant, + gvariant_to_json_array_foreach, + array); + + return json_node; +} + +static gchar * +gvariant_simple_to_string (GVariant *variant) +{ + GVariantClass class; + gchar *str; + + class = g_variant_classify (variant); + switch (class) + { + case G_VARIANT_CLASS_BOOLEAN: + if (g_variant_get_boolean (variant)) + str = g_strdup ("true"); + else + str = g_strdup ("false"); + break; + + case G_VARIANT_CLASS_BYTE: + str = g_strdup_printf ("%u", g_variant_get_byte (variant)); + break; + case G_VARIANT_CLASS_INT16: + str = g_strdup_printf ("%d", g_variant_get_int16 (variant)); + break; + case G_VARIANT_CLASS_UINT16: + str = g_strdup_printf ("%u", g_variant_get_uint16 (variant)); + break; + case G_VARIANT_CLASS_INT32: + str = g_strdup_printf ("%d", g_variant_get_int32 (variant)); + break; + case G_VARIANT_CLASS_UINT32: + str = g_strdup_printf ("%u", g_variant_get_uint32 (variant)); + break; + case G_VARIANT_CLASS_INT64: + str = g_strdup_printf ("%" G_GINT64_FORMAT, + g_variant_get_int64 (variant)); + break; + case G_VARIANT_CLASS_UINT64: + str = g_strdup_printf ("%" G_GUINT64_FORMAT, + g_variant_get_uint64 (variant)); + break; + case G_VARIANT_CLASS_HANDLE: + str = g_strdup_printf ("%d", g_variant_get_handle (variant)); + break; + + case G_VARIANT_CLASS_DOUBLE: + { + gchar buf[G_ASCII_DTOSTR_BUF_SIZE]; + + g_ascii_formatd (buf, + G_ASCII_DTOSTR_BUF_SIZE, + "%f", + g_variant_get_double (variant)); + + str = g_strdup (buf); + break; + } + + case G_VARIANT_CLASS_STRING: + case G_VARIANT_CLASS_OBJECT_PATH: + case G_VARIANT_CLASS_SIGNATURE: + str = g_strdup (g_variant_get_string (variant, NULL)); + break; + + default: + g_assert_not_reached (); + break; + } + + return str; +} + +static JsonNode * +gvariant_dict_entry_to_json (GVariant *variant, gchar **member_name) +{ + GVariant *member; + GVariant *value; + JsonNode *json_node; + + member = g_variant_get_child_value (variant, 0); + *member_name = gvariant_simple_to_string (member); + + value = g_variant_get_child_value (variant, 1); + json_node = json_gvariant_serialize (value); + + g_variant_unref (member); + g_variant_unref (value); + + return json_node; +} + +static void +gvariant_to_json_object_foreach (GVariant *variant_child, gpointer user_data) +{ + gchar *member_name; + JsonNode *json_child; + JsonObject *object = (JsonObject *) user_data; + + json_child = gvariant_dict_entry_to_json (variant_child, &member_name); + json_object_set_member (object, member_name, json_child); + g_free (member_name); +} + +static JsonNode * +gvariant_to_json_object (GVariant *variant) +{ + JsonNode *json_node; + JsonObject *object; + + json_node = json_node_new (JSON_NODE_OBJECT); + object = json_object_new (); + json_node_set_object (json_node, object); + json_object_unref (object); + + gvariant_foreach (variant, + gvariant_to_json_object_foreach, + object); + + return json_node; +} + +/** + * json_gvariant_serialize: + * @variant: A `GVariant` to convert + * + * Converts `variant` to a JSON tree. + * + * Return value: (transfer full): the root of the JSON data structure + * obtained from `variant` + * + * Since: 0.14 + */ +JsonNode * +json_gvariant_serialize (GVariant *variant) +{ + JsonNode *json_node = NULL; + GVariantClass class; + + g_return_val_if_fail (variant != NULL, NULL); + + class = g_variant_classify (variant); + + if (! g_variant_is_container (variant)) + { + json_node = json_node_new (JSON_NODE_VALUE); + + switch (class) + { + case G_VARIANT_CLASS_BOOLEAN: + json_node_set_boolean (json_node, g_variant_get_boolean (variant)); + break; + + case G_VARIANT_CLASS_BYTE: + json_node_set_int (json_node, g_variant_get_byte (variant)); + break; + case G_VARIANT_CLASS_INT16: + json_node_set_int (json_node, g_variant_get_int16 (variant)); + break; + case G_VARIANT_CLASS_UINT16: + json_node_set_int (json_node, g_variant_get_uint16 (variant)); + break; + case G_VARIANT_CLASS_INT32: + json_node_set_int (json_node, g_variant_get_int32 (variant)); + break; + case G_VARIANT_CLASS_UINT32: + json_node_set_int (json_node, g_variant_get_uint32 (variant)); + break; + case G_VARIANT_CLASS_INT64: + json_node_set_int (json_node, g_variant_get_int64 (variant)); + break; + case G_VARIANT_CLASS_UINT64: + json_node_set_int (json_node, g_variant_get_uint64 (variant)); + break; + case G_VARIANT_CLASS_HANDLE: + json_node_set_int (json_node, g_variant_get_handle (variant)); + break; + + case G_VARIANT_CLASS_DOUBLE: + json_node_set_double (json_node, g_variant_get_double (variant)); + break; + + case G_VARIANT_CLASS_STRING: + case G_VARIANT_CLASS_OBJECT_PATH: + case G_VARIANT_CLASS_SIGNATURE: + json_node_set_string (json_node, g_variant_get_string (variant, NULL)); + break; + + default: + break; + } + } + else + { + switch (class) + { + case G_VARIANT_CLASS_MAYBE: + { + GVariant *value; + + value = g_variant_get_maybe (variant); + if (value == NULL) + { + json_node = json_node_new (JSON_NODE_NULL); + } + else + { + json_node = json_gvariant_serialize (value); + g_variant_unref (value); + } + + break; + } + + case G_VARIANT_CLASS_VARIANT: + { + GVariant *value; + + value = g_variant_get_variant (variant); + json_node = json_gvariant_serialize (value); + g_variant_unref (value); + + break; + } + + case G_VARIANT_CLASS_ARRAY: + { + const gchar *type; + + type = g_variant_get_type_string (variant); + + if (type[1] == G_VARIANT_CLASS_DICT_ENTRY) + { + /* array of dictionary entries => JsonObject */ + json_node = gvariant_to_json_object (variant); + } + else + { + /* array of anything else => JsonArray */ + json_node = gvariant_to_json_array (variant); + } + + break; + } + + case G_VARIANT_CLASS_DICT_ENTRY: + { + gchar *member_name; + JsonObject *object; + JsonNode *child; + + /* a single dictionary entry => JsonObject */ + json_node = json_node_new (JSON_NODE_OBJECT); + object = json_object_new (); + json_node_set_object (json_node, object); + json_object_unref (object); + + child = gvariant_dict_entry_to_json (variant, &member_name); + + json_object_set_member (object, member_name, child); + g_free (member_name); + + break; + } + + case G_VARIANT_CLASS_TUPLE: + json_node = gvariant_to_json_array (variant); + break; + + default: + break; + } + } + + return json_node; +} + +/** + * json_gvariant_serialize_data: + * @variant: A #GVariant to convert + * @length: (out) (optional): the length of the returned string + * + * Converts @variant to its JSON encoded string representation. + * + * This is a convenience function around [func@Json.gvariant_serialize], to + * obtain the JSON tree, and then [class@Json.Generator] to stringify it. + * + * Return value: (transfer full): The JSON encoded string corresponding to + * the given variant + * + * Since: 0.14 + */ +gchar * +json_gvariant_serialize_data (GVariant *variant, gsize *length) +{ + JsonNode *json_node; + JsonGenerator *generator; + gchar *json; + + json_node = json_gvariant_serialize (variant); + + generator = json_generator_new (); + + json_generator_set_root (generator, json_node); + json = json_generator_to_data (generator, length); + + g_object_unref (generator); + + json_node_unref (json_node); + + return json; +} + +/* ========================================================================== */ +/* JSON to GVariant */ +/* ========================================================================== */ + +static GVariantClass +json_to_gvariant_get_next_class (JsonNode *json_node, + const gchar **signature) +{ + if (signature == NULL) + { + GVariantClass class = 0; + + switch (json_node_get_node_type (json_node)) + { + case JSON_NODE_VALUE: + switch (json_node_get_value_type (json_node)) + { + case G_TYPE_BOOLEAN: + class = G_VARIANT_CLASS_BOOLEAN; + break; + + case G_TYPE_INT64: + class = G_VARIANT_CLASS_INT64; + break; + + case G_TYPE_DOUBLE: + class = G_VARIANT_CLASS_DOUBLE; + break; + + case G_TYPE_STRING: + class = G_VARIANT_CLASS_STRING; + break; + } + + break; + + case JSON_NODE_ARRAY: + class = G_VARIANT_CLASS_ARRAY; + break; + + case JSON_NODE_OBJECT: + class = JSON_G_VARIANT_CLASS_DICTIONARY; + break; + + case JSON_NODE_NULL: + class = G_VARIANT_CLASS_MAYBE; + break; + } + + return class; + } + else + { + if ((*signature)[0] == G_VARIANT_CLASS_ARRAY && + (*signature)[1] == G_VARIANT_CLASS_DICT_ENTRY) + return JSON_G_VARIANT_CLASS_DICTIONARY; + else + return (*signature)[0]; + } +} + +static gboolean +json_node_assert_type (JsonNode *json_node, + JsonNodeType type, + GType sub_type, + GError **error) +{ + if (JSON_NODE_TYPE (json_node) != type || + (type == JSON_NODE_VALUE && + (json_node_get_value_type (json_node) != sub_type))) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + /* translators: the '%s' is the type name */ + _("Unexpected type “%s” in JSON node"), + g_type_name (json_node_get_value_type (json_node))); + return FALSE; + } + else + { + return TRUE; + } +} + +static void +json_to_gvariant_foreach_add (gpointer data, + gpointer user_data) +{ + GVariantBuilder *builder = user_data; + GVariant *child = data; + + g_variant_builder_add_value (builder, child); +} + +static void +json_to_gvariant_foreach_free (gpointer data, + gpointer user_data G_GNUC_UNUSED) +{ + GVariant *child = data; + + g_variant_unref (child); +} + +static GVariant * +json_to_gvariant_build_from_glist (GList *list, + const char *signature) +{ + GVariantBuilder *builder; + GVariant *result; + + builder = g_variant_builder_new (G_VARIANT_TYPE (signature)); + + g_list_foreach (list, json_to_gvariant_foreach_add, builder); + result = g_variant_builder_end (builder); + + g_variant_builder_unref (builder); + + return result; +} + +static GVariant * +json_to_gvariant_tuple (JsonNode *json_node, + const gchar **signature, + GError **error) +{ + GVariant *variant = NULL; + JsonArray *array; + guint i; + GList *children = NULL; + gboolean roll_back = FALSE; + const gchar *initial_signature; + + array = json_node_get_array (json_node); + + initial_signature = *signature; + (*signature)++; + i = 1; + while ((*signature)[0] != ')' && (*signature)[0] != '\0') + { + JsonNode *json_child; + GVariant *variant_child; + + if (i - 1 >= json_array_get_length (array)) + { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + _("Missing elements in JSON array to conform to a tuple")); + roll_back = TRUE; + break; + } + + json_child = json_array_get_element (array, i - 1); + + variant_child = json_to_gvariant_recurse (json_child, signature, error); + if (variant_child != NULL) + { + children = g_list_prepend (children, variant_child); + } + else + { + roll_back = TRUE; + break; + } + + i++; + } + children = g_list_reverse (children); + + if (! roll_back) + { + if ( (*signature)[0] != ')') + { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + _("Missing closing symbol “)” in the GVariant tuple type")); + roll_back = TRUE; + } + else if (json_array_get_length (array) >= i) + { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + _("Unexpected extra elements in JSON array")); + roll_back = TRUE; + } + else + { + gchar *tuple_type; + + tuple_type = g_strndup (initial_signature, + (*signature) - initial_signature + 1); + + variant = json_to_gvariant_build_from_glist (children, tuple_type); + + g_free (tuple_type); + } + } + + if (roll_back) + g_list_foreach (children, json_to_gvariant_foreach_free, NULL); + + g_list_free (children); + + return variant; +} + +static gchar * +signature_get_next_complete_type (const gchar **signature) +{ + GVariantClass class; + const gchar *initial_signature; + gchar *result; + + /* here it is assumed that 'signature' is a valid type string */ + + initial_signature = *signature; + class = (*signature)[0]; + + if (class == G_VARIANT_CLASS_TUPLE || class == G_VARIANT_CLASS_DICT_ENTRY) + { + gchar stack[256] = {0}; + guint stack_len = 0; + + do + { + if ( (*signature)[0] == G_VARIANT_CLASS_TUPLE) + { + stack[stack_len] = ')'; + stack_len++; + } + else if ( (*signature)[0] == G_VARIANT_CLASS_DICT_ENTRY) + { + stack[stack_len] = '}'; + stack_len++; + } + + (*signature)++; + + if ( (*signature)[0] == stack[stack_len - 1]) + stack_len--; + } + while (stack_len > 0); + + (*signature)++; + } + else if (class == G_VARIANT_CLASS_ARRAY || class == G_VARIANT_CLASS_MAYBE) + { + gchar *tmp_sig; + + (*signature)++; + tmp_sig = signature_get_next_complete_type (signature); + g_free (tmp_sig); + } + else + { + (*signature)++; + } + + result = g_strndup (initial_signature, (*signature) - initial_signature); + + return result; +} + +static GVariant * +json_to_gvariant_maybe (JsonNode *json_node, + const gchar **signature, + GError **error) +{ + GVariant *variant = NULL; + GVariant *value; + gchar *maybe_signature; + + if (signature) + { + (*signature)++; + maybe_signature = signature_get_next_complete_type (signature); + } + else + { + maybe_signature = g_strdup ("v"); + } + + if (json_node_get_node_type (json_node) == JSON_NODE_NULL) + { + variant = g_variant_new_maybe (G_VARIANT_TYPE (maybe_signature), NULL); + } + else + { + const gchar *tmp_signature; + + tmp_signature = maybe_signature; + value = json_to_gvariant_recurse (json_node, + &tmp_signature, + error); + + if (value != NULL) + variant = g_variant_new_maybe (G_VARIANT_TYPE (maybe_signature), value); + } + + g_free (maybe_signature); + + /* compensate the (*signature)++ call at the end of 'recurse()' */ + if (signature) + (*signature)--; + + return variant; +} + +static GVariant * +json_to_gvariant_array (JsonNode *json_node, + const gchar **signature, + GError **error) +{ + GVariant *variant = NULL; + JsonArray *array; + GList *children = NULL; + gboolean roll_back = FALSE; + const gchar *orig_signature = NULL; + gchar *child_signature; + + array = json_node_get_array (json_node); + + if (signature != NULL) + { + orig_signature = *signature; + + (*signature)++; + child_signature = signature_get_next_complete_type (signature); + } + else + child_signature = g_strdup ("v"); + + if (json_array_get_length (array) > 0) + { + guint i; + guint len; + + len = json_array_get_length (array); + for (i = 0; i < len; i++) + { + JsonNode *json_child; + GVariant *variant_child; + const gchar *tmp_signature; + + json_child = json_array_get_element (array, i); + + tmp_signature = child_signature; + variant_child = json_to_gvariant_recurse (json_child, + &tmp_signature, + error); + if (variant_child != NULL) + { + children = g_list_prepend (children, variant_child); + } + else + { + roll_back = TRUE; + break; + } + } + children = g_list_reverse (children); + } + + if (!roll_back) + { + gchar *array_signature; + + if (signature) + array_signature = g_strndup (orig_signature, (*signature) - orig_signature); + else + array_signature = g_strdup ("av"); + + variant = json_to_gvariant_build_from_glist (children, array_signature); + + g_free (array_signature); + + /* compensate the (*signature)++ call at the end of 'recurse()' */ + if (signature) + (*signature)--; + } + else + g_list_foreach (children, json_to_gvariant_foreach_free, NULL); + + g_list_free (children); + g_free (child_signature); + + return variant; +} + +static GVariant * +gvariant_simple_from_string (const gchar *st, + GVariantClass class, + GError **error) +{ + GVariant *variant = NULL; + gchar *nptr = NULL; + gboolean conversion_error = FALSE; + gint64 signed_value; + guint64 unsigned_value; + gdouble double_value; + + errno = 0; + + switch (class) + { + case G_VARIANT_CLASS_BOOLEAN: + if (g_strcmp0 (st, "true") == 0) + variant = g_variant_new_boolean (TRUE); + else if (g_strcmp0 (st, "false") == 0) + variant = g_variant_new_boolean (FALSE); + else + conversion_error = TRUE; + break; + + case G_VARIANT_CLASS_BYTE: + signed_value = g_ascii_strtoll (st, &nptr, 10); + conversion_error = errno != 0 || nptr == st; + variant = g_variant_new_byte (signed_value); + break; + + case G_VARIANT_CLASS_INT16: + signed_value = g_ascii_strtoll (st, &nptr, 10); + conversion_error = errno != 0 || nptr == st; + variant = g_variant_new_int16 (signed_value); + break; + + case G_VARIANT_CLASS_UINT16: + signed_value = g_ascii_strtoll (st, &nptr, 10); + conversion_error = errno != 0 || nptr == st; + variant = g_variant_new_uint16 (signed_value); + break; + + case G_VARIANT_CLASS_INT32: + signed_value = g_ascii_strtoll (st, &nptr, 10); + conversion_error = errno != 0 || nptr == st; + variant = g_variant_new_int32 (signed_value); + break; + + case G_VARIANT_CLASS_UINT32: + unsigned_value = g_ascii_strtoull (st, &nptr, 10); + conversion_error = errno != 0 || nptr == st; + variant = g_variant_new_uint32 (unsigned_value); + break; + + case G_VARIANT_CLASS_INT64: + signed_value = g_ascii_strtoll (st, &nptr, 10); + conversion_error = errno != 0 || nptr == st; + variant = g_variant_new_int64 (signed_value); + break; + + case G_VARIANT_CLASS_UINT64: + unsigned_value = g_ascii_strtoull (st, &nptr, 10); + conversion_error = errno != 0 || nptr == st; + variant = g_variant_new_uint64 (unsigned_value); + break; + + case G_VARIANT_CLASS_HANDLE: + signed_value = strtol (st, &nptr, 10); + conversion_error = errno != 0 || nptr == st; + variant = g_variant_new_handle (signed_value); + break; + + case G_VARIANT_CLASS_DOUBLE: + double_value = g_ascii_strtod (st, &nptr); + conversion_error = errno != 0 || nptr == st; + variant = g_variant_new_double (double_value); + break; + + case G_VARIANT_CLASS_STRING: + case G_VARIANT_CLASS_OBJECT_PATH: + case G_VARIANT_CLASS_SIGNATURE: + variant = g_variant_new_string (st); + break; + + default: + g_assert_not_reached (); + break; + } + + if (conversion_error) + { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + _("Invalid string value converting to GVariant")); + if (variant != NULL) + { + g_variant_unref (variant); + variant = NULL; + } + } + + return variant; +} + +static void +parse_dict_entry_signature (const gchar **signature, + gchar **entry_signature, + gchar **key_signature, + gchar **value_signature) +{ + const gchar *tmp_sig; + + if (signature != NULL) + *entry_signature = signature_get_next_complete_type (signature); + else + *entry_signature = g_strdup ("{sv}"); + + tmp_sig = (*entry_signature) + 1; + *key_signature = signature_get_next_complete_type (&tmp_sig); + *value_signature = signature_get_next_complete_type (&tmp_sig); +} + +static GVariant * +json_to_gvariant_dict_entry (JsonNode *json_node, + const gchar **signature, + GError **error) +{ + GVariant *variant = NULL; + JsonObject *obj; + + gchar *entry_signature; + gchar *key_signature; + gchar *value_signature; + const gchar *tmp_signature; + + GQueue *members; + const gchar *json_member; + JsonNode *json_value; + GVariant *variant_member; + GVariant *variant_value; + + obj = json_node_get_object (json_node); + + if (json_object_get_size (obj) != 1) + { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + _("A GVariant dictionary entry expects a JSON object with exactly one member")); + return NULL; + } + + parse_dict_entry_signature (signature, + &entry_signature, + &key_signature, + &value_signature); + + members = json_object_get_members_internal (obj); + json_member = (const gchar *) members->head->data; + variant_member = gvariant_simple_from_string (json_member, + key_signature[0], + error); + if (variant_member != NULL) + { + json_value = json_object_get_member (obj, json_member); + + tmp_signature = value_signature; + variant_value = json_to_gvariant_recurse (json_value, + &tmp_signature, + error); + + if (variant_value != NULL) + { + GVariantBuilder *builder; + + builder = g_variant_builder_new (G_VARIANT_TYPE (entry_signature)); + g_variant_builder_add_value (builder, variant_member); + g_variant_builder_add_value (builder, variant_value); + variant = g_variant_builder_end (builder); + + g_variant_builder_unref (builder); + } + } + + g_free (value_signature); + g_free (key_signature); + g_free (entry_signature); + + /* compensate the (*signature)++ call at the end of 'recurse()' */ + if (signature) + (*signature)--; + + return variant; +} + +static GVariant * +json_to_gvariant_dictionary (JsonNode *json_node, + const gchar **signature, + GError **error) +{ + GVariant *variant = NULL; + JsonObject *obj; + gboolean roll_back = FALSE; + + gchar *dict_signature; + gchar *entry_signature; + gchar *key_signature; + gchar *value_signature; + const gchar *tmp_signature; + + GVariantBuilder *builder; + GQueue *members; + GList *member; + + obj = json_node_get_object (json_node); + + if (signature != NULL) + (*signature)++; + + parse_dict_entry_signature (signature, + &entry_signature, + &key_signature, + &value_signature); + + dict_signature = g_strdup_printf ("a%s", entry_signature); + + builder = g_variant_builder_new (G_VARIANT_TYPE (dict_signature)); + + members = json_object_get_members_internal (obj); + + for (member = members->head; member != NULL; member = member->next) + { + const gchar *json_member; + JsonNode *json_value; + GVariant *variant_member; + GVariant *variant_value; + + json_member = (const gchar *) member->data; + variant_member = gvariant_simple_from_string (json_member, + key_signature[0], + error); + if (variant_member == NULL) + { + roll_back = TRUE; + break; + } + + json_value = json_object_get_member (obj, json_member); + + tmp_signature = value_signature; + variant_value = json_to_gvariant_recurse (json_value, + &tmp_signature, + error); + + if (variant_value != NULL) + { + g_variant_builder_open (builder, G_VARIANT_TYPE (entry_signature)); + g_variant_builder_add_value (builder, variant_member); + g_variant_builder_add_value (builder, variant_value); + g_variant_builder_close (builder); + } + else + { + roll_back = TRUE; + break; + } + } + + if (! roll_back) + variant = g_variant_builder_end (builder); + + g_variant_builder_unref (builder); + g_free (value_signature); + g_free (key_signature); + g_free (entry_signature); + g_free (dict_signature); + + /* compensate the (*signature)++ call at the end of 'recurse()' */ + if (signature != NULL) + (*signature)--; + + return variant; +} + +static GVariant * +json_to_gvariant_recurse (JsonNode *json_node, + const gchar **signature, + GError **error) +{ + GVariant *variant = NULL; + GVariantClass class; + + class = json_to_gvariant_get_next_class (json_node, signature); + + if (class == JSON_G_VARIANT_CLASS_DICTIONARY) + { + if (json_node_assert_type (json_node, JSON_NODE_OBJECT, 0, error)) + variant = json_to_gvariant_dictionary (json_node, signature, error); + + goto out; + } + + if (JSON_NODE_TYPE (json_node) == JSON_NODE_VALUE && + json_node_get_value_type (json_node) == G_TYPE_STRING) + { + const gchar* str = json_node_get_string (json_node); + switch (class) + { + case G_VARIANT_CLASS_BOOLEAN: + case G_VARIANT_CLASS_BYTE: + case G_VARIANT_CLASS_INT16: + case G_VARIANT_CLASS_UINT16: + case G_VARIANT_CLASS_INT32: + case G_VARIANT_CLASS_UINT32: + case G_VARIANT_CLASS_INT64: + case G_VARIANT_CLASS_UINT64: + case G_VARIANT_CLASS_HANDLE: + case G_VARIANT_CLASS_DOUBLE: + case G_VARIANT_CLASS_STRING: + variant = gvariant_simple_from_string (str, class, error); + goto out; + default: + break; + } + } + + switch (class) + { + case G_VARIANT_CLASS_BOOLEAN: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_BOOLEAN, error)) + variant = g_variant_new_boolean (json_node_get_boolean (json_node)); + break; + + case G_VARIANT_CLASS_BYTE: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_INT64, error)) + variant = g_variant_new_byte (json_node_get_int (json_node)); + break; + + case G_VARIANT_CLASS_INT16: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_INT64, error)) + variant = g_variant_new_int16 (json_node_get_int (json_node)); + break; + + case G_VARIANT_CLASS_UINT16: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_INT64, error)) + variant = g_variant_new_uint16 (json_node_get_int (json_node)); + break; + + case G_VARIANT_CLASS_INT32: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_INT64, error)) + variant = g_variant_new_int32 (json_node_get_int (json_node)); + break; + + case G_VARIANT_CLASS_UINT32: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_INT64, error)) + variant = g_variant_new_uint32 (json_node_get_int (json_node)); + break; + + case G_VARIANT_CLASS_INT64: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_INT64, error)) + variant = g_variant_new_int64 (json_node_get_int (json_node)); + break; + + case G_VARIANT_CLASS_UINT64: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_INT64, error)) + variant = g_variant_new_uint64 (json_node_get_int (json_node)); + break; + + case G_VARIANT_CLASS_HANDLE: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_INT64, error)) + variant = g_variant_new_handle (json_node_get_int (json_node)); + break; + + case G_VARIANT_CLASS_DOUBLE: + /* Doubles can look like ints to the json parser: when they don't have a dot */ + if (JSON_NODE_TYPE (json_node) == JSON_NODE_VALUE && + json_node_get_value_type (json_node) == G_TYPE_INT64) + variant = g_variant_new_double (json_node_get_int (json_node)); + else if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_DOUBLE, error)) + variant = g_variant_new_double (json_node_get_double (json_node)); + break; + + case G_VARIANT_CLASS_STRING: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_STRING, error)) + variant = g_variant_new_string (json_node_get_string (json_node)); + break; + + case G_VARIANT_CLASS_OBJECT_PATH: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_STRING, error)) + variant = g_variant_new_object_path (json_node_get_string (json_node)); + break; + + case G_VARIANT_CLASS_SIGNATURE: + if (json_node_assert_type (json_node, JSON_NODE_VALUE, G_TYPE_STRING, error)) + variant = g_variant_new_signature (json_node_get_string (json_node)); + break; + + case G_VARIANT_CLASS_VARIANT: + variant = g_variant_new_variant (json_to_gvariant_recurse (json_node, + NULL, + error)); + break; + + case G_VARIANT_CLASS_MAYBE: + variant = json_to_gvariant_maybe (json_node, signature, error); + break; + + case G_VARIANT_CLASS_ARRAY: + if (json_node_assert_type (json_node, JSON_NODE_ARRAY, 0, error)) + variant = json_to_gvariant_array (json_node, signature, error); + break; + + case G_VARIANT_CLASS_TUPLE: + if (json_node_assert_type (json_node, JSON_NODE_ARRAY, 0, error)) + variant = json_to_gvariant_tuple (json_node, signature, error); + break; + + case G_VARIANT_CLASS_DICT_ENTRY: + if (json_node_assert_type (json_node, JSON_NODE_OBJECT, 0, error)) + variant = json_to_gvariant_dict_entry (json_node, signature, error); + break; + + default: + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + _("GVariant class “%c” not supported"), class); + break; + } + +out: + if (signature) + (*signature)++; + + return variant; +} + +/** + * json_gvariant_deserialize: + * @json_node: the node to convert + * @signature: (nullable): a valid `GVariant` type string + * @error: (nullable): return location for a #GError, or `NULL` + * + * Converts a JSON data structure to a `GVariant`. + * + * If `signature` is not `NULL`, it will be used to resolve ambiguous + * data types. + * + * If no error occurs, the resulting `GVariant` is guaranteed to conform + * to `signature`. + * + * If `signature` is not `NULL` but does not represent a valid `GVariant` type + * string, `NULL` is returned and the `error` is set to + * `G_IO_ERROR_INVALID_ARGUMENT`. + * + * If a `signature` is provided but the JSON structure cannot be mapped to it, + * `NULL` is returned and the `error` is set to `G_IO_ERROR_INVALID_DATA`. + * + * If `signature` is `NULL`, the conversion is done based strictly on the types + * in the JSON nodes. + * + * The returned variant has a floating reference that will need to be sunk + * by the caller code. + * + * Return value: (transfer floating) (nullable): A newly created `GVariant` + * + * Since: 0.14 + */ +GVariant * +json_gvariant_deserialize (JsonNode *json_node, + const gchar *signature, + GError **error) +{ + g_return_val_if_fail (json_node != NULL, NULL); + + if (signature != NULL && ! g_variant_type_string_is_valid (signature)) + { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + _("Invalid GVariant signature")); + return NULL; + } + + return json_to_gvariant_recurse (json_node, signature ? &signature : NULL, error); +} + +/** + * json_gvariant_deserialize_data: + * @json: A JSON data string + * @length: The length of @json, or -1 if `NUL`-terminated + * @signature: (nullable): A valid `GVariant` type string + * @error: A pointer to a #GError + * + * Converts a JSON string to a `GVariant` value. + * + * This function works exactly like [func@Json.gvariant_deserialize], but + * takes a JSON encoded string instead. + * + * The string is first converted to a [struct@Json.Node] using + * [class@Json.Parser], and then `json_gvariant_deserialize` is called on + * the node. + * + * The returned variant has a floating reference that will need to be sunk + * by the caller code. + * + * Returns: (transfer floating) (nullable): A newly created `GVariant`D compliant + * + * Since: 0.14 + */ +GVariant * +json_gvariant_deserialize_data (const gchar *json, + gssize length, + const gchar *signature, + GError **error) +{ + JsonParser *parser; + GVariant *variant = NULL; + JsonNode *root; + + parser = json_parser_new (); + + if (! json_parser_load_from_data (parser, json, length, error)) + { + g_object_unref (parser); + return NULL; + } + + root = json_parser_get_root (parser); + if (root == NULL) + { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + _("JSON data is empty")); + } + else + { + variant = + json_gvariant_deserialize (json_parser_get_root (parser), signature, error); + } + + g_object_unref (parser); + + return variant; +} diff --git a/lsp/deps/json-glib/json-gvariant.h b/lsp/deps/json-glib/json-gvariant.h new file mode 100644 index 000000000..f5886bc63 --- /dev/null +++ b/lsp/deps/json-glib/json-gvariant.h @@ -0,0 +1,50 @@ +/* json-gvariant.h - JSON GVariant integration + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Eduardo Lima Mitev + */ + +#pragma once + +#if !defined(__JSON_GLIB_INSIDE__) && !defined(JSON_COMPILATION) +#error "Only can be included directly." +#endif + +#include + +G_BEGIN_DECLS + +JSON_AVAILABLE_IN_1_0 +JsonNode * json_gvariant_serialize (GVariant *variant); +JSON_AVAILABLE_IN_1_0 +gchar * json_gvariant_serialize_data (GVariant *variant, + gsize *length); + +JSON_AVAILABLE_IN_1_0 +GVariant * json_gvariant_deserialize (JsonNode *json_node, + const gchar *signature, + GError **error); +JSON_AVAILABLE_IN_1_0 +GVariant * json_gvariant_deserialize_data (const gchar *json, + gssize length, + const gchar *signature, + GError **error); + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-node.c b/lsp/deps/json-glib/json-node.c new file mode 100644 index 000000000..1e573ebec --- /dev/null +++ b/lsp/deps/json-glib/json-node.c @@ -0,0 +1,1555 @@ +/* json-node.c - JSON object model node + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * Copyright (C) 2015 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + * Philip Withnall + */ + +#include "config.h" + +#include + +#include "json-types.h" +#include "json-types-private.h" +#include "json-debug.h" + +/** + * JsonNode: + * + * A generic container of JSON data types. + * + * `JsonNode` can contain fundamental types (integers, booleans, floating point + * numbers, strings) and complex types (arrays and objects). + * + * When parsing a JSON data stream you extract the root node and walk + * the node tree by retrieving the type of data contained inside the + * node with the `JSON_NODE_TYPE` macro. If the node contains a fundamental + * type you can retrieve a copy of the `GValue` holding it with the + * [method@Json.Node.get_value] function, and then use the `GValue` API to extract + * the data; if the node contains a complex type you can retrieve the + * [struct@Json.Object] or the [struct@Json.Array] using [method@Json.Node.get_object] + * or [method@Json.Node.get_array] respectively, and then retrieve the nodes + * they contain. + * + * A `JsonNode` may be marked as immutable using [method@Json.Node.seal]. This + * marks the node and all its descendents as read-only, and means that + * subsequent calls to setter functions (such as [method@Json.Node.set_array]) + * on them will abort as a programmer error. By marking a node tree as + * immutable, it may be referenced in multiple places and its hash value cached + * for fast lookups, without the possibility of a value deep within the tree + * changing and affecting hash values. Immutable nodes may be passed to + * functions which retain a reference to them without needing to take a copy. + * + * A `JsonNode` supports two types of memory management: `malloc`/`free` + * semantics, and reference counting semantics. The two may be mixed to a + * limited extent: nodes may be allocated (which gives them a reference count + * of 1), referenced one or more times, unreferenced exactly that number of + * times (using [method@Json.Node.unref]), then either unreferenced exactly + * once more or freed (using [method@Json.Node.free]) to destroy them. + * The [method@Json.Node.free] function must not be used when a node might + * have a reference count not equal to 1. To this end, JSON-GLib uses + * [method@Json.Node.copy] and [method@Json.Node.unref] internally. + */ + +G_DEFINE_BOXED_TYPE (JsonNode, json_node, json_node_copy, json_node_unref) + +/** + * json_node_get_value_type: + * @node: the node to check + * + * Returns the `GType` of the payload of the node. + * + * For `JSON_NODE_NULL` nodes, the returned type is `G_TYPE_INVALID`. + * + * Return value: the type for the payload + * + * Since: 0.4 + */ +GType +json_node_get_value_type (JsonNode *node) +{ + g_return_val_if_fail (node != NULL, G_TYPE_INVALID); + + switch (node->type) + { + case JSON_NODE_OBJECT: + return JSON_TYPE_OBJECT; + + case JSON_NODE_ARRAY: + return JSON_TYPE_ARRAY; + + case JSON_NODE_NULL: + return G_TYPE_INVALID; + + case JSON_NODE_VALUE: + if (node->data.value) + return JSON_VALUE_TYPE (node->data.value); + else + return G_TYPE_INVALID; + + default: + g_assert_not_reached (); + return G_TYPE_INVALID; + } +} + +/** + * json_node_alloc: (constructor) + * + * Allocates a new, uninitialized node. + * + * Use [method@Json.Node.init] and its variants to initialize the returned value. + * + * Return value: (transfer full): the newly allocated node + * + * Since: 0.16 + */ +JsonNode * +json_node_alloc (void) +{ + JsonNode *node = NULL; + + node = g_slice_new0 (JsonNode); + node->ref_count = 1; + node->allocated = TRUE; + + return node; +} + +static void +json_node_unset (JsonNode *node) +{ + /* Note: Don't use JSON_NODE_IS_VALID here because this may legitimately be + * called with (node->ref_count == 0) from json_node_unref(). */ + g_assert (node != NULL); + + switch (node->type) + { + case JSON_NODE_OBJECT: + g_clear_pointer (&(node->data.object), json_object_unref); + break; + + case JSON_NODE_ARRAY: + g_clear_pointer (&(node->data.array), json_array_unref); + break; + + case JSON_NODE_VALUE: + g_clear_pointer (&(node->data.value), json_value_unref); + break; + + case JSON_NODE_NULL: + break; + } +} + +/** + * json_node_init: + * @node: the node to initialize + * @type: the type of JSON node to initialize @node to + * + * Initializes a @node to a specific @type. + * + * If the node has already been initialized once, it will be reset to + * the given type, and any data contained will be cleared. + * + * Return value: (transfer none): the initialized node + * + * Since: 0.16 + */ +JsonNode * +json_node_init (JsonNode *node, + JsonNodeType type) +{ + g_return_val_if_fail (type >= JSON_NODE_OBJECT && + type <= JSON_NODE_NULL, NULL); + g_return_val_if_fail (node->ref_count == 1, NULL); + + json_node_unset (node); + + node->type = type; + + return node; +} + +/** + * json_node_init_object: + * @node: the node to initialize + * @object: (nullable): the JSON object to initialize @node with, or `NULL` + * + * Initializes @node to `JSON_NODE_OBJECT` and sets @object into it. + * + * This function will take a reference on @object. + * + * If the node has already been initialized once, it will be reset to + * the given type, and any data contained will be cleared. + * + * Return value: (transfer none): the initialized node + * + * Since: 0.16 + */ +JsonNode * +json_node_init_object (JsonNode *node, + JsonObject *object) +{ + g_return_val_if_fail (node != NULL, NULL); + + json_node_init (node, JSON_NODE_OBJECT); + json_node_set_object (node, object); + + return node; +} + +/** + * json_node_init_array: + * @node: the node to initialize + * @array: (nullable): the JSON array to initialize @node with, or `NULL` + * + * Initializes @node to `JSON_NODE_ARRAY` and sets @array into it. + * + * This function will take a reference on @array. + * + * If the node has already been initialized once, it will be reset to + * the given type, and any data contained will be cleared. + * + * Return value: (transfer none): the initialized node + * + * Since: 0.16 + */ +JsonNode * +json_node_init_array (JsonNode *node, + JsonArray *array) +{ + g_return_val_if_fail (node != NULL, NULL); + + json_node_init (node, JSON_NODE_ARRAY); + json_node_set_array (node, array); + + return node; +} + +/** + * json_node_init_int: + * @node: the node to initialize + * @value: an integer + * + * Initializes @node to `JSON_NODE_VALUE` and sets @value into it. + * + * If the node has already been initialized once, it will be reset to + * the given type, and any data contained will be cleared. + * + * Return value: (transfer none): the initialized node + * + * Since: 0.16 + */ +JsonNode * +json_node_init_int (JsonNode *node, + gint64 value) +{ + g_return_val_if_fail (node != NULL, NULL); + + json_node_init (node, JSON_NODE_VALUE); + json_node_set_int (node, value); + + return node; +} + +/** + * json_node_init_double: + * @node: the node to initialize + * @value: a floating point value + * + * Initializes @node to `JSON_NODE_VALUE` and sets @value into it. + * + * If the node has already been initialized once, it will be reset to + * the given type, and any data contained will be cleared. + * + * Return value: (transfer none): the initialized node + * + * Since: 0.16 + */ +JsonNode * +json_node_init_double (JsonNode *node, + gdouble value) +{ + g_return_val_if_fail (node != NULL, NULL); + + json_node_init (node, JSON_NODE_VALUE); + json_node_set_double (node, value); + + return node; +} + +/** + * json_node_init_boolean: + * @node: the node to initialize + * @value: a boolean value + * + * Initializes @node to `JSON_NODE_VALUE` and sets @value into it. + * + * If the node has already been initialized once, it will be reset to + * the given type, and any data contained will be cleared. + * + * Return value: (transfer none): the initialized node + * + * Since: 0.16 + */ +JsonNode * +json_node_init_boolean (JsonNode *node, + gboolean value) +{ + g_return_val_if_fail (node != NULL, NULL); + + json_node_init (node, JSON_NODE_VALUE); + json_node_set_boolean (node, value); + + return node; +} + +/** + * json_node_init_string: + * @node: the node to initialize + * @value: (nullable): a string value + * + * Initializes @node to `JSON_NODE_VALUE` and sets @value into it. + * + * If the node has already been initialized once, it will be reset to + * the given type, and any data contained will be cleared. + * + * Return value: (transfer none): the initialized node + * + * Since: 0.16 + */ +JsonNode * +json_node_init_string (JsonNode *node, + const char *value) +{ + g_return_val_if_fail (node != NULL, NULL); + + json_node_init (node, JSON_NODE_VALUE); + json_node_set_string (node, value); + + return node; +} + +/** + * json_node_init_null: + * @node: the node to initialize + * + * Initializes @node to `JSON_NODE_NULL`. + * + * If the node has already been initialized once, it will be reset to + * the given type, and any data contained will be cleared. + * + * Return value: (transfer none): the initialized node + * + * Since: 0.16 + */ +JsonNode * +json_node_init_null (JsonNode *node) +{ + g_return_val_if_fail (node != NULL, NULL); + + return json_node_init (node, JSON_NODE_NULL); +} + +/** + * json_node_new: (constructor) + * @type: the type of the node to create + * + * Creates a new node holding the given @type. + * + * This is a convenience function for [ctor@Json.Node.alloc] and + * [method@Json.Node.init], and it's the equivalent of: + * + * ```c + json_node_init (json_node_alloc (), type); + * ``` + * + * Return value: (transfer full): the newly created node + */ +JsonNode * +json_node_new (JsonNodeType type) +{ + g_return_val_if_fail (type >= JSON_NODE_OBJECT && + type <= JSON_NODE_NULL, NULL); + + return json_node_init (json_node_alloc (), type); +} + +/** + * json_node_copy: + * @node: the node to copy + * + * Copies @node. + * + * If the node contains complex data types, their reference + * counts are increased, regardless of whether the node is mutable or + * immutable. + * + * The copy will be immutable if, and only if, @node is immutable. However, + * there should be no need to copy an immutable node. + * + * Return value: (transfer full): the copied of the given node + */ +JsonNode * +json_node_copy (JsonNode *node) +{ + JsonNode *copy; + + g_return_val_if_fail (JSON_NODE_IS_VALID (node), NULL); + + copy = json_node_alloc (); + copy->type = node->type; + copy->immutable = node->immutable; + +#ifdef JSON_ENABLE_DEBUG + if (node->immutable) + { + JSON_NOTE (NODE, "Copying immutable JsonNode %p of type %s", + node, + json_node_type_name (node)); + } +#endif + + switch (copy->type) + { + case JSON_NODE_OBJECT: + copy->data.object = json_node_dup_object (node); + break; + + case JSON_NODE_ARRAY: + copy->data.array = json_node_dup_array (node); + break; + + case JSON_NODE_VALUE: + if (node->data.value) + copy->data.value = json_value_ref (node->data.value); + break; + + case JSON_NODE_NULL: + break; + + default: + g_assert_not_reached (); + } + + return copy; +} + +/** + * json_node_ref: + * @node: the node to reference + * + * Increments the reference count of @node. + * + * Since: 1.2 + * Returns: (transfer full): a pointer to @node + */ +JsonNode * +json_node_ref (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), NULL); + + g_atomic_int_inc (&node->ref_count); + + return node; +} + +/** + * json_node_unref: + * @node: (transfer full): the node to unreference + * + * Decrements the reference count of @node. + * + * If the reference count reaches zero, the node is freed. + * + * Since: 1.2 + */ +void +json_node_unref (JsonNode *node) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + + if (g_atomic_int_dec_and_test (&node->ref_count)) + { + json_node_unset (node); + if (node->allocated) + g_slice_free (JsonNode, node); + } +} + +/** + * json_node_set_object: + * @node: a node initialized to `JSON_NODE_OBJECT` + * @object: (nullable): a JSON object + * + * Sets @objects inside @node. + * + * The reference count of @object is increased. + * + * If @object is `NULL`, the node’s existing object is cleared. + * + * It is an error to call this on an immutable node, or on a node which is not + * an object node. + */ +void +json_node_set_object (JsonNode *node, + JsonObject *object) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_OBJECT); + g_return_if_fail (!node->immutable); + + if (node->data.object != NULL) + json_object_unref (node->data.object); + + if (object) + node->data.object = json_object_ref (object); + else + node->data.object = NULL; +} + +/** + * json_node_take_object: + * @node: a node initialized to `JSON_NODE_OBJECT` + * @object: (transfer full): a JSON object + * + * Sets @object inside @node. + * + * The reference count of @object is not increased. + * + * It is an error to call this on an immutable node, or on a node which is not + * an object node. + */ +void +json_node_take_object (JsonNode *node, + JsonObject *object) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_OBJECT); + g_return_if_fail (!node->immutable); + + if (node->data.object) + { + json_object_unref (node->data.object); + node->data.object = NULL; + } + + if (object) + node->data.object = object; +} + +/** + * json_node_get_object: + * @node: a node holding a JSON object + * + * Retrieves the object stored inside a node. + * + * It is a programmer error to call this on a node which doesn’t hold an + * object value. Use `JSON_NODE_HOLDS_OBJECT` first. + * + * Return value: (transfer none) (nullable): the JSON object + */ +JsonObject * +json_node_get_object (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), NULL); + g_return_val_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_OBJECT, NULL); + + return node->data.object; +} + +/** + * json_node_dup_object: + * @node: a node holding a JSON object + * + * Retrieves the object inside @node. + * + * The reference count of the returned object is increased. + * + * It is a programmer error to call this on a node which doesn’t hold an + * object value. Use `JSON_NODE_HOLDS_OBJECT` first. + * + * Return value: (transfer full) (nullable): the JSON object + */ +JsonObject * +json_node_dup_object (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), NULL); + g_return_val_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_OBJECT, NULL); + + if (node->data.object) + return json_object_ref (node->data.object); + + return NULL; +} + +/** + * json_node_set_array: + * @node: a node initialized to `JSON_NODE_ARRAY` + * @array: a JSON array + * + * Sets @array inside @node. + * + * The reference count of @array is increased. + * + * It is a programmer error to call this on a node which doesn’t hold an + * array value. Use `JSON_NODE_HOLDS_ARRAY` first. + */ +void +json_node_set_array (JsonNode *node, + JsonArray *array) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_ARRAY); + g_return_if_fail (!node->immutable); + + if (node->data.array) + json_array_unref (node->data.array); + + if (array) + node->data.array = json_array_ref (array); + else + node->data.array = NULL; +} + +/** + * json_node_take_array: + * @node: a node initialized to `JSON_NODE_ARRAY` + * @array: (transfer full): a JSON array + * + * Sets @array inside @node. + * + * The reference count of @array is not increased. + * + * It is a programmer error to call this on a node which doesn’t hold an + * array value. Use `JSON_NODE_HOLDS_ARRAY` first. + */ +void +json_node_take_array (JsonNode *node, + JsonArray *array) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_ARRAY); + g_return_if_fail (!node->immutable); + + if (node->data.array) + { + json_array_unref (node->data.array); + node->data.array = NULL; + } + + if (array) + node->data.array = array; +} + +/** + * json_node_get_array: + * @node: a node holding an array + * + * Retrieves the JSON array stored inside a node. + * + * It is a programmer error to call this on a node which doesn’t hold an + * array value. Use `JSON_NODE_HOLDS_ARRAY` first. + * + * Return value: (transfer none) (nullable): the JSON array + */ +JsonArray * +json_node_get_array (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), NULL); + g_return_val_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_ARRAY, NULL); + + return node->data.array; +} + +/** + * json_node_dup_array: + * @node: a node holding an array + * + * Retrieves the JSON array inside @node. + * + * The reference count of the returned array is increased. + * + * It is a programmer error to call this on a node which doesn’t hold an + * array value. Use `JSON_NODE_HOLDS_ARRAY` first. + * + * Return value: (transfer full) (nullable): the JSON array with its reference + * count increased. + */ +JsonArray * +json_node_dup_array (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), NULL); + g_return_val_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_ARRAY, NULL); + + if (node->data.array) + return json_array_ref (node->data.array); + + return NULL; +} + +/** + * json_node_get_value: + * @node: a node + * @value: (out caller-allocates): return location for an uninitialized value + * + * Retrieves a value from a node and copies into @value. + * + * When done using it, call `g_value_unset()` on the `GValue` to free the + * associated resources. + * + * It is a programmer error to call this on a node which doesn’t hold a scalar + * value. Use `JSON_NODE_HOLDS_VALUE` first. + */ +void +json_node_get_value (JsonNode *node, + GValue *value) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE); + + if (node->data.value) + { + g_value_init (value, JSON_VALUE_TYPE (node->data.value)); + switch (JSON_VALUE_TYPE (node->data.value)) + { + case G_TYPE_INT64: + g_value_set_int64 (value, json_value_get_int (node->data.value)); + break; + + case G_TYPE_DOUBLE: + g_value_set_double (value, json_value_get_double (node->data.value)); + break; + + case G_TYPE_BOOLEAN: + g_value_set_boolean (value, json_value_get_boolean (node->data.value)); + break; + + case G_TYPE_STRING: + g_value_set_string (value, json_value_get_string (node->data.value)); + break; + + default: + break; + } + } +} + +/** + * json_node_set_value: + * @node: a node initialized to `JSON_NODE_VALUE` + * @value: the value to set + * + * Sets a scalar value inside the given node. + * + * The contents of the given `GValue` are copied into the `JsonNode`. + * + * The following `GValue` types have a direct mapping to JSON types: + * + * - `G_TYPE_INT64` + * - `G_TYPE_DOUBLE` + * - `G_TYPE_BOOLEAN` + * - `G_TYPE_STRING` + * + * JSON-GLib will also automatically promote the following `GValue` types: + * + * - `G_TYPE_INT` to `G_TYPE_INT64` + * - `G_TYPE_FLOAT` to `G_TYPE_DOUBLE` + * + * It is an error to call this on an immutable node, or on a node which is not + * a value node. + */ +void +json_node_set_value (JsonNode *node, + const GValue *value) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE); + g_return_if_fail (G_VALUE_TYPE (value) != G_TYPE_INVALID); + g_return_if_fail (!node->immutable); + + if (node->data.value == NULL) + node->data.value = json_value_alloc (); + + switch (G_VALUE_TYPE (value)) + { + /* auto-promote machine integers to 64 bit integers */ + case G_TYPE_INT64: + case G_TYPE_INT: + json_value_init (node->data.value, JSON_VALUE_INT); + if (G_VALUE_TYPE (value) == G_TYPE_INT64) + json_value_set_int (node->data.value, g_value_get_int64 (value)); + else + json_value_set_int (node->data.value, g_value_get_int (value)); + break; + + case G_TYPE_BOOLEAN: + json_value_init (node->data.value, JSON_VALUE_BOOLEAN); + json_value_set_boolean (node->data.value, g_value_get_boolean (value)); + break; + + /* auto-promote single-precision floats to double precision floats */ + case G_TYPE_DOUBLE: + case G_TYPE_FLOAT: + json_value_init (node->data.value, JSON_VALUE_DOUBLE); + if (G_VALUE_TYPE (value) == G_TYPE_DOUBLE) + json_value_set_double (node->data.value, g_value_get_double (value)); + else + json_value_set_double (node->data.value, g_value_get_float (value)); + break; + + case G_TYPE_STRING: + json_value_init (node->data.value, JSON_VALUE_STRING); + json_value_set_string (node->data.value, g_value_get_string (value)); + break; + + default: + g_message ("Invalid value of type '%s'", + g_type_name (G_VALUE_TYPE (value))); + return; + } + +} + +/** + * json_node_free: + * @node: the node to free + * + * Frees the resources allocated by the node. + */ +void +json_node_free (JsonNode *node) +{ + g_return_if_fail (node == NULL || JSON_NODE_IS_VALID (node)); + g_return_if_fail (node == NULL || node->allocated); + + if (G_LIKELY (node)) + { + if (node->ref_count > 1) + g_warning ("Freeing a JsonNode %p owned by other code.", node); + + json_node_unset (node); + g_slice_free (JsonNode, node); + } +} + +/** + * json_node_seal: + * @node: the node to seal + * + * Seals the given node, making it immutable to further changes. + * + * In order to be sealed, the @node must have a type and value set. The value + * will be recursively sealed — if the node holds an object, that JSON object + * will be sealed, etc. + * + * If the `node` is already immutable, this is a no-op. + * + * Since: 1.2 + */ +void +json_node_seal (JsonNode *node) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + + if (node->immutable) + return; + + switch (node->type) + { + case JSON_NODE_OBJECT: + g_return_if_fail (node->data.object != NULL); + json_object_seal (node->data.object); + break; + case JSON_NODE_ARRAY: + g_return_if_fail (node->data.array != NULL); + json_array_seal (node->data.array); + break; + case JSON_NODE_NULL: + break; + case JSON_NODE_VALUE: + g_return_if_fail (node->data.value != NULL); + json_value_seal (node->data.value); + break; + default: + g_assert_not_reached (); + } + + node->immutable = TRUE; +} + +/** + * json_node_is_immutable: + * @node: the node to check + * + * Check whether the given @node has been marked as immutable by calling + * [method@Json.Node.seal] on it. + * + * Since: 1.2 + * Returns: `TRUE` if the @node is immutable + */ +gboolean +json_node_is_immutable (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), FALSE); + + return node->immutable; +} + +/** + * json_node_type_name: + * @node: a node + * + * Retrieves the user readable name of the data type contained by @node. + * + * **Note**: The name is only meant for debugging purposes, and there is no + * guarantee the name will stay the same across different versions. + * + * Return value: (transfer none): a string containing the name of the type + */ +const gchar * +json_node_type_name (JsonNode *node) +{ + g_return_val_if_fail (node != NULL, "(null)"); + + switch (node->type) + { + case JSON_NODE_OBJECT: + case JSON_NODE_ARRAY: + case JSON_NODE_NULL: + return json_node_type_get_name (node->type); + + case JSON_NODE_VALUE: + if (node->data.value) + return json_value_type_get_name (node->data.value->type); + } + + return "unknown"; +} + +const gchar * +json_node_type_get_name (JsonNodeType node_type) +{ + switch (node_type) + { + case JSON_NODE_OBJECT: + return "JsonObject"; + + case JSON_NODE_ARRAY: + return "JsonArray"; + + case JSON_NODE_NULL: + return "NULL"; + + case JSON_NODE_VALUE: + return "Value"; + + default: + g_assert_not_reached (); + break; + } + + return "unknown"; +} + +/** + * json_node_set_parent: + * @node: the node to change + * @parent: (transfer none) (nullable): the parent node + * + * Sets the parent node for the given `node`. + * + * It is an error to call this with an immutable @parent. + * + * The @node may be immutable. + * + * Since: 0.8 + */ +void +json_node_set_parent (JsonNode *node, + JsonNode *parent) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + g_return_if_fail (parent == NULL || + !json_node_is_immutable (parent)); + + node->parent = parent; +} + +/** + * json_node_get_parent: + * @node: the node to query + * + * Retrieves the parent node of the given @node. + * + * Return value: (transfer none) (nullable): the parent node, or `NULL` if @node + * is the root node + */ +JsonNode * +json_node_get_parent (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), NULL); + + return node->parent; +} + +/** + * json_node_set_string: + * @node: a node initialized to `JSON_NODE_VALUE` + * @value: a string value + * + * Sets @value as the string content of the @node, replacing any existing + * content. + * + * It is an error to call this on an immutable node, or on a node which is not + * a value node. + */ +void +json_node_set_string (JsonNode *node, + const gchar *value) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE); + g_return_if_fail (!node->immutable); + + if (node->data.value == NULL) + node->data.value = json_value_init (json_value_alloc (), JSON_VALUE_STRING); + else + json_value_init (node->data.value, JSON_VALUE_STRING); + + json_value_set_string (node->data.value, value); +} + +/** + * json_node_get_string: + * @node: a node holding a string + * + * Gets the string value stored inside a node. + * + * If the node does not hold a string value, `NULL` is returned. + * + * Return value: (nullable): a string value. + */ +const gchar * +json_node_get_string (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), NULL); + + if (JSON_NODE_TYPE (node) == JSON_NODE_NULL) + return NULL; + + if (JSON_VALUE_HOLDS_STRING (node->data.value)) + return json_value_get_string (node->data.value); + + return NULL; +} + +/** + * json_node_dup_string: + * @node: a node holding a string + * + * Gets a copy of the string value stored inside a node. + * + * If the node does not hold a string value, `NULL` is returned. + * + * Return value: (transfer full) (nullable): a copy of the string + * inside the node + */ +gchar * +json_node_dup_string (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), NULL); + + return g_strdup (json_node_get_string (node)); +} + +/** + * json_node_set_int: + * @node: a node initialized to `JSON_NODE_VALUE` + * @value: an integer value + * + * Sets @value as the integer content of the @node, replacing any existing + * content. + * + * It is an error to call this on an immutable node, or on a node which is not + * a value node. + */ +void +json_node_set_int (JsonNode *node, + gint64 value) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE); + g_return_if_fail (!node->immutable); + + if (node->data.value == NULL) + node->data.value = json_value_init (json_value_alloc (), JSON_VALUE_INT); + else + json_value_init (node->data.value, JSON_VALUE_INT); + + json_value_set_int (node->data.value, value); +} + +/** + * json_node_get_int: + * @node: a node holding an integer + * + * Gets the integer value stored inside a node. + * + * If the node holds a double value, its integer component is returned. + * + * If the node holds a `FALSE` boolean value, `0` is returned; otherwise, + * a non-zero integer is returned. + * + * If the node holds a `JSON_NODE_NULL` value or a value of another + * non-integer type, `0` is returned. + * + * Return value: an integer value. + */ +gint64 +json_node_get_int (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), 0); + + if (JSON_NODE_TYPE (node) == JSON_NODE_NULL) + return 0; + + if (JSON_VALUE_HOLDS_INT (node->data.value)) + return json_value_get_int (node->data.value); + + if (JSON_VALUE_HOLDS_DOUBLE (node->data.value)) + return json_value_get_double (node->data.value); + + if (JSON_VALUE_HOLDS_BOOLEAN (node->data.value)) + return json_value_get_boolean (node->data.value); + + return 0; +} + +/** + * json_node_set_double: + * @node: a node initialized to `JSON_NODE_VALUE` + * @value: a double value + * + * Sets @value as the double content of the @node, replacing any existing + * content. + * + * It is an error to call this on an immutable node, or on a node which is not + * a value node. + */ +void +json_node_set_double (JsonNode *node, + gdouble value) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE); + g_return_if_fail (!node->immutable); + + if (node->data.value == NULL) + node->data.value = json_value_init (json_value_alloc (), JSON_VALUE_DOUBLE); + else + json_value_init (node->data.value, JSON_VALUE_DOUBLE); + + json_value_set_double (node->data.value, value); +} + +/** + * json_node_get_double: + * @node: a node holding a floating point value + * + * Gets the double value stored inside a node. + * + * If the node holds an integer value, it is returned as a double. + * + * If the node holds a `FALSE` boolean value, `0.0` is returned; otherwise + * a non-zero double is returned. + * + * If the node holds a `JSON_NODE_NULL` value or a value of another + * non-double type, `0.0` is returned. + * + * Return value: a double value. + */ +gdouble +json_node_get_double (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), 0.0); + + if (JSON_NODE_TYPE (node) == JSON_NODE_NULL) + return 0; + + if (JSON_VALUE_HOLDS_DOUBLE (node->data.value)) + return json_value_get_double (node->data.value); + + if (JSON_VALUE_HOLDS_INT (node->data.value)) + return (gdouble) json_value_get_int (node->data.value); + + if (JSON_VALUE_HOLDS_BOOLEAN (node->data.value)) + return (gdouble) json_value_get_boolean (node->data.value); + + return 0.0; +} + +/** + * json_node_set_boolean: + * @node: a node initialized to `JSON_NODE_VALUE` + * @value: a boolean value + * + * Sets @value as the boolean content of the @node, replacing any existing + * content. + * + * It is an error to call this on an immutable node, or on a node which is not + * a value node. + */ +void +json_node_set_boolean (JsonNode *node, + gboolean value) +{ + g_return_if_fail (JSON_NODE_IS_VALID (node)); + g_return_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE); + g_return_if_fail (!node->immutable); + + if (node->data.value == NULL) + node->data.value = json_value_init (json_value_alloc (), JSON_VALUE_BOOLEAN); + else + json_value_init (node->data.value, JSON_VALUE_BOOLEAN); + + json_value_set_boolean (node->data.value, value); +} + +/** + * json_node_get_boolean: + * @node: a node holding a boolean value + * + * Gets the boolean value stored inside a node. + * + * If the node holds an integer or double value which is zero, `FALSE` is + * returned; otherwise `TRUE` is returned. + * + * If the node holds a `JSON_NODE_NULL` value or a value of another + * non-boolean type, `FALSE` is returned. + * + * Return value: a boolean value. + */ +gboolean +json_node_get_boolean (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), FALSE); + + if (JSON_NODE_TYPE (node) == JSON_NODE_NULL) + return FALSE; + + if (JSON_VALUE_HOLDS_BOOLEAN (node->data.value)) + return json_value_get_boolean (node->data.value); + + if (JSON_VALUE_HOLDS_INT (node->data.value)) + return json_value_get_int (node->data.value) != 0; + + if (JSON_VALUE_HOLDS_DOUBLE (node->data.value)) + return json_value_get_double (node->data.value) != 0.0; + + return FALSE; +} + +/** + * json_node_get_node_type: + * @node: the node to check + * + * Retrieves the type of a @node. + * + * Return value: the type of the node + * + * Since: 0.8 + */ +JsonNodeType +json_node_get_node_type (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), JSON_NODE_NULL); + + return node->type; +} + +/** + * json_node_is_null: + * @node: the node to check + * + * Checks whether @node is a `JSON_NODE_NULL`. + * + * A `JSON_NODE_NULL` node is not the same as a `NULL` node; a `JSON_NODE_NULL` + * represents a literal `null` value in the JSON tree. + * + * Return value: `TRUE` if the node is null + * + * Since: 0.8 + */ +gboolean +json_node_is_null (JsonNode *node) +{ + g_return_val_if_fail (JSON_NODE_IS_VALID (node), TRUE); + + return node->type == JSON_NODE_NULL; +} + +/*< private > + * json_type_is_a: + * @sub: sub-type + * @super: super-type + * + * Check whether @sub is a sub-type of, or equal to, @super. + * + * The only sub-type relationship in the JSON Schema type system is that + * an integer type is a sub-type of a number type. + * + * Formally, this function calculates: `@sub <: @super`. + * + * Reference: http://json-schema.org/latest/json-schema-core.html#rfc.section.3.5 + * + * Returns: `TRUE` if @sub is a sub-type of, or equal to, @super; `FALSE` otherwise + * Since: 1.2 + */ +static gboolean +json_type_is_a (JsonNode *sub, + JsonNode *super) +{ + if (super->type == JSON_NODE_VALUE && sub->type == JSON_NODE_VALUE) + { + JsonValueType super_value_type, sub_value_type; + + if (super->data.value == NULL || sub->data.value == NULL) + return FALSE; + + super_value_type = super->data.value->type; + sub_value_type = sub->data.value->type; + + return (super_value_type == sub_value_type || + (super_value_type == JSON_VALUE_DOUBLE && + sub_value_type == JSON_VALUE_INT)); + } + + return (super->type == sub->type); +} + +/** + * json_string_hash: + * @key: (type utf8): a JSON string to hash + * + * Calculate a hash value for the given @key (a UTF-8 JSON string). + * + * Note: Member names are compared byte-wise, without applying any Unicode + * decomposition or normalisation. This is not explicitly mentioned in the JSON + * standard (ECMA-404), but is assumed. + * + * Returns: hash value for @key + * Since: 1.2 + */ +guint +json_string_hash (gconstpointer key) +{ + return g_str_hash (key); +} + +/** + * json_string_equal: + * @a: (type utf8): a JSON string + * @b: (type utf8): another JSON string + * + * Check whether @a and @b are equal UTF-8 JSON strings. + * + * Returns: `TRUE` if @a and @b are equal; `FALSE` otherwise + * Since: 1.2 + */ +gboolean +json_string_equal (gconstpointer a, + gconstpointer b) +{ + return g_str_equal (a, b); +} + +/** + * json_string_compare: + * @a: (type utf8): a JSON string + * @b: (type utf8): another JSON string + * + * Check whether @a and @b are equal UTF-8 JSON strings and return an ordering + * over them in `strcmp()` style. + * + * Returns: an integer less than zero if `a < b`, equal to zero if `a == b`, and + * greater than zero if `a > b` + * + * Since: 1.2 + */ +gint +json_string_compare (gconstpointer a, + gconstpointer b) +{ + return g_strcmp0 (a, b); +} + +/** + * json_node_hash: + * @key: (type JsonNode): a JSON node to hash + * + * Calculate a hash value for the given @key. + * + * The hash is calculated over the node and its value, recursively. If the node + * is immutable, this is a fast operation; otherwise, it scales proportionally + * with the size of the node’s value (for example, with the number of members + * in the JSON object if this node contains an object). + * + * Returns: hash value for @key + * Since: 1.2 + */ +guint +json_node_hash (gconstpointer key) +{ + JsonNode *node; /* unowned */ + + /* These are all randomly generated and arbitrary. */ + const guint value_hash = 0xc19e75ad; + const guint array_hash = 0x865acfc2; + const guint object_hash = 0x3c8f3135; + + node = (JsonNode *) key; + + /* XOR the hash values with a (constant) random number depending on the node’s + * type so that empty values, arrays and objects do not all collide at the + * hash value 0. */ + switch (node->type) + { + case JSON_NODE_NULL: + return 0; + case JSON_NODE_VALUE: + return value_hash ^ json_value_hash (node->data.value); + case JSON_NODE_ARRAY: + return array_hash ^ json_array_hash (json_node_get_array (node)); + case JSON_NODE_OBJECT: + return object_hash ^ json_object_hash (json_node_get_object (node)); + default: + g_assert_not_reached (); + } +} + +/** + * json_node_equal: + * @a: (type JsonNode): a JSON node + * @b: (type JsonNode): another JSON node + * + * Check whether @a and @b are equal node, meaning they have the same + * type and same values (checked recursively). + * + * Note that integer values are compared numerically, ignoring type, so a + * double value 4.0 is equal to the integer value 4. + * + * Returns: `TRUE` if @a and @b are equal; `FALSE` otherwise + * Since: 1.2 + */ +gboolean +json_node_equal (gconstpointer a, + gconstpointer b) +{ + JsonNode *node_a, *node_b; /* unowned */ + + node_a = (JsonNode *) a; + node_b = (JsonNode *) b; + + /* Identity comparison. */ + if (node_a == node_b) + return TRUE; + + /* Eliminate mismatched types rapidly. */ + if (!json_type_is_a (node_a, node_b) && + !json_type_is_a (node_b, node_a)) + { + return FALSE; + } + + switch (node_a->type) + { + case JSON_NODE_NULL: + /* Types match already. */ + return TRUE; + case JSON_NODE_ARRAY: + return json_array_equal (json_node_get_array (node_a), + json_node_get_array (node_b)); + case JSON_NODE_OBJECT: + return json_object_equal (json_node_get_object (node_a), + json_node_get_object (node_b)); + case JSON_NODE_VALUE: + /* Handled below. */ + break; + default: + g_assert_not_reached (); + } + + /* Handle values. */ + switch (node_a->data.value->type) + { + case JSON_VALUE_NULL: + /* Types already match. */ + return TRUE; + case JSON_VALUE_BOOLEAN: + return (json_node_get_boolean (node_a) == json_node_get_boolean (node_b)); + case JSON_VALUE_STRING: + return json_string_equal (json_node_get_string (node_a), + json_node_get_string (node_b)); + case JSON_VALUE_DOUBLE: + case JSON_VALUE_INT: { + gdouble val_a, val_b; + JsonValueType value_type_a, value_type_b; + + value_type_a = node_a->data.value->type; + value_type_b = node_b->data.value->type; + + /* Integer comparison doesn’t need to involve doubles… */ + if (value_type_a == JSON_VALUE_INT && + value_type_b == JSON_VALUE_INT) + { + return (json_node_get_int (node_a) == + json_node_get_int (node_b)); + } + + /* …but everything else does. We can use bitwise double equality here, + * since we’re not doing any calculations which could introduce floating + * point error. We expect that the doubles in the JSON nodes come directly + * from strtod() or similar, so should be bitwise equal for equal string + * representations. + * + * Interesting background reading: + * http://randomascii.wordpress.com/2012/06/26/\ + * doubles-are-not-floats-so-dont-compare-them/ + */ + if (value_type_a == JSON_VALUE_INT) + val_a = json_node_get_int (node_a); + else + val_a = json_node_get_double (node_a); + + if (value_type_b == JSON_VALUE_INT) + val_b = json_node_get_int (node_b); + else + val_b = json_node_get_double (node_b); + + return (val_a == val_b); + } + case JSON_VALUE_INVALID: + default: + g_assert_not_reached (); + } +} diff --git a/lsp/deps/json-glib/json-object.c b/lsp/deps/json-glib/json-object.c new file mode 100644 index 000000000..b109abd64 --- /dev/null +++ b/lsp/deps/json-glib/json-object.c @@ -0,0 +1,1261 @@ +/* json-object.c - JSON object implementation + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +#include "config.h" + +#include +#include + +#include "json-types-private.h" + +/** + * JsonObject: + * + * `JsonObject` is the representation of the object type inside JSON. + * + * A `JsonObject` contains [struct@Json.Node] "members", which may contain + * fundamental types, arrays or other objects; each member of an object is + * accessed using a unique string, or "name". + * + * Since objects can be arbitrarily big, copying them can be expensive; for + * this reason they are reference counted. You can control the lifetime of + * a `JsonObject` using [method@Json.Object.ref] and [method@Json.Object.unref]. + * + * To add or overwrite a member with a given name, use [method@Json.Object.set_member]. + * + * To extract a member with a given name, use [method@Json.Object.get_member]. + * + * To retrieve the list of members, use [method@Json.Object.get_members]. + * + * To retrieve the size of the object (that is, the number of members it has), + * use [method@Json.Object.get_size]. + */ + +G_DEFINE_BOXED_TYPE (JsonObject, json_object, json_object_ref, json_object_unref); + +/** + * json_object_new: (constructor) + * + * Creates a new object. + * + * Returns: (transfer full): the newly created object + */ +JsonObject * +json_object_new (void) +{ + JsonObject *object; + + object = g_slice_new0 (JsonObject); + + object->age = 0; + object->ref_count = 1; + object->members = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + (GDestroyNotify) json_node_unref); + g_queue_init (&object->members_ordered); + + return object; +} + +/** + * json_object_ref: + * @object: a JSON object + * + * Acquires a reference on the given object. + * + * Returns: (transfer none): the given object, with the reference count + * increased by one. + */ +JsonObject * +json_object_ref (JsonObject *object) +{ + g_return_val_if_fail (object != NULL, NULL); + g_return_val_if_fail (object->ref_count > 0, NULL); + + object->ref_count++; + + return object; +} + +/** + * json_object_unref: + * @object: a JSON object + * + * Releases a reference on the given object. + * + * If the reference count reaches zero, the object is destroyed and + * all its resources are freed. + */ +void +json_object_unref (JsonObject *object) +{ + g_return_if_fail (object != NULL); + g_return_if_fail (object->ref_count > 0); + + if (--object->ref_count == 0) + { + g_queue_clear (&object->members_ordered); + g_hash_table_destroy (object->members); + object->members = NULL; + + g_slice_free (JsonObject, object); + } +} + +/** + * json_object_seal: + * @object: a JSON object + * + * Seals the object, making it immutable to further changes. + * + * This function will recursively seal all members of the object too. + * + * If the object is already immutable, this is a no-op. + * + * Since: 1.2 + */ +void +json_object_seal (JsonObject *object) +{ + JsonObjectIter iter; + JsonNode *node; + + g_return_if_fail (object != NULL); + g_return_if_fail (object->ref_count > 0); + + if (object->immutable) + return; + + /* Propagate to all members. */ + json_object_iter_init (&iter, object); + + while (json_object_iter_next (&iter, NULL, &node)) + json_node_seal (node); + + object->immutable_hash = json_object_hash (object); + object->immutable = TRUE; +} + +/** + * json_object_is_immutable: + * @object: a JSON object + * + * Checks whether the given object has been marked as immutable by calling + * [method@Json.Object.seal] on it. + * + * Since: 1.2 + * Returns: `TRUE` if the object is immutable + */ +gboolean +json_object_is_immutable (JsonObject *object) +{ + g_return_val_if_fail (object != NULL, FALSE); + g_return_val_if_fail (object->ref_count > 0, FALSE); + + return object->immutable; +} + +static inline void +object_set_member_internal (JsonObject *object, + const gchar *member_name, + JsonNode *node) +{ + gchar *name = g_strdup (member_name); + + if (g_hash_table_lookup (object->members, name) == NULL) + { + g_queue_push_tail (&object->members_ordered, name); + object->age += 1; + } + else + { + GList *l; + + /* if the member already exists then we need to replace the + * pointer to its name, to avoid keeping invalid pointers + * once we replace the key in the hash table + */ + l = g_queue_find_custom (&object->members_ordered, name, (GCompareFunc) strcmp); + if (l != NULL) + l->data = name; + } + + g_hash_table_replace (object->members, name, node); +} + +/** + * json_object_add_member: + * @object: a JSON object + * @member_name: the name of the member + * @node: (transfer full): the value of the member + * + * Adds a new member for the given name and value into an object. + * + * This function will return if the object already contains a member + * with the same name. + * + * Deprecated: 0.8: Use [method@Json.Object.set_member] instead + */ +void +json_object_add_member (JsonObject *object, + const gchar *member_name, + JsonNode *node) +{ + g_return_if_fail (object != NULL); + g_return_if_fail (member_name != NULL); + g_return_if_fail (node != NULL); + + if (json_object_has_member (object, member_name)) + { + g_warning ("JsonObject already has a `%s' member of type `%s'", + member_name, + json_node_type_name (node)); + return; + } + + object_set_member_internal (object, member_name, node); +} + +/** + * json_object_set_member: + * @object: a JSON object + * @member_name: the name of the member + * @node: (transfer full): the value of the member + * + * Sets the value of a member inside an object. + * + * If the object does not have a member with the given name, a new member + * is created. + * + * If the object already has a member with the given name, the current + * value is overwritten with the new. + * + * Since: 0.8 + */ +void +json_object_set_member (JsonObject *object, + const gchar *member_name, + JsonNode *node) +{ + JsonNode *old_node; + + g_return_if_fail (object != NULL); + g_return_if_fail (member_name != NULL); + g_return_if_fail (node != NULL); + + old_node = g_hash_table_lookup (object->members, member_name); + if (old_node == NULL) + goto set_member; + + if (old_node == node) + return; + +set_member: + object_set_member_internal (object, member_name, node); +} + +/** + * json_object_set_int_member: + * @object: a JSON object + * @member_name: the name of the member + * @value: the value of the member + * + * Convenience function for setting an object member with an integer value. + * + * See also: [method@Json.Object.set_member], [method@Json.Node.init_int] + * + * Since: 0.8 + */ +void +json_object_set_int_member (JsonObject *object, + const gchar *member_name, + gint64 value) +{ + g_return_if_fail (object != NULL); + g_return_if_fail (member_name != NULL); + + object_set_member_internal (object, member_name, json_node_init_int (json_node_alloc (), value)); +} + +/** + * json_object_set_double_member: + * @object: a JSON object + * @member_name: the name of the member + * @value: the value of the member + * + * Convenience function for setting an object member with a floating point value. + * + * See also: [method@Json.Object.set_member], [method@Json.Node.init_double] + * + * Since: 0.8 + */ +void +json_object_set_double_member (JsonObject *object, + const gchar *member_name, + gdouble value) +{ + g_return_if_fail (object != NULL); + g_return_if_fail (member_name != NULL); + + object_set_member_internal (object, member_name, json_node_init_double (json_node_alloc (), value)); +} + +/** + * json_object_set_boolean_member: + * @object: a JSON object + * @member_name: the name of the member + * @value: the value of the member + * + * Convenience function for setting an object member with a boolean value. + * + * See also: [method@Json.Object.set_member], [method@Json.Node.init_boolean] + * + * Since: 0.8 + */ +void +json_object_set_boolean_member (JsonObject *object, + const gchar *member_name, + gboolean value) +{ + g_return_if_fail (object != NULL); + g_return_if_fail (member_name != NULL); + + object_set_member_internal (object, member_name, json_node_init_boolean (json_node_alloc (), value)); +} + +/** + * json_object_set_string_member: + * @object: a JSON object + * @member_name: the name of the member + * @value: the value of the member + * + * Convenience function for setting an object member with a string value. + * + * See also: [method@Json.Object.set_member], [method@Json.Node.init_string] + * + * Since: 0.8 + */ +void +json_object_set_string_member (JsonObject *object, + const gchar *member_name, + const gchar *value) +{ + JsonNode *node; + + g_return_if_fail (object != NULL); + g_return_if_fail (member_name != NULL); + + node = json_node_alloc (); + + if (value != NULL) + json_node_init_string (node, value); + else + json_node_init_null (node); + + object_set_member_internal (object, member_name, node); +} + +/** + * json_object_set_null_member: + * @object: a JSON object + * @member_name: the name of the member + * + * Convenience function for setting an object member with a `null` value. + * + * See also: [method@Json.Object.set_member], [method@Json.Node.init_null] + * + * Since: 0.8 + */ +void +json_object_set_null_member (JsonObject *object, + const gchar *member_name) +{ + g_return_if_fail (object != NULL); + g_return_if_fail (member_name != NULL); + + object_set_member_internal (object, member_name, json_node_init_null (json_node_alloc ())); +} + +/** + * json_object_set_array_member: + * @object: a JSON object + * @member_name: the name of the member + * @value: (transfer full): the value of the member + * + * Convenience function for setting an object member with an array value. + * + * See also: [method@Json.Object.set_member], [method@Json.Node.take_array] + * + * Since: 0.8 + */ +void +json_object_set_array_member (JsonObject *object, + const gchar *member_name, + JsonArray *value) +{ + JsonNode *node; + + g_return_if_fail (object != NULL); + g_return_if_fail (member_name != NULL); + + node = json_node_alloc (); + + if (value != NULL) + { + json_node_init_array (node, value); + json_array_unref (value); + } + else + json_node_init_null (node); + + object_set_member_internal (object, member_name, node); +} + +/** + * json_object_set_object_member: + * @object: a JSON object + * @member_name: the name of the member + * @value: (transfer full): the value of the member + * + * Convenience function for setting an object member with an object value. + * + * See also: [method@Json.Object.set_member], [method@Json.Node.take_object] + * + * Since: 0.8 + */ +void +json_object_set_object_member (JsonObject *object, + const gchar *member_name, + JsonObject *value) +{ + JsonNode *node; + + g_return_if_fail (object != NULL); + g_return_if_fail (member_name != NULL); + + node = json_node_alloc (); + + if (value != NULL) + { + json_node_init_object (node, value); + json_object_unref (value); + } + else + json_node_init_null (node); + + object_set_member_internal (object, member_name, node); +} + +/** + * json_object_get_members: + * @object: a JSON object + * + * Retrieves all the names of the members of an object. + * + * You can obtain the value for each member by iterating the returned list + * and calling [method@Json.Object.get_member]. + * + * Returns: (element-type utf8) (transfer container) (nullable): the + * member names of the object + */ +GList * +json_object_get_members (JsonObject *object) +{ + g_return_val_if_fail (object != NULL, NULL); + + return g_list_copy (object->members_ordered.head); +} + + +GQueue * +json_object_get_members_internal (JsonObject *object) +{ + g_return_val_if_fail (object != NULL, NULL); + + return &object->members_ordered; +} + +/** + * json_object_get_values: + * @object: a JSON object + * + * Retrieves all the values of the members of an object. + * + * Returns: (element-type JsonNode) (transfer container) (nullable): the + * member values of the object + */ +GList * +json_object_get_values (JsonObject *object) +{ + GList *values, *l; + + g_return_val_if_fail (object != NULL, NULL); + + values = NULL; + for (l = object->members_ordered.tail; l != NULL; l = l->prev) + values = g_list_prepend (values, g_hash_table_lookup (object->members, l->data)); + + return values; +} + +/** + * json_object_dup_member: + * @object: a JSON object + * @member_name: the name of the JSON object member to access + * + * Retrieves a copy of the value of the given member inside an object. + * + * Returns: (transfer full) (nullable): a copy of the value for the + * requested object member + * + * Since: 0.6 + */ +JsonNode * +json_object_dup_member (JsonObject *object, + const gchar *member_name) +{ + JsonNode *retval; + + g_return_val_if_fail (object != NULL, NULL); + g_return_val_if_fail (member_name != NULL, NULL); + + retval = json_object_get_member (object, member_name); + if (!retval) + return NULL; + + return json_node_copy (retval); +} + +static inline JsonNode * +object_get_member_internal (JsonObject *object, + const gchar *member_name) +{ + return g_hash_table_lookup (object->members, member_name); +} + +/** + * json_object_get_member: + * @object: a JSON object + * @member_name: the name of the JSON object member to access + * + * Retrieves the value of the given member inside an object. + * + * Returns: (transfer none) (nullable): the value for the + * requested object member + */ +JsonNode * +json_object_get_member (JsonObject *object, + const gchar *member_name) +{ + g_return_val_if_fail (object != NULL, NULL); + g_return_val_if_fail (member_name != NULL, NULL); + + return object_get_member_internal (object, member_name); +} + +#define JSON_OBJECT_GET(ret_type,type_name) \ +ret_type \ +json_object_get_ ##type_name## _member (JsonObject *object, \ + const char *member_name) \ +{ \ + g_return_val_if_fail (object != NULL, (ret_type) 0); \ + g_return_val_if_fail (member_name != NULL, (ret_type) 0); \ +\ + JsonNode *node = object_get_member_internal (object, member_name); \ + g_return_val_if_fail (node != NULL, (ret_type) 0); \ +\ + if (JSON_NODE_HOLDS_NULL (node)) \ + return (ret_type) 0; \ +\ + g_return_val_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE, (ret_type) 0); \ +\ + return json_node_get_ ##type_name (node); \ +} + +#define JSON_OBJECT_GET_DEFAULT(ret_type,type_name) \ +ret_type \ +json_object_get_ ##type_name## _member_with_default (JsonObject *object, \ + const char *member_name, \ + ret_type default_value) \ +{ \ + g_return_val_if_fail (object != NULL, default_value); \ + g_return_val_if_fail (member_name != NULL, default_value); \ +\ + JsonNode *node = object_get_member_internal (object, member_name); \ + if (node == NULL) \ + return default_value; \ +\ + if (JSON_NODE_HOLDS_NULL (node)) \ + return default_value; \ +\ + g_return_val_if_fail (JSON_NODE_TYPE (node) == JSON_NODE_VALUE, default_value); \ +\ + return json_node_get_ ##type_name (node); \ +} + +/** + * json_object_get_int_member: + * @object: a JSON object + * @member_name: the name of the object member + * + * Convenience function that retrieves the integer value + * stored in @member_name of @object. It is an error to specify a + * @member_name which does not exist. + * + * See also: [method@Json.Object.get_int_member_with_default], + * [method@Json.Object.get_member], [method@Json.Object.has_member] + * + * Returns: the integer value of the object's member + * + * Since: 0.8 + */ +JSON_OBJECT_GET (gint64, int) + +/** + * json_object_get_int_member_with_default: + * @object: a JSON object + * @member_name: the name of the object member + * @default_value: the value to return if @member_name is not valid + * + * Convenience function that retrieves the integer value + * stored in @member_name of @object. + * + * If @member_name does not exist, does not contain a scalar value, + * or contains `null`, then @default_value is returned instead. + * + * Returns: the integer value of the object's member, or the + * given default + * + * Since: 1.6 + */ +JSON_OBJECT_GET_DEFAULT (gint64, int) + +/** + * json_object_get_double_member: + * @object: a JSON object + * @member_name: the name of the member + * + * Convenience function that retrieves the floating point value + * stored in @member_name of @object. It is an error to specify a + * @member_name which does not exist. + * + * See also: [method@Json.Object.get_double_member_with_default], + * [method@Json.Object.get_member], [method@Json.Object.has_member] + * + * Returns: the floating point value of the object's member + * + * Since: 0.8 + */ +JSON_OBJECT_GET (gdouble, double) + +/** + * json_object_get_double_member_with_default: + * @object: a JSON object + * @member_name: the name of the @object member + * @default_value: the value to return if @member_name is not valid + * + * Convenience function that retrieves the floating point value + * stored in @member_name of @object. + * + * If @member_name does not exist, does not contain a scalar value, + * or contains `null`, then @default_value is returned instead. + * + * Returns: the floating point value of the object's member, or the + * given default + * + * Since: 1.6 + */ +JSON_OBJECT_GET_DEFAULT (double, double) + +/** + * json_object_get_boolean_member: + * @object: a JSON object + * @member_name: the name of the member + * + * Convenience function that retrieves the boolean value + * stored in @member_name of @object. It is an error to specify a + * @member_name which does not exist. + * + * See also: [method@Json.Object.get_boolean_member_with_default], + * [method@Json.Object.get_member], [method@Json.Object.has_member] + * + * Returns: the boolean value of the object's member + * + * Since: 0.8 + */ +JSON_OBJECT_GET (gboolean, boolean) + +/** + * json_object_get_boolean_member_with_default: + * @object: a JSON object + * @member_name: the name of the @object member + * @default_value: the value to return if @member_name is not valid + * + * Convenience function that retrieves the boolean value + * stored in @member_name of @object. + * + * If @member_name does not exist, does not contain a scalar value, + * or contains `null`, then @default_value is returned instead. + * + * Returns: the boolean value of the object's member, or the + * given default + * + * Since: 1.6 + */ +JSON_OBJECT_GET_DEFAULT (gboolean, boolean) + +/** + * json_object_get_string_member: + * @object: a JSON object + * @member_name: the name of the member + * + * Convenience function that retrieves the string value + * stored in @member_name of @object. It is an error to specify a + * @member_name that does not exist. + * + * See also: [method@Json.Object.get_string_member_with_default], + * [method@Json.Object.get_member], [method@Json.Object.has_member] + * + * Returns: the string value of the object's member + * + * Since: 0.8 + */ +JSON_OBJECT_GET (const gchar *, string) + +/** + * json_object_get_string_member_with_default: + * @object: a JSON object + * @member_name: the name of the @object member + * @default_value: the value to return if @member_name is not valid + * + * Convenience function that retrieves the string value + * stored in @member_name of @object. + * + * If @member_name does not exist, does not contain a scalar value, + * or contains `null`, then @default_value is returned instead. + * + * Returns: the string value of the object's member, or the + * given default + * + * Since: 1.6 + */ +JSON_OBJECT_GET_DEFAULT (const char *, string) + +/** + * json_object_get_null_member: + * @object: a JSON object + * @member_name: the name of the member + * + * Convenience function that checks whether the value + * stored in @member_name of @object is null. It is an error to + * specify a @member_name which does not exist. + * + * See also: [method@Json.Object.get_member], [method@Json.Object.has_member] + * + * Returns: `TRUE` if the value is `null` + * + * Since: 0.8 + */ +gboolean +json_object_get_null_member (JsonObject *object, + const gchar *member_name) +{ + JsonNode *node; + + g_return_val_if_fail (object != NULL, FALSE); + g_return_val_if_fail (member_name != NULL, FALSE); + + node = object_get_member_internal (object, member_name); + g_return_val_if_fail (node != NULL, FALSE); + + if (JSON_NODE_HOLDS_NULL (node)) + return TRUE; + + if (JSON_NODE_HOLDS_OBJECT (node)) + return json_node_get_object (node) == NULL; + + if (JSON_NODE_HOLDS_ARRAY (node)) + return json_node_get_array (node) == NULL; + + return FALSE; +} + +/** + * json_object_get_array_member: + * @object: a JSON object + * @member_name: the name of the member + * + * Convenience function that retrieves the array + * stored in @member_name of @object. It is an error to specify a + * @member_name which does not exist. + * + * If @member_name contains `null`, then this function will return `NULL`. + * + * See also: [method@Json.Object.get_member], [method@Json.Object.has_member] + * + * Returns: (transfer none) (nullable): the array inside the object's member + * + * Since: 0.8 + */ +JsonArray * +json_object_get_array_member (JsonObject *object, + const gchar *member_name) +{ + JsonNode *node; + + g_return_val_if_fail (object != NULL, NULL); + g_return_val_if_fail (member_name != NULL, NULL); + + node = object_get_member_internal (object, member_name); + g_return_val_if_fail (node != NULL, NULL); + g_return_val_if_fail (JSON_NODE_HOLDS_ARRAY (node) || JSON_NODE_HOLDS_NULL (node), NULL); + + if (JSON_NODE_HOLDS_NULL (node)) + return NULL; + + return json_node_get_array (node); +} + +/** + * json_object_get_object_member: + * @object: a JSON object + * @member_name: the name of the member + * + * Convenience function that retrieves the object + * stored in @member_name of @object. It is an error to specify a @member_name + * which does not exist. + * + * If @member_name contains `null`, then this function will return `NULL`. + * + * See also: [method@Json.Object.get_member], [method@Json.Object.has_member] + * + * Returns: (transfer none) (nullable): the object inside the object's member + * + * Since: 0.8 + */ +JsonObject * +json_object_get_object_member (JsonObject *object, + const gchar *member_name) +{ + JsonNode *node; + + g_return_val_if_fail (object != NULL, NULL); + g_return_val_if_fail (member_name != NULL, NULL); + + node = object_get_member_internal (object, member_name); + g_return_val_if_fail (node != NULL, NULL); + g_return_val_if_fail (JSON_NODE_HOLDS_OBJECT (node) || JSON_NODE_HOLDS_NULL (node), NULL); + + if (JSON_NODE_HOLDS_NULL (node)) + return NULL; + + return json_node_get_object (node); +} + +/** + * json_object_has_member: + * @object: a JSON object + * @member_name: the name of a JSON object member + * + * Checks whether @object has a member named @member_name. + * + * Returns: `TRUE` if the JSON object has the requested member + */ +gboolean +json_object_has_member (JsonObject *object, + const gchar *member_name) +{ + g_return_val_if_fail (object != NULL, FALSE); + g_return_val_if_fail (member_name != NULL, FALSE); + + return (g_hash_table_lookup (object->members, member_name) != NULL); +} + +/** + * json_object_get_size: + * @object: a JSON object + * + * Retrieves the number of members of a JSON object. + * + * Returns: the number of members + */ +guint +json_object_get_size (JsonObject *object) +{ + g_return_val_if_fail (object != NULL, 0); + + return g_hash_table_size (object->members); +} + +/** + * json_object_remove_member: + * @object: a JSON object + * @member_name: the name of the member to remove + * + * Removes @member_name from @object, freeing its allocated resources. + */ +void +json_object_remove_member (JsonObject *object, + const gchar *member_name) +{ + GList *l; + + g_return_if_fail (object != NULL); + g_return_if_fail (member_name != NULL); + + for (l = object->members_ordered.head; l != NULL; l = l->next) + { + const gchar *name = l->data; + + if (g_strcmp0 (name, member_name) == 0) + { + g_queue_delete_link (&object->members_ordered, l); + break; + } + } + + g_hash_table_remove (object->members, member_name); +} + +/** + * json_object_foreach_member: + * @object: a JSON object + * @func: (scope call): the function to be called on each member + * @data: (closure): data to be passed to the function + * + * Iterates over all members of @object and calls @func on + * each one of them. + * + * It is safe to change the value of a member of the oobject + * from within the iterator function, but it is not safe to add or + * remove members from the object. + * + * The order in which the object members are iterated is the + * insertion order. + * + * Since: 0.8 + */ +void +json_object_foreach_member (JsonObject *object, + JsonObjectForeach func, + gpointer data) +{ + GList *l; + int age; + + g_return_if_fail (object != NULL); + g_return_if_fail (func != NULL); + + age = object->age; + + for (l = object->members_ordered.head; l != NULL; l = l->next) + { + const gchar *member_name = l->data; + JsonNode *member_node = g_hash_table_lookup (object->members, member_name); + + func (object, member_name, member_node, data); + + g_assert (object->age == age); + } +} + +/** + * json_object_hash: + * @key: (type JsonObject): a JSON object to hash + * + * Calculate a hash value for the given @key (a JSON object). + * + * The hash is calculated over the object and all its members, recursively. If + * the object is immutable, this is a fast operation; otherwise, it scales + * proportionally with the number of members in the object. + * + * Returns: hash value for @key + * Since: 1.2 + */ +guint +json_object_hash (gconstpointer key) +{ + JsonObject *object = (JsonObject *) key; + guint hash = 0; + JsonObjectIter iter; + const gchar *member_name; + JsonNode *node; + + g_return_val_if_fail (object != NULL, 0); + + /* If the object is immutable, use the cached hash. */ + if (object->immutable) + return object->immutable_hash; + + /* Otherwise, calculate from scratch. */ + json_object_iter_init (&iter, object); + + while (json_object_iter_next (&iter, &member_name, &node)) + hash ^= (json_string_hash (member_name) ^ json_node_hash (node)); + + return hash; +} + +/** + * json_object_equal: + * @a: (type JsonObject): a JSON object + * @b: (type JsonObject): another JSON object + * + * Check whether @a and @b are equal objects, meaning they have the same + * set of members, and the values of corresponding members are equal. + * + * Returns: `TRUE` if @a and @b are equal, and `FALSE` otherwise + * Since: 1.2 + */ +gboolean +json_object_equal (gconstpointer a, + gconstpointer b) +{ + JsonObject *object_a, *object_b; + guint size_a, size_b; + JsonObjectIter iter_a; + JsonNode *child_a, *child_b; /* unowned */ + const gchar *member_name; + + object_a = (JsonObject *) a; + object_b = (JsonObject *) b; + + /* Identity comparison. */ + if (object_a == object_b) + return TRUE; + + /* Check sizes. */ + size_a = json_object_get_size (object_a); + size_b = json_object_get_size (object_b); + + if (size_a != size_b) + return FALSE; + + /* Check member names and values. Check the member names first + * to avoid expensive recursive value comparisons which might + * be unnecessary. */ + json_object_iter_init (&iter_a, object_a); + + while (json_object_iter_next (&iter_a, &member_name, NULL)) + { + if (!json_object_has_member (object_b, member_name)) + return FALSE; + } + + json_object_iter_init (&iter_a, object_a); + + while (json_object_iter_next (&iter_a, &member_name, &child_a)) + { + child_b = json_object_get_member (object_b, member_name); + + if (!json_node_equal (child_a, child_b)) + return FALSE; + } + + return TRUE; +} + +/** + * json_object_iter_init: + * @iter: an uninitialised JSON object iterator + * @object: the JSON object to iterate over + * + * Initialises the @iter and associate it with @object. + * + * ```c + * JsonObjectIter iter; + * const gchar *member_name; + * JsonNode *member_node; + * + * json_object_iter_init (&iter, some_object); + * while (json_object_iter_next (&iter, &member_name, &member_node)) + * { + * // Do something with @member_name and @member_node. + * } + * ``` + * + * The iterator initialized with this function will iterate the + * members of the object in an undefined order. + * + * See also: [method@Json.ObjectIter.init_ordered] + * + * Since: 1.2 + */ +void +json_object_iter_init (JsonObjectIter *iter, + JsonObject *object) +{ + JsonObjectIterReal *iter_real = (JsonObjectIterReal *) iter;; + + g_return_if_fail (iter != NULL); + g_return_if_fail (object != NULL); + g_return_if_fail (object->ref_count > 0); + + iter_real->object = object; + g_hash_table_iter_init (&iter_real->members_iter, object->members); +} + +/** + * json_object_iter_next: + * @iter: a JSON object iterator + * @member_name: (out callee-allocates) (transfer none) (optional): return + * location for the member name, or %NULL to ignore + * @member_node: (out callee-allocates) (transfer none) (optional): return + * location for the member value, or %NULL to ignore + * + * Advances the iterator and retrieves the next member in the object. + * + * If the end of the object is reached, `FALSE` is returned and @member_name + * and @member_node are set to invalid values. After that point, the @iter + * is invalid. + * + * The order in which members are returned by the iterator is undefined. The + * iterator is invalidated if the object is modified during iteration. + * + * You must use this function with an iterator initialized with + * [method@Json.ObjectIter.init]; using this function with an iterator + * initialized with [method@Json.ObjectIter.init_ordered] yields undefined + * behavior. + * + * See also: [method@Json.ObjectIter.next_ordered] + * + * Returns: `TRUE` if @member_name and @member_node are valid; `FALSE` if + * there are no more members + * + * Since: 1.2 + */ +gboolean +json_object_iter_next (JsonObjectIter *iter, + const gchar **member_name, + JsonNode **member_node) +{ + JsonObjectIterReal *iter_real = (JsonObjectIterReal *) iter; + + g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (iter_real->object != NULL, FALSE); + g_return_val_if_fail (iter_real->object->ref_count > 0, FALSE); + + return g_hash_table_iter_next (&iter_real->members_iter, + (gpointer *) member_name, + (gpointer *) member_node); +} + +/** + * json_object_iter_init_ordered: + * @iter: an uninitialised iterator + * @object: the JSON object to iterate over + * + * Initialises the @iter and associate it with @object. + * + * ```c + * JsonObjectIter iter; + * const gchar *member_name; + * JsonNode *member_node; + * + * json_object_iter_init_ordered (&iter, some_object); + * while (json_object_iter_next_ordered (&iter, &member_name, &member_node)) + * { + * // Do something with @member_name and @member_node. + * } + * ``` + * + * See also: [method@Json.ObjectIter.init] + * + * Since: 1.6 + */ +void +json_object_iter_init_ordered (JsonObjectIter *iter, + JsonObject *object) +{ + JsonObjectOrderedIterReal *iter_real = (JsonObjectOrderedIterReal *) iter; + + g_return_if_fail (iter != NULL); + g_return_if_fail (object != NULL); + g_return_if_fail (object->ref_count > 0); + + iter_real->object = object; + iter_real->cur_member = NULL; + iter_real->next_member = NULL; + iter_real->age = iter_real->object->age; +} + +/** + * json_object_iter_next_ordered: + * @iter: an ordered JSON object iterator + * @member_name: (out callee-allocates) (transfer none) (optional): return + * location for the member name, or %NULL to ignore + * @member_node: (out callee-allocates) (transfer none) (optional): return + * location for the member value, or %NULL to ignore + * + * Advances the iterator and retrieves the next member in the object. + * + * If the end of the object is reached, `FALSE` is returned and @member_name and + * @member_node are set to invalid values. After that point, the @iter is invalid. + * + * The order in which members are returned by the iterator is the same order in + * which the members were added to the `JsonObject`. The iterator is invalidated + * if its `JsonObject` is modified during iteration. + * + * You must use this function with an iterator initialized with + * [method@Json.ObjectIter.init_ordered]; using this function with an iterator + * initialized with [method@Json.ObjectIter.init] yields undefined behavior. + * + * See also: [method@Json.ObjectIter.next] + * + * Returns: `TRUE `if @member_name and @member_node are valid; `FALSE` if the end + * of the object has been reached + * + * Since: 1.6 + */ +gboolean +json_object_iter_next_ordered (JsonObjectIter *iter, + const gchar **member_name, + JsonNode **member_node) +{ + JsonObjectOrderedIterReal *iter_real = (JsonObjectOrderedIterReal *) iter; + const char *name = NULL; + + g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (iter_real->object != NULL, FALSE); + g_return_val_if_fail (iter_real->object->ref_count > 0, FALSE); + g_return_val_if_fail (iter_real->age == iter_real->object->age, FALSE); + + if (iter_real->cur_member == NULL) + iter_real->cur_member = iter_real->object->members_ordered.head; + else + iter_real->cur_member = iter_real->cur_member->next; + + name = iter_real->cur_member != NULL ? iter_real->cur_member->data : NULL; + + if (member_name != NULL) + *member_name = name; + if (member_node != NULL) + { + if (name != NULL) + *member_node = g_hash_table_lookup (iter_real->object->members, name); + else + *member_name = NULL; + } + + return iter_real->cur_member != NULL; +} diff --git a/lsp/deps/json-glib/json-parser.c b/lsp/deps/json-glib/json-parser.c new file mode 100644 index 000000000..9b112ba91 --- /dev/null +++ b/lsp/deps/json-glib/json-parser.c @@ -0,0 +1,1707 @@ +/* json-parser.c - JSON streams parser + * + * This file is part of JSON-GLib + * + * Copyright © 2007, 2008, 2009 OpenedHand Ltd + * Copyright © 2009, 2010 Intel Corp. + * Copyright © 2015 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + * Philip Withnall + */ + +/** + * JsonParser: + * + * `JsonParser` provides an object for parsing a JSON data stream, either + * inside a file or inside a static buffer. + * + * ## Using `JsonParser` + * + * The `JsonParser` API is fairly simple: + * + * ```c + * gboolean + * parse_json (const char *filename) + * { + * g_autoptr(JsonParser) parser = json_parser_new (); + * g_autoptr(GError) error = NULL + * + * json_parser_load_from_file (parser, filename, &error); + * if (error != NULL) + * { + * g_critical ("Unable to parse '%s': %s", filename, error->message); + * return FALSE; + * } + * + * g_autoptr(JsonNode) root = json_parser_get_root (parser); + * + * // manipulate the object tree from the root node + * + * return TRUE + * } + * ``` + * + * By default, the entire process of loading the data and parsing it is + * synchronous; the [method@Json.Parser.load_from_stream_async] API will + * load the data asynchronously, but parse it in the main context as the + * signals of the parser must be emitted in the same thread. If you do + * not use signals, and you wish to also parse the JSON data without blocking, + * you should use a `GTask` and the synchronous `JsonParser` API inside the + * task itself. + */ + +#include "config.h" + +#include + +#include + +#include "json-types-private.h" + +#include "json-debug.h" +#include "json-parser.h" +#include "json-scanner.h" + +struct _JsonParserPrivate +{ + JsonNode *root; + JsonNode *current_node; + + JsonScanner *scanner; + + JsonParserError error_code; + GError *last_error; + + gchar *variable_name; + gchar *filename; + + guint has_assignment : 1; + guint is_filename : 1; + guint is_immutable : 1; + guint is_strict : 1; +}; + +enum +{ + PARSE_START, + OBJECT_START, + OBJECT_MEMBER, + OBJECT_END, + ARRAY_START, + ARRAY_ELEMENT, + ARRAY_END, + PARSE_END, + ERROR, + + LAST_SIGNAL +}; + +static guint parser_signals[LAST_SIGNAL] = { 0, }; + +enum +{ + PROP_IMMUTABLE = 1, + PROP_STRICT, + PROP_LAST +}; + +static GParamSpec *parser_props[PROP_LAST] = { NULL, }; + +G_DEFINE_QUARK (json-parser-error-quark, json_parser_error) + +G_DEFINE_TYPE_WITH_PRIVATE (JsonParser, json_parser, G_TYPE_OBJECT) + +static guint json_parse_array (JsonParser *parser, + JsonScanner *scanner, + JsonNode **node, + unsigned int nesting); +static guint json_parse_object (JsonParser *parser, + JsonScanner *scanner, + JsonNode **node, + unsigned int nesting); + +static inline void +json_parser_clear (JsonParser *parser) +{ + JsonParserPrivate *priv = parser->priv; + + g_clear_pointer (&priv->variable_name, g_free); + g_clear_pointer (&priv->last_error, g_error_free); + g_clear_pointer (&priv->root, json_node_unref); +} + +static void +json_parser_dispose (GObject *gobject) +{ + json_parser_clear (JSON_PARSER (gobject)); + + G_OBJECT_CLASS (json_parser_parent_class)->dispose (gobject); +} + +static void +json_parser_finalize (GObject *gobject) +{ + JsonParserPrivate *priv = JSON_PARSER (gobject)->priv; + + g_free (priv->variable_name); + g_free (priv->filename); + + G_OBJECT_CLASS (json_parser_parent_class)->finalize (gobject); +} + +static void +json_parser_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + JsonParserPrivate *priv = JSON_PARSER (gobject)->priv; + + switch (prop_id) + { + case PROP_IMMUTABLE: + /* Construct-only. */ + priv->is_immutable = g_value_get_boolean (value); + break; + + case PROP_STRICT: + json_parser_set_strict (JSON_PARSER (gobject), + g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +json_parser_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + JsonParserPrivate *priv = JSON_PARSER (gobject)->priv; + + switch (prop_id) + { + case PROP_IMMUTABLE: + g_value_set_boolean (value, priv->is_immutable); + break; + + case PROP_STRICT: + g_value_set_boolean (value, priv->is_strict); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +json_parser_class_init (JsonParserClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->set_property = json_parser_set_property; + gobject_class->get_property = json_parser_get_property; + gobject_class->dispose = json_parser_dispose; + gobject_class->finalize = json_parser_finalize; + + /** + * JsonParser:immutable: + * + * Whether the tree built by the parser should be immutable + * when created. + * + * Making the output immutable on creation avoids the expense + * of traversing it to make it immutable later. + * + * Since: 1.2 + */ + parser_props[PROP_IMMUTABLE] = + g_param_spec_boolean ("immutable", NULL, NULL, + FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + + /** + * JsonParser:strict: + * + * Whether the parser should be strictly conforming to the + * JSON format, or allow custom extensions like comments. + * + * Since: 1.10 + */ + parser_props[PROP_STRICT] = + g_param_spec_boolean ("strict", NULL, NULL, FALSE, + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, PROP_LAST, parser_props); + + /** + * JsonParser::parse-start: + * @parser: the parser that emitted the signal + * + * This signal is emitted when a parser starts parsing a JSON data stream. + */ + parser_signals[PARSE_START] = + g_signal_new ("parse-start", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonParserClass, parse_start), + NULL, NULL, + NULL, + G_TYPE_NONE, 0); + /** + * JsonParser::parse-end: + * @parser: the parser that emitted the signal + * + * This signal is emitted when a parser successfully finished parsing a + * JSON data stream + */ + parser_signals[PARSE_END] = + g_signal_new ("parse-end", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonParserClass, parse_end), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + /** + * JsonParser::object-start: + * @parser: the parser that emitted the signal + * + * This signal is emitted each time a parser starts parsing a JSON object. + */ + parser_signals[OBJECT_START] = + g_signal_new ("object-start", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonParserClass, object_start), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + /** + * JsonParser::object-member: + * @parser: the parser that emitted the signal + * @object: the JSON object being parsed + * @member_name: the name of the newly parsed member + * + * The `::object-member` signal is emitted each time a parser + * has successfully parsed a single member of a JSON object + */ + parser_signals[OBJECT_MEMBER] = + g_signal_new ("object-member", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonParserClass, object_member), + NULL, NULL, NULL, + G_TYPE_NONE, 2, + JSON_TYPE_OBJECT, + G_TYPE_STRING); + /** + * JsonParser::object-end: + * @parser: the parser that emitted the signal + * @object: the parsed JSON object + * + * The `::object-end` signal is emitted each time a parser + * has successfully parsed an entire JSON object. + */ + parser_signals[OBJECT_END] = + g_signal_new ("object-end", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonParserClass, object_end), + NULL, NULL, NULL, + G_TYPE_NONE, 1, + JSON_TYPE_OBJECT); + /** + * JsonParser::array-start: + * @parser: the parser that emitted the signal + * + * The `::array-start` signal is emitted each time a parser + * starts parsing a JSON array. + */ + parser_signals[ARRAY_START] = + g_signal_new ("array-start", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonParserClass, array_start), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + /** + * JsonParser::array-element: + * @parser: the parser that emitted the signal + * @array: a JSON array + * @index_: the index of the newly parsed array element + * + * The `::array-element` signal is emitted each time a parser + * has successfully parsed a single element of a JSON array. + */ + parser_signals[ARRAY_ELEMENT] = + g_signal_new ("array-element", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonParserClass, array_element), + NULL, NULL, NULL, + G_TYPE_NONE, 2, + JSON_TYPE_ARRAY, + G_TYPE_INT); + /** + * JsonParser::array-end: + * @parser: the parser that emitted the signal + * @array: the parsed JSON array + * + * The `::array-end` signal is emitted each time a parser + * has successfully parsed an entire JSON array. + */ + parser_signals[ARRAY_END] = + g_signal_new ("array-end", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonParserClass, array_end), + NULL, NULL, NULL, + G_TYPE_NONE, 1, + JSON_TYPE_ARRAY); + /** + * JsonParser::error: + * @parser: the parser that emitted the signal + * @error: the error + * + * The `::error` signal is emitted each time a parser encounters + * an error in a JSON stream. + */ + parser_signals[ERROR] = + g_signal_new ("error", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonParserClass, error), + NULL, NULL, NULL, + G_TYPE_NONE, 1, + G_TYPE_POINTER); +} + +static void +json_parser_init (JsonParser *parser) +{ + JsonParserPrivate *priv = json_parser_get_instance_private (parser); + + parser->priv = priv; + + priv->root = NULL; + priv->current_node = NULL; + + priv->error_code = JSON_PARSER_ERROR_PARSE; + priv->last_error = NULL; + + priv->has_assignment = FALSE; + priv->variable_name = NULL; + + priv->is_filename = FALSE; + priv->filename = FALSE; +} + +static guint +json_parse_value (JsonParser *parser, + JsonScanner *scanner, + guint token, + JsonNode **node) +{ + JsonParserPrivate *priv = parser->priv; + JsonNode *current_node = priv->current_node; + + switch (token) + { + case JSON_TOKEN_INT: + { + gint64 value = json_scanner_get_int64_value (scanner); + + JSON_NOTE (PARSER, "node: %" G_GINT64_FORMAT, value); + *node = json_node_init_int (json_node_alloc (), value); + } + break; + + case JSON_TOKEN_FLOAT: + { + double value = json_scanner_get_float_value (scanner); + + JSON_NOTE (PARSER, "abs(node): %.6f", value); + *node = json_node_init_double (json_node_alloc (), value); + } + break; + + case JSON_TOKEN_STRING: + { + const char *value = json_scanner_get_string_value (scanner); + + JSON_NOTE (PARSER, "node: '%s'", value); + *node = json_node_init_string (json_node_alloc (), value); + } + break; + + case JSON_TOKEN_TRUE: + case JSON_TOKEN_FALSE: + JSON_NOTE (PARSER, "node: '%s'", + JSON_TOKEN_TRUE ? "" : ""); + *node = json_node_init_boolean (json_node_alloc (), token == JSON_TOKEN_TRUE ? TRUE : FALSE); + break; + + case JSON_TOKEN_NULL: + JSON_NOTE (PARSER, "node: "); + *node = json_node_init_null (json_node_alloc ()); + break; + + case JSON_TOKEN_IDENTIFIER: + JSON_NOTE (PARSER, "node: identifier '%s'", json_scanner_get_identifier (scanner)); + priv->error_code = JSON_PARSER_ERROR_INVALID_BAREWORD; + *node = NULL; + return JSON_TOKEN_SYMBOL; + + default: + { + JsonNodeType cur_type; + + *node = NULL; + + JSON_NOTE (PARSER, "node: invalid token"); + + cur_type = json_node_get_node_type (current_node); + if (cur_type == JSON_NODE_ARRAY) + { + priv->error_code = JSON_PARSER_ERROR_PARSE; + return JSON_TOKEN_RIGHT_BRACE; + } + else if (cur_type == JSON_NODE_OBJECT) + { + priv->error_code = JSON_PARSER_ERROR_PARSE; + return JSON_TOKEN_RIGHT_CURLY; + } + else + { + priv->error_code = JSON_PARSER_ERROR_INVALID_BAREWORD; + return JSON_TOKEN_SYMBOL; + } + } + break; + } + + if (priv->is_immutable && *node != NULL) + json_node_seal (*node); + + return JSON_TOKEN_NONE; +} + +static guint +json_parse_array (JsonParser *parser, + JsonScanner *scanner, + JsonNode **node, + unsigned int nesting_level) +{ + JsonParserPrivate *priv = parser->priv; + JsonNode *old_current; + JsonArray *array; + guint token; + gint idx; + + if (nesting_level >= JSON_PARSER_MAX_RECURSION_DEPTH) + { + priv->error_code = JSON_PARSER_ERROR_NESTING; + return JSON_TOKEN_RIGHT_BRACE; + } + + old_current = priv->current_node; + priv->current_node = json_node_init_array (json_node_alloc (), NULL); + + array = json_array_new (); + + token = json_scanner_get_next_token (scanner); + g_assert (token == JSON_TOKEN_LEFT_BRACE); + + g_signal_emit (parser, parser_signals[ARRAY_START], 0); + + idx = 0; + while (token != JSON_TOKEN_RIGHT_BRACE) + { + guint next_token = json_scanner_peek_next_token (scanner); + JsonNode *element = NULL; + + /* parse the element */ + switch (next_token) + { + case JSON_TOKEN_LEFT_BRACE: + JSON_NOTE (PARSER, "Nested array at index %d", idx); + token = json_parse_array (parser, scanner, &element, nesting_level + 1); + break; + + case JSON_TOKEN_LEFT_CURLY: + JSON_NOTE (PARSER, "Nested object at index %d", idx); + token = json_parse_object (parser, scanner, &element, nesting_level + 1); + break; + + case JSON_TOKEN_RIGHT_BRACE: + goto array_done; + + default: + token = json_scanner_get_next_token (scanner); + token = json_parse_value (parser, scanner, token, &element); + break; + } + + if (token != JSON_TOKEN_NONE || element == NULL) + { + /* the json_parse_* functions will have set the error code */ + json_array_unref (array); + json_node_unref (priv->current_node); + priv->current_node = old_current; + + return token; + } + + next_token = json_scanner_peek_next_token (scanner); + + /* look for missing commas */ + if (next_token != JSON_TOKEN_COMMA && next_token != JSON_TOKEN_RIGHT_BRACE) + { + priv->error_code = JSON_PARSER_ERROR_MISSING_COMMA; + + json_array_unref (array); + json_node_free (priv->current_node); + json_node_free (element); + priv->current_node = old_current; + + return JSON_TOKEN_COMMA; + } + + /* look for trailing commas */ + if (next_token == JSON_TOKEN_COMMA) + { + token = json_scanner_get_next_token (scanner); + next_token = json_scanner_peek_next_token (scanner); + + if (next_token == JSON_TOKEN_RIGHT_BRACE) + { + priv->error_code = JSON_PARSER_ERROR_TRAILING_COMMA; + + json_array_unref (array); + json_node_unref (priv->current_node); + json_node_unref (element); + priv->current_node = old_current; + + return JSON_TOKEN_RIGHT_BRACE; + } + } + + JSON_NOTE (PARSER, "Array element %d completed", idx); + json_node_set_parent (element, priv->current_node); + if (priv->is_immutable) + json_node_seal (element); + json_array_add_element (array, element); + + g_signal_emit (parser, parser_signals[ARRAY_ELEMENT], 0, + array, + idx); + + idx += 1; + token = next_token; + } + +array_done: + json_scanner_get_next_token (scanner); + + if (priv->is_immutable) + json_array_seal (array); + + json_node_take_array (priv->current_node, array); + if (priv->is_immutable) + json_node_seal (priv->current_node); + json_node_set_parent (priv->current_node, old_current); + + g_signal_emit (parser, parser_signals[ARRAY_END], 0, array); + + if (node != NULL && *node == NULL) + *node = priv->current_node; + + priv->current_node = old_current; + + return JSON_TOKEN_NONE; +} + +static guint +json_parse_object (JsonParser *parser, + JsonScanner *scanner, + JsonNode **node, + unsigned int nesting) +{ + JsonParserPrivate *priv = parser->priv; + JsonObject *object; + JsonNode *old_current; + guint token; + + if (nesting >= JSON_PARSER_MAX_RECURSION_DEPTH) + { + priv->error_code = JSON_PARSER_ERROR_NESTING; + return JSON_TOKEN_RIGHT_CURLY; + } + + old_current = priv->current_node; + priv->current_node = json_node_init_object (json_node_alloc (), NULL); + + object = json_object_new (); + + token = json_scanner_get_next_token (scanner); + g_assert (token == JSON_TOKEN_LEFT_CURLY); + + g_signal_emit (parser, parser_signals[OBJECT_START], 0); + + while (token != JSON_TOKEN_RIGHT_CURLY) + { + guint next_token = json_scanner_peek_next_token (scanner); + JsonNode *member = NULL; + gchar *name; + + /* we need to abort here because empty objects do not + * have member names + */ + if (next_token == JSON_TOKEN_RIGHT_CURLY) + break; + + /* parse the member's name */ + if (next_token != JSON_TOKEN_STRING) + { + JSON_NOTE (PARSER, "Missing object member name"); + + priv->error_code = JSON_PARSER_ERROR_INVALID_BAREWORD; + + json_object_unref (object); + json_node_unref (priv->current_node); + priv->current_node = old_current; + + return JSON_TOKEN_STRING; + } + + /* member name */ + token = json_scanner_get_next_token (scanner); + name = json_scanner_dup_string_value (scanner); + if (name == NULL) + { + JSON_NOTE (PARSER, "Empty object member name"); + + priv->error_code = JSON_PARSER_ERROR_EMPTY_MEMBER_NAME; + + json_object_unref (object); + json_node_unref (priv->current_node); + priv->current_node = old_current; + + return JSON_TOKEN_STRING; + } + + JSON_NOTE (PARSER, "Object member '%s'", name); + + /* a colon separates names from values */ + next_token = json_scanner_peek_next_token (scanner); + if (next_token != JSON_TOKEN_COLON) + { + JSON_NOTE (PARSER, "Missing object member name separator"); + + priv->error_code = JSON_PARSER_ERROR_MISSING_COLON; + + g_free (name); + json_object_unref (object); + json_node_unref (priv->current_node); + priv->current_node = old_current; + + return JSON_TOKEN_COLON; + } + + /* we swallow the ':' */ + token = json_scanner_get_next_token (scanner); + g_assert (token == JSON_TOKEN_COLON); + next_token = json_scanner_peek_next_token (scanner); + + /* parse the member's value */ + switch (next_token) + { + case JSON_TOKEN_LEFT_BRACE: + JSON_NOTE (PARSER, "Nested array at member %s", name); + token = json_parse_array (parser, scanner, &member, nesting + 1); + break; + + case JSON_TOKEN_LEFT_CURLY: + JSON_NOTE (PARSER, "Nested object at member %s", name); + token = json_parse_object (parser, scanner, &member, nesting + 1); + break; + + default: + /* once a member name is defined we need a value */ + token = json_scanner_get_next_token (scanner); + token = json_parse_value (parser, scanner, token, &member); + break; + } + + if (token != JSON_TOKEN_NONE || member == NULL) + { + /* the json_parse_* functions will have set the error code */ + g_free (name); + json_object_unref (object); + json_node_unref (priv->current_node); + priv->current_node = old_current; + + return token; + } + + next_token = json_scanner_peek_next_token (scanner); + if (next_token == JSON_TOKEN_COMMA) + { + token = json_scanner_get_next_token (scanner); + next_token = json_scanner_peek_next_token (scanner); + + /* look for trailing commas */ + if (next_token == JSON_TOKEN_RIGHT_CURLY) + { + priv->error_code = JSON_PARSER_ERROR_TRAILING_COMMA; + + g_free (name); + json_object_unref (object); + json_node_unref (member); + json_node_unref (priv->current_node); + priv->current_node = old_current; + + return JSON_TOKEN_RIGHT_BRACE; + } + } + else if (next_token == JSON_TOKEN_STRING) + { + priv->error_code = JSON_PARSER_ERROR_MISSING_COMMA; + + g_free (name); + json_object_unref (object); + json_node_unref (member); + json_node_unref (priv->current_node); + priv->current_node = old_current; + + return JSON_TOKEN_COMMA; + } + + JSON_NOTE (PARSER, "Object member '%s' completed", name); + json_node_set_parent (member, priv->current_node); + if (priv->is_immutable) + json_node_seal (member); + json_object_set_member (object, name, member); + + g_signal_emit (parser, parser_signals[OBJECT_MEMBER], 0, + object, + name); + + g_free (name); + + token = next_token; + } + + json_scanner_get_next_token (scanner); + + if (priv->is_immutable) + json_object_seal (object); + + json_node_take_object (priv->current_node, object); + if (priv->is_immutable) + json_node_seal (priv->current_node); + json_node_set_parent (priv->current_node, old_current); + + g_signal_emit (parser, parser_signals[OBJECT_END], 0, object); + + if (node != NULL && *node == NULL) + *node = priv->current_node; + + priv->current_node = old_current; + + return JSON_TOKEN_NONE; +} + +static guint +json_parse_statement (JsonParser *parser, + JsonScanner *scanner) +{ + JsonParserPrivate *priv = parser->priv; + guint token; + + token = json_scanner_peek_next_token (scanner); + switch (token) + { + case JSON_TOKEN_LEFT_CURLY: + if (priv->is_strict && priv->root != NULL) + { + JSON_NOTE (PARSER, "Only one top level object is possible"); + json_scanner_get_next_token (scanner); + priv->error_code = JSON_PARSER_ERROR_INVALID_STRUCTURE; + return JSON_TOKEN_EOF; + } + JSON_NOTE (PARSER, "Statement is object declaration"); + return json_parse_object (parser, scanner, &priv->root, 0); + + case JSON_TOKEN_LEFT_BRACE: + if (priv->is_strict && priv->root != NULL) + { + JSON_NOTE (PARSER, "Only one top level array is possible"); + json_scanner_get_next_token (scanner); + priv->error_code = JSON_PARSER_ERROR_INVALID_STRUCTURE; + return JSON_TOKEN_EOF; + } + JSON_NOTE (PARSER, "Statement is array declaration"); + return json_parse_array (parser, scanner, &priv->root, 0); + + /* some web APIs are not only passing the data structures: they are + * also passing an assigment, which makes parsing horribly complicated + * only because web developers are lazy, and writing "var foo = " is + * evidently too much to request from them. + */ + case JSON_TOKEN_VAR: + { + guint next_token; + gchar *name; + + JSON_NOTE (PARSER, "Statement is an assignment"); + + if (priv->is_strict) + { + json_scanner_get_next_token (scanner); + priv->error_code = JSON_PARSER_ERROR_INVALID_ASSIGNMENT; + return JSON_TOKEN_EOF; + } + + /* swallow the 'var' token... */ + token = json_scanner_get_next_token (scanner); + + /* ... swallow the variable name... */ + next_token = json_scanner_get_next_token (scanner); + if (next_token != JSON_TOKEN_IDENTIFIER) + { + priv->error_code = JSON_PARSER_ERROR_INVALID_BAREWORD; + return JSON_TOKEN_IDENTIFIER; + } + + name = json_scanner_dup_identifier (scanner); + + /* ... and finally swallow the '=' */ + next_token = json_scanner_get_next_token (scanner); + if (next_token != '=') + { + priv->error_code = JSON_PARSER_ERROR_INVALID_BAREWORD; + g_free (name); + return '='; + } + + if (priv->has_assignment) + g_free (priv->variable_name); + priv->has_assignment = TRUE; + priv->variable_name = name; + + token = json_parse_statement (parser, scanner); + + /* remove the trailing semi-colon */ + next_token = json_scanner_peek_next_token (scanner); + if (next_token == ';') + { + token = json_scanner_get_next_token (scanner); + return JSON_TOKEN_NONE; + } + + return token; + } + break; + + case JSON_TOKEN_NULL: + case JSON_TOKEN_TRUE: + case JSON_TOKEN_FALSE: + case '-': + case JSON_TOKEN_INT: + case JSON_TOKEN_FLOAT: + case JSON_TOKEN_STRING: + case JSON_TOKEN_IDENTIFIER: + if (priv->root != NULL) + { + JSON_NOTE (PARSER, "Only one top level statement is possible"); + json_scanner_get_next_token (scanner); + priv->error_code = JSON_PARSER_ERROR_INVALID_BAREWORD; + return JSON_TOKEN_EOF; + } + + JSON_NOTE (PARSER, "Statement is a value"); + token = json_scanner_get_next_token (scanner); + return json_parse_value (parser, scanner, token, &priv->root); + + default: + JSON_NOTE (PARSER, "Unknown statement"); + json_scanner_get_next_token (scanner); + priv->error_code = JSON_PARSER_ERROR_INVALID_BAREWORD; + return priv->root != NULL ? JSON_TOKEN_EOF : JSON_TOKEN_SYMBOL; + } +} + +static void +json_scanner_msg_handler (JsonScanner *scanner, + const char *message, + gpointer user_data) +{ + JsonParser *parser = user_data; + JsonParserPrivate *priv = parser->priv; + GError *error = NULL; + + g_set_error (&error, JSON_PARSER_ERROR, + priv->error_code, + /* translators: %s: is the file name, the first %d is the line + * number, the second %d is the position on the line, and %s is + * the error message + */ + _("%s:%d:%d: Parse error: %s"), + priv->is_filename ? priv->filename : "", + json_scanner_get_current_line (scanner), + json_scanner_get_current_position (scanner), + message); + + parser->priv->last_error = error; + g_signal_emit (parser, parser_signals[ERROR], 0, error); +} + +static JsonScanner * +json_scanner_create (JsonParser *parser) +{ + JsonParserPrivate *priv = json_parser_get_instance_private (parser); + JsonScanner *scanner; + + scanner = json_scanner_new (priv->is_strict); + json_scanner_set_msg_handler (scanner, json_scanner_msg_handler, parser); + + return scanner; +} + +/** + * json_parser_new: + * + * Creates a new JSON parser. + * + * You can use the `JsonParser` to load a JSON stream from either a file or a + * buffer and then walk the hierarchy using the data types API. + * + * Returns: (transfer full): the newly created parser + */ +JsonParser * +json_parser_new (void) +{ + return g_object_new (JSON_TYPE_PARSER, NULL); +} + +/** + * json_parser_new_immutable: + * + * Creates a new parser instance with its [property@Json.Parser:immutable] + * property set to `TRUE` to create immutable output trees. + * + * Since: 1.2 + * Returns: (transfer full): the newly created parser + */ +JsonParser * +json_parser_new_immutable (void) +{ + return g_object_new (JSON_TYPE_PARSER, "immutable", TRUE, NULL); +} + +static gboolean +json_parser_load (JsonParser *parser, + const gchar *input_data, + gsize length, + GError **error) +{ + JsonParserPrivate *priv = parser->priv; + JsonScanner *scanner; + gboolean done; + gboolean retval = TRUE; + const char *data = input_data; + + if (priv->is_strict && (length == 0 || data == NULL || *data == '\0')) + { + g_set_error_literal (error, JSON_PARSER_ERROR, + JSON_PARSER_ERROR_INVALID_DATA, + "JSON data must not be empty"); + g_signal_emit (parser, parser_signals[ERROR], 0, *error); + return FALSE; + } + + json_parser_clear (parser); + + if (!g_utf8_validate (data, length, NULL)) + { + g_set_error_literal (error, JSON_PARSER_ERROR, + JSON_PARSER_ERROR_INVALID_DATA, + _("JSON data must be UTF-8 encoded")); + g_signal_emit (parser, parser_signals[ERROR], 0, *error); + return FALSE; + } + + if (length >= 3) + { + /* Check for UTF-8 signature and skip it if necessary */ + if (((data[0] & 0xFF) == 0xEF) && + ((data[1] & 0xFF) == 0xBB) && + ((data[2] & 0xFF) == 0xBF)) + { + JSON_NOTE (PARSER, "Skipping BOM"); + data += 3; + length -= 3; + } + + if (priv->is_strict && length == 0) + { + g_set_error_literal (error, JSON_PARSER_ERROR, + JSON_PARSER_ERROR_INVALID_DATA, + "JSON data must not be empty after BOM character"); + g_signal_emit (parser, parser_signals[ERROR], 0, *error); + return FALSE; + } + } + + /* Skip leading space */ + if (priv->is_strict) + { + const char *p = data; + while (length > 0 && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) + { + length -= 1; + data += 1; + p = data; + } + + if (length == 0) + { + g_set_error_literal (error, JSON_PARSER_ERROR, + JSON_PARSER_ERROR_INVALID_DATA, + "JSON data must not be empty after leading whitespace"); + g_signal_emit (parser, parser_signals[ERROR], 0, *error); + return FALSE; + } + } + + scanner = json_scanner_create (parser); + json_scanner_input_text (scanner, data, length); + + priv->scanner = scanner; + + g_signal_emit (parser, parser_signals[PARSE_START], 0); + + done = FALSE; + while (!done) + { + if (json_scanner_peek_next_token (scanner) == JSON_TOKEN_EOF) + done = TRUE; + else + { + unsigned int expected_token; + + /* we try to show the expected token, if possible */ + expected_token = json_parse_statement (parser, scanner); + if (expected_token != JSON_TOKEN_NONE) + { + /* this will emit the ::error signal via the custom + * message handler we install + */ + json_scanner_unknown_token (scanner, expected_token); + + /* and this will propagate the error we create in the + * same message handler + */ + if (priv->last_error) + { + g_propagate_error (error, priv->last_error); + priv->last_error = NULL; + } + + retval = FALSE; + done = TRUE; + } + } + } + + g_signal_emit (parser, parser_signals[PARSE_END], 0); + + /* remove the scanner */ + json_scanner_destroy (scanner); + priv->scanner = NULL; + priv->current_node = NULL; + + return retval; +} + +/** + * json_parser_load_from_file: + * @parser: a parser + * @filename: (type filename): the path for the file to parse + * @error: return location for a #GError + * + * Loads a JSON stream from the content of `filename` and parses it. + * + * If the file is large or shared between processes, + * [method@Json.Parser.load_from_mapped_file] may be a more efficient + * way to load it. + * + * See also: [method@Json.Parser.load_from_data] + * + * Returns: `TRUE` if the file was successfully loaded and parsed. + */ +gboolean +json_parser_load_from_file (JsonParser *parser, + const gchar *filename, + GError **error) +{ + JsonParserPrivate *priv; + GError *internal_error; + gchar *data; + gsize length; + gboolean retval = TRUE; + + g_return_val_if_fail (JSON_IS_PARSER (parser), FALSE); + g_return_val_if_fail (filename != NULL, FALSE); + + priv = parser->priv; + + internal_error = NULL; + if (!g_file_get_contents (filename, &data, &length, &internal_error)) + { + g_propagate_error (error, internal_error); + return FALSE; + } + + g_free (priv->filename); + + priv->is_filename = TRUE; + priv->filename = g_strdup (filename); + + if (!json_parser_load (parser, data, length, &internal_error)) + { + g_propagate_error (error, internal_error); + retval = FALSE; + } + + g_free (data); + + return retval; +} + +/** + * json_parser_load_from_mapped_file: + * @parser: a parser + * @filename: (type filename): the path for the file to parse + * @error: return location for a #GError + * + * Loads a JSON stream from the content of `filename` and parses it. + * + * Unlike [method@Json.Parser.load_from_file], `filename` will be memory + * mapped as read-only and parsed. `filename` will be unmapped before this + * function returns. + * + * If mapping or reading the file fails, a `G_FILE_ERROR` will be returned. + * + * Returns: `TRUE` if the file was successfully loaded and parsed. + * Since: 1.6 + */ +gboolean +json_parser_load_from_mapped_file (JsonParser *parser, + const gchar *filename, + GError **error) +{ + JsonParserPrivate *priv; + GError *internal_error = NULL; + gboolean retval = TRUE; + GMappedFile *mapped_file = NULL; + + g_return_val_if_fail (JSON_IS_PARSER (parser), FALSE); + g_return_val_if_fail (filename != NULL, FALSE); + + priv = parser->priv; + + mapped_file = g_mapped_file_new (filename, FALSE, &internal_error); + if (mapped_file == NULL) + { + g_propagate_error (error, internal_error); + return FALSE; + } + + g_free (priv->filename); + + priv->is_filename = TRUE; + priv->filename = g_strdup (filename); + + if (!json_parser_load (parser, g_mapped_file_get_contents (mapped_file), + g_mapped_file_get_length (mapped_file), &internal_error)) + { + g_propagate_error (error, internal_error); + retval = FALSE; + } + + g_clear_pointer (&mapped_file, g_mapped_file_unref); + + return retval; +} + +/** + * json_parser_load_from_data: + * @parser: a parser + * @data: the buffer to parse + * @length: the length of the buffer, or -1 if it is `NUL` terminated + * @error: return location for a #GError + * + * Loads a JSON stream from a buffer and parses it. + * + * You can call this function multiple times with the same parser, but the + * contents of the parser will be destroyed each time. + * + * Returns: `TRUE` if the buffer was succesfully parsed + */ +gboolean +json_parser_load_from_data (JsonParser *parser, + const gchar *data, + gssize length, + GError **error) +{ + JsonParserPrivate *priv; + GError *internal_error; + gboolean retval = TRUE; + + g_return_val_if_fail (JSON_IS_PARSER (parser), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + priv = parser->priv; + + if (length < 0) + length = strlen (data); + + priv->is_filename = FALSE; + g_free (priv->filename); + priv->filename = NULL; + + internal_error = NULL; + if (!json_parser_load (parser, data, length, &internal_error)) + { + g_propagate_error (error, internal_error); + retval = FALSE; + } + + return retval; +} + +/** + * json_parser_get_root: + * @parser: a parser + * + * Retrieves the top level node from the parsed JSON stream. + * + * If the parser input was an empty string, or if parsing failed, the root + * will be `NULL`. It will also be `NULL` if it has been stolen using + * [method@Json.Parser.steal_root]. + * + * Returns: (transfer none) (nullable): the root node. + */ +JsonNode * +json_parser_get_root (JsonParser *parser) +{ + g_return_val_if_fail (JSON_IS_PARSER (parser), NULL); + + /* Sanity check. */ + g_assert (parser->priv->root == NULL || + !parser->priv->is_immutable || + json_node_is_immutable (parser->priv->root)); + + return parser->priv->root; +} + +/** + * json_parser_steal_root: + * @parser: a parser + * + * Steals the top level node from the parsed JSON stream. + * + * This will be `NULL` in the same situations as [method@Json.Parser.get_root] + * return `NULL`. + * + * Returns: (transfer full) (nullable): the root node + * + * Since: 1.4 + */ +JsonNode * +json_parser_steal_root (JsonParser *parser) +{ + JsonParserPrivate *priv = json_parser_get_instance_private (parser); + + g_return_val_if_fail (JSON_IS_PARSER (parser), NULL); + + /* Sanity check. */ + g_assert (parser->priv->root == NULL || + !parser->priv->is_immutable || + json_node_is_immutable (parser->priv->root)); + + return g_steal_pointer (&priv->root); +} + +/** + * json_parser_get_current_line: + * @parser: a parser + * + * Retrieves the line currently parsed, starting from 1. + * + * This function has defined behaviour only while parsing; calling this + * function from outside the signal handlers emitted by the parser will + * yield 0. + * + * Returns: the currently parsed line, or 0. + */ +guint +json_parser_get_current_line (JsonParser *parser) +{ + g_return_val_if_fail (JSON_IS_PARSER (parser), 0); + + if (parser->priv->scanner != NULL) + return json_scanner_get_current_line (parser->priv->scanner); + + return 0; +} + +/** + * json_parser_get_current_pos: + * @parser: a parser + * + * Retrieves the current position inside the current line, starting + * from 0. + * + * This function has defined behaviour only while parsing; calling this + * function from outside the signal handlers emitted by the parser will + * yield 0. + * + * Returns: the position in the current line, or 0. + */ +guint +json_parser_get_current_pos (JsonParser *parser) +{ + g_return_val_if_fail (JSON_IS_PARSER (parser), 0); + + if (parser->priv->scanner != NULL) + return json_scanner_get_current_position (parser->priv->scanner); + + return 0; +} + +/** + * json_parser_has_assignment: + * @parser: a parser + * @variable_name: (out) (optional) (transfer none): the variable name + * + * A JSON data stream might sometimes contain an assignment, like: + * + * ``` + * var _json_data = { "member_name" : [ ... + * ``` + * + * even though it would technically constitute a violation of the RFC. + * + * `JsonParser` will ignore the left hand identifier and parse the right + * hand value of the assignment. `JsonParser` will record, though, the + * existence of the assignment in the data stream and the variable name + * used. + * + * Returns: `TRUE` if there was an assignment, and `FALSE` otherwise + * + * Since: 0.4 + */ +gboolean +json_parser_has_assignment (JsonParser *parser, + gchar **variable_name) +{ + JsonParserPrivate *priv; + + g_return_val_if_fail (JSON_IS_PARSER (parser), FALSE); + + priv = parser->priv; + + if (priv->has_assignment && variable_name) + *variable_name = priv->variable_name; + + return priv->has_assignment; +} + +#define GET_DATA_BLOCK_SIZE 8192 + +/** + * json_parser_load_from_stream: + * @parser: a parser + * @stream: the input stream with the JSON data + * @cancellable: (nullable): a #GCancellable + * @error: the return location for a #GError + * + * Loads the contents of an input stream and parses them. + * + * If `cancellable` is not `NULL`, then the operation can be cancelled by + * triggering the cancellable object from another thread. If the + * operation was cancelled, `G_IO_ERROR_CANCELLED` will be set + * on the given `error`. + * + * Returns: `TRUE` if the data stream was successfully read and + * parsed, and `FALSE` otherwise + * + * Since: 0.12 + */ +gboolean +json_parser_load_from_stream (JsonParser *parser, + GInputStream *stream, + GCancellable *cancellable, + GError **error) +{ + GByteArray *content; + gsize pos; + gssize res; + gboolean retval = FALSE; + GError *internal_error; + + g_return_val_if_fail (JSON_IS_PARSER (parser), FALSE); + g_return_val_if_fail (G_IS_INPUT_STREAM (stream), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + content = g_byte_array_new (); + pos = 0; + + g_byte_array_set_size (content, pos + GET_DATA_BLOCK_SIZE + 1); + while ((res = g_input_stream_read (stream, content->data + pos, + GET_DATA_BLOCK_SIZE, + cancellable, error)) > 0) + { + pos += res; + g_byte_array_set_size (content, pos + GET_DATA_BLOCK_SIZE + 1); + } + + if (res < 0) + { + /* error has already been set */ + retval = FALSE; + goto out; + } + + /* zero-terminate the content; we allocated an extra byte for this */ + content->data[pos] = 0; + + internal_error = NULL; + retval = json_parser_load (parser, (const gchar *) content->data, pos, &internal_error); + + if (internal_error != NULL) + g_propagate_error (error, internal_error); + +out: + g_byte_array_free (content, TRUE); + + return retval; +} + +typedef struct { + GInputStream *stream; + GByteArray *content; + gsize pos; +} LoadData; + +static void +load_data_free (gpointer data_) +{ + if (data_ != NULL) + { + LoadData *data = data_; + + g_object_unref (data->stream); + g_byte_array_unref (data->content); + g_free (data); + } +} + +/** + * json_parser_load_from_stream_finish: + * @parser: a parser + * @result: the result of the asynchronous operation + * @error: the return location for a #GError + * + * Finishes an asynchronous stream loading started with + * [method@Json.Parser.load_from_stream_async]. + * + * Returns: `TRUE` if the content of the stream was successfully retrieved + * and parsed, and `FALSE` otherwise + * + * Since: 0.12 + */ +gboolean +json_parser_load_from_stream_finish (JsonParser *parser, + GAsyncResult *result, + GError **error) +{ + gboolean res; + + g_return_val_if_fail (JSON_IS_PARSER (parser), FALSE); + g_return_val_if_fail (g_task_is_valid (result, parser), FALSE); + + res = g_task_propagate_boolean (G_TASK (result), error); + if (res) + { + LoadData *data = g_task_get_task_data (G_TASK (result)); + GError *internal_error = NULL; + + /* We need to do this inside the finish() function because JsonParser will emit + * signals, and we need to ensure that the signals are emitted in the right + * context; it's easier to do that if we just rely on the async callback being + * called in the right context, even if it means making the finish() function + * necessary to complete the async operation. + */ + res = json_parser_load (parser, (const gchar *) data->content->data, data->pos, &internal_error); + if (internal_error != NULL) + g_propagate_error (error, internal_error); + } + + return res; +} + +static void +read_from_stream (GTask *task, + gpointer source_obj G_GNUC_UNUSED, + gpointer task_data, + GCancellable *cancellable) +{ + LoadData *data = task_data; + GError *error = NULL; + gssize res; + + data->pos = 0; + g_byte_array_set_size (data->content, data->pos + GET_DATA_BLOCK_SIZE + 1); + while ((res = g_input_stream_read (data->stream, + data->content->data + data->pos, + GET_DATA_BLOCK_SIZE, + cancellable, &error)) > 0) + { + data->pos += res; + g_byte_array_set_size (data->content, data->pos + GET_DATA_BLOCK_SIZE + 1); + } + + if (res < 0) + { + g_task_return_error (task, error); + return; + } + + /* zero-terminate the content; we allocated an extra byte for this */ + data->content->data[data->pos] = 0; + g_task_return_boolean (task, TRUE); +} + +/** + * json_parser_load_from_stream_async: + * @parser: a parser + * @stream: the input stream with the JSON data + * @cancellable: (nullable): a #GCancellable + * @callback: (scope async): the function to call when the request is satisfied + * @user_data: the data to pass to @callback + * + * Asynchronously reads the contents of a stream. + * + * For more details, see [method@Json.Parser.load_from_stream], which is the + * synchronous version of this call. + * + * When the operation is finished, @callback will be called. You should + * then call [method@Json.Parser.load_from_stream_finish] to get the result + * of the operation. + * + * Since: 0.12 + */ +void +json_parser_load_from_stream_async (JsonParser *parser, + GInputStream *stream, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + LoadData *data; + GTask *task; + + g_return_if_fail (JSON_IS_PARSER (parser)); + g_return_if_fail (G_IS_INPUT_STREAM (stream)); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + data = g_new (LoadData, 1); + data->stream = g_object_ref (stream); + data->content = g_byte_array_new (); + data->pos = 0; + + task = g_task_new (parser, cancellable, callback, user_data); + g_task_set_task_data (task, data, load_data_free); + + g_task_run_in_thread (task, read_from_stream); + g_object_unref (task); +} + +/** + * json_parser_set_strict: + * @parser: the JSON parser + * @strict: whether the parser should be strict + * + * Sets whether the parser should operate in strict mode. + * + * If @strict is true, `JsonParser` will strictly conform to + * the JSON format. + * + * If @strict is false, `JsonParser` will allow custom extensions + * to the JSON format, like comments. + * + * Since: 1.10 + */ +void +json_parser_set_strict (JsonParser *parser, + gboolean strict) +{ + g_return_if_fail (JSON_IS_PARSER (parser)); + + JsonParserPrivate *priv = json_parser_get_instance_private (parser); + + strict = !!strict; + + if (priv->is_strict != strict) + { + priv->is_strict = strict; + g_object_notify_by_pspec (G_OBJECT (parser), parser_props[PROP_STRICT]); + } +} + +/** + * json_parser_get_strict: + * @parser: the JSON parser + * + * Retrieves whether the parser is operating in strict mode. + * + * Returns: true if the parser is strict, and false otherwise + * + * Since: 1.10 + */ +gboolean +json_parser_get_strict (JsonParser *parser) +{ + g_return_val_if_fail (JSON_IS_PARSER (parser), FALSE); + + JsonParserPrivate *priv = json_parser_get_instance_private (parser); + + return priv->is_strict; +} diff --git a/lsp/deps/json-glib/json-parser.h b/lsp/deps/json-glib/json-parser.h new file mode 100644 index 000000000..65a5b622e --- /dev/null +++ b/lsp/deps/json-glib/json-parser.h @@ -0,0 +1,244 @@ +/* json-parser.h - JSON streams parser + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ +#pragma once + +#if !defined(__JSON_GLIB_INSIDE__) && !defined(JSON_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include + +G_BEGIN_DECLS + +#define JSON_TYPE_PARSER (json_parser_get_type ()) +#define JSON_PARSER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), JSON_TYPE_PARSER, JsonParser)) +#define JSON_IS_PARSER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), JSON_TYPE_PARSER)) +#define JSON_PARSER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), JSON_TYPE_PARSER, JsonParserClass)) +#define JSON_IS_PARSER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), JSON_TYPE_PARSER)) +#define JSON_PARSER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), JSON_TYPE_PARSER, JsonParserClass)) + +/** + * JSON_PARSER_ERROR: + * + * Error domain for `JsonParser`. + */ +#define JSON_PARSER_ERROR (json_parser_error_quark ()) + +/** + * JSON_PARSER_MAX_RECURSION_DEPTH: + * + * The maximum recursion depth for a JSON tree. + * + * Since: 1.10 + */ +#define JSON_PARSER_MAX_RECURSION_DEPTH (1024) + +typedef struct _JsonParser JsonParser; +typedef struct _JsonParserPrivate JsonParserPrivate; +typedef struct _JsonParserClass JsonParserClass; + +/** + * JsonParserError: + * @JSON_PARSER_ERROR_PARSE: parse error + * @JSON_PARSER_ERROR_TRAILING_COMMA: unexpected trailing comma + * @JSON_PARSER_ERROR_MISSING_COMMA: expected comma + * @JSON_PARSER_ERROR_MISSING_COLON: expected colon + * @JSON_PARSER_ERROR_INVALID_BAREWORD: invalid bareword + * @JSON_PARSER_ERROR_UNKNOWN: unknown error + * + * Error codes for `JSON_PARSER_ERROR`. + * + * This enumeration can be extended at later date + */ +typedef enum { + JSON_PARSER_ERROR_PARSE, + JSON_PARSER_ERROR_TRAILING_COMMA, + JSON_PARSER_ERROR_MISSING_COMMA, + JSON_PARSER_ERROR_MISSING_COLON, + JSON_PARSER_ERROR_INVALID_BAREWORD, + /** + * JSON_PARSER_ERROR_EMPTY_MEMBER_NAME: + * + * Empty member name. + * + * Since: 0.16 + */ + JSON_PARSER_ERROR_EMPTY_MEMBER_NAME, + /** + * JSON_PARSER_ERROR_INVALID_DATA: + * + * Invalid data. + * + * Since: 0.18 + */ + JSON_PARSER_ERROR_INVALID_DATA, + JSON_PARSER_ERROR_UNKNOWN, + /** + * JSON_PARSER_ERROR_NESTING: + * + * Too many levels of nesting. + * + * Since: 1.10 + */ + JSON_PARSER_ERROR_NESTING, + /** + * JSON_PARSER_ERROR_INVALID_STRUCTURE: + * + * Invalid structure. + * + * Since: 1.10 + */ + JSON_PARSER_ERROR_INVALID_STRUCTURE, + /** + * JSON_PARSER_ERROR_INVALID_ASSIGNMENT: + * + * Invalid assignment. + * + * Since: 1.10 + */ + JSON_PARSER_ERROR_INVALID_ASSIGNMENT +} JsonParserError; + +struct _JsonParser +{ + /*< private >*/ + GObject parent_instance; + + JsonParserPrivate *priv; +}; + +/** + * JsonParserClass: + * @parse_start: class handler for the JsonParser::parse-start signal + * @object_start: class handler for the JsonParser::object-start signal + * @object_member: class handler for the JsonParser::object-member signal + * @object_end: class handler for the JsonParser::object-end signal + * @array_start: class handler for the JsonParser::array-start signal + * @array_element: class handler for the JsonParser::array-element signal + * @array_end: class handler for the JsonParser::array-end signal + * @parse_end: class handler for the JsonParser::parse-end signal + * @error: class handler for the JsonParser::error signal + * + * The class structure for the JsonParser type. + */ +struct _JsonParserClass +{ + /*< private >*/ + GObjectClass parent_class; + + /*< public >*/ + void (* parse_start) (JsonParser *parser); + + void (* object_start) (JsonParser *parser); + void (* object_member) (JsonParser *parser, + JsonObject *object, + const gchar *member_name); + void (* object_end) (JsonParser *parser, + JsonObject *object); + + void (* array_start) (JsonParser *parser); + void (* array_element) (JsonParser *parser, + JsonArray *array, + gint index_); + void (* array_end) (JsonParser *parser, + JsonArray *array); + + void (* parse_end) (JsonParser *parser); + + void (* error) (JsonParser *parser, + const GError *error); + + /*< private >*/ + /* padding for future expansion */ + void (* _json_reserved1) (void); + void (* _json_reserved2) (void); + void (* _json_reserved3) (void); + void (* _json_reserved4) (void); + void (* _json_reserved5) (void); + void (* _json_reserved6) (void); + void (* _json_reserved7) (void); + void (* _json_reserved8) (void); +}; + +JSON_AVAILABLE_IN_1_0 +GQuark json_parser_error_quark (void); +JSON_AVAILABLE_IN_1_0 +GType json_parser_get_type (void) G_GNUC_CONST; + +JSON_AVAILABLE_IN_1_0 +JsonParser *json_parser_new (void); +JSON_AVAILABLE_IN_1_2 +JsonParser *json_parser_new_immutable (void); +JSON_AVAILABLE_IN_1_10 +void json_parser_set_strict (JsonParser *parser, + gboolean strict); +JSON_AVAILABLE_IN_1_10 +gboolean json_parser_get_strict (JsonParser *parser); +JSON_AVAILABLE_IN_1_0 +gboolean json_parser_load_from_file (JsonParser *parser, + const gchar *filename, + GError **error); +JSON_AVAILABLE_IN_1_6 +gboolean json_parser_load_from_mapped_file (JsonParser *parser, + const gchar *filename, + GError **error); +JSON_AVAILABLE_IN_1_0 +gboolean json_parser_load_from_data (JsonParser *parser, + const gchar *data, + gssize length, + GError **error); +JSON_AVAILABLE_IN_1_0 +gboolean json_parser_load_from_stream (JsonParser *parser, + GInputStream *stream, + GCancellable *cancellable, + GError **error); +JSON_AVAILABLE_IN_1_0 +void json_parser_load_from_stream_async (JsonParser *parser, + GInputStream *stream, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +JSON_AVAILABLE_IN_1_0 +gboolean json_parser_load_from_stream_finish (JsonParser *parser, + GAsyncResult *result, + GError **error); + +JSON_AVAILABLE_IN_1_0 +JsonNode * json_parser_get_root (JsonParser *parser); +JSON_AVAILABLE_IN_1_4 +JsonNode * json_parser_steal_root (JsonParser *parser); + +JSON_AVAILABLE_IN_1_0 +guint json_parser_get_current_line (JsonParser *parser); +JSON_AVAILABLE_IN_1_0 +guint json_parser_get_current_pos (JsonParser *parser); +JSON_AVAILABLE_IN_1_0 +gboolean json_parser_has_assignment (JsonParser *parser, + gchar **variable_name); + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC (JsonParser, g_object_unref) +#endif + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-path.c b/lsp/deps/json-glib/json-path.c new file mode 100644 index 000000000..5e4c0ef9d --- /dev/null +++ b/lsp/deps/json-glib/json-path.c @@ -0,0 +1,998 @@ +/* json-path.h - JSONPath implementation + * + * This file is part of JSON-GLib + * Copyright © 2011 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +/** + * JsonPath: + * + * `JsonPath` is a simple class implementing the JSONPath syntax for extracting + * data out of a JSON tree. + * + * While the semantics of the JSONPath expressions are heavily borrowed by the + * XPath specification for XML, the syntax follows the ECMAScript origins of + * JSON. + * + * Once a `JsonPath` instance has been created, it has to compile a JSONPath + * expression using [method@Json.Path.compile] before being able to match it to + * a JSON tree; the same `JsonPath` instance can be used to match multiple JSON + * trees. It it also possible to compile a new JSONPath expression using the + * same `JsonPath` instance; the previous expression will be discarded only if + * the compilation of the new expression is successful. + * + * The simple convenience function [func@Json.Path.query] can be used for + * one-off matching. + * + * ## Syntax of the JSONPath expressions + * + * A JSONPath expression is composed by path indices and operators. + * Each path index can either be a member name or an element index inside + * a JSON tree. A JSONPath expression must start with the `$` operator; each + * path index is separated using either the dot notation or the bracket + * notation, e.g.: + * + * ``` + * // dot notation + * $.store.book[0].title + * + * // bracket notation + * $['store']['book'][0]['title'] + * ``` + * + * The available operators are: + * + * * The `$` character represents the root node of the JSON tree, and + * matches the entire document. + * + * * Child nodes can either be matched using `.` or `[]`. For instance, + * both `$.store.book` and `$['store']['book']` match the contents of + * the book member of the store object. + * + * * Child nodes can be reached without specifying the whole tree structure + * through the recursive descent operator, or `..`. For instance, + * `$..author` matches all author member in every object. + * + * * Child nodes can grouped through the wildcard operator, or `*`. For + * instance, `$.store.book[*].author` matches all author members of any + * object element contained in the book array of the store object. + * + * * Element nodes can be accessed using their index (starting from zero) + * in the subscript operator `[]`. For instance, `$.store.book[0]` matches + * the first element of the book array of the store object. + * + * * Subsets of element nodes can be accessed using the set notation + * operator `[i,j,...]`. For instance, `$.store.book[0,2]` matches the + * elements 0 and 2 (the first and third) of the book array of the store + * object. + * + * * Slices of element nodes can be accessed using the slice notation + * operation `[start:end:step]`. If start is omitted, the starting index + * of the slice is implied to be zero; if end is omitted, the ending index + * of the slice is implied to be the length of the array; if step is + * omitted, the step of the slice is implied to be 1. For instance, + * `$.store.book[:2]` matches the first two elements of the book array + * of the store object. + * + * More information about JSONPath is available on Stefan Gössner's + * [JSONPath website](http://goessner.net/articles/JsonPath/). + * + * ## Example of JSONPath matches + * + * The following example shows some of the results of using `JsonPath` + * on a JSON tree. We use the following JSON description of a bookstore: + * + * ```json + * { "store": { + * "book": [ + * { "category": "reference", "author": "Nigel Rees", + * "title": "Sayings of the Century", "price": "8.95" }, + * { "category": "fiction", "author": "Evelyn Waugh", + * "title": "Sword of Honour", "price": "12.99" }, + * { "category": "fiction", "author": "Herman Melville", + * "title": "Moby Dick", "isbn": "0-553-21311-3", + * "price": "8.99" }, + * { "category": "fiction", "author": "J. R. R. Tolkien", + * "title": "The Lord of the Rings", "isbn": "0-395-19395-8", + * "price": "22.99" } + * ], + * "bicycle": { "color": "red", "price": "19.95" } + * } + * } + * ``` + * + * We can parse the JSON using [class@Json.Parser]: + * + * ```c + * JsonParser *parser = json_parser_new (); + * json_parser_load_from_data (parser, json_data, -1, NULL); + * ``` + * + * If we run the following code: + * + * ```c + * JsonNode *result; + * JsonPath *path = json_path_new (); + * json_path_compile (path, "$.store..author", NULL); + * result = json_path_match (path, json_parser_get_root (parser)); + * ``` + * + * The `result` node will contain an array with all values of the + * author member of the objects in the JSON tree. If we use a + * [class@Json.Generator] to convert the `result` node to a string + * and print it: + * + * ```c + * JsonGenerator *generator = json_generator_new (); + * json_generator_set_root (generator, result); + * char *str = json_generator_to_data (generator, NULL); + * g_print ("Results: %s\n", str); + * ``` + * + * The output will be: + * + * ```json + * ["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"] + * ``` + * + * Since: 0.14 + */ + +#include "config.h" + +#include + +#include + +#include "json-path.h" + +#include "json-debug.h" +#include "json-types-private.h" + +typedef enum { + JSON_PATH_NODE_ROOT, + JSON_PATH_NODE_CHILD_MEMBER, + JSON_PATH_NODE_CHILD_ELEMENT, + JSON_PATH_NODE_RECURSIVE_DESCENT, + JSON_PATH_NODE_WILDCARD_MEMBER, + JSON_PATH_NODE_WILDCARD_ELEMENT, + JSON_PATH_NODE_ELEMENT_SET, + JSON_PATH_NODE_ELEMENT_SLICE +} PathNodeType; + +typedef struct _PathNode PathNode; + +struct _JsonPath +{ + GObject parent_instance; + + /* the compiled path */ + GList *nodes; + + guint is_compiled : 1; +}; + +struct _JsonPathClass +{ + GObjectClass parent_class; +}; + +struct _PathNode +{ + PathNodeType node_type; + + union { + /* JSON_PATH_NODE_CHILD_ELEMENT */ + guint element_index; + + /* JSON_PATH_NODE_CHILD_MEMBER */ + char *member_name; + + /* JSON_PATH_NODE_ELEMENT_SET */ + struct { int n_indices; int *indices; } set; + + /* JSON_PATH_NODE_ELEMENT_SLICE */ + struct { int start, end, step; } slice; + } data; +}; + +G_DEFINE_QUARK (json-path-error-quark, json_path_error) + +G_DEFINE_TYPE (JsonPath, json_path, G_TYPE_OBJECT) + +static void +path_node_free (gpointer data) +{ + if (data != NULL) + { + PathNode *node = data; + + switch (node->node_type) + { + case JSON_PATH_NODE_CHILD_MEMBER: + g_free (node->data.member_name); + break; + + case JSON_PATH_NODE_ELEMENT_SET: + g_free (node->data.set.indices); + break; + + default: + break; + } + + g_free (node); + } +} + +static void +json_path_finalize (GObject *gobject) +{ + JsonPath *self = JSON_PATH (gobject); + + g_list_free_full (self->nodes, path_node_free); + + G_OBJECT_CLASS (json_path_parent_class)->finalize (gobject); +} + +static void +json_path_class_init (JsonPathClass *klass) +{ + G_OBJECT_CLASS (klass)->finalize = json_path_finalize; +} + +static void +json_path_init (JsonPath *self G_GNUC_UNUSED) +{ +} + +/** + * json_path_new: + * + * Creates a new `JsonPath` instance. + * + * Once created, the `JsonPath` object should be used with + * [method@Json.Path.compile] and [method@Json.Path.match]. + * + * Return value: (transfer full): the newly created path + * + * Since: 0.14 + */ +JsonPath * +json_path_new (void) +{ + return g_object_new (JSON_TYPE_PATH, NULL); +} + +#ifdef JSON_ENABLE_DEBUG +/* used as the function for a g_list_foreach() on a list of PathNode; needs + * a GString as the payload to build the output string + */ +static void +json_path_foreach_print (gpointer data, + gpointer user_data) +{ + PathNode *cur_node = data; + GString *buf = user_data; + + switch (cur_node->node_type) + { + case JSON_PATH_NODE_ROOT: + g_string_append (buf, "data.member_name); + break; + + case JSON_PATH_NODE_CHILD_ELEMENT: + g_string_append_printf (buf, "data.element_index); + break; + + case JSON_PATH_NODE_RECURSIVE_DESCENT: + g_string_append (buf, "data.set.n_indices - 1; i++) + g_string_append_printf (buf, "'%d', ", cur_node->data.set.indices[i]); + + g_string_append_printf (buf, "'%d'", cur_node->data.set.indices[i]); + } + break; + + case JSON_PATH_NODE_ELEMENT_SLICE: + g_string_append_printf (buf, "data.slice.start, + cur_node->data.slice.end, + cur_node->data.slice.step); + break; + + default: + g_string_append (buf, ""); +} +#endif /* JSON_ENABLE_DEBUG */ + +/** + * json_path_compile: + * @path: a path + * @expression: a JSONPath expression + * @error: return location for a #GError, or %NULL + * + * Validates and decomposes the given expression. + * + * A JSONPath expression must be compiled before calling + * [method@Json.Path.match]. + * + * Return value: `TRUE` if the compilation was successful, and `FALSE` + * otherwise + * + * Since: 0.14 + */ +gboolean +json_path_compile (JsonPath *path, + const char *expression, + GError **error) +{ + const char *p, *end_p; + PathNode *root = NULL; + GList *nodes = NULL; + + g_return_val_if_fail (expression != NULL, FALSE); + + p = expression; + + while (*p != '\0') + { + switch (*p) + { + case '$': + { + PathNode *node; + + if (root != NULL) + { + g_set_error_literal (error, JSON_PATH_ERROR, + JSON_PATH_ERROR_INVALID_QUERY, + _("Only one root node is allowed in a JSONPath expression")); + return FALSE; + } + + if (!(*(p + 1) == '.' || *(p + 1) == '[' || *(p + 1) == '\0')) + { + g_set_error (error, JSON_PATH_ERROR, + JSON_PATH_ERROR_INVALID_QUERY, + /* translators: the %c is the invalid character */ + _("Root node followed by invalid character “%c”"), + *(p + 1)); + return FALSE; + } + + node = g_new0 (PathNode, 1); + node->node_type = JSON_PATH_NODE_ROOT; + + root = node; + nodes = g_list_prepend (NULL, root); + } + break; + + case '.': + case '[': + { + PathNode *node = NULL; + + if (*p == '.' && *(p + 1) == '.') + { + node = g_new0 (PathNode, 1); + node->node_type = JSON_PATH_NODE_RECURSIVE_DESCENT; + } + else if (*p == '.' && *(p + 1) == '*') + { + node = g_new0 (PathNode, 1); + node->node_type = JSON_PATH_NODE_WILDCARD_MEMBER; + + p += 1; + } + else if (*p == '.') + { + end_p = p + 1; + while (!(*end_p == '.' || *end_p == '[' || *end_p == '\0')) + end_p += 1; + + if (end_p == p + 1) + { + g_set_error_literal (error, JSON_PATH_ERROR, + JSON_PATH_ERROR_INVALID_QUERY, + _("Missing member name or wildcard after . character")); + goto fail; + } + + node = g_new0 (PathNode, 1); + node->node_type = JSON_PATH_NODE_CHILD_MEMBER; + node->data.member_name = g_strndup (p + 1, end_p - p - 1); + + p = end_p - 1; + } + else if (*p == '[' && *(p + 1) == '\'') + { + if (*(p + 2) == '*' && *(p + 3) == '\'' && *(p + 4) == ']') + { + node = g_new0 (PathNode, 1); + node->node_type = JSON_PATH_NODE_WILDCARD_MEMBER; + + p += 4; + } + else + { + node = g_new0 (PathNode, 1); + node->node_type = JSON_PATH_NODE_CHILD_MEMBER; + + end_p = strchr (p + 2, '\''); + node->data.member_name = g_strndup (p + 2, end_p - p - 2); + + p = end_p + 1; + } + } + else if (*p == '[' && *(p + 1) == '*' && *(p + 2) == ']') + { + node = g_new0 (PathNode, 1); + node->node_type = JSON_PATH_NODE_WILDCARD_ELEMENT; + + p += 1; + } + else if (*p == '[') + { + int sign = 1; + int idx; + + end_p = p + 1; + + if (*end_p == '-') + { + sign = -1; + end_p += 1; + } + + /* slice with missing start */ + if (*end_p == ':') + { + int slice_end = g_ascii_strtoll (end_p + 1, (char **) &end_p, 10) * sign; + int slice_step = 1; + + if (*end_p == ':') + { + end_p += 1; + + if (*end_p == '-') + { + sign = -1; + end_p += 1; + } + else + sign = 1; + + slice_step = g_ascii_strtoll (end_p, (char **) &end_p, 10) * sign; + + if (*end_p != ']') + { + g_set_error (error, JSON_PATH_ERROR, + JSON_PATH_ERROR_INVALID_QUERY, + _("Malformed slice expression “%*s”"), + (int)(end_p - p), + p + 1); + goto fail; + } + } + + node = g_new0 (PathNode, 1); + node->node_type = JSON_PATH_NODE_ELEMENT_SLICE; + node->data.slice.start = 0; + node->data.slice.end = slice_end; + node->data.slice.step = slice_step; + + nodes = g_list_prepend (nodes, node); + p = end_p; + break; + } + + idx = g_ascii_strtoll (end_p, (char **) &end_p, 10) * sign; + + if (*end_p == ',') + { + GArray *indices = g_array_new (FALSE, TRUE, sizeof (int)); + + g_array_append_val (indices, idx); + + while (*end_p != ']') + { + end_p += 1; + + if (*end_p == '-') + { + sign = -1; + end_p += 1; + } + else + sign = 1; + + idx = g_ascii_strtoll (end_p, (char **) &end_p, 10) * sign; + if (!(*end_p == ',' || *end_p == ']')) + { + g_array_unref (indices); + g_set_error (error, JSON_PATH_ERROR, + JSON_PATH_ERROR_INVALID_QUERY, + _("Invalid set definition “%*s”"), + (int)(end_p - p), + p + 1); + goto fail; + } + + g_array_append_val (indices, idx); + } + + node = g_new0 (PathNode, 1); + node->node_type = JSON_PATH_NODE_ELEMENT_SET; + node->data.set.n_indices = indices->len; + node->data.set.indices = (int *) g_array_free (indices, FALSE); + nodes = g_list_prepend (nodes, node); + p = end_p; + break; + } + else if (*end_p == ':') + { + int slice_start = idx; + int slice_end = 0; + int slice_step = 1; + + end_p += 1; + + if (*end_p == '-') + { + sign = -1; + end_p += 1; + } + else + sign = 1; + + slice_end = g_ascii_strtoll (end_p, (char **) &end_p, 10) * sign; + if (*end_p == ':') + { + end_p += 1; + + if (*end_p == '-') + { + sign = -1; + end_p += 1; + } + else + sign = 1; + + slice_step = g_ascii_strtoll (end_p + 1, (char **) &end_p, 10) * sign; + } + + if (*end_p != ']') + { + g_set_error (error, JSON_PATH_ERROR, + JSON_PATH_ERROR_INVALID_QUERY, + _("Invalid slice definition “%*s”"), + (int)(end_p - p), + p + 1); + goto fail; + } + + node = g_new0 (PathNode, 1); + node->node_type = JSON_PATH_NODE_ELEMENT_SLICE; + node->data.slice.start = slice_start; + node->data.slice.end = slice_end; + node->data.slice.step = slice_step; + nodes = g_list_prepend (nodes, node); + p = end_p; + break; + } + else if (*end_p == ']') + { + node = g_new0 (PathNode, 1); + node->node_type = JSON_PATH_NODE_CHILD_ELEMENT; + node->data.element_index = idx; + nodes = g_list_prepend (nodes, node); + p = end_p; + break; + } + else + { + g_set_error (error, JSON_PATH_ERROR, + JSON_PATH_ERROR_INVALID_QUERY, + _("Invalid array index definition “%*s”"), + (int)(end_p - p), + p + 1); + goto fail; + } + } + else + break; + + if (node != NULL) + nodes = g_list_prepend (nodes, node); + } + break; + + default: + if (nodes == NULL) + { + g_set_error(error, JSON_PATH_ERROR, + JSON_PATH_ERROR_INVALID_QUERY, + _("Invalid first character “%c”"), + *p); + return FALSE; + } + break; + } + + p += 1; + } + + nodes = g_list_reverse (nodes); + +#ifdef JSON_ENABLE_DEBUG + if (JSON_HAS_DEBUG (PATH)) + { + GString *buf = g_string_new (NULL); + + g_list_foreach (nodes, json_path_foreach_print, buf); + + g_message ("[PATH] " G_STRLOC ": expression '%s' => '%s'", expression, buf->str); + g_string_free (buf, TRUE); + } +#endif /* JSON_ENABLE_DEBUG */ + + g_list_free_full (path->nodes, path_node_free); + + path->nodes = nodes; + path->is_compiled = (path->nodes != NULL); + + return path->nodes != NULL; + +fail: + g_list_free_full (nodes, path_node_free); + + return FALSE; +} + +static void +walk_path_node (GList *path, + JsonNode *root, + JsonArray *results) +{ + PathNode *node = path->data; + + switch (node->node_type) + { + case JSON_PATH_NODE_ROOT: + if (path->next != NULL) + walk_path_node (path->next, root, results); + else + json_array_add_element (results, json_node_copy (root)); + break; + + case JSON_PATH_NODE_CHILD_MEMBER: + if (JSON_NODE_HOLDS_OBJECT (root)) + { + JsonObject *object = json_node_get_object (root); + + if (json_object_has_member (object, node->data.member_name)) + { + JsonNode *member = json_object_get_member (object, node->data.member_name); + + if (path->next == NULL) + { + JSON_NOTE (PATH, "end of path at member '%s'", node->data.member_name); + json_array_add_element (results, json_node_copy (member)); + } + else + walk_path_node (path->next, member, results); + } + } + break; + + case JSON_PATH_NODE_CHILD_ELEMENT: + if (JSON_NODE_HOLDS_ARRAY (root)) + { + JsonArray *array = json_node_get_array (root); + + if (json_array_get_length (array) >= node->data.element_index) + { + JsonNode *element = json_array_get_element (array, node->data.element_index); + + if (path->next == NULL) + { + JSON_NOTE (PATH, "end of path at element '%d'", node->data.element_index); + json_array_add_element (results, json_node_copy (element)); + } + else + walk_path_node (path->next, element, results); + } + } + break; + + case JSON_PATH_NODE_RECURSIVE_DESCENT: + { + PathNode *tmp = path->next->data; + + switch (json_node_get_node_type (root)) + { + case JSON_NODE_OBJECT: + { + JsonObject *object = json_node_get_object (root); + GQueue *members = json_object_get_members_internal (object); + GList *l; + + for (l = members->head; l != NULL; l = l->next) + { + JsonNode *m = json_object_get_member (object, l->data); + + if (tmp->node_type == JSON_PATH_NODE_CHILD_MEMBER && + strcmp (tmp->data.member_name, l->data) == 0) + { + JSON_NOTE (PATH, "entering '%s'", tmp->data.member_name); + walk_path_node (path->next, root, results); + } + else + { + JSON_NOTE (PATH, "recursing into '%s'", (char *) l->data); + walk_path_node (path, m, results); + } + } + } + break; + + case JSON_NODE_ARRAY: + { + JsonArray *array = json_node_get_array (root); + GList *members, *l; + guint i; + + members = json_array_get_elements (array); + for (l = members, i = 0; l != NULL; l = l->next, i += 1) + { + JsonNode *m = l->data; + + if (tmp->node_type == JSON_PATH_NODE_CHILD_ELEMENT && + tmp->data.element_index == i) + { + JSON_NOTE (PATH, "entering '%d'", tmp->data.element_index); + walk_path_node (path->next, root, results); + } + else + { + JSON_NOTE (PATH, "recursing into '%d'", i); + walk_path_node (path, m, results); + } + } + g_list_free (members); + } + break; + + default: + break; + } + } + break; + + case JSON_PATH_NODE_WILDCARD_MEMBER: + if (JSON_NODE_HOLDS_OBJECT (root)) + { + JsonObject *object = json_node_get_object (root); + GQueue *members = json_object_get_members_internal (object); + GList *l; + + for (l = members->head; l != NULL; l = l->next) + { + JsonNode *member = json_object_get_member (object, l->data); + + if (path->next != NULL) + walk_path_node (path->next, member, results); + else + { + JSON_NOTE (PATH, "glob match member '%s'", (char *) l->data); + json_array_add_element (results, json_node_copy (member)); + } + } + } + else + json_array_add_element (results, json_node_copy (root)); + break; + + case JSON_PATH_NODE_WILDCARD_ELEMENT: + if (JSON_NODE_HOLDS_ARRAY (root)) + { + JsonArray *array = json_node_get_array (root); + GList *elements, *l; + int i; + + elements = json_array_get_elements (array); + for (l = elements, i = 0; l != NULL; l = l->next, i += 1) + { + JsonNode *element = l->data; + + if (path->next != NULL) + walk_path_node (path->next, element, results); + else + { + JSON_NOTE (PATH, "glob match element '%d'", i); + json_array_add_element (results, json_node_copy (element)); + } + } + g_list_free (elements); + } + else + json_array_add_element (results, json_node_copy (root)); + break; + + case JSON_PATH_NODE_ELEMENT_SET: + if (JSON_NODE_HOLDS_ARRAY (root)) + { + JsonArray *array = json_node_get_array (root); + int i; + + for (i = 0; i < node->data.set.n_indices; i += 1) + { + int idx = node->data.set.indices[i]; + JsonNode *element = json_array_get_element (array, idx); + + if (path->next != NULL) + walk_path_node (path->next, element, results); + else + { + JSON_NOTE (PATH, "set element '%d'", idx); + json_array_add_element (results, json_node_copy (element)); + } + } + } + break; + + case JSON_PATH_NODE_ELEMENT_SLICE: + if (JSON_NODE_HOLDS_ARRAY (root)) + { + JsonArray *array = json_node_get_array (root); + int i, start, end; + + if (node->data.slice.start < 0) + { + start = json_array_get_length (array) + + node->data.slice.start; + + end = json_array_get_length (array) + + node->data.slice.end; + } + else + { + start = node->data.slice.start; + end = node->data.slice.end; + } + + for (i = start; i < end; i += node->data.slice.step) + { + JsonNode *element = json_array_get_element (array, i); + + if (path->next != NULL) + walk_path_node (path->next, element, results); + else + { + JSON_NOTE (PATH, "slice element '%d'", i); + json_array_add_element (results, json_node_copy (element)); + } + } + } + break; + + default: + break; + } +} + +/** + * json_path_match: + * @path: a compiled path + * @root: the root node of the JSON data to match + * + * Matches the JSON tree pointed by `root` using the expression compiled + * into the `JsonPath`. + * + * The nodes matching the expression will be copied into an array. + * + * Return value: (transfer full): a newly-created node of type + * `JSON_NODE_ARRAY` containing the array of matching nodes + * + * Since: 0.14 + */ +JsonNode * +json_path_match (JsonPath *path, + JsonNode *root) +{ + JsonArray *results; + JsonNode *retval; + + g_return_val_if_fail (JSON_IS_PATH (path), NULL); + g_return_val_if_fail (path->is_compiled, NULL); + g_return_val_if_fail (root != NULL, NULL); + + results = json_array_new (); + + walk_path_node (path->nodes, root, results); + + retval = json_node_new (JSON_NODE_ARRAY); + json_node_take_array (retval, results); + + return retval; +} + +/** + * json_path_query: + * @expression: a JSONPath expression + * @root: the root of a JSON tree + * @error: return location for a #GError, or %NULL + * + * Queries a JSON tree using a JSONPath expression. + * + * This function is a simple wrapper around [ctor@Json.Path.new], + * [method@Json.Path.compile], and [method@Json.Path.match]. It implicitly + * creates a `JsonPath` instance, compiles the given expression and matches + * it against the JSON tree pointed by `root`. + * + * Return value: (transfer full): a newly-created node of type + * `JSON_NODE_ARRAY` containing the array of matching nodes + * + * Since: 0.14 + */ +JsonNode * +json_path_query (const char *expression, + JsonNode *root, + GError **error) +{ + JsonPath *path = json_path_new (); + JsonNode *retval; + + if (!json_path_compile (path, expression, error)) + { + g_object_unref (path); + return NULL; + } + + retval = json_path_match (path, root); + + g_object_unref (path); + + return retval; +} diff --git a/lsp/deps/json-glib/json-path.h b/lsp/deps/json-glib/json-path.h new file mode 100644 index 000000000..150be83f5 --- /dev/null +++ b/lsp/deps/json-glib/json-path.h @@ -0,0 +1,88 @@ +/* json-path.h - JSONPath implementation + * + * This file is part of JSON-GLib + * Copyright © 2011 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +#pragma once + +#if !defined(__JSON_GLIB_INSIDE__) && !defined(JSON_COMPILATION) +#error "Only can be included directly." +#endif + +#include + +G_BEGIN_DECLS + +#define JSON_TYPE_PATH (json_path_get_type ()) +#define JSON_PATH(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), JSON_TYPE_PATH, JsonPath)) +#define JSON_IS_PATH(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), JSON_TYPE_PATH)) + +/** + * JSON_PATH_ERROR: + * + * Error domain for `JsonPath`. + * + * Since: 0.14 + */ +#define JSON_PATH_ERROR (json_path_error_quark ()) + +/** + * JsonPathError: + * @JSON_PATH_ERROR_INVALID_QUERY: Invalid query + * + * Error codes for `JSON_PATH_ERROR`. + * + * This enumeration can be extended at later date + * + * Since: 0.14 + */ +typedef enum { + JSON_PATH_ERROR_INVALID_QUERY +} JsonPathError; + +typedef struct _JsonPath JsonPath; +typedef struct _JsonPathClass JsonPathClass; + +JSON_AVAILABLE_IN_1_0 +GType json_path_get_type (void) G_GNUC_CONST; +JSON_AVAILABLE_IN_1_0 +GQuark json_path_error_quark (void); + +JSON_AVAILABLE_IN_1_0 +JsonPath * json_path_new (void); + +JSON_AVAILABLE_IN_1_0 +gboolean json_path_compile (JsonPath *path, + const char *expression, + GError **error); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_path_match (JsonPath *path, + JsonNode *root); + +JSON_AVAILABLE_IN_1_0 +JsonNode * json_path_query (const char *expression, + JsonNode *root, + GError **error); + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC (JsonPath, g_object_unref) +#endif + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-reader.c b/lsp/deps/json-glib/json-reader.c new file mode 100644 index 000000000..3e588536f --- /dev/null +++ b/lsp/deps/json-glib/json-reader.c @@ -0,0 +1,1138 @@ +/* json-reader.h - JSON cursor parser + * + * This file is part of JSON-GLib + * Copyright (C) 2010 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +/** + * JsonReader: + * + * `JsonReader` provides a simple, cursor-based API for parsing a JSON DOM. + * + * It is similar, in spirit, to the XML Reader API. + * + * ## Using `JsonReader` + * + * ```c + * g_autoptr(JsonParser) parser = json_parser_new (); + * + * // str is defined elsewhere + * json_parser_load_from_data (parser, str, -1, NULL); + * + * g_autoptr(JsonReader) reader = json_reader_new (json_parser_get_root (parser)); + * + * json_reader_read_member (reader, "url"); + * const char *url = json_reader_get_string_value (reader); + * json_reader_end_member (reader); + * + * json_reader_read_member (reader, "size"); + * json_reader_read_element (reader, 0); + * int width = json_reader_get_int_value (reader); + * json_reader_end_element (reader); + * json_reader_read_element (reader, 1); + * int height = json_reader_get_int_value (reader); + * json_reader_end_element (reader); + * json_reader_end_member (reader); + * ``` + * + * ## Error handling + * + * In case of error, `JsonReader` will be set in an error state; all subsequent + * calls will simply be ignored until a function that resets the error state is + * called, e.g.: + * + * ```c + * // ask for the 7th element; if the element does not exist, the + * // reader will be put in an error state + * json_reader_read_element (reader, 6); + * + * // in case of error, this will return NULL, otherwise it will + * // return the value of the element + * str = json_reader_get_string_value (value); + * + * // this function resets the error state if any was set + * json_reader_end_element (reader); + * ``` + * + * If you want to detect the error state as soon as possible, you can use + * [method@Json.Reader.get_error]: + * + * ```c + * // like the example above, but in this case we print out the + * // error immediately + * if (!json_reader_read_element (reader, 6)) + * { + * const GError *error = json_reader_get_error (reader); + * g_print ("Unable to read the element: %s", error->message); + * } + * ``` + * + * Since: 0.12 + */ + +#include "config.h" + +#include + +#include + +#include "json-reader.h" +#include "json-types-private.h" +#include "json-debug.h" + +#define json_reader_return_if_error_set(r) G_STMT_START { \ + if (((JsonReader *) (r))->priv->error != NULL) \ + return; } G_STMT_END + +#define json_reader_return_val_if_error_set(r,v) G_STMT_START { \ + if (((JsonReader *) (r))->priv->error != NULL) \ + return (v); } G_STMT_END + +struct _JsonReaderPrivate +{ + JsonNode *root; + + JsonNode *current_node; + JsonNode *previous_node; + + /* Stack of member names. */ + GPtrArray *members; + + GError *error; +}; + +enum +{ + PROP_0, + + PROP_ROOT, + + PROP_LAST +}; + +static GParamSpec *reader_properties[PROP_LAST] = { NULL, }; + +G_DEFINE_TYPE_WITH_PRIVATE (JsonReader, json_reader, G_TYPE_OBJECT) + +G_DEFINE_QUARK (json-reader-error-quark, json_reader_error) + +static void +json_reader_finalize (GObject *gobject) +{ + JsonReaderPrivate *priv = JSON_READER (gobject)->priv; + + if (priv->root != NULL) + json_node_unref (priv->root); + + if (priv->error != NULL) + g_clear_error (&priv->error); + + if (priv->members != NULL) + g_ptr_array_unref (priv->members); + + G_OBJECT_CLASS (json_reader_parent_class)->finalize (gobject); +} + +static void +json_reader_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) + { + case PROP_ROOT: + json_reader_set_root (JSON_READER (gobject), g_value_get_boxed (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +json_reader_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) + { + case PROP_ROOT: + g_value_set_boxed (value, JSON_READER (gobject)->priv->root); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +json_reader_class_init (JsonReaderClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + /** + * JsonReader:root: (attributes org.gtk.Property.set=json_reader_set_root) + * + * The root of the JSON tree that the reader should read. + * + * Since: 0.12 + */ + reader_properties[PROP_ROOT] = + g_param_spec_boxed ("root", + "Root Node", + "The root of the tree to read", + JSON_TYPE_NODE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS); + + gobject_class->finalize = json_reader_finalize; + gobject_class->set_property = json_reader_set_property; + gobject_class->get_property = json_reader_get_property; + g_object_class_install_properties (gobject_class, PROP_LAST, reader_properties); +} + +static void +json_reader_init (JsonReader *self) +{ + self->priv = json_reader_get_instance_private (self); + self->priv->members = g_ptr_array_new_with_free_func (g_free); +} + +/** + * json_reader_new: + * @node: (nullable): the root node + * + * Creates a new reader. + * + * You can use this object to read the contents of the JSON tree starting + * from the given node. + * + * Return value: the newly created reader + * + * Since: 0.12 + */ +JsonReader * +json_reader_new (JsonNode *node) +{ + return g_object_new (JSON_TYPE_READER, "root", node, NULL); +} + +/* + * json_reader_unset_error: + * @reader: a reader + * + * Unsets the error state of @reader, if set + * + * Return value: TRUE if an error was set. + */ +static inline gboolean +json_reader_unset_error (JsonReader *reader) +{ + if (reader->priv->error != NULL) + { + g_clear_error (&(reader->priv->error)); + return TRUE; + } + return FALSE; +} + +/** + * json_reader_set_root: (attributes org.gtk.Method.set_property=root) + * @reader: a reader + * @root: (nullable): the root node + * + * Sets the root node of the JSON tree to be read by @reader. + * + * The reader will take a copy of the node. + * + * Since: 0.12 + */ +void +json_reader_set_root (JsonReader *reader, + JsonNode *root) +{ + JsonReaderPrivate *priv; + + g_return_if_fail (JSON_IS_READER (reader)); + + priv = reader->priv; + + if (priv->root == root) + return; + + if (priv->root != NULL) + { + json_node_unref (priv->root); + priv->root = NULL; + priv->current_node = NULL; + priv->previous_node = NULL; + } + + if (root != NULL) + { + priv->root = json_node_copy (root); + priv->current_node = priv->root; + priv->previous_node = NULL; + } + + g_object_notify_by_pspec (G_OBJECT (reader), reader_properties[PROP_ROOT]); +} + +/* + * json_reader_set_error: + * @reader: a reader + * @error_code: the [error@Json.ReaderError] code for the error + * @error_fmt: format string + * @Varargs: list of arguments for the `error_fmt` string + * + * Sets the error state of the reader using the given error code + * and string. + * + * Return value: `FALSE`, to be used to return immediately from + * the caller function + */ +G_GNUC_PRINTF (3, 4) +static gboolean +json_reader_set_error (JsonReader *reader, + JsonReaderError error_code, + const gchar *error_fmt, + ...) +{ + JsonReaderPrivate *priv = reader->priv; + va_list args; + gchar *error_msg; + + if (priv->error != NULL) + g_clear_error (&priv->error); + + va_start (args, error_fmt); + error_msg = g_strdup_vprintf (error_fmt, args); + va_end (args); + + g_set_error_literal (&priv->error, JSON_READER_ERROR, + error_code, + error_msg); + + g_free (error_msg); + + return FALSE; +} + +/** + * json_reader_get_error: + * @reader: a reader + * + * Retrieves the error currently set on the reader. + * + * Return value: (nullable) (transfer none): the current error + * + * Since: 0.12 + */ +const GError * +json_reader_get_error (JsonReader *reader) +{ + g_return_val_if_fail (JSON_IS_READER (reader), NULL); + + return reader->priv->error; +} + +/** + * json_reader_is_array: + * @reader: a reader + * + * Checks whether the reader is currently on an array. + * + * Return value: `TRUE` if the reader is on an array + * + * Since: 0.12 + */ +gboolean +json_reader_is_array (JsonReader *reader) +{ + g_return_val_if_fail (JSON_IS_READER (reader), FALSE); + json_reader_return_val_if_error_set (reader, FALSE); + + if (reader->priv->current_node == NULL) + return FALSE; + + return JSON_NODE_HOLDS_ARRAY (reader->priv->current_node); +} + +/** + * json_reader_is_object: + * @reader: a reader + * + * Checks whether the reader is currently on an object. + * + * Return value: `TRUE` if the reader is on an object + * + * Since: 0.12 + */ +gboolean +json_reader_is_object (JsonReader *reader) +{ + g_return_val_if_fail (JSON_IS_READER (reader), FALSE); + json_reader_return_val_if_error_set (reader, FALSE); + + if (reader->priv->current_node == NULL) + return FALSE; + + return JSON_NODE_HOLDS_OBJECT (reader->priv->current_node); +} + +/** + * json_reader_is_value: + * @reader: a reader + * + * Checks whether the reader is currently on a value. + * + * Return value: `TRUE` if the reader is on a value + * + * Since: 0.12 + */ +gboolean +json_reader_is_value (JsonReader *reader) +{ + g_return_val_if_fail (JSON_IS_READER (reader), FALSE); + json_reader_return_val_if_error_set (reader, FALSE); + + if (reader->priv->current_node == NULL) + return FALSE; + + return JSON_NODE_HOLDS_VALUE (reader->priv->current_node) || + JSON_NODE_HOLDS_NULL (reader->priv->current_node); +} + +/** + * json_reader_read_element: + * @reader: a reader + * @index_: the index of the element + * + * Advances the cursor of the reader to the element of the array or + * the member of the object at the given position. + * + * You can use [method@Json.Reader.get_value] and its wrapper functions to + * retrieve the value of the element; for instance, the following code will + * read the first element of the array at the current cursor position: + * + * ```c + * json_reader_read_element (reader, 0); + * int_value = json_reader_get_int_value (reader); + * ``` + * + * After reading the value, you should call [method@Json.Reader.end_element] + * to reposition the cursor inside the reader, e.g.: + * + * ```c + * const char *str_value = NULL; + * + * json_reader_read_element (reader, 1); + * str_value = json_reader_get_string_value (reader); + * json_reader_end_element (reader); + * + * json_reader_read_element (reader, 2); + * str_value = json_reader_get_string_value (reader); + * json_reader_end_element (reader); + * ``` + * + * If the reader is not currently on an array or an object, or if the index is + * bigger than the size of the array or the object, the reader will be + * put in an error state until [method@Json.Reader.end_element] is called. This + * means that, if used conditionally, [method@Json.Reader.end_element] must be + * called on all branches: + * + * ```c + * if (!json_reader_read_element (reader, 1)) + * { + * g_propagate_error (error, json_reader_get_error (reader)); + * json_reader_end_element (reader); + * return FALSE; + * } + * else + * { + * const char *str_value = json_reader_get_string_value (reader); + * json_reader_end_element (reader); + * + * // use str_value + * + * return TRUE; + * } + * ```c + * + * Return value: `TRUE` on success, and `FALSE` otherwise + * + * Since: 0.12 + */ +gboolean +json_reader_read_element (JsonReader *reader, + guint index_) +{ + JsonReaderPrivate *priv; + + g_return_val_if_fail (JSON_READER (reader), FALSE); + json_reader_return_val_if_error_set (reader, FALSE); + + priv = reader->priv; + + if (priv->current_node == NULL) + priv->current_node = priv->root; + + if (!(JSON_NODE_HOLDS_ARRAY (priv->current_node) || + JSON_NODE_HOLDS_OBJECT (priv->current_node))) + return json_reader_set_error (reader, JSON_READER_ERROR_NO_ARRAY, + _("The current node is of type “%s”, but " + "an array or an object was expected."), + json_node_type_name (priv->current_node)); + + switch (json_node_get_node_type (priv->current_node)) + { + case JSON_NODE_ARRAY: + { + JsonArray *array = json_node_get_array (priv->current_node); + + if (index_ >= json_array_get_length (array)) + return json_reader_set_error (reader, JSON_READER_ERROR_INVALID_INDEX, + _("The index “%d” is greater than the size " + "of the array at the current position."), + index_); + + priv->previous_node = priv->current_node; + priv->current_node = json_array_get_element (array, index_); + } + break; + + case JSON_NODE_OBJECT: + { + JsonObject *object = json_node_get_object (priv->current_node); + GQueue *members; + const gchar *name; + + if (index_ >= json_object_get_size (object)) + return json_reader_set_error (reader, JSON_READER_ERROR_INVALID_INDEX, + _("The index “%d” is greater than the size " + "of the object at the current position."), + index_); + + priv->previous_node = priv->current_node; + + members = json_object_get_members_internal (object); + name = g_queue_peek_nth (members, index_); + + priv->current_node = json_object_get_member (object, name); + g_ptr_array_add (priv->members, g_strdup (name)); + } + break; + + default: + g_assert_not_reached (); + return FALSE; + } + + return TRUE; +} + +/** + * json_reader_end_element: + * @reader: a reader + * + * Moves the cursor back to the previous node after being positioned + * inside an array. + * + * This function resets the error state of the reader, if any was set. + * + * Since: 0.12 + */ +void +json_reader_end_element (JsonReader *reader) +{ + JsonReaderPrivate *priv; + JsonNode *tmp; + + g_return_if_fail (JSON_IS_READER (reader)); + + if (json_reader_unset_error (reader)) + return; + + priv = reader->priv; + + if (priv->previous_node != NULL) + tmp = json_node_get_parent (priv->previous_node); + else + tmp = NULL; + + if (json_node_get_node_type (priv->previous_node) == JSON_NODE_OBJECT) + g_ptr_array_remove_index (priv->members, priv->members->len - 1); + + priv->current_node = priv->previous_node; + priv->previous_node = tmp; +} + +/** + * json_reader_count_elements: + * @reader: a reader + * + * Counts the elements of the current position, if the reader is + * positioned on an array. + * + * In case of failure, the reader is set to an error state. + * + * Return value: the number of elements, or -1. + * + * Since: 0.12 + */ +gint +json_reader_count_elements (JsonReader *reader) +{ + JsonReaderPrivate *priv; + + g_return_val_if_fail (JSON_IS_READER (reader), -1); + + priv = reader->priv; + + if (priv->current_node == NULL) + { + json_reader_set_error (reader, JSON_READER_ERROR_INVALID_NODE, + _("No node available at the current position")); + return -1; + } + + if (!JSON_NODE_HOLDS_ARRAY (priv->current_node)) + { + json_reader_set_error (reader, JSON_READER_ERROR_NO_ARRAY, + _("The current position holds a “%s” and not an array"), + json_node_type_get_name (JSON_NODE_TYPE (priv->current_node))); + return -1; + } + + return json_array_get_length (json_node_get_array (priv->current_node)); +} + +/** + * json_reader_read_member: + * @reader: a reader + * @member_name: the name of the member to read + * + * Advances the cursor of the reader to the `member_name` of the object at + * the current position. + * + * You can use [method@Json.Reader.get_value] and its wrapper functions to + * retrieve the value of the member; for instance: + * + * ```c + * json_reader_read_member (reader, "width"); + * width = json_reader_get_int_value (reader); + * ``` + * + * After reading the value, `json_reader_end_member()` should be called to + * reposition the cursor inside the reader, e.g.: + * + * ```c + * json_reader_read_member (reader, "author"); + * author = json_reader_get_string_value (reader); + * json_reader_end_member (reader); + * + * json_reader_read_member (reader, "title"); + * title = json_reader_get_string_value (reader); + * json_reader_end_member (reader); + * ``` + * + * If the reader is not currently on an object, or if the `member_name` is not + * defined in the object, the reader will be put in an error state until + * [method@Json.Reader.end_member] is called. This means that if used + * conditionally, [method@Json.Reader.end_member] must be called on all branches: + * + * ```c + * if (!json_reader_read_member (reader, "title")) + * { + * g_propagate_error (error, json_reader_get_error (reader)); + * json_reader_end_member (reader); + * return FALSE; + * } + * else + * { + * const char *str_value = json_reader_get_string_value (reader); + * json_reader_end_member (reader); + * + * // use str_value + * + * return TRUE; + * } + * ``` + * + * Return value: `TRUE` on success, and `FALSE` otherwise + * + * Since: 0.12 + */ +gboolean +json_reader_read_member (JsonReader *reader, + const gchar *member_name) +{ + JsonReaderPrivate *priv; + JsonObject *object; + + g_return_val_if_fail (JSON_READER (reader), FALSE); + g_return_val_if_fail (member_name != NULL, FALSE); + json_reader_return_val_if_error_set (reader, FALSE); + + priv = reader->priv; + + if (priv->current_node == NULL) + priv->current_node = priv->root; + + if (!JSON_NODE_HOLDS_OBJECT (priv->current_node)) + return json_reader_set_error (reader, JSON_READER_ERROR_NO_OBJECT, + _("The current node is of type “%s”, but " + "an object was expected."), + json_node_type_name (priv->current_node)); + + object = json_node_get_object (priv->current_node); + if (!json_object_has_member (object, member_name)) + return json_reader_set_error (reader, JSON_READER_ERROR_INVALID_MEMBER, + _("The member “%s” is not defined in the " + "object at the current position."), + member_name); + + priv->previous_node = priv->current_node; + priv->current_node = json_object_get_member (object, member_name); + g_ptr_array_add (priv->members, g_strdup (member_name)); + + return TRUE; +} + +/** + * json_reader_end_member: + * @reader: a reader + * + * Moves the cursor back to the previous node after being positioned + * inside an object. + * + * This function resets the error state of the reader, if any was set. + * + * Since: 0.12 + */ +void +json_reader_end_member (JsonReader *reader) +{ + JsonReaderPrivate *priv; + JsonNode *tmp; + + g_return_if_fail (JSON_IS_READER (reader)); + + if (json_reader_unset_error (reader)) + return; + + priv = reader->priv; + + if (priv->previous_node != NULL) + tmp = json_node_get_parent (priv->previous_node); + else + tmp = NULL; + + g_ptr_array_remove_index (priv->members, priv->members->len - 1); + + priv->current_node = priv->previous_node; + priv->previous_node = tmp; +} + +/** + * json_reader_list_members: + * @reader: a reader + * + * Retrieves a list of member names from the current position, if the reader + * is positioned on an object. + * + * In case of failure, the reader is set to an error state. + * + * Return value: (transfer full) (array zero-terminated=1): the members of + * the object + * + * Since: 0.14 + */ +gchar ** +json_reader_list_members (JsonReader *reader) +{ + JsonReaderPrivate *priv; + JsonObject *object; + GQueue *members; + GList *l; + gchar **retval; + gint i; + + g_return_val_if_fail (JSON_IS_READER (reader), NULL); + + priv = reader->priv; + + if (priv->current_node == NULL) + { + json_reader_set_error (reader, JSON_READER_ERROR_INVALID_NODE, + _("No node available at the current position")); + return NULL; + } + + if (!JSON_NODE_HOLDS_OBJECT (priv->current_node)) + { + json_reader_set_error (reader, JSON_READER_ERROR_NO_OBJECT, + _("The current position holds a “%s” and not an object"), + json_node_type_get_name (JSON_NODE_TYPE (priv->current_node))); + return NULL; + } + + object = json_node_get_object (priv->current_node); + members = json_object_get_members_internal (object); + + retval = g_new (gchar*, g_queue_get_length (members) + 1); + for (l = members->head, i = 0; l != NULL; l = l->next, i += 1) + retval[i] = g_strdup (l->data); + + retval[i] = NULL; + + return retval; +} + +/** + * json_reader_count_members: + * @reader: a reader + * + * Counts the members of the current position, if the reader is + * positioned on an object. + * + * In case of failure, the reader is set to an error state. + * + * Return value: the number of members, or -1 + * + * Since: 0.12 + */ +gint +json_reader_count_members (JsonReader *reader) +{ + JsonReaderPrivate *priv; + + g_return_val_if_fail (JSON_IS_READER (reader), -1); + + priv = reader->priv; + + if (priv->current_node == NULL) + { + json_reader_set_error (reader, JSON_READER_ERROR_INVALID_NODE, + _("No node available at the current position")); + return -1; + } + + if (!JSON_NODE_HOLDS_OBJECT (priv->current_node)) + { + json_reader_set_error (reader, JSON_READER_ERROR_NO_OBJECT, + _("The current position holds a “%s” and not an object"), + json_node_type_get_name (JSON_NODE_TYPE (priv->current_node))); + return -1; + } + + return json_object_get_size (json_node_get_object (priv->current_node)); +} + +/** + * json_reader_get_value: + * @reader: a reader + * + * Retrieves the value node at the current position of the reader. + * + * If the current position does not contain a scalar value, the reader + * is set to an error state. + * + * Return value: (nullable) (transfer none): the current value node + * + * Since: 0.12 + */ +JsonNode * +json_reader_get_value (JsonReader *reader) +{ + JsonNode *node; + + g_return_val_if_fail (JSON_IS_READER (reader), NULL); + json_reader_return_val_if_error_set (reader, NULL); + + if (reader->priv->current_node == NULL) + { + json_reader_set_error (reader, JSON_READER_ERROR_INVALID_NODE, + _("No node available at the current position")); + return NULL; + } + + node = reader->priv->current_node; + + if (!JSON_NODE_HOLDS_VALUE (node) && !JSON_NODE_HOLDS_NULL (node)) + { + json_reader_set_error (reader, JSON_READER_ERROR_NO_VALUE, + _("The current position holds a “%s” and not a value"), + json_node_type_get_name (JSON_NODE_TYPE (node))); + return NULL; + } + + return reader->priv->current_node; +} + +/** + * json_reader_get_int_value: + * @reader: a reader + * + * Retrieves the integer value of the current position of the reader. + * + * See also: [method@Json.Reader.get_value] + * + * Return value: the integer value + * + * Since: 0.12 + */ +gint64 +json_reader_get_int_value (JsonReader *reader) +{ + JsonNode *node; + + g_return_val_if_fail (JSON_IS_READER (reader), 0); + json_reader_return_val_if_error_set (reader, 0); + + if (reader->priv->current_node == NULL) + { + json_reader_set_error (reader, JSON_READER_ERROR_INVALID_NODE, + _("No node available at the current position")); + return 0; + } + + node = reader->priv->current_node; + + if (!JSON_NODE_HOLDS_VALUE (node)) + { + json_reader_set_error (reader, JSON_READER_ERROR_NO_VALUE, + _("The current position holds a “%s” and not a value"), + json_node_type_get_name (JSON_NODE_TYPE (node))); + return 0; + } + + return json_node_get_int (reader->priv->current_node); +} + +/** + * json_reader_get_double_value: + * @reader: a reader + * + * Retrieves the floating point value of the current position of the reader. + * + * See also: [method@Json.Reader.get_value] + * + * Return value: the floating point value + * + * Since: 0.12 + */ +gdouble +json_reader_get_double_value (JsonReader *reader) +{ + JsonNode *node; + + g_return_val_if_fail (JSON_IS_READER (reader), 0.0); + json_reader_return_val_if_error_set (reader, 0.0); + + if (reader->priv->current_node == NULL) + { + json_reader_set_error (reader, JSON_READER_ERROR_INVALID_NODE, + _("No node available at the current position")); + return 0.0; + } + + node = reader->priv->current_node; + + if (!JSON_NODE_HOLDS_VALUE (node)) + { + json_reader_set_error (reader, JSON_READER_ERROR_NO_VALUE, + _("The current position holds a “%s” and not a value"), + json_node_type_get_name (JSON_NODE_TYPE (node))); + return 0.0; + } + + return json_node_get_double (reader->priv->current_node); +} + +/** + * json_reader_get_string_value: + * @reader: a reader + * + * Retrieves the string value of the current position of the reader. + * + * See also: [method@Json.Reader.get_value] + * + * Return value: the string value + * + * Since: 0.12 + */ +const gchar * +json_reader_get_string_value (JsonReader *reader) +{ + JsonNode *node; + + g_return_val_if_fail (JSON_IS_READER (reader), NULL); + json_reader_return_val_if_error_set (reader, NULL); + + if (reader->priv->current_node == NULL) + { + json_reader_set_error (reader, JSON_READER_ERROR_INVALID_NODE, + _("No node available at the current position")); + return NULL; + } + + node = reader->priv->current_node; + + if (!JSON_NODE_HOLDS_VALUE (node)) + { + json_reader_set_error (reader, JSON_READER_ERROR_NO_VALUE, + _("The current position holds a “%s” and not a value"), + json_node_type_get_name (JSON_NODE_TYPE (node))); + return NULL; + } + + if (json_node_get_value_type (node) != G_TYPE_STRING) + { + json_reader_set_error (reader, JSON_READER_ERROR_INVALID_TYPE, + _("The current position does not hold a string type")); + return NULL; + } + + return json_node_get_string (reader->priv->current_node); +} + +/** + * json_reader_get_boolean_value: + * @reader: a reader + * + * Retrieves the boolean value of the current position of the reader. + * + * See also: [method@Json.Reader.get_value] + * + * Return value: the boolean value + * + * Since: 0.12 + */ +gboolean +json_reader_get_boolean_value (JsonReader *reader) +{ + JsonNode *node; + + g_return_val_if_fail (JSON_IS_READER (reader), FALSE); + json_reader_return_val_if_error_set (reader, FALSE); + + if (reader->priv->current_node == NULL) + { + json_reader_set_error (reader, JSON_READER_ERROR_INVALID_NODE, + _("No node available at the current position")); + return FALSE; + } + + node = reader->priv->current_node; + + if (!JSON_NODE_HOLDS_VALUE (node)) + { + json_reader_set_error (reader, JSON_READER_ERROR_NO_VALUE, + _("The current position holds a “%s” and not a value"), + json_node_type_get_name (JSON_NODE_TYPE (node))); + return FALSE; + } + + return json_node_get_boolean (node); +} + +/** + * json_reader_get_null_value: + * @reader: a reader + * + * Checks whether the value of the current position of the reader is `null`. + * + * See also: [method@Json.Reader.get_value] + * + * Return value: `TRUE` if `null` is set, and `FALSE` otherwise + * + * Since: 0.12 + */ +gboolean +json_reader_get_null_value (JsonReader *reader) +{ + g_return_val_if_fail (JSON_IS_READER (reader), FALSE); + json_reader_return_val_if_error_set (reader, FALSE); + + if (reader->priv->current_node == NULL) + { + json_reader_set_error (reader, JSON_READER_ERROR_INVALID_NODE, + _("No node available at the current position")); + return FALSE; + } + + return JSON_NODE_HOLDS_NULL (reader->priv->current_node); +} + +/** + * json_reader_get_member_name: + * @reader: a reader + * + * Retrieves the name of the current member. + * + * In case of failure, the reader is set to an error state. + * + * Return value: (nullable) (transfer none): the name of the member + * + * Since: 0.14 + */ +const gchar * +json_reader_get_member_name (JsonReader *reader) +{ + g_return_val_if_fail (JSON_IS_READER (reader), NULL); + json_reader_return_val_if_error_set (reader, NULL); + + if (reader->priv->current_node == NULL) + { + json_reader_set_error (reader, JSON_READER_ERROR_INVALID_NODE, + _("No node available at the current position")); + return NULL; + } + + if (reader->priv->members->len == 0) + return NULL; + + return g_ptr_array_index (reader->priv->members, + reader->priv->members->len - 1); +} + +/** + * json_reader_get_current_node: + * @reader: a reader + * + * Retrieves the reader node at the current position. + * + * Return value: (nullable) (transfer none): the current node of the reader + * + * Since: 1.8 + */ +JsonNode * +json_reader_get_current_node (JsonReader *reader) +{ + g_return_val_if_fail (JSON_IS_READER (reader), NULL); + json_reader_return_val_if_error_set (reader, NULL); + + return reader->priv->current_node; +} diff --git a/lsp/deps/json-glib/json-reader.h b/lsp/deps/json-glib/json-reader.h new file mode 100644 index 000000000..b394f8be1 --- /dev/null +++ b/lsp/deps/json-glib/json-reader.h @@ -0,0 +1,161 @@ +/* json-reader.h - JSON cursor parser + * + * This file is part of JSON-GLib + * Copyright (C) 2010 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +#pragma once + +#if !defined(__JSON_GLIB_INSIDE__) && !defined(JSON_COMPILATION) +#error "Only can be included directly." +#endif + +#include + +G_BEGIN_DECLS + +#define JSON_TYPE_READER (json_reader_get_type ()) +#define JSON_READER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), JSON_TYPE_READER, JsonReader)) +#define JSON_IS_READER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), JSON_TYPE_READER)) +#define JSON_READER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), JSON_TYPE_READER, JsonReaderClass)) +#define JSON_IS_READER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), JSON_TYPE_READER)) +#define JSON_READER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), JSON_TYPE_READER, JsonReaderClass)) + +/** + * JSON_READER_ERROR: + * + * Error domain for `JsonReader`. + * + * Since: 0.12 + */ +#define JSON_READER_ERROR (json_reader_error_quark ()) + +typedef struct _JsonReader JsonReader; +typedef struct _JsonReaderPrivate JsonReaderPrivate; +typedef struct _JsonReaderClass JsonReaderClass; + +/** + * JsonReaderError: + * @JSON_READER_ERROR_NO_ARRAY: No array found at the current position + * @JSON_READER_ERROR_INVALID_INDEX: Index out of bounds + * @JSON_READER_ERROR_NO_OBJECT: No object found at the current position + * @JSON_READER_ERROR_INVALID_MEMBER: Member not found + * @JSON_READER_ERROR_INVALID_NODE: No valid node found at the current position + * @JSON_READER_ERROR_NO_VALUE: The node at the current position does not + * hold a value + * @JSON_READER_ERROR_INVALID_TYPE: The node at the current position does not + * hold a value of the desired type + * + * Error codes for `JSON_READER_ERROR`. + * + * This enumeration can be extended at later date + * + * Since: 0.12 + */ +typedef enum { + JSON_READER_ERROR_NO_ARRAY, + JSON_READER_ERROR_INVALID_INDEX, + JSON_READER_ERROR_NO_OBJECT, + JSON_READER_ERROR_INVALID_MEMBER, + JSON_READER_ERROR_INVALID_NODE, + JSON_READER_ERROR_NO_VALUE, + JSON_READER_ERROR_INVALID_TYPE +} JsonReaderError; + +struct _JsonReader +{ + /*< private >*/ + GObject parent_instance; + + JsonReaderPrivate *priv; +}; + +struct _JsonReaderClass +{ + /*< private >*/ + GObjectClass parent_class; + + void (*_json_padding0) (void); + void (*_json_padding1) (void); + void (*_json_padding2) (void); + void (*_json_padding3) (void); + void (*_json_padding4) (void); +}; + +JSON_AVAILABLE_IN_1_0 +GQuark json_reader_error_quark (void); +JSON_AVAILABLE_IN_1_0 +GType json_reader_get_type (void) G_GNUC_CONST; + +JSON_AVAILABLE_IN_1_0 +JsonReader * json_reader_new (JsonNode *node); + +JSON_AVAILABLE_IN_1_0 +void json_reader_set_root (JsonReader *reader, + JsonNode *root); + +JSON_AVAILABLE_IN_1_0 +const GError * json_reader_get_error (JsonReader *reader); + +JSON_AVAILABLE_IN_1_0 +gboolean json_reader_is_array (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +gboolean json_reader_read_element (JsonReader *reader, + guint index_); +JSON_AVAILABLE_IN_1_0 +void json_reader_end_element (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +gint json_reader_count_elements (JsonReader *reader); + +JSON_AVAILABLE_IN_1_0 +gboolean json_reader_is_object (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +gboolean json_reader_read_member (JsonReader *reader, + const gchar *member_name); +JSON_AVAILABLE_IN_1_0 +void json_reader_end_member (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +gint json_reader_count_members (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +gchar ** json_reader_list_members (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +const gchar * json_reader_get_member_name (JsonReader *reader); + +JSON_AVAILABLE_IN_1_0 +gboolean json_reader_is_value (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_reader_get_value (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +gint64 json_reader_get_int_value (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +gdouble json_reader_get_double_value (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +const gchar * json_reader_get_string_value (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +gboolean json_reader_get_boolean_value (JsonReader *reader); +JSON_AVAILABLE_IN_1_0 +gboolean json_reader_get_null_value (JsonReader *reader); +JSON_AVAILABLE_IN_1_8 +JsonNode * json_reader_get_current_node (JsonReader *reader); + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC (JsonReader, g_object_unref) +#endif + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-scanner.c b/lsp/deps/json-glib/json-scanner.c new file mode 100644 index 000000000..9bbaa241b --- /dev/null +++ b/lsp/deps/json-glib/json-scanner.c @@ -0,0 +1,1371 @@ +/* json-scanner.c: Tokenizer for JSON + * Copyright (C) 2008 OpenedHand + * + * Based on JsonScanner: Flexible lexical scanner for general purpose. + * Copyright (C) 1997, 1998 Tim Janik + * + * Modified by Emmanuele Bassi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include "config.h" + +#include "json-scanner.h" + +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_UNISTD_H +#include +#endif + +#include +#include + +typedef enum +{ + JSON_ERROR_TYPE_UNKNOWN, + JSON_ERROR_TYPE_UNEXP_EOF, + JSON_ERROR_TYPE_UNEXP_EOF_IN_STRING, + JSON_ERROR_TYPE_UNEXP_EOF_IN_COMMENT, + JSON_ERROR_TYPE_NON_DIGIT_IN_CONST, + JSON_ERROR_TYPE_DIGIT_RADIX, + JSON_ERROR_TYPE_FLOAT_RADIX, + JSON_ERROR_TYPE_FLOAT_MALFORMED, + JSON_ERROR_TYPE_MALFORMED_SURROGATE_PAIR, + JSON_ERROR_TYPE_LEADING_ZERO, + JSON_ERROR_TYPE_UNESCAPED_CTRL, + JSON_ERROR_TYPE_MALFORMED_UNICODE +} JsonErrorType; + +typedef struct +{ + const char *cset_skip_characters; + const char *cset_identifier_first; + const char *cset_identifier_nth; + const char *cpair_comment_single; + bool strict; +} JsonScannerConfig; + +typedef union +{ + gpointer v_symbol; + char *v_identifier; + gint64 v_int64; + double v_float; + char *v_string; + unsigned int v_error; +} JsonTokenValue; + +/*< private > + * JsonScanner: + * + * Tokenizer scanner for JSON. See #GScanner + * + * Since: 0.6 + */ +struct _JsonScanner +{ + /* name of input stream, featured by the default message handler */ + const char *input_name; + + /* link into the scanner configuration */ + JsonScannerConfig config; + + /* fields filled in after json_scanner_get_next_token() */ + unsigned int token; + JsonTokenValue value; + unsigned int line; + unsigned int position; + + /* fields filled in after json_scanner_peek_next_token() */ + unsigned int next_token; + JsonTokenValue next_value; + unsigned int next_line; + unsigned int next_position; + + /* to be considered private */ + const char *text; + const char *text_end; + char *buffer; + + /* handler function for _warn and _error */ + JsonScannerMsgFunc msg_handler; + gpointer user_data; +}; + +#define to_lower(c) ( \ + (guchar) ( \ + ( (((guchar)(c))>='A' && ((guchar)(c))<='Z') * ('a'-'A') ) | \ + ( (((guchar)(c))>=192 && ((guchar)(c))<=214) * (224-192) ) | \ + ( (((guchar)(c))>=216 && ((guchar)(c))<=222) * (248-216) ) | \ + ((guchar)(c)) \ + ) \ +) + +static const gchar json_symbol_names[] = + "true\0" + "false\0" + "null\0" + "var\0"; + +static const struct +{ + guint name_offset; + guint token; +} json_symbols[] = { + { 0, JSON_TOKEN_TRUE }, + { 5, JSON_TOKEN_FALSE }, + { 11, JSON_TOKEN_NULL }, + { 16, JSON_TOKEN_VAR } +}; + +static void json_scanner_get_token_ll (JsonScanner *scanner, + unsigned int *token_p, + JsonTokenValue *value_p, + guint *line_p, + guint *position_p); +static void json_scanner_get_token_i (JsonScanner *scanner, + unsigned int *token_p, + JsonTokenValue *value_p, + guint *line_p, + guint *position_p); + +static guchar json_scanner_peek_next_char (JsonScanner *scanner); +static guchar json_scanner_get_char (JsonScanner *scanner, + guint *line_p, + guint *position_p); +static bool json_scanner_get_unichar (JsonScanner *scanner, + gunichar *ucs, + guint *line_p, + guint *position_p); +static void json_scanner_error (JsonScanner *scanner, + const char *format, + ...) G_GNUC_PRINTF (2,3); + +static inline gint +json_scanner_char_2_num (guchar c, + guchar base) +{ + if (c >= '0' && c <= '9') + c -= '0'; + else if (c >= 'A' && c <= 'Z') + c -= 'A' - 10; + else if (c >= 'a' && c <= 'z') + c -= 'a' - 10; + else + return -1; + + if (c < base) + return c; + + return -1; +} + +JsonScanner * +json_scanner_new (bool strict) +{ + JsonScanner *scanner; + + scanner = g_new0 (JsonScanner, 1); + + scanner->config = (JsonScannerConfig) { + // Skip whitespace + .cset_skip_characters = ( " \t\r\n" ), + + // Identifiers can only be lower case + .cset_identifier_first = ( + G_CSET_a_2_z + ), + .cset_identifier_nth = ( + G_CSET_a_2_z + ), + + // Only used if strict = false + .cpair_comment_single = ( "//\n" ), + .strict = strict, + }; + + scanner->token = JSON_TOKEN_NONE; + scanner->value.v_int64 = 0; + scanner->line = 1; + scanner->position = 0; + + scanner->next_token = JSON_TOKEN_NONE; + scanner->next_value.v_int64 = 0; + scanner->next_line = 1; + scanner->next_position = 0; + + return scanner; +} + +static inline void +json_scanner_free_value (JsonTokenType *token_p, + JsonTokenValue *value_p) +{ + switch (*token_p) + { + case JSON_TOKEN_STRING: + case JSON_TOKEN_IDENTIFIER: + case JSON_TOKEN_COMMENT_SINGLE: + case JSON_TOKEN_COMMENT_MULTI: + g_free (value_p->v_string); + break; + + default: + break; + } + + *token_p = JSON_TOKEN_NONE; +} + +void +json_scanner_destroy (JsonScanner *scanner) +{ + g_return_if_fail (scanner != NULL); + + json_scanner_free_value (&scanner->token, &scanner->value); + json_scanner_free_value (&scanner->next_token, &scanner->next_value); + + g_free (scanner->buffer); + g_free (scanner); +} + +void +json_scanner_set_msg_handler (JsonScanner *scanner, + JsonScannerMsgFunc msg_handler, + gpointer user_data) +{ + g_return_if_fail (scanner != NULL); + + scanner->msg_handler = msg_handler; + scanner->user_data = user_data; +} + +static void +json_scanner_error (JsonScanner *scanner, + const char *format, + ...) +{ + g_return_if_fail (scanner != NULL); + g_return_if_fail (format != NULL); + + if (scanner->msg_handler) + { + va_list args; + char *string; + + va_start (args, format); + string = g_strdup_vprintf (format, args); + va_end (args); + + scanner->msg_handler (scanner, string, scanner->user_data); + + g_free (string); + } +} + +unsigned int +json_scanner_peek_next_token (JsonScanner *scanner) +{ + g_return_val_if_fail (scanner != NULL, JSON_TOKEN_EOF); + + if (scanner->next_token == JSON_TOKEN_NONE) + { + scanner->next_line = scanner->line; + scanner->next_position = scanner->position; + json_scanner_get_token_i (scanner, + &scanner->next_token, + &scanner->next_value, + &scanner->next_line, + &scanner->next_position); + } + + return scanner->next_token; +} + +unsigned int +json_scanner_get_next_token (JsonScanner *scanner) +{ + g_return_val_if_fail (scanner != NULL, JSON_TOKEN_EOF); + + if (scanner->next_token != JSON_TOKEN_NONE) + { + json_scanner_free_value (&scanner->token, &scanner->value); + + scanner->token = scanner->next_token; + scanner->value = scanner->next_value; + scanner->line = scanner->next_line; + scanner->position = scanner->next_position; + scanner->next_token = JSON_TOKEN_NONE; + } + else + json_scanner_get_token_i (scanner, + &scanner->token, + &scanner->value, + &scanner->line, + &scanner->position); + + return scanner->token; +} + +void +json_scanner_input_text (JsonScanner *scanner, + const char *text, + guint text_len) +{ + g_return_if_fail (scanner != NULL); + if (text_len) + g_return_if_fail (text != NULL); + else + text = NULL; + + scanner->token = JSON_TOKEN_NONE; + scanner->value.v_int64 = 0; + scanner->line = 1; + scanner->position = 0; + scanner->next_token = JSON_TOKEN_NONE; + + scanner->text = text; + scanner->text_end = text + text_len; + + g_clear_pointer (&scanner->buffer, g_free); +} + +static guchar +json_scanner_peek_next_char (JsonScanner *scanner) +{ + if (scanner->text < scanner->text_end) + return *scanner->text; + else + return 0; +} + +static guchar +json_scanner_get_char (JsonScanner *scanner, + guint *line_p, + guint *position_p) +{ + guchar fchar; + + if (scanner->text < scanner->text_end) + fchar = *(scanner->text++); + else + fchar = 0; + + if (fchar == '\n') + { + (*position_p) = 0; + (*line_p)++; + } + else if (fchar) + { + (*position_p)++; + } + + return fchar; +} + +#define is_hex_digit(c) (((c) >= '0' && (c) <= '9') || \ + ((c) >= 'a' && (c) <= 'f') || \ + ((c) >= 'A' && (c) <= 'F')) +#define to_hex_digit(c) (((c) <= '9') ? (c) - '0' : ((c) & 7) + 9) + +static bool +json_scanner_get_unichar (JsonScanner *scanner, + gunichar *ucs, + guint *line_p, + guint *position_p) +{ + gunichar uchar; + + uchar = 0; + for (int i = 0; i < 4; i++) + { + char ch = json_scanner_get_char (scanner, line_p, position_p); + + if (is_hex_digit (ch)) + uchar += ((gunichar) to_hex_digit (ch) << ((3 - i) * 4)); + else + return false; + } + + *ucs = uchar; + + return true; +} + +/* + * decode_utf16_surrogate_pair: + * @units: (array length=2): a pair of UTF-16 code points + * + * Decodes a surrogate pair of UTF-16 code points into the equivalent + * Unicode code point. + * + * Returns: the Unicode code point equivalent to the surrogate pair + */ +static inline gunichar +decode_utf16_surrogate_pair (const gunichar units[2]) +{ + gunichar ucs; + + /* Already checked by caller */ + g_assert (0xd800 <= units[0] && units[0] <= 0xdbff); + g_assert (0xdc00 <= units[1] && units[1] <= 0xdfff); + + ucs = 0x10000; + ucs += (units[0] & 0x3ff) << 10; + ucs += (units[1] & 0x3ff); + + return ucs; +} + +static void +json_scanner_unexp_token (JsonScanner *scanner, + unsigned int expected_token, + const char *identifier_spec, + const char *symbol_spec, + const char *symbol_name, + const char *message) +{ + char *token_string; + gsize token_string_len; + char *expected_string; + gsize expected_string_len; + const char *message_prefix; + bool print_unexp; + + g_return_if_fail (scanner != NULL); + + if (identifier_spec == NULL) + identifier_spec = "identifier"; + if (symbol_spec == NULL) + symbol_spec = "symbol"; + + token_string_len = 56; + token_string = g_new (char, token_string_len + 1); + expected_string_len = 64; + expected_string = g_new (char, expected_string_len + 1); + print_unexp = true; + + switch (scanner->token) + { + case JSON_TOKEN_EOF: + g_snprintf (token_string, token_string_len, "end of file"); + break; + + default: + if (scanner->token >= 1 && scanner->token <= 255) + { + if ((scanner->token >= ' ' && scanner->token <= '~') || + strchr (scanner->config.cset_identifier_first, scanner->token) || + strchr (scanner->config.cset_identifier_nth, scanner->token)) + g_snprintf (token_string, token_string_len, "character `%c'", scanner->token); + else + g_snprintf (token_string, token_string_len, "character `\\%o'", scanner->token); + break; + } + /* fall through */ + case JSON_TOKEN_SYMBOL: + if (expected_token == JSON_TOKEN_SYMBOL || expected_token > JSON_TOKEN_LAST) + print_unexp = false; + if (symbol_name) + g_snprintf (token_string, token_string_len, + "%s%s `%s'", + print_unexp ? "" : "invalid ", + symbol_spec, + symbol_name); + else + g_snprintf (token_string, token_string_len, + "%s%s", + print_unexp ? "" : "invalid ", + symbol_spec); + break; + + case JSON_TOKEN_ERROR: + print_unexp = false; + expected_token = JSON_TOKEN_NONE; + switch (scanner->value.v_error) + { + case JSON_ERROR_TYPE_UNEXP_EOF: + g_snprintf (token_string, token_string_len, "scanner: unexpected end of file"); + break; + + case JSON_ERROR_TYPE_UNEXP_EOF_IN_STRING: + g_snprintf (token_string, token_string_len, "scanner: unterminated string constant"); + break; + + case JSON_ERROR_TYPE_UNEXP_EOF_IN_COMMENT: + g_snprintf (token_string, token_string_len, "scanner: unterminated comment"); + break; + + case JSON_ERROR_TYPE_NON_DIGIT_IN_CONST: + g_snprintf (token_string, token_string_len, "scanner: non digit in constant"); + break; + + case JSON_ERROR_TYPE_FLOAT_RADIX: + g_snprintf (token_string, token_string_len, "scanner: invalid radix for floating constant"); + break; + + case JSON_ERROR_TYPE_FLOAT_MALFORMED: + g_snprintf (token_string, token_string_len, "scanner: malformed floating constant"); + break; + + case JSON_ERROR_TYPE_DIGIT_RADIX: + g_snprintf (token_string, token_string_len, "scanner: digit is beyond radix"); + break; + + case JSON_ERROR_TYPE_MALFORMED_SURROGATE_PAIR: + g_snprintf (token_string, token_string_len, "scanner: malformed surrogate pair"); + break; + + case JSON_ERROR_TYPE_LEADING_ZERO: + g_snprintf (token_string, token_string_len, "scanner: leading zero in number"); + break; + + case JSON_ERROR_TYPE_UNESCAPED_CTRL: + g_snprintf (token_string, token_string_len, "scanner: unescaped control charater"); + break; + + case JSON_ERROR_TYPE_MALFORMED_UNICODE: + g_snprintf (token_string, token_string_len, "scanner: malformed Unicode escape"); + break; + + case JSON_ERROR_TYPE_UNKNOWN: + default: + g_snprintf (token_string, token_string_len, "scanner: unknown error"); + break; + } + break; + + case JSON_TOKEN_IDENTIFIER: + if (expected_token == JSON_TOKEN_IDENTIFIER) + print_unexp = false; + g_snprintf (token_string, token_string_len, + "%s%s `%s'", + print_unexp ? "" : "invalid ", + identifier_spec, + scanner->value.v_string); + break; + + case JSON_TOKEN_INT: + g_snprintf (token_string, token_string_len, "number `%" G_GINT64_FORMAT "'", scanner->value.v_int64); + break; + + case JSON_TOKEN_FLOAT: + g_snprintf (token_string, token_string_len, "number `%.3f'", scanner->value.v_float); + break; + + case JSON_TOKEN_STRING: + if (expected_token == JSON_TOKEN_STRING) + print_unexp = false; + g_snprintf (token_string, token_string_len, + "%s%sstring constant \"%s\"", + print_unexp ? "" : "invalid ", + scanner->value.v_string[0] == 0 ? "empty " : "", + scanner->value.v_string); + token_string[token_string_len - 2] = '"'; + token_string[token_string_len - 1] = 0; + break; + + case JSON_TOKEN_COMMENT_SINGLE: + case JSON_TOKEN_COMMENT_MULTI: + g_snprintf (token_string, token_string_len, "comment"); + break; + + case JSON_TOKEN_NONE: + /* somehow the user's parsing code is screwed, there isn't much + * we can do about it. + * Note, a common case to trigger this is + * json_scanner_peek_next_token(); json_scanner_unexp_token(); + * without an intermediate json_scanner_get_next_token(). + */ + g_assert_not_reached (); + break; + } + + + switch (expected_token) + { + case JSON_TOKEN_EOF: + g_snprintf (expected_string, expected_string_len, "end of file"); + break; + default: + if (expected_token >= 1 && expected_token <= 255) + { + if ((expected_token >= ' ' && expected_token <= '~') || + strchr (scanner->config.cset_identifier_first, expected_token) || + strchr (scanner->config.cset_identifier_nth, expected_token)) + g_snprintf (expected_string, expected_string_len, "character `%c'", expected_token); + else + g_snprintf (expected_string, expected_string_len, "character `\\%o'", expected_token); + break; + } + /* fall through */ + case JSON_TOKEN_SYMBOL: + { + bool need_valid = (scanner->token == JSON_TOKEN_SYMBOL || scanner->token > JSON_TOKEN_LAST); + g_snprintf (expected_string, expected_string_len, + "%s%s", + need_valid ? "valid " : "", + symbol_spec); + } + break; + case JSON_TOKEN_INT: + g_snprintf (expected_string, + expected_string_len, + "%snumber (integer)", + scanner->token == expected_token ? "valid " : ""); + break; + case JSON_TOKEN_FLOAT: + g_snprintf (expected_string, + expected_string_len, + "%snumber (float)", + scanner->token == expected_token ? "valid " : ""); + break; + case JSON_TOKEN_STRING: + g_snprintf (expected_string, + expected_string_len, + "%sstring constant", + scanner->token == JSON_TOKEN_STRING ? "valid " : ""); + break; + case JSON_TOKEN_IDENTIFIER: + g_snprintf (expected_string, + expected_string_len, + "%s%s", + scanner->token == JSON_TOKEN_IDENTIFIER ? "valid " : "", + identifier_spec); + break; + case JSON_TOKEN_COMMENT_SINGLE: + g_snprintf (expected_string, + expected_string_len, + "%scomment (single-line)", + scanner->token == expected_token ? "valid " : ""); + break; + case JSON_TOKEN_COMMENT_MULTI: + g_snprintf (expected_string, + expected_string_len, + "%scomment (multi-line)", + scanner->token == expected_token ? "valid " : ""); + break; + case JSON_TOKEN_NONE: + case JSON_TOKEN_ERROR: + /* this is handled upon printout */ + break; + } + + if (message && message[0] != 0) + message_prefix = " - "; + else + { + message_prefix = ""; + message = ""; + } + if (expected_token == JSON_TOKEN_ERROR) + { + json_scanner_error (scanner, + "failure around %s%s%s", + token_string, + message_prefix, + message); + } + else if (expected_token == JSON_TOKEN_NONE) + { + if (print_unexp) + json_scanner_error (scanner, + "unexpected %s%s%s", + token_string, + message_prefix, + message); + else + json_scanner_error (scanner, + "%s%s%s", + token_string, + message_prefix, + message); + } + else + { + if (print_unexp) + json_scanner_error (scanner, + "unexpected %s, expected %s%s%s", + token_string, + expected_string, + message_prefix, + message); + else + json_scanner_error (scanner, + "%s, expected %s%s%s", + token_string, + expected_string, + message_prefix, + message); + } + + g_free (token_string); + g_free (expected_string); +} + +void +json_scanner_unknown_token (JsonScanner *scanner, + unsigned int token) +{ + const char *symbol_name; + char *msg; + unsigned int cur_token; + + cur_token = json_scanner_get_current_token (scanner); + msg = NULL; + + symbol_name = NULL; + for (unsigned i = 0; i < G_N_ELEMENTS (json_symbols); i++) + if (json_symbols[i].token == token) + symbol_name = json_symbol_names + json_symbols[i].name_offset; + + if (symbol_name != NULL) + msg = g_strconcat ("e.g. '", symbol_name, "'", NULL); + + symbol_name = "???"; + for (unsigned i = 0; i < G_N_ELEMENTS (json_symbols); i++) + if (json_symbols[i].token == cur_token) + symbol_name = json_symbol_names + json_symbols[i].name_offset; + + json_scanner_unexp_token (scanner, token, + NULL, "value", + symbol_name, + msg); + + g_free (msg); +} + +static void +json_scanner_get_token_i (JsonScanner *scanner, + unsigned int *token_p, + JsonTokenValue *value_p, + guint *line_p, + guint *position_p) +{ + do + { + json_scanner_free_value (token_p, value_p); + json_scanner_get_token_ll (scanner, token_p, value_p, line_p, position_p); + } + while (((*token_p > 0 && *token_p < 256) && + strchr (scanner->config.cset_skip_characters, *token_p)) || + *token_p == JSON_TOKEN_COMMENT_MULTI || + *token_p == JSON_TOKEN_COMMENT_SINGLE); + + switch (*token_p) + { + case JSON_TOKEN_IDENTIFIER: + break; + + case JSON_TOKEN_SYMBOL: + *token_p = GPOINTER_TO_UINT (value_p->v_symbol); + break; + + default: + break; + } + + errno = 0; +} + +static void +json_scanner_get_token_ll (JsonScanner *scanner, + unsigned int *token_p, + JsonTokenValue *value_p, + guint *line_p, + guint *position_p) +{ + const JsonScannerConfig *config; + unsigned int token; + bool in_comment_multi = false; + bool in_comment_single = false; + bool in_string_sq = false; + bool in_string_dq = false; + GString *gstring = NULL; + JsonTokenValue value; + guchar ch; + + config = &scanner->config; + (*value_p).v_int64 = 0; + + if (scanner->text >= scanner->text_end || + scanner->token == JSON_TOKEN_EOF) + { + *token_p = JSON_TOKEN_EOF; + return; + } + + gstring = NULL; + + do /* while (ch != 0) */ + { + ch = json_scanner_get_char (scanner, line_p, position_p); + + value.v_int64 = 0; + token = JSON_TOKEN_NONE; + + /* this is *evil*, but needed ;( + * we first check for identifier first character, because it + * might interfere with other key chars like slashes or numbers + */ + if (ch != 0 && strchr (config->cset_identifier_first, ch)) + goto identifier_precedence; + + switch (ch) + { + case 0: + token = JSON_TOKEN_EOF; + (*position_p)++; + /* ch = 0; */ + break; + + case '/': + if (config->strict || json_scanner_peek_next_char (scanner) != '*') + goto default_case; + json_scanner_get_char (scanner, line_p, position_p); + token = JSON_TOKEN_COMMENT_MULTI; + in_comment_multi = true; + gstring = g_string_new (NULL); + while ((ch = json_scanner_get_char (scanner, line_p, position_p)) != 0) + { + if (ch == '*' && json_scanner_peek_next_char (scanner) == '/') + { + json_scanner_get_char (scanner, line_p, position_p); + in_comment_multi = false; + break; + } + else + gstring = g_string_append_c (gstring, ch); + } + ch = 0; + break; + + case '"': + token = JSON_TOKEN_STRING; + in_string_dq = true; + gstring = g_string_new (NULL); + while ((ch = json_scanner_get_char (scanner, line_p, position_p)) != 0) + { + if (ch == '"' || token == JSON_TOKEN_ERROR) + { + in_string_dq = false; + break; + } + else + { + if (ch == '\\') + { + ch = json_scanner_get_char (scanner, line_p, position_p); + switch (ch) + { + guint i; + guint fchar; + + case 0: + break; + + case '"': + gstring = g_string_append_c (gstring, '"'); + break; + + case '\\': + gstring = g_string_append_c (gstring, '\\'); + break; + + case '/': + gstring = g_string_append_c (gstring, '/'); + break; + + case 'n': + gstring = g_string_append_c (gstring, '\n'); + break; + + case 't': + gstring = g_string_append_c (gstring, '\t'); + break; + + case 'r': + gstring = g_string_append_c (gstring, '\r'); + break; + + case 'b': + gstring = g_string_append_c (gstring, '\b'); + break; + + case 'f': + gstring = g_string_append_c (gstring, '\f'); + break; + + case 'u': + fchar = json_scanner_peek_next_char (scanner); + if (is_hex_digit (fchar)) + { + gunichar ucs; + + if (!json_scanner_get_unichar (scanner, &ucs, line_p, position_p)) + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_MALFORMED_UNICODE; + g_string_free (gstring, TRUE); + gstring = NULL; + break; + } + + /* resolve UTF-16 surrogates for Unicode characters not in the BMP, + * as per ECMA 404, § 9, "String" + */ + if (g_unichar_type (ucs) == G_UNICODE_SURROGATE) + { + unsigned int next_ch; + + next_ch = json_scanner_peek_next_char (scanner); + if (next_ch != '\\') + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_MALFORMED_SURROGATE_PAIR; + g_string_free (gstring, TRUE); + gstring = NULL; + break; + } + else + ch = json_scanner_get_char (scanner, line_p, position_p); + + next_ch = json_scanner_peek_next_char (scanner); + if (next_ch != 'u') + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_MALFORMED_SURROGATE_PAIR; + g_string_free (gstring, TRUE); + gstring = NULL; + break; + } + else + ch = json_scanner_get_char (scanner, line_p, position_p); + + /* read next surrogate */ + gunichar units[2]; + + units[0] = ucs; + + if (!json_scanner_get_unichar (scanner, &ucs, line_p, position_p)) + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_MALFORMED_UNICODE; + g_string_free (gstring, TRUE); + gstring = NULL; + break; + } + + units[1] = ucs; + + if (0xdc00 <= units[1] && units[1] <= 0xdfff && + 0xd800 <= units[0] && units[0] <= 0xdbff) + { + ucs = decode_utf16_surrogate_pair (units); + if (!g_unichar_validate (ucs)) + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_MALFORMED_UNICODE; + g_string_free (gstring, TRUE); + gstring = NULL; + break; + } + } + else + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_MALFORMED_SURROGATE_PAIR; + g_string_free (gstring, TRUE); + gstring = NULL; + break; + } + } + else + { + if (!g_unichar_validate (ucs)) + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_MALFORMED_UNICODE; + g_string_free (gstring, TRUE); + gstring = NULL; + break; + } + } + + gstring = g_string_append_unichar (gstring, ucs); + } + else + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_MALFORMED_UNICODE; + g_string_free (gstring, TRUE); + gstring = NULL; + } + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + i = ch - '0'; + fchar = json_scanner_peek_next_char (scanner); + if (fchar >= '0' && fchar <= '7') + { + ch = json_scanner_get_char (scanner, line_p, position_p); + i = i * 8 + ch - '0'; + fchar = json_scanner_peek_next_char (scanner); + if (fchar >= '0' && fchar <= '7') + { + ch = json_scanner_get_char (scanner, line_p, position_p); + i = i * 8 + ch - '0'; + } + } + gstring = g_string_append_c (gstring, i); + break; + + default: + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_UNESCAPED_CTRL; + g_string_free (gstring, TRUE); + gstring = NULL; + break; + } + } + else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == '\f' || ch == '\b') + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_UNESCAPED_CTRL; + g_string_free (gstring, TRUE); + gstring = NULL; + break; + } + else + gstring = g_string_append_c (gstring, ch); + } + } + ch = 0; + break; + + /* {{{ number parsing */ + case '-': + if (!g_ascii_isdigit (json_scanner_peek_next_char (scanner))) + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_NON_DIGIT_IN_CONST; + ch = 0; + break; + } + G_GNUC_FALLTHROUGH; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + bool in_number = true; + bool leading_sign = ch == '-'; + bool leading_zero = ch == '0'; + char *endptr; + + if (token == JSON_TOKEN_NONE) + token = JSON_TOKEN_INT; + + gstring = g_string_new (""); + gstring = g_string_append_c (gstring, ch); + + if (leading_sign) + { + ch = json_scanner_get_char (scanner, line_p, position_p); + leading_zero = ch == '0'; + g_string_append_c (gstring, ch); + } + + do /* while (in_number) */ + { + bool is_E = token == JSON_TOKEN_FLOAT && (ch == 'e' || ch == 'E'); + + ch = json_scanner_peek_next_char (scanner); + + if (json_scanner_char_2_num (ch, 36) >= 0 || + ch == '.' || + (is_E && (ch == '+' || ch == '-'))) + { + ch = json_scanner_get_char (scanner, line_p, position_p); + + switch (ch) + { + case '.': + { + unsigned int next_ch = json_scanner_peek_next_char (scanner); + + if (!g_ascii_isdigit (next_ch)) + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_FLOAT_MALFORMED; + in_number = false; + } + else + { + token = JSON_TOKEN_FLOAT; + gstring = g_string_append_c (gstring, ch); + } + } + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + if (leading_zero && token != JSON_TOKEN_FLOAT) + { + token = JSON_TOKEN_ERROR; + value.v_error= JSON_ERROR_TYPE_LEADING_ZERO; + in_number = false; + } + else + gstring = g_string_append_c (gstring, ch); + break; + + case '-': + case '+': + if (token != JSON_TOKEN_FLOAT) + { + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_NON_DIGIT_IN_CONST; + in_number = false; + } + else + gstring = g_string_append_c (gstring, ch); + break; + + case 'e': + case 'E': + token = JSON_TOKEN_FLOAT; + gstring = g_string_append_c (gstring, ch); + break; + + default: + token = JSON_TOKEN_ERROR; + value.v_error = JSON_ERROR_TYPE_NON_DIGIT_IN_CONST; + in_number = false; + break; + } + } + else + in_number = false; + } + while (in_number); + + if (token != JSON_TOKEN_ERROR) + { + endptr = NULL; + if (token == JSON_TOKEN_FLOAT) + value.v_float = g_strtod (gstring->str, &endptr); + else if (token == JSON_TOKEN_INT) + value.v_int64 = g_ascii_strtoll (gstring->str, &endptr, 10); + + if (endptr && *endptr) + { + token = JSON_TOKEN_ERROR; + if (*endptr == 'e' || *endptr == 'E') + value.v_error = JSON_ERROR_TYPE_NON_DIGIT_IN_CONST; + else + value.v_error = JSON_ERROR_TYPE_DIGIT_RADIX; + } + } + g_string_free (gstring, TRUE); + gstring = NULL; + ch = 0; + } + break; /* number parsing }}} */ + + default: + default_case: + { + if (!config->strict && + config->cpair_comment_single && + ch == config->cpair_comment_single[0]) + { + token = JSON_TOKEN_COMMENT_SINGLE; + in_comment_single = true; + gstring = g_string_new (NULL); + ch = json_scanner_get_char (scanner, line_p, position_p); + while (ch != 0) + { + if (ch == config->cpair_comment_single[1]) + { + in_comment_single = false; + ch = 0; + break; + } + + gstring = g_string_append_c (gstring, ch); + ch = json_scanner_get_char (scanner, line_p, position_p); + } + /* ignore a missing newline at EOF for single line comments */ + if (in_comment_single && + config->cpair_comment_single[1] == '\n') + in_comment_single = false; + } + else if (ch && strchr (config->cset_identifier_first, ch)) + { + identifier_precedence: + + if (config->cset_identifier_nth && ch && + strchr (config->cset_identifier_nth, + json_scanner_peek_next_char (scanner))) + { + token = JSON_TOKEN_IDENTIFIER; + gstring = g_string_new (NULL); + gstring = g_string_append_c (gstring, ch); + do + { + ch = json_scanner_get_char (scanner, line_p, position_p); + gstring = g_string_append_c (gstring, ch); + ch = json_scanner_peek_next_char (scanner); + } + while (ch && strchr (config->cset_identifier_nth, ch)); + ch = 0; + } + } + if (ch) + { + token = ch; + ch = 0; + } + } /* default_case:... */ + break; + } + g_assert (ch == 0 && token != JSON_TOKEN_NONE); /* paranoid */ + } + while (ch != 0); + + if (in_comment_multi || in_comment_single || + in_string_sq || in_string_dq) + { + token = JSON_TOKEN_ERROR; + if (gstring) + { + g_string_free (gstring, TRUE); + gstring = NULL; + } + (*position_p)++; + if (in_comment_multi || in_comment_single) + value.v_error = JSON_ERROR_TYPE_UNEXP_EOF_IN_COMMENT; + else /* (in_string_sq || in_string_dq) */ + value.v_error = JSON_ERROR_TYPE_UNEXP_EOF_IN_STRING; + } + + if (gstring) + { + value.v_string = g_string_free (gstring, FALSE); + gstring = NULL; + } + + if (token == JSON_TOKEN_IDENTIFIER) + { + for (unsigned i = 0; i < G_N_ELEMENTS (json_symbols); i++) + { + const char *symbol = json_symbol_names + json_symbols[i].name_offset; + if (strcmp (value.v_identifier, symbol) == 0) + { + g_free (value.v_identifier); + token = JSON_TOKEN_SYMBOL; + value.v_symbol = GUINT_TO_POINTER (json_symbols[i].token); + break; + } + } + } + + *token_p = token; + *value_p = value; +} + +gint64 +json_scanner_get_int64_value (const JsonScanner *scanner) +{ + return scanner->value.v_int64; +} + +double +json_scanner_get_float_value (const JsonScanner *scanner) +{ + return scanner->value.v_float; +} + +const char * +json_scanner_get_string_value (const JsonScanner *scanner) +{ + return scanner->value.v_string; +} + +char * +json_scanner_dup_string_value (const JsonScanner *scanner) +{ + return g_strdup (scanner->value.v_string); +} + +const char * +json_scanner_get_identifier (const JsonScanner *scanner) +{ + return scanner->value.v_identifier; +} + +char * +json_scanner_dup_identifier (const JsonScanner *scanner) +{ + return g_strdup (scanner->value.v_identifier); +} + +unsigned int +json_scanner_get_current_line (const JsonScanner *scanner) +{ + return scanner->line; +} + +unsigned int +json_scanner_get_current_position (const JsonScanner *scanner) +{ + return scanner->position; +} + +unsigned int +json_scanner_get_current_token (const JsonScanner *scanner) +{ + return scanner->token; +} diff --git a/lsp/deps/json-glib/json-scanner.h b/lsp/deps/json-glib/json-scanner.h new file mode 100644 index 000000000..8665c0d7f --- /dev/null +++ b/lsp/deps/json-glib/json-scanner.h @@ -0,0 +1,120 @@ +/* json-scanner.h: Tokenizer for JSON + * + * This file is part of JSON-GLib + * Copyright (C) 2008 OpenedHand + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +/* + * JsonScanner is a specialized tokenizer for JSON adapted from + * the GScanner tokenizer in GLib; GScanner came with this notice: + * + * Modified by the GLib Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GLib Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GLib at ftp://ftp.gtk.org/pub/gtk/. + * + * JsonScanner: modified by Emmanuele Bassi + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +typedef struct _JsonScanner JsonScanner; + +typedef void (* JsonScannerMsgFunc) (JsonScanner *scanner, + const char *message, + gpointer user_data); + +/* Token types */ +typedef enum +{ + JSON_TOKEN_EOF = 0, + + JSON_TOKEN_LEFT_CURLY = '{', + JSON_TOKEN_RIGHT_CURLY = '}', + JSON_TOKEN_LEFT_BRACE = '[', + JSON_TOKEN_RIGHT_BRACE = ']', + JSON_TOKEN_EQUAL_SIGN = '=', + JSON_TOKEN_COMMA = ',', + JSON_TOKEN_COLON = ':', + + JSON_TOKEN_NONE = 256, + + JSON_TOKEN_ERROR, + + JSON_TOKEN_INT, + JSON_TOKEN_FLOAT, + JSON_TOKEN_STRING, + + JSON_TOKEN_SYMBOL, + JSON_TOKEN_IDENTIFIER, + + JSON_TOKEN_COMMENT_SINGLE, + JSON_TOKEN_COMMENT_MULTI, + + JSON_TOKEN_TRUE, + JSON_TOKEN_FALSE, + JSON_TOKEN_NULL, + JSON_TOKEN_VAR, + + JSON_TOKEN_LAST +} JsonTokenType; + +G_GNUC_INTERNAL +JsonScanner *json_scanner_new (bool strict); +G_GNUC_INTERNAL +void json_scanner_destroy (JsonScanner *scanner); +G_GNUC_INTERNAL +void json_scanner_input_text (JsonScanner *scanner, + const char *text, + unsigned int text_len); +G_GNUC_INTERNAL +unsigned int json_scanner_get_next_token (JsonScanner *scanner); +G_GNUC_INTERNAL +unsigned int json_scanner_peek_next_token (JsonScanner *scanner); +G_GNUC_INTERNAL +void json_scanner_set_msg_handler (JsonScanner *scanner, + JsonScannerMsgFunc msg_handler, + gpointer user_data); +G_GNUC_INTERNAL +void json_scanner_unknown_token (JsonScanner *scanner, + unsigned int token); + +G_GNUC_INTERNAL +gint64 json_scanner_get_int64_value (const JsonScanner *scanner); +G_GNUC_INTERNAL +double json_scanner_get_float_value (const JsonScanner *scanner); +G_GNUC_INTERNAL +const char * json_scanner_get_string_value (const JsonScanner *scanner); +G_GNUC_INTERNAL +char * json_scanner_dup_string_value (const JsonScanner *scanner); +G_GNUC_INTERNAL +const char * json_scanner_get_identifier (const JsonScanner *scanner); +G_GNUC_INTERNAL +char * json_scanner_dup_identifier (const JsonScanner *scanner); + +G_GNUC_INTERNAL +unsigned int json_scanner_get_current_line (const JsonScanner *scanner); +G_GNUC_INTERNAL +unsigned int json_scanner_get_current_position (const JsonScanner *scanner); +G_GNUC_INTERNAL +unsigned int json_scanner_get_current_token (const JsonScanner *scanner); + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-serializable.c b/lsp/deps/json-glib/json-serializable.c new file mode 100644 index 000000000..207a2c65f --- /dev/null +++ b/lsp/deps/json-glib/json-serializable.c @@ -0,0 +1,386 @@ +/* json-gobject.c - JSON GObject integration + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Author: + * Emmanuele Bassi + */ + +/** + * JsonSerializable: + * + * `JsonSerializable` is an interface for controlling the serialization + * and deserialization of `GObject` classes. + * + * Implementing this interface allows controlling how the class is going + * to be serialized or deserialized by [func@Json.construct_gobject] and + * [func@Json.serialize_gobject], respectively. + */ + +#include "config.h" + +#include +#include + +#include "json-types-private.h" +#include "json-gobject-private.h" +#include "json-debug.h" + +/** + * json_serializable_serialize_property: + * @serializable: a serializable object + * @property_name: the name of the property to serialize + * @value: the value of the property to serialize + * @pspec: a property description + * + * Asks a `JsonSerializable` implementation to serialize an object + * property into a JSON node. + * + * Return value: (transfer full): a node containing the serialized property + */ +JsonNode * +json_serializable_serialize_property (JsonSerializable *serializable, + const gchar *property_name, + const GValue *value, + GParamSpec *pspec) +{ + JsonSerializableIface *iface; + + g_return_val_if_fail (JSON_IS_SERIALIZABLE (serializable), NULL); + g_return_val_if_fail (property_name != NULL, NULL); + g_return_val_if_fail (value != NULL, NULL); + g_return_val_if_fail (pspec != NULL, NULL); + + iface = JSON_SERIALIZABLE_GET_IFACE (serializable); + + return iface->serialize_property (serializable, property_name, value, pspec); +} + +/** + * json_serializable_deserialize_property: + * @serializable: a serializable object + * @property_name: the name of the property to serialize + * @value: (out): a pointer to an uninitialized value + * @pspec: a property description + * @property_node: the JSON node containing the serialized property + * + * Asks a `JsonSerializable` implementation to deserialize the + * property contained inside `property_node` and place its value + * into `value`. + * + * The `value` can be: + * + * - an empty `GValue` initialized by `G_VALUE_INIT`, which will be automatically + * initialized with the expected type of the property by using the given + * property description (since JSON-GLib 1.6) + * - a `GValue` initialized with the expected type of the property + * + * This function will not be called for properties that are marked as + * as `G_PARAM_CONSTRUCT_ONLY`. + * + * Returns: `TRUE` if the property was successfully deserialized + */ +gboolean +json_serializable_deserialize_property (JsonSerializable *serializable, + const gchar *property_name, + GValue *value, + GParamSpec *pspec, + JsonNode *property_node) +{ + JsonSerializableIface *iface; + + g_return_val_if_fail (JSON_IS_SERIALIZABLE (serializable), FALSE); + g_return_val_if_fail (property_name != NULL, FALSE); + g_return_val_if_fail (value != NULL, FALSE); + g_return_val_if_fail (pspec != NULL, FALSE); + g_return_val_if_fail (property_node != NULL, FALSE); + + iface = JSON_SERIALIZABLE_GET_IFACE (serializable); + + return iface->deserialize_property (serializable, + property_name, + value, + pspec, + property_node); +} + +static gboolean +json_serializable_real_deserialize (JsonSerializable *serializable G_GNUC_UNUSED, + const gchar *name G_GNUC_UNUSED, + GValue *value, + GParamSpec *pspec, + JsonNode *node) +{ + JSON_NOTE (GOBJECT, "Default deserialization for property '%s'", pspec->name); + + if (!G_IS_VALUE (value)) + g_value_init (value, G_PARAM_SPEC_VALUE_TYPE (pspec)); + + return json_deserialize_pspec (value, pspec, node); +} + +static JsonNode * +json_serializable_real_serialize (JsonSerializable *serializable G_GNUC_UNUSED, + const gchar *name G_GNUC_UNUSED, + const GValue *value, + GParamSpec *pspec) +{ + JSON_NOTE (GOBJECT, "Default serialization for property '%s'", pspec->name); + + if (g_param_value_defaults (pspec, (GValue *)value)) + return NULL; + + return json_serialize_pspec (value, pspec); +} + +static GParamSpec * +json_serializable_real_find_property (JsonSerializable *serializable, + const char *name) +{ + return g_object_class_find_property (G_OBJECT_GET_CLASS (serializable), name); +} + +static GParamSpec ** +json_serializable_real_list_properties (JsonSerializable *serializable, + guint *n_pspecs) +{ + return g_object_class_list_properties (G_OBJECT_GET_CLASS (serializable), n_pspecs); +} + +static void +json_serializable_real_set_property (JsonSerializable *serializable, + GParamSpec *pspec, + const GValue *value) +{ + g_object_set_property (G_OBJECT (serializable), pspec->name, value); +} + +static void +json_serializable_real_get_property (JsonSerializable *serializable, + GParamSpec *pspec, + GValue *value) +{ + g_object_get_property (G_OBJECT (serializable), pspec->name, value); +} + +/* typedef to satisfy G_DEFINE_INTERFACE's naming */ +typedef JsonSerializableIface JsonSerializableInterface; + +static void +json_serializable_default_init (JsonSerializableInterface *iface) +{ + iface->serialize_property = json_serializable_real_serialize; + iface->deserialize_property = json_serializable_real_deserialize; + iface->find_property = json_serializable_real_find_property; + iface->list_properties = json_serializable_real_list_properties; + iface->set_property = json_serializable_real_set_property; + iface->get_property = json_serializable_real_get_property; +} + +G_DEFINE_INTERFACE (JsonSerializable, json_serializable, G_TYPE_OBJECT) + +/** + * json_serializable_default_serialize_property: + * @serializable: a serializable object + * @property_name: the name of the property to serialize + * @value: the value of the property to serialize + * @pspec: a property description + * + * Calls the default implementation of the [vfunc@Json.Serializable.serialize_property] + * virtual function. + * + * This function can be used inside a custom implementation of the + * `serialize_property()` virtual function in lieu of calling the + * default implementation through `g_type_default_interface_peek()`: + * + * ```c + * JsonSerializable *iface; + * JsonNode *node; + * + * iface = g_type_default_interface_peek (JSON_TYPE_SERIALIZABLE); + * node = iface->serialize_property (serializable, property_name, + * value, + * pspec); + * ``` + * + * This function will return `NULL` if the property could not be + * serialized. + * + * Returns: (transfer full) (nullable): a node containing the + * serialized property + * + * Since: 0.10 + */ +JsonNode * +json_serializable_default_serialize_property (JsonSerializable *serializable, + const gchar *property_name, + const GValue *value, + GParamSpec *pspec) +{ + g_return_val_if_fail (JSON_IS_SERIALIZABLE (serializable), NULL); + g_return_val_if_fail (property_name != NULL, NULL); + g_return_val_if_fail (value != NULL, NULL); + g_return_val_if_fail (pspec != NULL, NULL); + + return json_serializable_real_serialize (serializable, + property_name, + value, pspec); +} + +/** + * json_serializable_default_deserialize_property: + * @serializable: a serializable object + * @property_name: the name of the property to deserialize + * @value: a pointer to an uninitialized value + * @pspec: a property description + * @property_node: the JSON node containing the serialized property + * + * Calls the default implementation of the [vfunc@Json.Serializable.deserialize_property] + * virtual function. + * + * This function can be used inside a custom implementation of the + * `deserialize_property()` virtual function in lieu of calling the + * default implementation through `g_type_default_interface_peek()`: + * + * ```c + * JsonSerializable *iface; + * gboolean res; + * + * iface = g_type_default_interface_peek (JSON_TYPE_SERIALIZABLE); + * res = iface->deserialize_property (serializable, property_name, + * value, + * pspec, + * property_node); + * ``` + * + * Return value: `TRUE` if the property was successfully deserialized + * + * Since: 0.10 + */ +gboolean +json_serializable_default_deserialize_property (JsonSerializable *serializable, + const gchar *property_name, + GValue *value, + GParamSpec *pspec, + JsonNode *property_node) +{ + g_return_val_if_fail (JSON_IS_SERIALIZABLE (serializable), FALSE); + g_return_val_if_fail (property_name != NULL, FALSE); + g_return_val_if_fail (value != NULL, FALSE); + g_return_val_if_fail (pspec != NULL, FALSE); + g_return_val_if_fail (property_node != NULL, FALSE); + + return json_serializable_real_deserialize (serializable, + property_name, + value, pspec, + property_node); +} + +/** + * json_serializable_find_property: + * @serializable: a serializable object + * @name: the name of the property + * + * Calls the [vfunc@Json.Serializable.find_property] implementation on + * the `JsonSerializable` instance, which will return the property + * description for the given name. + * + * Return value: (nullable) (transfer none): the property description + * + * Since: 0.14 + */ +GParamSpec * +json_serializable_find_property (JsonSerializable *serializable, + const char *name) +{ + g_return_val_if_fail (JSON_IS_SERIALIZABLE (serializable), NULL); + g_return_val_if_fail (name != NULL, NULL); + + return JSON_SERIALIZABLE_GET_IFACE (serializable)->find_property (serializable, name); +} + +/** + * json_serializable_list_properties: + * @serializable: a serializable object + * @n_pspecs: (out): return location for the length of the returned array + * + * Calls the [vfunc@Json.Serializable.list_properties] implementation on + * the `JsonSerializable` instance, which will return the list of serializable + * properties. + * + * Return value: (array length=n_pspecs) (transfer container): the serializable + * properties of the object + * + * Since: 0.14 + */ +GParamSpec ** +json_serializable_list_properties (JsonSerializable *serializable, + guint *n_pspecs) +{ + g_return_val_if_fail (JSON_IS_SERIALIZABLE (serializable), NULL); + + return JSON_SERIALIZABLE_GET_IFACE (serializable)->list_properties (serializable, n_pspecs); +} + +/** + * json_serializable_set_property: + * @serializable: a serializable object + * @pspec: a property description + * @value: the property value to set + * + * Calls the [vfunc@Json.Serializable.set_property] implementation + * on the `JsonSerializable` instance, which will set the property + * with the given value. + * + * Since: 0.14 + */ +void +json_serializable_set_property (JsonSerializable *serializable, + GParamSpec *pspec, + const GValue *value) +{ + g_return_if_fail (JSON_IS_SERIALIZABLE (serializable)); + g_return_if_fail (G_IS_PARAM_SPEC (pspec)); + g_return_if_fail (value != NULL); + + JSON_SERIALIZABLE_GET_IFACE (serializable)->set_property (serializable, + pspec, + value); +} + +/** + * json_serializable_get_property: + * @serializable: a serializable object + * @pspec: a property description + * @value: (out): return location for the property value + * + * Calls the [vfunc@Json.Serializable.get_property] implementation + * on the `JsonSerializable` instance, which will get the value of + * the given property. + * + * Since: 0.14 + */ +void +json_serializable_get_property (JsonSerializable *serializable, + GParamSpec *pspec, + GValue *value) +{ + g_return_if_fail (JSON_IS_SERIALIZABLE (serializable)); + g_return_if_fail (G_IS_PARAM_SPEC (pspec)); + g_return_if_fail (value != NULL); + + JSON_SERIALIZABLE_GET_IFACE (serializable)->get_property (serializable, + pspec, + value); +} diff --git a/lsp/deps/json-glib/json-types-private.h b/lsp/deps/json-glib/json-types-private.h new file mode 100644 index 000000000..a8795d946 --- /dev/null +++ b/lsp/deps/json-glib/json-types-private.h @@ -0,0 +1,185 @@ +/* json-types-private.h - JSON data types private header + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +#pragma once + +#include "json-types.h" + +G_BEGIN_DECLS + +#define JSON_NODE_IS_VALID(n) \ + ((n) != NULL && \ + (n)->type >= JSON_NODE_OBJECT && \ + (n)->type <= JSON_NODE_NULL && \ + (n)->ref_count >= 1) + +typedef struct _JsonValue JsonValue; + +typedef enum { + JSON_VALUE_INVALID = 0, + JSON_VALUE_INT, + JSON_VALUE_DOUBLE, + JSON_VALUE_BOOLEAN, + JSON_VALUE_STRING, + JSON_VALUE_NULL +} JsonValueType; + +struct _JsonNode +{ + /*< private >*/ + JsonNodeType type; + + volatile gint ref_count; + gboolean immutable : 1; + gboolean allocated : 1; + + union { + JsonObject *object; + JsonArray *array; + JsonValue *value; + } data; + + JsonNode *parent; +}; + +#define JSON_VALUE_INIT { JSON_VALUE_INVALID, 1, FALSE, { 0 }, NULL } +#define JSON_VALUE_INIT_TYPE(t) { (t), 1, FALSE, { 0 }, NULL } +#define JSON_VALUE_IS_VALID(v) ((v) != NULL && (v)->type != JSON_VALUE_INVALID) +#define JSON_VALUE_HOLDS(v,t) ((v) != NULL && (v)->type == (t)) +#define JSON_VALUE_HOLDS_INT(v) (JSON_VALUE_HOLDS((v), JSON_VALUE_INT)) +#define JSON_VALUE_HOLDS_DOUBLE(v) (JSON_VALUE_HOLDS((v), JSON_VALUE_DOUBLE)) +#define JSON_VALUE_HOLDS_BOOLEAN(v) (JSON_VALUE_HOLDS((v), JSON_VALUE_BOOLEAN)) +#define JSON_VALUE_HOLDS_STRING(v) (JSON_VALUE_HOLDS((v), JSON_VALUE_STRING)) +#define JSON_VALUE_HOLDS_NULL(v) (JSON_VALUE_HOLDS((v), JSON_VALUE_NULL)) +#define JSON_VALUE_TYPE(v) (json_value_type((v))) + +struct _JsonValue +{ + JsonValueType type; + + volatile gint ref_count; + gboolean immutable : 1; + + union { + gint64 v_int; + gdouble v_double; + gboolean v_bool; + gchar *v_str; + } data; +}; + +struct _JsonArray +{ + GPtrArray *elements; + + guint immutable_hash; /* valid iff immutable */ + volatile gint ref_count; + gboolean immutable : 1; +}; + +struct _JsonObject +{ + GHashTable *members; + + GQueue members_ordered; + + int age; + guint immutable_hash; /* valid iff immutable */ + volatile gint ref_count; + gboolean immutable : 1; +}; + +typedef struct +{ + JsonObject *object; /* unowned */ + GHashTableIter members_iter; /* iterator over @members */ + gpointer padding[2]; /* for future expansion */ +} JsonObjectIterReal; + +G_STATIC_ASSERT (sizeof (JsonObjectIterReal) == sizeof (JsonObjectIter)); + +typedef struct +{ + JsonObject *object; /* unowned */ + GList *cur_member; + GList *next_member; + gpointer priv_pointer[3]; + int age; + int priv_int[1]; + gboolean priv_boolean; +} JsonObjectOrderedIterReal; + +G_STATIC_ASSERT (sizeof (JsonObjectOrderedIterReal) == sizeof (JsonObjectIter)); + +G_GNUC_INTERNAL +const gchar * json_node_type_get_name (JsonNodeType node_type); +G_GNUC_INTERNAL +const gchar * json_value_type_get_name (JsonValueType value_type); + +G_GNUC_INTERNAL +GQueue * json_object_get_members_internal (JsonObject *object); + +G_GNUC_INTERNAL +GType json_value_type (const JsonValue *value); + +G_GNUC_INTERNAL +JsonValue * json_value_alloc (void); +G_GNUC_INTERNAL +JsonValue * json_value_init (JsonValue *value, + JsonValueType value_type); +G_GNUC_INTERNAL +JsonValue * json_value_ref (JsonValue *value); +G_GNUC_INTERNAL +void json_value_unref (JsonValue *value); +G_GNUC_INTERNAL +void json_value_unset (JsonValue *value); +G_GNUC_INTERNAL +void json_value_free (JsonValue *value); +G_GNUC_INTERNAL +void json_value_set_int (JsonValue *value, + gint64 v_int); +G_GNUC_INTERNAL +gint64 json_value_get_int (const JsonValue *value); +G_GNUC_INTERNAL +void json_value_set_double (JsonValue *value, + gdouble v_double); +G_GNUC_INTERNAL +gdouble json_value_get_double (const JsonValue *value); +G_GNUC_INTERNAL +void json_value_set_boolean (JsonValue *value, + gboolean v_bool); +G_GNUC_INTERNAL +gboolean json_value_get_boolean (const JsonValue *value); +G_GNUC_INTERNAL +void json_value_set_string (JsonValue *value, + const gchar *v_str); +G_GNUC_INTERNAL +const gchar * json_value_get_string (const JsonValue *value); + +G_GNUC_INTERNAL +void json_value_seal (JsonValue *value); + +G_GNUC_INTERNAL +guint json_value_hash (gconstpointer key); + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-types.h b/lsp/deps/json-glib/json-types.h new file mode 100644 index 000000000..4cfd2d171 --- /dev/null +++ b/lsp/deps/json-glib/json-types.h @@ -0,0 +1,534 @@ +/* json-types.h - JSON data types + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +#pragma once + +#if !defined(__JSON_GLIB_INSIDE__) && !defined(JSON_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include + +G_BEGIN_DECLS + +/** + * JSON_NODE_TYPE: + * @node: (type Json.Node): the [struct@Json.Node] to check + * + * Evaluates to the [enum@Json.NodeType] value contained by the node. + */ +#define JSON_NODE_TYPE(node) (json_node_get_node_type ((node))) + +/** + * JSON_NODE_HOLDS: + * @node: (type Json.Node): the [struct@Json.Node] to check + * @t: (type Json.NodeType): the desired [enum@Json.NodeType] + * + * Evaluates to `TRUE` if the node holds the given type. + * + * Since: 0.10 + */ +#define JSON_NODE_HOLDS(node,t) (json_node_get_node_type ((node)) == (t)) + +/** + * JSON_NODE_HOLDS_VALUE: + * @node: (type Json.Node): the [struct@Json.Node] to check + * + * Evaluates to `TRUE` if the node holds a scalar value. + * + * Since: 0.10 + */ +#define JSON_NODE_HOLDS_VALUE(node) (JSON_NODE_HOLDS ((node), JSON_NODE_VALUE)) + +/** + * JSON_NODE_HOLDS_OBJECT: + * @node: (type Json.Node): the [struct@Json.Node] to check + * + * Evaluates to `TRUE` if the node holds a JSON object. + * + * Since: 0.10 + */ +#define JSON_NODE_HOLDS_OBJECT(node) (JSON_NODE_HOLDS ((node), JSON_NODE_OBJECT)) + +/** + * JSON_NODE_HOLDS_ARRAY: + * @node: (type Json.Node): the [struct@Json.Node] to check + * + * Evaluates to `TRUE` if the node holds a JSON array. + * + * Since: 0.10 + */ +#define JSON_NODE_HOLDS_ARRAY(node) (JSON_NODE_HOLDS ((node), JSON_NODE_ARRAY)) + +/** + * JSON_NODE_HOLDS_NULL: + * @node: (type Json.Node): the [struct@Json.Node] to check + * + * Evaluates to `TRUE` if the node holds `null`. + * + * Since: 0.10 + */ +#define JSON_NODE_HOLDS_NULL(node) (JSON_NODE_HOLDS ((node), JSON_NODE_NULL)) + +#define JSON_TYPE_NODE (json_node_get_type ()) +#define JSON_TYPE_OBJECT (json_object_get_type ()) +#define JSON_TYPE_ARRAY (json_array_get_type ()) + +typedef struct _JsonNode JsonNode; +typedef struct _JsonObject JsonObject; +typedef struct _JsonArray JsonArray; + +/** + * JsonNodeType: + * @JSON_NODE_OBJECT: The node contains a JSON object + * @JSON_NODE_ARRAY: The node contains a JSON array + * @JSON_NODE_VALUE: The node contains a fundamental type + * @JSON_NODE_NULL: Special type, for nodes containing null + * + * Indicates the content of a node. + */ +typedef enum { + JSON_NODE_OBJECT, + JSON_NODE_ARRAY, + JSON_NODE_VALUE, + JSON_NODE_NULL +} JsonNodeType; + +/** + * JsonObjectForeach: + * @object: the iterated JSON object + * @member_name: the name of the member + * @member_node: the value of the member + * @user_data: data passed to the function + * + * The function to be passed to [method@Json.Object.foreach_member]. + * + * You should not add or remove members to and from @object within + * this function. + * + * It is safe to change the value of @member_node. + * + * Since: 0.8 + */ +typedef void (* JsonObjectForeach) (JsonObject *object, + const gchar *member_name, + JsonNode *member_node, + gpointer user_data); + +/** + * JsonArrayForeach: + * @array: the iterated JSON array + * @index_: the index of the element + * @element_node: the value of the element at the given @index_ + * @user_data: data passed to the function + * + * The function to be passed to [method@Json.Array.foreach_element]. + * + * You should not add or remove elements to and from @array within + * this function. + * + * It is safe to change the value of @element_node. + * + * Since: 0.8 + */ +typedef void (* JsonArrayForeach) (JsonArray *array, + guint index_, + JsonNode *element_node, + gpointer user_data); + +/* + * JsonNode + */ + +JSON_AVAILABLE_IN_1_0 +GType json_node_get_type (void) G_GNUC_CONST; +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_new (JsonNodeType type); + +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_alloc (void); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_init (JsonNode *node, + JsonNodeType type); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_init_object (JsonNode *node, + JsonObject *object); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_init_array (JsonNode *node, + JsonArray *array); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_init_int (JsonNode *node, + gint64 value); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_init_double (JsonNode *node, + gdouble value); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_init_boolean (JsonNode *node, + gboolean value); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_init_string (JsonNode *node, + const char *value); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_init_null (JsonNode *node); + +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_copy (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +void json_node_free (JsonNode *node); + +JSON_AVAILABLE_IN_1_2 +JsonNode * json_node_ref (JsonNode *node); +JSON_AVAILABLE_IN_1_2 +void json_node_unref (JsonNode *node); + +JSON_AVAILABLE_IN_1_0 +JsonNodeType json_node_get_node_type (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +GType json_node_get_value_type (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +void json_node_set_parent (JsonNode *node, + JsonNode *parent); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_node_get_parent (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +const gchar * json_node_type_name (JsonNode *node); + +JSON_AVAILABLE_IN_1_0 +void json_node_set_object (JsonNode *node, + JsonObject *object); +JSON_AVAILABLE_IN_1_0 +void json_node_take_object (JsonNode *node, + JsonObject *object); +JSON_AVAILABLE_IN_1_0 +JsonObject * json_node_get_object (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +JsonObject * json_node_dup_object (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +void json_node_set_array (JsonNode *node, + JsonArray *array); +JSON_AVAILABLE_IN_1_0 +void json_node_take_array (JsonNode *node, + JsonArray *array); +JSON_AVAILABLE_IN_1_0 +JsonArray * json_node_get_array (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +JsonArray * json_node_dup_array (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +void json_node_set_value (JsonNode *node, + const GValue *value); +JSON_AVAILABLE_IN_1_0 +void json_node_get_value (JsonNode *node, + GValue *value); +JSON_AVAILABLE_IN_1_0 +void json_node_set_string (JsonNode *node, + const gchar *value); +JSON_AVAILABLE_IN_1_0 +const gchar * json_node_get_string (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +gchar * json_node_dup_string (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +void json_node_set_int (JsonNode *node, + gint64 value); +JSON_AVAILABLE_IN_1_0 +gint64 json_node_get_int (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +void json_node_set_double (JsonNode *node, + gdouble value); +JSON_AVAILABLE_IN_1_0 +gdouble json_node_get_double (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +void json_node_set_boolean (JsonNode *node, + gboolean value); +JSON_AVAILABLE_IN_1_0 +gboolean json_node_get_boolean (JsonNode *node); +JSON_AVAILABLE_IN_1_0 +gboolean json_node_is_null (JsonNode *node); + +JSON_AVAILABLE_IN_1_2 +void json_node_seal (JsonNode *node); +JSON_AVAILABLE_IN_1_2 +gboolean json_node_is_immutable (JsonNode *node); + +JSON_AVAILABLE_IN_1_2 +guint json_string_hash (gconstpointer key); +JSON_AVAILABLE_IN_1_2 +gboolean json_string_equal (gconstpointer a, + gconstpointer b); +JSON_AVAILABLE_IN_1_2 +gint json_string_compare (gconstpointer a, + gconstpointer b); + +JSON_AVAILABLE_IN_1_2 +guint json_node_hash (gconstpointer key); +JSON_AVAILABLE_IN_1_2 +gboolean json_node_equal (gconstpointer a, + gconstpointer b); + +/* + * JsonObject + */ +JSON_AVAILABLE_IN_1_0 +GType json_object_get_type (void) G_GNUC_CONST; +JSON_AVAILABLE_IN_1_0 +JsonObject * json_object_new (void); +JSON_AVAILABLE_IN_1_0 +JsonObject * json_object_ref (JsonObject *object); +JSON_AVAILABLE_IN_1_0 +void json_object_unref (JsonObject *object); + +JSON_DEPRECATED_IN_1_0_FOR(json_object_set_member) +void json_object_add_member (JsonObject *object, + const gchar *member_name, + JsonNode *node); + +JSON_AVAILABLE_IN_1_0 +void json_object_set_member (JsonObject *object, + const gchar *member_name, + JsonNode *node); +JSON_AVAILABLE_IN_1_0 +void json_object_set_int_member (JsonObject *object, + const gchar *member_name, + gint64 value); +JSON_AVAILABLE_IN_1_0 +void json_object_set_double_member (JsonObject *object, + const gchar *member_name, + gdouble value); +JSON_AVAILABLE_IN_1_0 +void json_object_set_boolean_member (JsonObject *object, + const gchar *member_name, + gboolean value); +JSON_AVAILABLE_IN_1_0 +void json_object_set_string_member (JsonObject *object, + const gchar *member_name, + const gchar *value); +JSON_AVAILABLE_IN_1_0 +void json_object_set_null_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_0 +void json_object_set_array_member (JsonObject *object, + const gchar *member_name, + JsonArray *value); +JSON_AVAILABLE_IN_1_0 +void json_object_set_object_member (JsonObject *object, + const gchar *member_name, + JsonObject *value); +JSON_AVAILABLE_IN_1_0 +GList * json_object_get_members (JsonObject *object); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_object_get_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_object_dup_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_0 +gint64 json_object_get_int_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_6 +gint64 json_object_get_int_member_with_default (JsonObject *object, + const char *member_name, + gint64 default_value); +JSON_AVAILABLE_IN_1_0 +gdouble json_object_get_double_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_6 +double json_object_get_double_member_with_default (JsonObject *object, + const char *member_name, + double default_value); +JSON_AVAILABLE_IN_1_0 +gboolean json_object_get_boolean_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_6 +gboolean json_object_get_boolean_member_with_default (JsonObject *object, + const char *member_name, + gboolean default_value); +JSON_AVAILABLE_IN_1_0 +const gchar * json_object_get_string_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_6 +const char * json_object_get_string_member_with_default (JsonObject *object, + const char *member_name, + const char *default_value); +JSON_AVAILABLE_IN_1_0 +gboolean json_object_get_null_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_0 +JsonArray * json_object_get_array_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_0 +JsonObject * json_object_get_object_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_0 +gboolean json_object_has_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_0 +void json_object_remove_member (JsonObject *object, + const gchar *member_name); +JSON_AVAILABLE_IN_1_0 +GList * json_object_get_values (JsonObject *object); +JSON_AVAILABLE_IN_1_0 +guint json_object_get_size (JsonObject *object); +JSON_AVAILABLE_IN_1_0 +void json_object_foreach_member (JsonObject *object, + JsonObjectForeach func, + gpointer data); + +JSON_AVAILABLE_IN_1_2 +void json_object_seal (JsonObject *object); +JSON_AVAILABLE_IN_1_2 +gboolean json_object_is_immutable (JsonObject *object); + +JSON_AVAILABLE_IN_1_2 +guint json_object_hash (gconstpointer key); +JSON_AVAILABLE_IN_1_2 +gboolean json_object_equal (gconstpointer a, + gconstpointer b); + +/** + * JsonObjectIter: + * + * An iterator object used to iterate over the members of a JSON object. + * + * `JsonObjectIter` must be allocated on the stack and initialised using + * [method@Json.ObjectIter.init] or [method@Json.ObjectIter.init_ordered]. + * + * The iterator is invalidated if the object is modified during + * iteration. + * + * All the fields in the `JsonObjectIter` structure are private and should + * never be accessed directly. + * + * Since: 1.2 + */ +typedef struct { + /*< private >*/ + gpointer priv_pointer[6]; + int priv_int[2]; + gboolean priv_boolean[1]; +} JsonObjectIter; + +JSON_AVAILABLE_IN_1_2 +void json_object_iter_init (JsonObjectIter *iter, + JsonObject *object); +JSON_AVAILABLE_IN_1_2 +gboolean json_object_iter_next (JsonObjectIter *iter, + const gchar **member_name, + JsonNode **member_node); + +JSON_AVAILABLE_IN_1_6 +void json_object_iter_init_ordered (JsonObjectIter *iter, + JsonObject *object); +JSON_AVAILABLE_IN_1_6 +gboolean json_object_iter_next_ordered (JsonObjectIter *iter, + const char **member_name, + JsonNode **member_node); + +JSON_AVAILABLE_IN_1_0 +GType json_array_get_type (void) G_GNUC_CONST; +JSON_AVAILABLE_IN_1_0 +JsonArray * json_array_new (void); +JSON_AVAILABLE_IN_1_0 +JsonArray * json_array_sized_new (guint n_elements); +JSON_AVAILABLE_IN_1_0 +JsonArray * json_array_ref (JsonArray *array); +JSON_AVAILABLE_IN_1_0 +void json_array_unref (JsonArray *array); +JSON_AVAILABLE_IN_1_0 +void json_array_add_element (JsonArray *array, + JsonNode *node); +JSON_AVAILABLE_IN_1_0 +void json_array_add_int_element (JsonArray *array, + gint64 value); +JSON_AVAILABLE_IN_1_0 +void json_array_add_double_element (JsonArray *array, + gdouble value); +JSON_AVAILABLE_IN_1_0 +void json_array_add_boolean_element (JsonArray *array, + gboolean value); +JSON_AVAILABLE_IN_1_0 +void json_array_add_string_element (JsonArray *array, + const gchar *value); +JSON_AVAILABLE_IN_1_0 +void json_array_add_null_element (JsonArray *array); +JSON_AVAILABLE_IN_1_0 +void json_array_add_array_element (JsonArray *array, + JsonArray *value); +JSON_AVAILABLE_IN_1_0 +void json_array_add_object_element (JsonArray *array, + JsonObject *value); +JSON_AVAILABLE_IN_1_0 +GList * json_array_get_elements (JsonArray *array); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_array_get_element (JsonArray *array, + guint index_); +JSON_AVAILABLE_IN_1_0 +gint64 json_array_get_int_element (JsonArray *array, + guint index_); +JSON_AVAILABLE_IN_1_0 +gdouble json_array_get_double_element (JsonArray *array, + guint index_); +JSON_AVAILABLE_IN_1_0 +gboolean json_array_get_boolean_element (JsonArray *array, + guint index_); +JSON_AVAILABLE_IN_1_0 +const gchar * json_array_get_string_element (JsonArray *array, + guint index_); +JSON_AVAILABLE_IN_1_0 +gboolean json_array_get_null_element (JsonArray *array, + guint index_); +JSON_AVAILABLE_IN_1_0 +JsonArray * json_array_get_array_element (JsonArray *array, + guint index_); +JSON_AVAILABLE_IN_1_0 +JsonObject * json_array_get_object_element (JsonArray *array, + guint index_); +JSON_AVAILABLE_IN_1_0 +JsonNode * json_array_dup_element (JsonArray *array, + guint index_); +JSON_AVAILABLE_IN_1_0 +void json_array_remove_element (JsonArray *array, + guint index_); +JSON_AVAILABLE_IN_1_0 +guint json_array_get_length (JsonArray *array); +JSON_AVAILABLE_IN_1_0 +void json_array_foreach_element (JsonArray *array, + JsonArrayForeach func, + gpointer data); +JSON_AVAILABLE_IN_1_2 +void json_array_seal (JsonArray *array); +JSON_AVAILABLE_IN_1_2 +gboolean json_array_is_immutable (JsonArray *array); + +JSON_AVAILABLE_IN_1_2 +guint json_array_hash (gconstpointer key); +JSON_AVAILABLE_IN_1_2 +gboolean json_array_equal (gconstpointer a, + gconstpointer b); + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC (JsonArray, json_array_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (JsonObject, json_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (JsonNode, json_node_unref) +#endif + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-utils.c b/lsp/deps/json-glib/json-utils.c new file mode 100644 index 000000000..72c78e58d --- /dev/null +++ b/lsp/deps/json-glib/json-utils.c @@ -0,0 +1,95 @@ +/* json-utils.c - JSON utility API + * + * This file is part of JSON-GLib + * Copyright 2015 Emmanuele Bassi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "config.h" + +#include "json-utils.h" +#include "json-parser.h" +#include "json-generator.h" + +/** + * json_from_string: + * @str: a valid UTF-8 string containing JSON data + * @error: return location for a #GError + * + * Parses the given string and returns the corresponding JSON tree. + * + * If the string is empty, this function will return `NULL`. + * + * In case of parsing error, this function returns `NULL` and sets + * the error appropriately. + * + * Returns: (transfer full) (nullable): the root node of the JSON tree + * + * Since: 1.2 + */ +JsonNode * +json_from_string (const char *str, + GError **error) +{ + JsonParser *parser; + JsonNode *retval; + + g_return_val_if_fail (str != NULL, NULL); + + parser = json_parser_new (); + if (!json_parser_load_from_data (parser, str, -1, error)) + { + g_object_unref (parser); + return NULL; + } + + retval = json_parser_steal_root (parser); + + g_object_unref (parser); + + return retval; +} + +/** + * json_to_string: + * @node: a JSON tree + * @pretty: whether the output should be prettyfied for printing + * + * Generates a stringified JSON representation of the contents of + * the given `node`. + * + * Returns: (transfer full): the string representation of the node + * + * Since: 1.2 + */ +char * +json_to_string (JsonNode *node, + gboolean pretty) +{ + JsonGenerator *generator; + char *retval; + + g_return_val_if_fail (node != NULL, NULL); + + generator = json_generator_new (); + json_generator_set_pretty (generator, pretty); + json_generator_set_root (generator, node); + + retval = json_generator_to_data (generator, NULL); + + g_object_unref (generator); + + return retval; +} diff --git a/lsp/deps/json-glib/json-utils.h b/lsp/deps/json-glib/json-utils.h new file mode 100644 index 000000000..56b470b09 --- /dev/null +++ b/lsp/deps/json-glib/json-utils.h @@ -0,0 +1,37 @@ +/* json-utils.h - JSON utility API + * + * This file is part of JSON-GLib + * Copyright 2015 Emmanuele Bassi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +#if !defined(__JSON_GLIB_INSIDE__) && !defined(JSON_COMPILATION) +#error "Only can be included directly." +#endif + +#include + +G_BEGIN_DECLS + +JSON_AVAILABLE_IN_1_2 +JsonNode * json_from_string (const char *str, + GError **error); +JSON_AVAILABLE_IN_1_2 +char * json_to_string (JsonNode *node, + gboolean pretty); + +G_END_DECLS diff --git a/lsp/deps/json-glib/json-value.c b/lsp/deps/json-glib/json-value.c new file mode 100644 index 000000000..3b02c35b2 --- /dev/null +++ b/lsp/deps/json-glib/json-value.c @@ -0,0 +1,280 @@ +/* json-value.c - JSON value container + * + * This file is part of JSON-GLib + * Copyright (C) 2012 Emmanuele Bassi + * Copyright (C) 2015 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + * Philip Withnall + */ + +#include "config.h" + +#include + +#include "json-types-private.h" + +const gchar * +json_value_type_get_name (JsonValueType value_type) +{ + switch (value_type) + { + case JSON_VALUE_INVALID: + return "Unset"; + + case JSON_VALUE_INT: + return "Integer"; + + case JSON_VALUE_DOUBLE: + return "Floating Point"; + + case JSON_VALUE_BOOLEAN: + return "Boolean"; + + case JSON_VALUE_STRING: + return "String"; + + case JSON_VALUE_NULL: + return "Null"; + } + + return "Undefined"; +} + +GType +json_value_type (const JsonValue *value) +{ + switch (value->type) + { + case JSON_VALUE_INVALID: + return G_TYPE_INVALID; + + case JSON_VALUE_INT: + return G_TYPE_INT64; + + case JSON_VALUE_DOUBLE: + return G_TYPE_DOUBLE; + + case JSON_VALUE_BOOLEAN: + return G_TYPE_BOOLEAN; + + case JSON_VALUE_STRING: + return G_TYPE_STRING; + + case JSON_VALUE_NULL: + return G_TYPE_INVALID; + } + + return G_TYPE_INVALID; +} + +JsonValue * +json_value_alloc (void) +{ + JsonValue *res = g_slice_new0 (JsonValue); + + res->ref_count = 1; + + return res; +} + +JsonValue * +json_value_init (JsonValue *value, + JsonValueType value_type) +{ + g_return_val_if_fail (value != NULL, NULL); + + if (value->type != JSON_VALUE_INVALID) + json_value_unset (value); + + value->type = value_type; + + return value; +} + +JsonValue * +json_value_ref (JsonValue *value) +{ + g_return_val_if_fail (value != NULL, NULL); + + value->ref_count++; + + return value; +} + +void +json_value_unref (JsonValue *value) +{ + g_return_if_fail (value != NULL); + + if (--value->ref_count == 0) + json_value_free (value); +} + +void +json_value_unset (JsonValue *value) +{ + g_return_if_fail (value != NULL); + + switch (value->type) + { + case JSON_VALUE_INVALID: + break; + + case JSON_VALUE_INT: + value->data.v_int = 0; + break; + + case JSON_VALUE_DOUBLE: + value->data.v_double = 0.0; + break; + + case JSON_VALUE_BOOLEAN: + value->data.v_bool = FALSE; + break; + + case JSON_VALUE_STRING: + g_free (value->data.v_str); + value->data.v_str = NULL; + break; + + case JSON_VALUE_NULL: + break; + } +} + +void +json_value_free (JsonValue *value) +{ + if (G_LIKELY (value != NULL)) + { + json_value_unset (value); + g_slice_free (JsonValue, value); + } +} + +/*< private > + * json_value_seal: + * @value: a JSON scalar value + * + * Seals the value, making it immutable to further changes. + * + * If the value is already immutable, this is a no-op. + */ +void +json_value_seal (JsonValue *value) +{ + g_return_if_fail (JSON_VALUE_IS_VALID (value)); + g_return_if_fail (value->ref_count > 0); + + value->immutable = TRUE; +} + +guint +json_value_hash (gconstpointer key) +{ + JsonValue *value; + guint value_hash; + guint type_hash; + + value = (JsonValue *) key; + + /* Hash the type and value separately. + * Use the top 3 bits to store the type. */ + type_hash = value->type << (sizeof (guint) * 8 - 3); + + switch (value->type) + { + case JSON_VALUE_NULL: + value_hash = 0; + break; + case JSON_VALUE_BOOLEAN: + value_hash = json_value_get_boolean (value) ? 1 : 0; + break; + case JSON_VALUE_STRING: + value_hash = json_string_hash (json_value_get_string (value)); + break; + case JSON_VALUE_INT: { + gint64 v = json_value_get_int (value); + value_hash = g_int64_hash (&v); + break; + } + case JSON_VALUE_DOUBLE: { + gdouble v = json_value_get_double (value); + value_hash = g_double_hash (&v); + break; + } + case JSON_VALUE_INVALID: + default: + g_assert_not_reached (); + } + + /* Mask out the top 3 bits of the @value_hash. */ + value_hash &= ~(7u << (sizeof (guint) * 8 - 3)); + + return (type_hash | value_hash); +} + +#define _JSON_VALUE_DEFINE_SET(Type,EType,CType,VField) \ +void \ +json_value_set_##Type (JsonValue *value, CType VField) \ +{ \ + g_return_if_fail (JSON_VALUE_IS_VALID (value)); \ + g_return_if_fail (JSON_VALUE_HOLDS (value, JSON_VALUE_##EType)); \ + g_return_if_fail (!value->immutable); \ +\ + value->data.VField = VField; \ +\ +} + +#define _JSON_VALUE_DEFINE_GET(Type,EType,CType,VField) \ +CType \ +json_value_get_##Type (const JsonValue *value) \ +{ \ + g_return_val_if_fail (JSON_VALUE_IS_VALID (value), 0); \ + g_return_val_if_fail (JSON_VALUE_HOLDS (value, JSON_VALUE_##EType), 0); \ +\ + return value->data.VField; \ +} + +#define _JSON_VALUE_DEFINE_SET_GET(Type,EType,CType,VField) \ +_JSON_VALUE_DEFINE_SET(Type,EType,CType,VField) \ +_JSON_VALUE_DEFINE_GET(Type,EType,CType,VField) + +_JSON_VALUE_DEFINE_SET_GET(int, INT, gint64, v_int) + +_JSON_VALUE_DEFINE_SET_GET(double, DOUBLE, gdouble, v_double) + +_JSON_VALUE_DEFINE_SET_GET(boolean, BOOLEAN, gboolean, v_bool) + +void +json_value_set_string (JsonValue *value, + const gchar *v_str) +{ + g_return_if_fail (JSON_VALUE_IS_VALID (value)); + g_return_if_fail (JSON_VALUE_HOLDS_STRING (value)); + g_return_if_fail (!value->immutable); + + g_free (value->data.v_str); + value->data.v_str = g_strdup (v_str); +} + +_JSON_VALUE_DEFINE_GET(string, STRING, const gchar *, v_str) + +#undef _JSON_VALUE_DEFINE_SET_GET +#undef _JSON_VALUE_DEFINE_GET +#undef _JSON_VALUE_DEFINE_SET diff --git a/lsp/deps/json-glib/json-version-macros.h b/lsp/deps/json-glib/json-version-macros.h new file mode 100644 index 000000000..be7222c39 --- /dev/null +++ b/lsp/deps/json-glib/json-version-macros.h @@ -0,0 +1,261 @@ +/* json-version-macros.h - JSON-GLib symbol versioning macros + * + * This file is part of JSON-GLib + * Copyright © 2014 Emmanuele Bassi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +#if !defined(__JSON_GLIB_INSIDE__) && !defined(JSON_COMPILATION) +#error "Only can be included directly." +#endif + +#include "json-version.h" + +#ifndef _JSON_EXTERN +#define _JSON_EXTERN extern +#endif + +#ifdef JSON_DISABLE_DEPRECATION_WARNINGS +#define JSON_DEPRECATED _JSON_EXTERN +#define JSON_DEPRECATED_FOR(f) _JSON_EXTERN +#define JSON_UNAVAILABLE(maj,min) _JSON_EXTERN +#else +#define JSON_DEPRECATED G_DEPRECATED _JSON_EXTERN +#define JSON_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f) _JSON_EXTERN +#define JSON_UNAVAILABLE(maj,min) G_UNAVAILABLE(maj,min) _JSON_EXTERN +#endif + +/* XXX: Each new cycle should add a new version symbol here */ +/** + * JSON_VERSION_1_0: + * + * The encoded representation of JSON-GLib version "1.0". + * + * Since: 1.0 + */ +#define JSON_VERSION_1_0 (G_ENCODE_VERSION (1, 0)) + +/** + * JSON_VERSION_1_2: + * + * The encoded representation of JSON-GLib version "1.2". + * + * Since: 1.2 + */ +#define JSON_VERSION_1_2 (G_ENCODE_VERSION (1, 2)) + +/** + * JSON_VERSION_1_4: + * + * The encoded representation of JSON-GLib version "1.4". + * + * Since: 1.4 + */ +#define JSON_VERSION_1_4 (G_ENCODE_VERSION (1, 4)) + +/** + * JSON_VERSION_1_6: + * + * The encoded representation of JSON-GLib version "1.6". + * + * Since: 1.6 + */ +#define JSON_VERSION_1_6 (G_ENCODE_VERSION (1, 6)) + +/** + * JSON_VERSION_1_8: + * + * The encoded representation of JSON-GLib version "1.8". + * + * Since: 1.8 + */ +#define JSON_VERSION_1_8 (G_ENCODE_VERSION (1, 8)) + +/** + * JSON_VERSION_1_10: + * + * The encoded representation of JSON-GLib version "1.10". + * + * Since: 1.10 + */ +#define JSON_VERSION_1_10 (G_ENCODE_VERSION (1, 10)) + +/* evaluates to the current stable version; for development cycles, + * this means the next stable target + */ +#if (JSON_MINOR_VERSION == 99) +#define JSON_VERSION_CUR_STABLE (G_ENCODE_VERSION (JSON_MAJOR_VERSION + 1, 0)) +#elif (JSON_MINOR_VERSION % 2) +#define JSON_VERSION_CUR_STABLE (G_ENCODE_VERSION (JSON_MAJOR_VERSION, JSON_MINOR_VERSION + 1)) +#else +#define JSON_VERSION_CUR_STABLE (G_ENCODE_VERSION (JSON_MAJOR_VERSION, JSON_MINOR_VERSION)) +#endif + +/* evaluates to the previous stable version */ +#if (JSON_MINOR_VERSION == 99) +#define JSON_VERSION_PREV_STABLE (G_ENCODE_VERSION (JSON_MAJOR_VERSION + 1, 0)) +#elif (JSON_MINOR_VERSION % 2) +#define JSON_VERSION_PREV_STABLE (G_ENCODE_VERSION (JSON_MAJOR_VERSION, JSON_MINOR_VERSION - 1)) +#else +#define JSON_VERSION_PREV_STABLE (G_ENCODE_VERSION (JSON_MAJOR_VERSION, JSON_MINOR_VERSION - 2)) +#endif + +/** + * JSON_VERSION_MIN_REQUIRED: + * + * A macro that should be defined by the user prior to including + * the `json-glib/json-glib.h` header. + * + * The definition should be one of the predefined JSON-GLib version + * macros: `JSON_VERSION_1_0`, `JSON_VERSION_1_2`, ... + * + * This macro defines the lower bound for the JSON-GLib API to use. + * + * If a function has been deprecated in a newer version of JSON-GLib, + * it is possible to use this symbol to avoid the compiler warnings + * without disabling warning for every deprecated function. + * + * Since: 1.0 + */ +#ifndef JSON_VERSION_MIN_REQUIRED +# define JSON_VERSION_MIN_REQUIRED (JSON_VERSION_CUR_STABLE) +#endif + +/** + * JSON_VERSION_MAX_ALLOWED: + * + * A macro that should be defined by the user prior to including + * the `json-glib/json-glib.h` header. + + * The definition should be one of the predefined JSON-GLib version + * macros: `JSON_VERSION_1_0`, `JSON_VERSION_1_2`, ... + * + * This macro defines the upper bound for the JSON API-GLib to use. + * + * If a function has been introduced in a newer version of JSON-GLib, + * it is possible to use this symbol to get compiler warnings when + * trying to use that function. + * + * Since: 1.0 + */ +#ifndef JSON_VERSION_MAX_ALLOWED +# if JSON_VERSION_MIN_REQUIRED > JSON_VERSION_PREV_STABLE +# define JSON_VERSION_MAX_ALLOWED (JSON_VERSION_MIN_REQUIRED) +# else +# define JSON_VERSION_MAX_ALLOWED (JSON_VERSION_CUR_STABLE) +# endif +#endif + +/* sanity checks */ +#if JSON_VERSION_MAX_ALLOWED < JSON_VERSION_MIN_REQUIRED +#error "JSON_VERSION_MAX_ALLOWED must be >= JSON_VERSION_MIN_REQUIRED" +#endif +#if JSON_VERSION_MIN_REQUIRED < JSON_VERSION_1_0 +#error "JSON_VERSION_MIN_REQUIRED must be >= JSON_VERSION_1_0" +#endif + +/* XXX: Every new stable minor release should add a set of macros here */ + +/* 1.0 */ +#if JSON_VERSION_MIN_REQUIRED >= JSON_VERSION_1_0 +# define JSON_DEPRECATED_IN_1_0 JSON_DEPRECATED +# define JSON_DEPRECATED_IN_1_0_FOR(f) JSON_DEPRECATED_FOR(f) +#else +# define JSON_DEPRECATED_IN_1_0 _JSON_EXTERN +# define JSON_DEPRECATED_IN_1_0_FOR(f) _JSON_EXTERN +#endif + +#if JSON_VERSION_MAX_ALLOWED < JSON_VERSION_1_0 +# define JSON_AVAILABLE_IN_1_0 JSON_UNAVAILABLE(1, 0) +#else +# define JSON_AVAILABLE_IN_1_0 _JSON_EXTERN +#endif + +/* 1.2 */ +#if JSON_VERSION_MIN_REQUIRED >= JSON_VERSION_1_2 +# define JSON_DEPRECATED_IN_1_2 JSON_DEPRECATED +# define JSON_DEPRECATED_IN_1_2_FOR(f) JSON_DEPRECATED_FOR(f) +#else +# define JSON_DEPRECATED_IN_1_2 _JSON_EXTERN +# define JSON_DEPRECATED_IN_1_2_FOR(f) _JSON_EXTERN +#endif + +#if JSON_VERSION_MAX_ALLOWED < JSON_VERSION_1_2 +# define JSON_AVAILABLE_IN_1_2 JSON_UNAVAILABLE(1, 2) +#else +# define JSON_AVAILABLE_IN_1_2 _JSON_EXTERN +#endif + +/* 1.4 */ +#if JSON_VERSION_MIN_REQUIRED >= JSON_VERSION_1_4 +# define JSON_DEPRECATED_IN_1_4 JSON_DEPRECATED +# define JSON_DEPRECATED_IN_1_4_FOR(f) JSON_DEPRECATED_FOR(f) +#else +# define JSON_DEPRECATED_IN_1_4 _JSON_EXTERN +# define JSON_DEPRECATED_IN_1_4_FOR(f) _JSON_EXTERN +#endif + +#if JSON_VERSION_MAX_ALLOWED < JSON_VERSION_1_4 +# define JSON_AVAILABLE_IN_1_4 JSON_UNAVAILABLE(1, 4) +#else +# define JSON_AVAILABLE_IN_1_4 _JSON_EXTERN +#endif + +/* 1.6 */ +#if JSON_VERSION_MIN_REQUIRED >= JSON_VERSION_1_6 +# define JSON_DEPRECATED_IN_1_6 JSON_DEPRECATED +# define JSON_DEPRECATED_IN_1_6_FOR(f) JSON_DEPRECATED_FOR(f) +#else +# define JSON_DEPRECATED_IN_1_6 _JSON_EXTERN +# define JSON_DEPRECATED_IN_1_6_FOR(f) _JSON_EXTERN +#endif + +#if JSON_VERSION_MAX_ALLOWED < JSON_VERSION_1_6 +# define JSON_AVAILABLE_IN_1_6 JSON_UNAVAILABLE(1, 6) +#else +# define JSON_AVAILABLE_IN_1_6 _JSON_EXTERN +#endif + +/* 1.8 */ +#if JSON_VERSION_MIN_REQUIRED >= JSON_VERSION_1_8 +# define JSON_DEPRECATED_IN_1_8 JSON_DEPRECATED +# define JSON_DEPRECATED_IN_1_8_FOR(f) JSON_DEPRECATED_FOR(f) +#else +# define JSON_DEPRECATED_IN_1_8 _JSON_EXTERN +# define JSON_DEPRECATED_IN_1_8_FOR(f) _JSON_EXTERN +#endif + +#if JSON_VERSION_MAX_ALLOWED < JSON_VERSION_1_8 +# define JSON_AVAILABLE_IN_1_8 JSON_UNAVAILABLE(1, 8) +#else +# define JSON_AVAILABLE_IN_1_8 _JSON_EXTERN +#endif + +/* 1.10 */ +#if JSON_VERSION_MIN_REQUIRED >= JSON_VERSION_1_10 +# define JSON_DEPRECATED_IN_1_10 JSON_DEPRECATED +# define JSON_DEPRECATED_IN_1_10_FOR(f) JSON_DEPRECATED_FOR(f) +#else +# define JSON_DEPRECATED_IN_1_10 _JSON_EXTERN +# define JSON_DEPRECATED_IN_1_10_FOR(f) _JSON_EXTERN +#endif + +#if JSON_VERSION_MAX_ALLOWED < JSON_VERSION_1_10 +# define JSON_AVAILABLE_IN_1_10 JSON_UNAVAILABLE(1, 10) +#else +# define JSON_AVAILABLE_IN_1_10 _JSON_EXTERN +#endif diff --git a/lsp/deps/json-glib/json-version.h b/lsp/deps/json-glib/json-version.h new file mode 100644 index 000000000..5d028e040 --- /dev/null +++ b/lsp/deps/json-glib/json-version.h @@ -0,0 +1,100 @@ +/* json-version.h - JSON-GLib versioning information + * + * This file is part of JSON-GLib + * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2009 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: + * Emmanuele Bassi + */ + +#pragma once + +#if !defined(__JSON_GLIB_INSIDE__) && !defined(JSON_COMPILATION) +#error "Only can be included directly." +#endif + +/** + * JSON_MAJOR_VERSION: + * + * Json major version component (e.g. 1 if `JSON_VERSION` is "1.2.3") + */ +#define JSON_MAJOR_VERSION (1) + +/** + * JSON_MINOR_VERSION: + * + * Json minor version component (e.g. 2 if `JSON_VERSION` is "1.2.3") + */ +#define JSON_MINOR_VERSION (9) + +/** + * JSON_MICRO_VERSION: + * + * Json micro version component (e.g. 3 if `JSON_VERSION` is "1.2.3") + */ +#define JSON_MICRO_VERSION (1) + +/** + * JSON_VERSION + * + * The version of JSON-GLib. + */ +#define JSON_VERSION (1.9.1) + +/** + * JSON_VERSION_S: + * + * The version of JSON-GLib, encoded as a string, useful for printing and + * concatenation. + */ +#define JSON_VERSION_S "1.9.1" + +/** + * JSON_ENCODE_VERSION: + * @major: (type int): the major version to encode + * @minor: (type int): the minor version to encode + * @micro: (type int): the micro version to encode + * + * Encodes a JSON-GLib version in an hexadecimal number, useful for + * integer comparisons. + */ +#define JSON_ENCODE_VERSION(major,minor,micro) \ + ((major) << 24 | (minor) << 16 | (micro) << 8) + +/** + * JSON_VERSION_HEX: + * + * The version of JSON-GLib, encoded as an hexadecimal number, useful for + * integer comparisons. + */ +#define JSON_VERSION_HEX \ + (JSON_ENCODE_VERSION (JSON_MAJOR_VERSION, JSON_MINOR_VERSION, JSON_MICRO_VERSION)) + +/** + * JSON_CHECK_VERSION: + * @major: required major version + * @minor: required minor version + * @micro: required micro version + * + * Compile-time version checking. Evaluates to `TRUE` if the version + * of JSON-GLib is greater than the required one. + */ +#define JSON_CHECK_VERSION(major,minor,micro) \ + (JSON_MAJOR_VERSION > (major) || \ + (JSON_MAJOR_VERSION == (major) && JSON_MINOR_VERSION > (minor)) || \ + (JSON_MAJOR_VERSION == (major) && JSON_MINOR_VERSION == (minor) && \ + JSON_MICRO_VERSION >= (micro))) diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-client.c b/lsp/deps/jsonrpc-glib/jsonrpc-client.c new file mode 100644 index 000000000..873f23050 --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-client.c @@ -0,0 +1,1752 @@ +/* jsonrpc-client.c + * + * Copyright (C) 2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +//#define G_LOG_DOMAIN "jsonrpc-client" + +#include "config.h" + +/** + * JsonrpcClient: + * + * A client for JSON-RPC communication + * + * The #JsonrpcClient class provides a convenient API to coordinate with a + * JSON-RPC server. You can provide the underlying [class@Gio.IOStream] to communicate + * with allowing you to control the negotiation of how you setup your + * communications channel. One such method might be to use a [class@Gio.Subprocess] and + * communicate over stdin and stdout. + * + * Because JSON-RPC allows for out-of-band notifications from the server to + * the client, it is important that the consumer of this API calls + * [method@Client.close] or [method@Client.close_async] when they no longer + * need the client. This is because #JsonrpcClient contains an asynchronous + * read-loop to process incoming messages. Until [method@Client.close] or + * [method@Client.close_async] have been called, this read loop will prevent + * the object from finalizing (being freed). + * + * To make an RPC call, use [method@Client.call] or + * [method@Client.call_async] and provide the method name and the parameters + * as a [struct@GLib.Variant] for call. + * + * It is a programming error to mix synchronous and asynchronous API calls + * of the #JsonrpcClient class. + * + * For synchronous calls, #JsonrpcClient will use the thread-default + * [struct@GLib.MainContext]. If you have special needs here ensure you've set the context + * before calling into any #JsonrpcClient API. + * + * Since: 3.26 + */ + +#include + +#include "jsonrpc-client.h" +#include "jsonrpc-input-stream.h" +#include "jsonrpc-input-stream-private.h" +#include "jsonrpc-marshalers.h" +#include "jsonrpc-output-stream.h" + +typedef struct +{ + /* + * The invocations field contains a hashtable that maps request ids to + * the GTask that is awaiting their completion. The tasks are removed + * from the hashtable automatically upon completion by connecting to + * the GTask::completed signal. When reading a message from the input + * stream, we use the request id as a string to lookup the inflight + * invocation. The result is passed as the result of the task. + */ + GHashTable *invocations; + + /* + * We hold an extra reference to the GIOStream pair to make things + * easier to construct and ensure that the streams are in tact in + * case they are poorly implemented. + */ + GIOStream *io_stream; + + /* + * The input_stream field contains our wrapper input stream around the + * underlying input stream provided by JsonrpcClient::io-stream. This + * allows us to conveniently write GVariant instances. + */ + JsonrpcInputStream *input_stream; + + /* + * The output_stream field contains our wrapper output stream around the + * underlying output stream provided by JsonrpcClient::io-stream. This + * allows us to convieniently read GVariant instances. + */ + JsonrpcOutputStream *output_stream; + + /* + * This cancellable is used for our async read loops so that we can + * cancel the operation to shutdown the client. Otherwise, we would + * indefinitely leak our client due to the self-reference on our + * read loop user_data parameter. + */ + GCancellable *read_loop_cancellable; + + /* + * Every JSONRPC invocation needs a request id. This is a monotonic + * integer that we encode as a string to the server. + */ + gint64 sequence; + + /* + * This bit indicates if we have sent a call yet. Once we send our + * first call, we start our read loop which will allow us to also + * dispatch notifications out of band. + */ + guint is_first_call : 1; + + /* + * This bit is set when the program has called jsonrpc_client_close() + * or jsonrpc_client_close_async(). When the read loop returns, it + * will check for this and discontinue further asynchronous reads. + */ + guint in_shutdown : 1; + + /* + * If we have panic'd, this will be set to TRUE so that we can short + * circuit on future operations sooner. + */ + guint failed : 1; + + /* + * Only set once we've emitted the ::failed signal (so we only do that + * a single time). + */ + guint emitted_failed : 1; + + /* + * If we should try to use gvariant encoding when communicating with + * our peer. This is helpful to be able to lower parser and memory + * overhead. + */ + guint use_gvariant : 1; +} JsonrpcClientPrivate; + +typedef struct +{ + GHashTable *invocations; + GError *error; +} PanicData; + +G_DEFINE_TYPE_WITH_PRIVATE (JsonrpcClient, jsonrpc_client, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_IO_STREAM, + PROP_USE_GVARIANT, + N_PROPS +}; + +enum { + FAILED, + HANDLE_CALL, + NOTIFICATION, + N_SIGNALS +}; + +static GParamSpec *properties [N_PROPS]; +static guint signals [N_SIGNALS]; + +/* + * Check to see if this looks like a jsonrpc 2.0 reply of any kind. + */ +static gboolean +is_jsonrpc_reply (GVariantDict *dict) +{ + const gchar *value = NULL; + + g_assert (dict != NULL); + + return (g_variant_dict_contains (dict, "jsonrpc") && + g_variant_dict_lookup (dict, "jsonrpc", "&s", &value) && + g_str_equal (value, "2.0")); +} + +/* + * Check to see if this looks like a notification reply. + */ +static gboolean +is_jsonrpc_notification (GVariantDict *dict) +{ + const gchar *method = NULL; + + g_assert (dict != NULL); + + return (!g_variant_dict_contains (dict, "id") && + g_variant_dict_contains (dict, "method") && + g_variant_dict_lookup (dict, "method", "&s", &method) && + method != NULL && *method != '\0'); +} + +/* + * Check to see if this looks like a proper result for an RPC. + */ +static gboolean +is_jsonrpc_result (GVariantDict *dict) +{ + g_assert (dict != NULL); + + return (g_variant_dict_contains (dict, "id") && + g_variant_dict_contains (dict, "result")); +} + + +/* + * Check to see if this looks like a proper method call for an RPC. + */ +static gboolean +is_jsonrpc_call (GVariantDict *dict) +{ + const gchar *method = NULL; + + g_assert (dict != NULL); + + return (g_variant_dict_contains (dict, "id") && + g_variant_dict_contains (dict, "method") && + g_variant_dict_lookup (dict, "method", "&s", &method) && + g_variant_dict_contains (dict, "params")); +} + +static gboolean +error_invocations_from_idle (gpointer data) +{ + PanicData *pd = data; + GHashTableIter iter; + GTask *task; + + g_assert (pd != NULL); + g_assert (pd->invocations != NULL); + g_assert (pd->error != NULL); + + g_hash_table_iter_init (&iter, pd->invocations); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&task)) + { + if (!g_task_get_completed (task)) + g_task_return_error (task, g_error_copy (pd->error)); + } + + g_clear_pointer (&pd->invocations, g_hash_table_unref); + g_clear_pointer (&pd->error, g_error_free); + g_slice_free (PanicData, pd); + + return G_SOURCE_REMOVE; +} + +static void +cancel_pending_from_main (JsonrpcClient *self, + const GError *error) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + PanicData *pd; + + g_assert (JSONRPC_IS_CLIENT (self)); + g_assert (error != NULL); + + /* + * Defer the completion of all tasks (with errors) until we've made it + * back to the main loop. Otherwise, we get into difficult to determine + * re-entrancy cases. + */ + pd = g_slice_new0 (PanicData); + pd->invocations = g_steal_pointer (&priv->invocations); + pd->error = g_error_copy (error); + g_idle_add_full (G_MAXINT, error_invocations_from_idle, pd, NULL); + + /* Keep a hashtable around for code that expects a pointer there */ + priv->invocations = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref); +} + +static gboolean +emit_failed_from_main (JsonrpcClient *self) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + + g_assert (JSONRPC_IS_CLIENT (self)); + + if (!priv->emitted_failed) + { + priv->emitted_failed = TRUE; + g_signal_emit (self, signals [FAILED], 0); + } + + return G_SOURCE_REMOVE; +} + +/* + * jsonrpc_client_panic: + * + * This function should be called to "tear down everything" and ensure we + * cleanup. + */ +static void +jsonrpc_client_panic (JsonrpcClient *self, + const GError *error) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + g_autoptr(JsonrpcClient) hold = NULL; + + g_assert (JSONRPC_IS_CLIENT (self)); + g_assert (error != NULL); + + hold = g_object_ref (self); + + priv->failed = TRUE; + + cancel_pending_from_main (self, error); + + /* Now close the connection */ + jsonrpc_client_close (self, NULL, NULL); + + /* + * Clear our input and output streams so that new calls + * fail immediately due to not being connected. + */ + g_clear_object (&priv->input_stream); + g_clear_object (&priv->output_stream); + + /* + * Queue a "failed" signal from a main loop callback so that we don't + * get the client into weird stuff from signal callbacks here. + */ + if (!priv->emitted_failed) + g_idle_add_full (G_MAXINT, + (GSourceFunc)emit_failed_from_main, + g_object_ref (self), + g_object_unref); +} + +/* + * jsonrpc_client_check_ready: + * + * Checks to see if the client is in a position to make requests. + * + * Returns: %TRUE if the client is ready for RPCs; otherwise %FALSE + * and @error is set. + */ +static gboolean +jsonrpc_client_check_ready (JsonrpcClient *self, + GError **error) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + + g_assert (JSONRPC_IS_CLIENT (self)); + + if (priv->failed || priv->in_shutdown || priv->output_stream == NULL || priv->input_stream == NULL) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_CONNECTED, + "No stream available to deliver invocation"); + return FALSE; + } + + return TRUE; +} + +static void +jsonrpc_client_constructed (GObject *object) +{ + JsonrpcClient *self = (JsonrpcClient *)object; + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + GInputStream *input_stream; + GOutputStream *output_stream; + + G_OBJECT_CLASS (jsonrpc_client_parent_class)->constructed (object); + + if (priv->io_stream == NULL) + { + g_warning ("%s requires a GIOStream to communicate. Disabling.", + G_OBJECT_TYPE_NAME (self)); + return; + } + + input_stream = g_io_stream_get_input_stream (priv->io_stream); + output_stream = g_io_stream_get_output_stream (priv->io_stream); + + priv->input_stream = jsonrpc_input_stream_new (input_stream); + priv->output_stream = jsonrpc_output_stream_new (output_stream); +} + +static void +jsonrpc_client_dispose (GObject *object) +{ + JsonrpcClient *self = (JsonrpcClient *)object; + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + + g_clear_pointer (&priv->invocations, g_hash_table_unref); + + g_clear_object (&priv->input_stream); + g_clear_object (&priv->output_stream); + g_clear_object (&priv->io_stream); + g_clear_object (&priv->read_loop_cancellable); + + G_OBJECT_CLASS (jsonrpc_client_parent_class)->dispose (object); +} + +static void +jsonrpc_client_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + JsonrpcClient *self = JSONRPC_CLIENT (object); + + switch (prop_id) + { + case PROP_USE_GVARIANT: + g_value_set_boolean (value, jsonrpc_client_get_use_gvariant (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +jsonrpc_client_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + JsonrpcClient *self = JSONRPC_CLIENT (object); + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + + switch (prop_id) + { + case PROP_IO_STREAM: + priv->io_stream = g_value_dup_object (value); + break; + + case PROP_USE_GVARIANT: + jsonrpc_client_set_use_gvariant (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +jsonrpc_client_class_init (JsonrpcClientClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = jsonrpc_client_constructed; + object_class->dispose = jsonrpc_client_dispose; + object_class->get_property = jsonrpc_client_get_property; + object_class->set_property = jsonrpc_client_set_property; + + /** + * JsonrpcClient:io-stream: + * + * The "io-stream" property is the [class@Gio.IOStream] to use for communicating + * with a JSON-RPC peer. + * + * Since: 3.26 + */ + properties [PROP_IO_STREAM] = + g_param_spec_object ("io-stream", + "IO Stream", + "The stream to communicate over", + G_TYPE_IO_STREAM, + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + /** + * JsonrpcClient:use-gvariant: + * + * The "use-gvariant" property denotes if [struct@GLib.Variant] should be used to + * communicate with the peer instead of JSON. You should only set this + * if you know the peer is also a Jsonrpc-GLib based client. + * + * Setting this property allows the peers to communicate using GVariant + * instead of JSON. This means that we can access the messages without + * expensive memory allocations and parsing costs associated with JSON. + * [struct@GLib.Variant] is much more optimal for memory-bassed message passing. + * + * Since: 3.26 + */ + properties [PROP_USE_GVARIANT] = + g_param_spec_boolean ("use-gvariant", + "Use GVariant", + "If GVariant encoding should be used", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + /** + * JsonrpcClient::failed: + * + * The "failed" signal is called when the client has failed communication + * or the connection has been knowingly closed. + * + * Since: 3.28 + */ + signals [FAILED] = + g_signal_new ("failed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonrpcClientClass, failed), + NULL, NULL, + NULL, + G_TYPE_NONE, 0); + + /** + * JsonrpcClient::handle-call: + * @self: A #JsonrpcClient + * @method: The method name + * @id: The "id" field of the JSONRPC message + * @params: (nullable): The "params" field of the JSONRPC message + * + * This signal is emitted when an RPC has been received from the peer we + * are connected to. Return %TRUE if you have handled this message, even + * asynchronously. If no handler has returned %TRUE an error will be + * synthesized. + * + * If you handle the message, you are responsible for replying to the peer + * in a timely manner using [method@Client.reply] or [method@Client.reply_async]. + * + * Additionally, since 3.28 you may connect to the "detail" of this signal + * to handle a specific method call. Use the method name as the detail of + * the signal. + * + * Since: 3.26 + */ + signals [HANDLE_CALL] = + g_signal_new ("handle-call", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + G_STRUCT_OFFSET (JsonrpcClientClass, handle_call), + g_signal_accumulator_true_handled, NULL, + _jsonrpc_marshal_BOOLEAN__STRING_VARIANT_VARIANT, + G_TYPE_BOOLEAN, + 3, + G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE, + G_TYPE_VARIANT, + G_TYPE_VARIANT); + g_signal_set_va_marshaller (signals [HANDLE_CALL], + G_TYPE_FROM_CLASS (klass), + _jsonrpc_marshal_BOOLEAN__STRING_VARIANT_VARIANTv); + + /** + * JsonrpcClient::notification: + * @self: A #JsonrpcClient + * @method: The method name of the notification + * @params: (nullable): Params for the notification + * + * This signal is emitted when a notification has been received from a + * peer. Unlike [signal@Client::handle-call], this does not have an "id" + * parameter because notifications do not have ids. They do not round + * trip. + * + * Since: 3.26 + */ + signals [NOTIFICATION] = + g_signal_new ("notification", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + G_STRUCT_OFFSET (JsonrpcClientClass, notification), + NULL, NULL, + _jsonrpc_marshal_VOID__STRING_VARIANT, + G_TYPE_NONE, + 2, + G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE, + G_TYPE_VARIANT); + g_signal_set_va_marshaller (signals [NOTIFICATION], + G_TYPE_FROM_CLASS (klass), + _jsonrpc_marshal_VOID__STRING_VARIANTv); +} + +static void +jsonrpc_client_init (JsonrpcClient *self) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + + priv->invocations = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref); + priv->is_first_call = TRUE; + priv->read_loop_cancellable = g_cancellable_new (); +} + +/** + * jsonrpc_client_new: + * @io_stream: A [class@Gio.IOStream] + * + * Creates a new #JsonrpcClient instance. + * + * If you want to communicate with a process using stdin/stdout, consider using + * [class@Gio.Subprocess] to launch the process and create a [class@Gio.SimpleIOStream] using the + * [method@Gio.Subprocess.get_stdin_pipe] and [method@Gio.Subprocess.get_stdout_pipe]. + * + * Returns: (transfer full): A newly created #JsonrpcClient + * + * Since: 3.26 + */ +JsonrpcClient * +jsonrpc_client_new (GIOStream *io_stream) +{ + g_return_val_if_fail (G_IS_IO_STREAM (io_stream), NULL); + + return g_object_new (JSONRPC_TYPE_CLIENT, + "io-stream", io_stream, + NULL); +} + +static void +jsonrpc_client_remove_from_invocations (JsonrpcClient *self, + GTask *task) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + gpointer id; + + g_assert (JSONRPC_IS_CLIENT (self)); + g_assert (G_IS_TASK (task)); + + id = g_task_get_task_data (task); + + g_hash_table_remove (priv->invocations, id); +} + +static void +jsonrpc_client_call_notify_completed (JsonrpcClient *self, + GParamSpec *pspec, + GTask *task) +{ + g_assert (JSONRPC_IS_CLIENT (self)); + g_assert (pspec != NULL); + g_assert (g_str_equal (pspec->name, "completed")); + g_assert (G_IS_TASK (task)); + + jsonrpc_client_remove_from_invocations (self, task); +} + +static void +jsonrpc_client_call_write_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + JsonrpcOutputStream *stream = (JsonrpcOutputStream *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + JsonrpcClient *self; + + g_assert (JSONRPC_IS_OUTPUT_STREAM (stream)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + g_assert (JSONRPC_IS_CLIENT (self)); + + if (!jsonrpc_output_stream_write_message_finish (stream, result, &error)) + { + /* Panic will cancel our task, no need to return error here */ + jsonrpc_client_panic (self, error); + return; + } + + /* We don't need to complete the task because it will get completed when the + * server replies with our return value. This is performed using an + * asynchronous read that will pump through incoming messages. + */ +} + +static void +jsonrpc_client_call_read_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + JsonrpcInputStream *stream = (JsonrpcInputStream *)object; + g_autoptr(JsonrpcClient) self = user_data; + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + g_autoptr(GVariant) message = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GVariantDict) dict = NULL; + + g_assert (JSONRPC_IS_INPUT_STREAM (stream)); + g_assert (JSONRPC_IS_CLIENT (self)); + + if (!jsonrpc_input_stream_read_message_finish (stream, result, &message, &error)) + { + /* Handle jsonrpc_client_close() conditions gracefully. */ + if (priv->in_shutdown && + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + /* + * If we fail to read a message, that means we couldn't even receive + * a message describing the error. All we can do in this case is panic + * and shutdown the whole client. + */ + jsonrpc_client_panic (self, error); + return; + } + + g_assert (message != NULL); + + /* If we received a gvariant-based message, upgrade connection */ + if (_jsonrpc_input_stream_get_has_seen_gvariant (stream)) + jsonrpc_client_set_use_gvariant (self, TRUE); + + /* Make sure we got a proper type back from the variant. */ + if (!g_variant_is_of_type (message, G_VARIANT_TYPE_VARDICT)) + { + error = g_error_new_literal (G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Improper reply from peer, not a vardict"); + jsonrpc_client_panic (self, error); + return; + } + + dict = g_variant_dict_new (message); + + /* + * If the message is malformed, we'll also need to perform another read. + * We do this to try to be relaxed against failures. That seems to be + * the JSONRPC way, although I'm not sure I like the idea. + */ + if (dict == NULL || !is_jsonrpc_reply (dict)) + { + error = g_error_new_literal (G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Improper reply from peer"); + jsonrpc_client_panic (self, error); + return; + } + + /* + * If the response does not have an "id" field, then it is a "notification" + * and we need to emit the "notificiation" signal. + */ + if (is_jsonrpc_notification (dict)) + { + g_autoptr(GVariant) params = NULL; + const gchar *method_name = NULL; + + if (g_variant_dict_lookup (dict, "method", "&s", &method_name)) + { + GQuark detail = g_quark_try_string (method_name); + + params = g_variant_dict_lookup_value (dict, "params", NULL); + g_signal_emit (self, signals [NOTIFICATION], detail, method_name, params); + } + + goto begin_next_read; + } + + if (is_jsonrpc_result (dict)) + { + g_autoptr(GVariant) params = NULL; + gint64 id = -1; + GTask *task; + + if (!g_variant_dict_lookup (dict, "id", "x", &id) || + NULL == (task = g_hash_table_lookup (priv->invocations, GINT_TO_POINTER (id)))) + { + error = g_error_new_literal (G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Reply to missing or invalid task"); + jsonrpc_client_panic (self, error); + return; + } + + if (NULL != (params = g_variant_dict_lookup_value (dict, "result", NULL))) + g_task_return_pointer (task, g_steal_pointer (¶ms), (GDestroyNotify)g_variant_unref); + else + g_task_return_pointer (task, NULL, NULL); + + goto begin_next_read; + } + + /* + * If this is a method call, emit the handle-call signal. + */ + if (is_jsonrpc_call (dict)) + { + g_autoptr(GVariant) id = NULL; + g_autoptr(GVariant) params = NULL; + const gchar *method_name = NULL; + gboolean ret = FALSE; + GQuark detail; + + if (!g_variant_dict_lookup (dict, "method", "&s", &method_name) || + NULL == (id = g_variant_dict_lookup_value (dict, "id", NULL))) + { + error = g_error_new_literal (G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Call contains invalid method or id field"); + jsonrpc_client_panic (self, error); + return; + } + + params = g_variant_dict_lookup_value (dict, "params", NULL); + + g_assert (method_name != NULL); + g_assert (id != NULL); + + detail = g_quark_try_string (method_name); + g_signal_emit (self, signals [HANDLE_CALL], detail, method_name, id, params, &ret); + + if (ret == FALSE) + jsonrpc_client_reply_error_async (self, id, JSONRPC_CLIENT_ERROR_METHOD_NOT_FOUND, + "The method does not exist or is not available", + NULL, NULL, NULL); + + goto begin_next_read; + } + + /* + * If we got an error destined for one of our inflight invocations, then + * we need to dispatch it now. + */ + + if (g_variant_dict_contains (dict, "id") && + g_variant_dict_contains (dict, "error")) + { + g_autoptr(GVariant) error_variant = NULL; + g_autofree gchar *errstr = NULL; + const char *errmsg = NULL; + gint64 id = -1; + gint64 errcode = -1; + + error_variant = g_variant_dict_lookup_value (dict, "error", NULL); + + if (error_variant != NULL && + g_variant_lookup (error_variant, "message", "&s", &errmsg) && + g_variant_lookup (error_variant, "code", "x", &errcode)) + errstr = g_strdup_printf ("%s (%d)", errmsg, (int)errcode); + else + errstr = g_variant_print (error_variant, FALSE); + + error = g_error_new_literal (JSONRPC_CLIENT_ERROR, errcode, errstr); + + if (g_variant_dict_lookup (dict, "id", "x", &id)) + { + GTask *task = g_hash_table_lookup (priv->invocations, GINT_TO_POINTER (id)); + + if (task != NULL) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_warning ("Received error for task %"G_GINT64_FORMAT" which is unknown", id); + + goto begin_next_read; + } + + /* + * Generic error, not tied to any specific task we had in flight. So + * take this as a failure case and panic on the line. + */ + jsonrpc_client_panic (self, error); + return; + } + + g_warning ("Unhandled RPC from peer!"); + +begin_next_read: + if (priv->input_stream != NULL && + priv->in_shutdown == FALSE && + priv->failed == FALSE) + jsonrpc_input_stream_read_message_async (priv->input_stream, + priv->read_loop_cancellable, + jsonrpc_client_call_read_cb, + g_steal_pointer (&self)); +} + +static void +jsonrpc_client_call_sync_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + JsonrpcClient *self = (JsonrpcClient *)object; + GTask *task = user_data; + g_autoptr(GVariant) return_value = NULL; + g_autoptr(GError) error = NULL; + + g_assert (JSONRPC_IS_CLIENT (self)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (G_IS_TASK (task)); + + if (!jsonrpc_client_call_finish (self, result, &return_value, &error)) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_pointer (task, g_steal_pointer (&return_value), (GDestroyNotify)g_variant_unref); +} + +/** + * jsonrpc_client_call: + * @self: A #JsonrpcClient + * @method: The name of the method to call + * @params: (transfer none) (nullable): A [struct@GLib.Variant] of parameters or %NULL + * @cancellable: (nullable): A #GCancellable or %NULL + * @return_value: (nullable) (out): A location for a [struct@GLib.Variant] + * + * Synchronously calls @method with @params on the remote peer. + * + * once a reply has been received, or failure, this function will return. + * If successful, @return_value will be set with the reslut field of + * the response. + * + * If @params is floating then this function consumes the reference. + * + * Returns: %TRUE on success; otherwise %FALSE and @error is set. + * + * Since: 3.26 + */ +gboolean +jsonrpc_client_call (JsonrpcClient *self, + const gchar *method, + GVariant *params, + GCancellable *cancellable, + GVariant **return_value, + GError **error) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GMainContext) main_context = NULL; + g_autoptr(GVariant) local_return_value = NULL; + gboolean ret; + + g_return_val_if_fail (JSONRPC_IS_CLIENT (self), FALSE); + g_return_val_if_fail (method != NULL, FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + main_context = g_main_context_ref_thread_default (); + + task = g_task_new (self, NULL, NULL, NULL); + g_task_set_source_tag (task, jsonrpc_client_call); + + jsonrpc_client_call_async (self, + method, + params, + cancellable, + jsonrpc_client_call_sync_cb, + task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (main_context, TRUE); + + local_return_value = g_task_propagate_pointer (task, error); + ret = local_return_value != NULL; + + if (return_value != NULL) + *return_value = g_steal_pointer (&local_return_value); + + return ret; +} + +/** + * jsonrpc_client_call_with_id_async: + * @self: A #JsonrpcClient + * @method: The name of the method to call + * @params: (transfer none) (nullable): A [struct@GLib.Variant] of parameters or %NULL + * @id: (out) (transfer full) (optional): A location for a [struct@GLib.Variant] + * describing the identifier used for the method call, or %NULL. + * @cancellable: (nullable): A #GCancellable or %NULL + * @callback: Callback to executed upon completion + * @user_data: User data for @callback + * + * Asynchronously calls @method with @params on the remote peer. + * + * Upon completion or failure, @callback is executed and it should + * call [method@Client.call_finish] to complete the request and release + * any memory held. + * + * This function is similar to [method@Client.call_async] except that + * it allows the caller to get the id of the command which might be useful + * in systems where you can cancel the operation (such as the Language + * Server Protocol). + * + * If @params is floating, the floating reference is consumed. + * + * Since: 3.30 + */ +void +jsonrpc_client_call_with_id_async (JsonrpcClient *self, + const gchar *method, + GVariant *params, + GVariant **id, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + g_autoptr(GVariant) message = NULL; + g_autoptr(GVariant) sunk_variant = NULL; + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + GVariantDict dict; + gint64 idval; + + g_return_if_fail (JSONRPC_IS_CLIENT (self)); + g_return_if_fail (method != NULL); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + if (id != NULL) + *id = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, jsonrpc_client_call_async); + + if (params == NULL) + params = g_variant_new_maybe (G_VARIANT_TYPE_VARIANT, NULL); + + /* If we got a floating reference, we should consume it */ + if (g_variant_is_floating (params)) + sunk_variant = g_variant_ref_sink (params); + + if (!jsonrpc_client_check_ready (self, &error)) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + g_signal_connect_object (task, + "notify::completed", + G_CALLBACK (jsonrpc_client_call_notify_completed), + self, + G_CONNECT_SWAPPED); + + idval = ++priv->sequence; + + g_task_set_task_data (task, GINT_TO_POINTER (idval), NULL); + + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "jsonrpc", "s", "2.0"); + g_variant_dict_insert (&dict, "id", "x", idval); + g_variant_dict_insert (&dict, "method", "s", method); + g_variant_dict_insert_value (&dict, "params", params); + + message = g_variant_take_ref (g_variant_dict_end (&dict)); + + g_hash_table_insert (priv->invocations, GINT_TO_POINTER (idval), g_object_ref (task)); + + jsonrpc_output_stream_write_message_async (priv->output_stream, + message, + cancellable, + jsonrpc_client_call_write_cb, + g_steal_pointer (&task)); + + if (priv->is_first_call) + jsonrpc_client_start_listening (self); + + if (id != NULL) + *id = g_variant_take_ref (g_variant_new_int64 (idval)); +} + +/** + * jsonrpc_client_call_async: + * @self: A #JsonrpcClient + * @method: The name of the method to call + * @params: (transfer none) (nullable): A [struct@GLib.Variant] of parameters or %NULL + * @cancellable: (nullable): A #GCancellable or %NULL + * @callback: a callback to executed upon completion + * @user_data: user data for @callback + * + * Asynchronously calls @method with @params on the remote peer. + * + * Upon completion or failure, @callback is executed and it should + * call [method@Client.call_finish] to complete the request and release + * any memory held. + * + * If @params is floating, the floating reference is consumed. + * + * Since: 3.26 + */ +void +jsonrpc_client_call_async (JsonrpcClient *self, + const gchar *method, + GVariant *params, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + jsonrpc_client_call_with_id_async (self, method, params, NULL, cancellable, callback, user_data); +} + +/** + * jsonrpc_client_call_finish: + * @self: A #JsonrpcClient. + * @result: A #GAsyncResult provided to the callback in [method@Client.call_async] + * @return_value: (out) (nullable): A location for a [struct@GLib.Variant] or %NULL + * @error: a location for a #GError or %NULL + * + * Completes an asynchronous call to [method@Client.call_async]. + * + * Returns: %TRUE if successful and @return_value is set, otherwise %FALSE and @error is set. + * + * Since: 3.26 + */ +gboolean +jsonrpc_client_call_finish (JsonrpcClient *self, + GAsyncResult *result, + GVariant **return_value, + GError **error) +{ + g_autoptr(GVariant) local_return_value = NULL; + gboolean ret; + + g_return_val_if_fail (JSONRPC_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + local_return_value = g_task_propagate_pointer (G_TASK (result), error); + ret = local_return_value != NULL; + + if (return_value != NULL) + *return_value = g_steal_pointer (&local_return_value); + + return ret; +} + +GQuark +jsonrpc_client_error_quark (void) +{ + return g_quark_from_static_string ("jsonrpc-client-error-quark"); +} + +static void +jsonrpc_client_send_notification_write_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + JsonrpcOutputStream *stream = (JsonrpcOutputStream *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + + g_assert (JSONRPC_IS_OUTPUT_STREAM (stream)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (G_IS_TASK (task)); + + if (!jsonrpc_output_stream_write_message_finish (stream, result, &error)) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_boolean (task, TRUE); +} + +/** + * jsonrpc_client_send_notification: + * @self: A #JsonrpcClient + * @method: The name of the method to call + * @params: (transfer none) (nullable): A [struct@GLib.Variant] of parameters or %NULL + * @cancellable: (nullable): A #GCancellable or %NULL + * + * Synchronously calls @method with @params on the remote peer. + * + * This function will not wait or expect a reply from the peer. + * + * If @params is floating then the reference is consumed. + * + * Returns: %TRUE on success; otherwise %FALSE and @error is set. + * + * Since: 3.26 + */ +gboolean +jsonrpc_client_send_notification (JsonrpcClient *self, + const gchar *method, + GVariant *params, + GCancellable *cancellable, + GError **error) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + g_autoptr(GVariant) message = NULL; + GVariantDict dict; + gboolean ret; + + g_return_val_if_fail (JSONRPC_IS_CLIENT (self), FALSE); + g_return_val_if_fail (method != NULL, FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + if (!jsonrpc_client_check_ready (self, error)) + return FALSE; + + /* Use empty maybe type for NULL params. The floating reference will + * be consumed below in g_variant_dict_insert_value(). */ + if (params == NULL) + params = g_variant_new_maybe (G_VARIANT_TYPE_VARIANT, NULL); + + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "jsonrpc", "s", "2.0"); + g_variant_dict_insert (&dict, "method", "s", method); + g_variant_dict_insert_value (&dict, "params", params); + + message = g_variant_take_ref (g_variant_dict_end (&dict)); + + ret = jsonrpc_output_stream_write_message (priv->output_stream, message, cancellable, error); + + return ret; +} + +/** + * jsonrpc_client_send_notification_async: + * @self: A #JsonrpcClient + * @method: The name of the method to call + * @params: (transfer none) (nullable): A [struct@GLib.Variant] of parameters or %NULL + * @cancellable: (nullable): A #GCancellable or %NULL + * + * Asynchronously calls @method with @params on the remote peer. + * + * This function will not wait or expect a reply from the peer. + * + * This function is useful when the caller wants to be notified that + * the bytes have been delivered to the underlying stream. This does + * not indicate that the peer has received them. + * + * If @params is floating then the reference is consumed. + * + * Since: 3.26 + */ +void +jsonrpc_client_send_notification_async (JsonrpcClient *self, + const gchar *method, + GVariant *params, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + g_autoptr(GVariant) message = NULL; + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + GVariantDict dict; + + g_return_if_fail (JSONRPC_IS_CLIENT (self)); + g_return_if_fail (method != NULL); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, jsonrpc_client_send_notification_async); + + if (!jsonrpc_client_check_ready (self, &error)) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + if (params == NULL) + params = g_variant_new_maybe (G_VARIANT_TYPE_VARIANT, NULL); + + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "jsonrpc", "s", "2.0"); + g_variant_dict_insert (&dict, "method", "s", method); + g_variant_dict_insert_value (&dict, "params", params); + + message = g_variant_take_ref (g_variant_dict_end (&dict)); + + jsonrpc_output_stream_write_message_async (priv->output_stream, + message, + cancellable, + jsonrpc_client_send_notification_write_cb, + g_steal_pointer (&task)); +} + +/** + * jsonrpc_client_send_notification_finish: + * @self: A #JsonrpcClient + * + * Completes an asynchronous call to [method@Client.send_notification_async]. + * + * Successful completion of this function only indicates that the request + * has been written to the underlying buffer, not that the peer has received + * the notification. + * + * Returns: %TRUE if the bytes have been flushed to the [class@Gio.IOStream]; otherwise + * %FALSE and @error is set. + * + * Since: 3.26 + */ +gboolean +jsonrpc_client_send_notification_finish (JsonrpcClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (JSONRPC_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +/** + * jsonrpc_client_close: + * @self: A #JsonrpcClient + * + * Closes the underlying streams and cancels any inflight operations of the + * #JsonrpcClient. + * + * This is important to call when you are done with the + * client so that any outstanding operations that have caused @self to + * hold additional references are cancelled. + * + * Failure to call this method results in a leak of #JsonrpcClient. + * + * Returns: %TRUE if successful; otherwise %FALSE and @error is set. + * + * Since: 3.26 + */ +gboolean +jsonrpc_client_close (JsonrpcClient *self, + GCancellable *cancellable, + GError **error) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + g_autoptr(GHashTable) invocations = NULL; + g_autoptr(GError) local_error = NULL; + gboolean ret; + + g_return_val_if_fail (JSONRPC_IS_CLIENT (self), FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + if (!jsonrpc_client_check_ready (self, error)) + return FALSE; + + priv->in_shutdown = TRUE; + + if (!g_cancellable_is_cancelled (priv->read_loop_cancellable)) + g_cancellable_cancel (priv->read_loop_cancellable); + + /* This can fail from "pending operations", but we will always cancel + * our tasks. But we should let the caller know either way. + */ + ret = g_io_stream_close (priv->io_stream, cancellable, error); + + /* + * Closing the input stream will fail, so just rely on the callback + * from the async function to complete/close the stream. + */ + local_error = g_error_new_literal (G_IO_ERROR, + G_IO_ERROR_CLOSED, + "The underlying stream was closed"); + cancel_pending_from_main (self, local_error); + + emit_failed_from_main (self); + + return ret; +} + +/** + * jsonrpc_client_close_async: + * @self: A #JsonrpcClient. + * + * Asynchronous version of [method@Client.close]. + * + * Currently this operation is implemented synchronously, but in the future may + * be converted to using asynchronous operations. + * + * Since: 3.26 + */ +void +jsonrpc_client_close_async (JsonrpcClient *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (JSONRPC_IS_CLIENT (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, jsonrpc_client_close_async); + + /* + * In practice, none of our close operations should block (unless they were + * a FUSE fd or something like that. So we'll just perform them synchronously + * for now. + */ + jsonrpc_client_close (self, cancellable, NULL); + + g_task_return_boolean (task, TRUE); +} + +/** + * jsonrpc_client_close_finish: + * @self: A #JsonrpcClient. + * + * Completes an asynchronous request of [method@Client.close_async]. + * + * Returns: %TRUE if successful; otherwise %FALSE and @error is set. + * + * Since: 3.26 + */ +gboolean +jsonrpc_client_close_finish (JsonrpcClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (JSONRPC_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +jsonrpc_client_reply_error_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + JsonrpcOutputStream *stream = (JsonrpcOutputStream *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + + g_assert (JSONRPC_IS_OUTPUT_STREAM (stream)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (G_IS_TASK (task)); + + if (!jsonrpc_output_stream_write_message_finish (stream, result, &error)) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_boolean (task, TRUE); +} + +/** + * jsonrpc_client_reply_error_async: + * @self: A #JsonrpcClient + * @id: (transfer none): A [struct@GLib.Variant] containing the call id + * @code: The error code + * @message: (nullable): An optional error message + * @cancellable: (nullable): A #GCancellable, or %NULL + * @callback: (nullable): A #GAsyncReadyCallback or %NULL + * @user_data: Closure data for @callback + * + * Asynchronously replies to the peer, sending a JSON-RPC error message. + * + * Call [method@Client.reply_error_finish] to get the result of this operation. + * + * If @id is floating, it's floating reference is consumed. + * + * Since: 3.28 + */ +void +jsonrpc_client_reply_error_async (JsonrpcClient *self, + GVariant *id, + gint code, + const gchar *message, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) vreply = NULL; + GVariantDict reply; + GVariantDict error_dict; + + g_return_if_fail (JSONRPC_IS_CLIENT (self)); + g_return_if_fail (id != NULL); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + if (message == NULL) + message = "An error occurred"; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, jsonrpc_client_reply_error_async); + g_task_set_priority (task, G_PRIORITY_LOW); + + if (!jsonrpc_client_check_ready (self, &error)) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + g_variant_dict_init (&error_dict, NULL); + g_variant_dict_insert (&error_dict, "code", "i", code); + g_variant_dict_insert (&error_dict, "message", "s", message); + + g_variant_dict_init (&reply, NULL); + g_variant_dict_insert (&reply, "jsonrpc", "s", "2.0"); + g_variant_dict_insert_value (&reply, "id", id); + g_variant_dict_insert_value (&reply, "error", g_variant_dict_end (&error_dict)); + + vreply = g_variant_take_ref (g_variant_dict_end (&reply)); + + jsonrpc_output_stream_write_message_async (priv->output_stream, + vreply, + cancellable, + jsonrpc_client_reply_error_cb, + g_steal_pointer (&task)); +} + +gboolean +jsonrpc_client_reply_error_finish (JsonrpcClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (JSONRPC_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_task_is_valid (G_TASK (result), self), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +/** + * jsonrpc_client_reply: + * @self: A #JsonrpcClient + * @id: (transfer none): The id of the message to reply + * @result: (transfer none) (nullable): The return value or %NULL + * @cancellable: (nullable): A #GCancellable, or %NULL + * @error: A #GError, or %NULL + * + * Synchronous variant of [method@Client.reply_async]. + * + * If @id or @result are floating, there floating references are consumed. + * + * Since: 3.26 + */ +gboolean +jsonrpc_client_reply (JsonrpcClient *self, + GVariant *id, + GVariant *result, + GCancellable *cancellable, + GError **error) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + g_autoptr(GVariant) message = NULL; + GVariantDict dict; + gboolean ret; + + g_return_val_if_fail (JSONRPC_IS_CLIENT (self), FALSE); + g_return_val_if_fail (id != NULL, FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + if (!jsonrpc_client_check_ready (self, error)) + return FALSE; + + if (result == NULL) + result = g_variant_new_maybe (G_VARIANT_TYPE_VARIANT, NULL); + + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "jsonrpc", "s", "2.0"); + g_variant_dict_insert_value (&dict, "id", id); + g_variant_dict_insert_value (&dict, "result", result); + + message = g_variant_take_ref (g_variant_dict_end (&dict)); + + ret = jsonrpc_output_stream_write_message (priv->output_stream, message, cancellable, error); + + return ret; +} + +static void +jsonrpc_client_reply_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + JsonrpcOutputStream *stream = (JsonrpcOutputStream *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + + g_assert (JSONRPC_IS_OUTPUT_STREAM (stream)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (G_IS_TASK (task)); + + if (!jsonrpc_output_stream_write_message_finish (stream, result, &error)) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_boolean (task, TRUE); +} + +/** + * jsonrcp_client_reply_async: + * @self: A #JsonrpcClient + * @id: (transfer none): The id of the message to reply + * @result: (transfer none) (nullable): The return value or %NULL + * @cancellable: A #GCancellable, or %NULL + * @callback: (nullable): A #GAsyncReadyCallback or %NULL + * @user_data: Closure data for @callback + * + * This function will reply to a method call identified by @id with the + * result provided. If @result is %NULL, then a null JSON node is returned + * for the "result" field of the JSONRPC message. + * + * JSONRPC allows either peer to call methods on each other, so this + * method is provided allowing #JsonrpcClient to be used for either + * side of communications. + * + * If no signal handler has handled [signal@Client::handle-call] then + * an error will be synthesized to the peer. + * + * Call [method@Client.reply_finish] to complete the operation. Note + * that since the peer does not reply to replies, completion of this + * asynchronous message does not indicate that the peer has received + * the message. + * + * If @id or @result are floating, there floating references are consumed. + * + * Since: 3.26 + */ +void +jsonrpc_client_reply_async (JsonrpcClient *self, + GVariant *id, + GVariant *result, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + g_autoptr(GTask) task = NULL; + g_autoptr(GVariant) message = NULL; + g_autoptr(GError) error = NULL; + GVariantDict dict; + + g_return_if_fail (JSONRPC_IS_CLIENT (self)); + g_return_if_fail (id != NULL); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, jsonrpc_client_reply_async); + + if (!jsonrpc_client_check_ready (self, &error)) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + if (result == NULL) + result = g_variant_new_maybe (G_VARIANT_TYPE_VARIANT, NULL); + + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "jsonrpc", "s", "2.0"); + g_variant_dict_insert_value (&dict, "id", id); + g_variant_dict_insert_value (&dict, "result", result); + + message = g_variant_take_ref (g_variant_dict_end (&dict)); + + jsonrpc_output_stream_write_message_async (priv->output_stream, + message, + cancellable, + jsonrpc_client_reply_cb, + g_steal_pointer (&task)); +} + +/** + * jsonrpc_client_reply_finish: + * @self: A #JsonrpcClient + * @result: A #GAsyncResult + * @error: A location for a #GError or %NULL + * + * Completes an asynchronous request to [method@Client.reply_async]. + * + * Returns: %TRUE if successful; otherwise %FALSE and @error is set. + * + * Since: 3.26 + */ +gboolean +jsonrpc_client_reply_finish (JsonrpcClient *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (JSONRPC_IS_CLIENT (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +/** + * jsonrpc_client_start_listening: + * @self: A #JsonrpcClient + * + * This function requests that client start processing incoming + * messages from the peer. + * + * Since: 3.26 + */ +void +jsonrpc_client_start_listening (JsonrpcClient *self) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + + g_return_if_fail (JSONRPC_IS_CLIENT (self)); + + /* + * If this is our very first message, then we need to start our + * async read loop. This will allow us to receive notifications + * out-of-band and intermixed with RPC calls. + */ + + if (priv->is_first_call) + { + priv->is_first_call = FALSE; + + /* + * Because we take a reference here in our read loop, it is important + * that the user calls jsonrpc_client_close() or + * jsonrpc_client_close_async() so that we can cancel the operation and + * allow it to cleanup any outstanding references. + */ + jsonrpc_input_stream_read_message_async (priv->input_stream, + priv->read_loop_cancellable, + jsonrpc_client_call_read_cb, + g_object_ref (self)); + } +} + +/** + * jsonrpc_client_get_use_gvariant: + * @self: A #JsonrpcClient + * + * Gets the [property@Client:use-gvariant] property. + * + * Indicates if [struct@GLib.Variant] is being used to communicate with the peer. + * + * Returns: %TRUE if [struct@GLib.Variant] is being used; otherwise %FALSE. + * + * Since: 3.26 + */ +gboolean +jsonrpc_client_get_use_gvariant (JsonrpcClient *self) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + + g_return_val_if_fail (JSONRPC_IS_CLIENT (self), FALSE); + + return priv->use_gvariant; +} + +/** + * jsonrpc_client_set_use_gvariant: + * @self: A #JsonrpcClient + * @use_gvariant: If [struct@GLib.Variant] should be used + * + * Sets the [property@Client:use-gvariant] property. + * + * This function sets if [struct@GLib.Variant] should be used to communicate with the + * peer. Doing so can allow for more efficient communication by avoiding + * expensive parsing overhead and memory allocations. However, it requires + * that the peer also supports [struct@GLib.Variant] encoding. + * + * Since: 3.26 + */ +void +jsonrpc_client_set_use_gvariant (JsonrpcClient *self, + gboolean use_gvariant) +{ + JsonrpcClientPrivate *priv = jsonrpc_client_get_instance_private (self); + + g_return_if_fail (JSONRPC_IS_CLIENT (self)); + + use_gvariant = !!use_gvariant; + + if (priv->use_gvariant != use_gvariant) + { + priv->use_gvariant = use_gvariant; + if (priv->output_stream != NULL) + jsonrpc_output_stream_set_use_gvariant (priv->output_stream, use_gvariant); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USE_GVARIANT]); + } +} diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-client.h b/lsp/deps/jsonrpc-glib/jsonrpc-client.h new file mode 100644 index 000000000..420ffad36 --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-client.h @@ -0,0 +1,165 @@ +/* jsonrpc-client.h + * + * Copyright (C) 2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef JSONRPC_CLIENT_H +#define JSONRPC_CLIENT_H + +#include + +#include "jsonrpc-version-macros.h" + +G_BEGIN_DECLS + +#define JSONRPC_TYPE_CLIENT (jsonrpc_client_get_type()) +#define JSONRPC_CLIENT_ERROR (jsonrpc_client_error_quark()) + +typedef enum +{ + JSONRPC_CLIENT_ERROR_PARSE_ERROR = -32700, + JSONRPC_CLIENT_ERROR_INVALID_REQUEST = -32600, + JSONRPC_CLIENT_ERROR_METHOD_NOT_FOUND = -32601, + JSONRPC_CLIENT_ERROR_INVALID_PARAMS = -32602, + JSONRPC_CLIENT_ERROR_INTERNAL_ERROR = -32603, +} JsonrpcClientError; + +JSONRPC_AVAILABLE_IN_3_26 +G_DECLARE_DERIVABLE_TYPE (JsonrpcClient, jsonrpc_client, JSONRPC, CLIENT, GObject) + +struct _JsonrpcClientClass +{ + GObjectClass parent_class; + + void (*notification) (JsonrpcClient *self, + const gchar *method_name, + GVariant *params); + gboolean (*handle_call) (JsonrpcClient *self, + const gchar *method, + GVariant *id, + GVariant *params); + void (*failed) (JsonrpcClient *self); + + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +JSONRPC_AVAILABLE_IN_3_26 +GQuark jsonrpc_client_error_quark (void); +JSONRPC_AVAILABLE_IN_3_26 +JsonrpcClient *jsonrpc_client_new (GIOStream *io_stream); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_client_get_use_gvariant (JsonrpcClient *self); +JSONRPC_AVAILABLE_IN_3_26 +void jsonrpc_client_set_use_gvariant (JsonrpcClient *self, + gboolean use_gvariant); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_client_close (JsonrpcClient *self, + GCancellable *cancellable, + GError **error); +JSONRPC_AVAILABLE_IN_3_26 +void jsonrpc_client_close_async (JsonrpcClient *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_client_close_finish (JsonrpcClient *self, + GAsyncResult *result, + GError **error); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_client_call (JsonrpcClient *self, + const gchar *method, + GVariant *params, + GCancellable *cancellable, + GVariant **return_value, + GError **error); +JSONRPC_AVAILABLE_IN_3_30 +void jsonrpc_client_call_with_id_async (JsonrpcClient *self, + const gchar *method, + GVariant *params, + GVariant **id, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +JSONRPC_AVAILABLE_IN_3_26 +void jsonrpc_client_call_async (JsonrpcClient *self, + const gchar *method, + GVariant *params, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_client_call_finish (JsonrpcClient *self, + GAsyncResult *result, + GVariant **return_value, + GError **error); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_client_send_notification (JsonrpcClient *self, + const gchar *method, + GVariant *params, + GCancellable *cancellable, + GError **error); +JSONRPC_AVAILABLE_IN_3_26 +void jsonrpc_client_send_notification_async (JsonrpcClient *self, + const gchar *method, + GVariant *params, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_client_send_notification_finish (JsonrpcClient *self, + GAsyncResult *result, + GError **error); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_client_reply (JsonrpcClient *self, + GVariant *id, + GVariant *result, + GCancellable *cancellable, + GError **error); +JSONRPC_AVAILABLE_IN_3_26 +void jsonrpc_client_reply_async (JsonrpcClient *self, + GVariant *id, + GVariant *result, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_client_reply_finish (JsonrpcClient *self, + GAsyncResult *result, + GError **error); +JSONRPC_AVAILABLE_IN_3_28 +void jsonrpc_client_reply_error_async (JsonrpcClient *self, + GVariant *id, + gint code, + const gchar *message, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +JSONRPC_AVAILABLE_IN_3_28 +gboolean jsonrpc_client_reply_error_finish (JsonrpcClient *self, + GAsyncResult *result, + GError **error); +JSONRPC_AVAILABLE_IN_3_26 +void jsonrpc_client_start_listening (JsonrpcClient *self); + +G_END_DECLS + +#endif /* JSONRPC_CLIENT_H */ diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-glib.h b/lsp/deps/jsonrpc-glib/jsonrpc-glib.h new file mode 100644 index 000000000..e7c0fc705 --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-glib.h @@ -0,0 +1,39 @@ +/* jsonrpc-glib.h + * + * Copyright (C) 2016 Christian Hergert + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef JSONRPC_GLIB_H +#define JSONRPC_GLIB_H + +#include + +G_BEGIN_DECLS + +#define JSONRPC_GLIB_INSIDE +# include "jsonrpc-client.h" +# include "jsonrpc-input-stream.h" +# include "jsonrpc-message.h" +# include "jsonrpc-output-stream.h" +# include "jsonrpc-server.h" +# include "jsonrpc-version.h" +# include "jsonrpc-version-macros.h" +#undef JSONRPC_GLIB_INSIDE + +G_END_DECLS + +#endif /* JSONRPC_GLIB_H */ diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-input-stream-private.h b/lsp/deps/jsonrpc-glib/jsonrpc-input-stream-private.h new file mode 100644 index 000000000..e9cbee849 --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-input-stream-private.h @@ -0,0 +1,30 @@ +/* jsonrpc-input-stream-private.h + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef JSONRPC_INPUT_STREAM_PRIVATE_H +#define JSONRPC_INPUT_STREAM_PRIVATE_H + +#include "jsonrpc-input-stream.h" + +G_BEGIN_DECLS + +gboolean _jsonrpc_input_stream_get_has_seen_gvariant (JsonrpcInputStream *self) G_GNUC_INTERNAL; + +G_END_DECLS + +#endif /* JSONRPC_INPUT_STREAM_PRIVATE_H */ diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-input-stream.c b/lsp/deps/jsonrpc-glib/jsonrpc-input-stream.c new file mode 100644 index 000000000..860071c60 --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-input-stream.c @@ -0,0 +1,398 @@ +/* jsonrpc-input-stream.c + * + * Copyright (C) 2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +//#define G_LOG_DOMAIN "jsonrpc-input-stream" + +#include "config.h" + +#include +#include +#include + +#include "jsonrpc-input-stream.h" +#include "jsonrpc-input-stream-private.h" + +typedef struct +{ + gssize content_length; + gchar *buffer; + GVariantType *gvariant_type; + gint16 priority; + guint use_gvariant : 1; +} ReadState; + +typedef struct +{ + gssize max_size_bytes; + guint has_seen_gvariant : 1; +} JsonrpcInputStreamPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (JsonrpcInputStream, jsonrpc_input_stream, G_TYPE_DATA_INPUT_STREAM) + +static gboolean jsonrpc_input_stream_debug; + +static void +read_state_free (gpointer data) +{ + ReadState *state = data; + + g_clear_pointer (&state->buffer, g_free); + g_clear_pointer (&state->gvariant_type, g_free); + g_slice_free (ReadState, state); +} + +static void +jsonrpc_input_stream_class_init (JsonrpcInputStreamClass *klass) +{ + jsonrpc_input_stream_debug = !!g_getenv ("JSONRPC_DEBUG"); +} + +static void +jsonrpc_input_stream_init (JsonrpcInputStream *self) +{ + JsonrpcInputStreamPrivate *priv = jsonrpc_input_stream_get_instance_private (self); + + /* 16 MB */ + priv->max_size_bytes = 16 * 1024 * 1024; + + g_data_input_stream_set_newline_type (G_DATA_INPUT_STREAM (self), + G_DATA_STREAM_NEWLINE_TYPE_ANY); +} + +JsonrpcInputStream * +jsonrpc_input_stream_new (GInputStream *base_stream) +{ + return g_object_new (JSONRPC_TYPE_INPUT_STREAM, + "base-stream", base_stream, + NULL); +} + +static void +jsonrpc_input_stream_read_body_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + JsonrpcInputStream *self = (JsonrpcInputStream *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) message = NULL; + ReadState *state; + gsize n_read; + + g_assert (JSONRPC_IS_INPUT_STREAM (self)); + g_assert (G_IS_TASK (task)); + + state = g_task_get_task_data (task); + + if (!g_input_stream_read_all_finish (G_INPUT_STREAM (self), result, &n_read, &error)) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + if ((gssize)n_read != state->content_length) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Failed to read %"G_GSSIZE_FORMAT" bytes", + state->content_length); + return; + } + + state->buffer [state->content_length] = '\0'; + + if G_UNLIKELY (jsonrpc_input_stream_debug && state->use_gvariant == FALSE) + g_message ("<<< %s", state->buffer); + + if (state->use_gvariant) + { + g_autoptr(GBytes) bytes = NULL; + + bytes = g_bytes_new_take (g_steal_pointer (&state->buffer), state->content_length); + message = g_variant_new_from_bytes (state->gvariant_type ? state->gvariant_type + : G_VARIANT_TYPE_VARDICT, + bytes, FALSE); + + if G_UNLIKELY (jsonrpc_input_stream_debug && state->use_gvariant) + { + g_autofree gchar *debugstr = g_variant_print (message, TRUE); + g_message ("<<< %s", debugstr); + } + } + else + { + message = json_gvariant_deserialize_data (state->buffer, state->content_length, NULL, &error); + g_clear_pointer (&state->buffer, g_free); + } + + g_assert (state->buffer == NULL); + g_assert (message != NULL || error != NULL); + + /* Don't let message be floating */ + if (message != NULL) + g_variant_take_ref (message); + + if (error != NULL) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_pointer (task, + g_steal_pointer (&message), + (GDestroyNotify)g_variant_unref); +} + +static void +jsonrpc_input_stream_read_headers_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + JsonrpcInputStream *self = (JsonrpcInputStream *)object; + JsonrpcInputStreamPrivate *priv = jsonrpc_input_stream_get_instance_private (self); + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + g_autofree gchar *line = NULL; + GCancellable *cancellable = NULL; + ReadState *state; + gsize length = 0; + + g_assert (JSONRPC_IS_INPUT_STREAM (self)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (G_IS_TASK (task)); + + state = g_task_get_task_data (task); + cancellable = g_task_get_cancellable (task); + + line = g_data_input_stream_read_line_finish_utf8 (G_DATA_INPUT_STREAM (self), result, &length, &error); + + if (line == NULL) + { + if (error != NULL) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "No data to read from peer"); + return; + } + + if (strncasecmp ("Content-Length: ", line, 16) == 0) + { + const gchar *lenptr = line + 16; + gint64 content_length; + + content_length = g_ascii_strtoll (lenptr, NULL, 10); + + if (((content_length == G_MININT64 || content_length == G_MAXINT64) && errno == ERANGE) || + (content_length < 0) || + (content_length == G_MAXSSIZE) || + (content_length > priv->max_size_bytes)) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Invalid Content-Length received from peer"); + return; + } + + state->content_length = content_length; + } + + if (strncasecmp ("Content-Type: ", line, 14) == 0) + { + if (NULL != strstr (line, "application/gvariant")) + state->use_gvariant = TRUE; + } + + if (strncasecmp ("X-GVariant-Type: ", line, 17) == 0) + { + const gchar *type_string = line + 17; + + if (!g_variant_type_string_is_valid (type_string)) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Invalid X-GVariant-Type received from peer"); + return; + } + + g_clear_pointer (&state->gvariant_type, g_free); + state->gvariant_type = (GVariantType *)g_strdup (type_string); + } + + /* + * If we are at the end of the headers, we can make progress towards + * parsing the JSON content. Otherwise we need to continue parsing + * the next header. + */ + + if (line[0] == '\0') + { + if (state->content_length <= 0) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Invalid or missing Content-Length header from peer"); + return; + } + + state->buffer = g_malloc (state->content_length + 1); + g_input_stream_read_all_async (G_INPUT_STREAM (self), + state->buffer, + state->content_length, + state->priority, + cancellable, + jsonrpc_input_stream_read_body_cb, + g_steal_pointer (&task)); + return; + } + + g_data_input_stream_read_line_async (G_DATA_INPUT_STREAM (self), + state->priority, + cancellable, + jsonrpc_input_stream_read_headers_cb, + g_steal_pointer (&task)); +} + +void +jsonrpc_input_stream_read_message_async (JsonrpcInputStream *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + ReadState *state; + + g_return_if_fail (JSONRPC_IS_INPUT_STREAM (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + state = g_slice_new0 (ReadState); + state->content_length = -1; + state->priority = G_PRIORITY_LOW; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, jsonrpc_input_stream_read_message_async); + g_task_set_task_data (task, state, read_state_free); + g_task_set_priority (task, state->priority); + + g_data_input_stream_read_line_async (G_DATA_INPUT_STREAM (self), + state->priority, + cancellable, + jsonrpc_input_stream_read_headers_cb, + g_steal_pointer (&task)); +} + +gboolean +jsonrpc_input_stream_read_message_finish (JsonrpcInputStream *self, + GAsyncResult *result, + GVariant **message, + GError **error) +{ + JsonrpcInputStreamPrivate *priv = jsonrpc_input_stream_get_instance_private (self); + g_autoptr(GVariant) local_message = NULL; + ReadState *state; + gboolean ret; + + g_return_val_if_fail (JSONRPC_IS_INPUT_STREAM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + /* track if we've seen an application/gvariant */ + state = g_task_get_task_data (G_TASK (result)); + priv->has_seen_gvariant |= state->use_gvariant; + + local_message = g_task_propagate_pointer (G_TASK (result), error); + ret = local_message != NULL; + + if (message != NULL) + { + /* Unbox the variant if it is in a wrapper */ + if (local_message && g_variant_is_of_type (local_message, G_VARIANT_TYPE_VARIANT)) + *message = g_variant_get_variant (local_message); + else + *message = g_steal_pointer (&local_message); + } + + return ret; +} + +static void +jsonrpc_input_stream_read_message_sync_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + JsonrpcInputStream *self = (JsonrpcInputStream *)object; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) message = NULL; + GTask *task = user_data; + + g_assert (JSONRPC_IS_INPUT_STREAM (self)); + g_assert (G_IS_TASK (task)); + + if (!jsonrpc_input_stream_read_message_finish (self, result, &message, &error)) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_pointer (task, g_steal_pointer (&message), (GDestroyNotify)g_variant_unref); +} + +gboolean +jsonrpc_input_stream_read_message (JsonrpcInputStream *self, + GCancellable *cancellable, + GVariant **message, + GError **error) +{ + g_autoptr(GMainContext) main_context = NULL; + g_autoptr(GVariant) local_message = NULL; + g_autoptr(GTask) task = NULL; + gboolean ret; + + g_return_val_if_fail (JSONRPC_IS_INPUT_STREAM (self), FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + main_context = g_main_context_ref_thread_default (); + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_source_tag (task, jsonrpc_input_stream_read_message); + + jsonrpc_input_stream_read_message_async (self, + cancellable, + jsonrpc_input_stream_read_message_sync_cb, + task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (main_context, TRUE); + + local_message = g_task_propagate_pointer (task, error); + ret = local_message != NULL; + + if (message != NULL) + *message = g_steal_pointer (&local_message); + + return ret; +} + +gboolean +_jsonrpc_input_stream_get_has_seen_gvariant (JsonrpcInputStream *self) +{ + JsonrpcInputStreamPrivate *priv = jsonrpc_input_stream_get_instance_private (self); + + g_return_val_if_fail (JSONRPC_IS_INPUT_STREAM (self), FALSE); + + return priv->has_seen_gvariant; +} diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-input-stream.h b/lsp/deps/jsonrpc-glib/jsonrpc-input-stream.h new file mode 100644 index 000000000..8bd93b1c8 --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-input-stream.h @@ -0,0 +1,67 @@ +/* jsonrpc-input-stream.h + * + * Copyright (C) 2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef JSONRPC_INPUT_STREAM_H +#define JSONRPC_INPUT_STREAM_H + +#include + +#include "jsonrpc-version-macros.h" + +G_BEGIN_DECLS + +#define JSONRPC_TYPE_INPUT_STREAM (jsonrpc_input_stream_get_type()) + +JSONRPC_AVAILABLE_IN_3_26 +G_DECLARE_DERIVABLE_TYPE (JsonrpcInputStream, jsonrpc_input_stream, JSONRPC, INPUT_STREAM, GDataInputStream) + +struct _JsonrpcInputStreamClass +{ + GDataInputStreamClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +JSONRPC_AVAILABLE_IN_3_26 +JsonrpcInputStream *jsonrpc_input_stream_new (GInputStream *base_stream); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_input_stream_read_message (JsonrpcInputStream *self, + GCancellable *cancellable, + GVariant **message, + GError **error); +JSONRPC_AVAILABLE_IN_3_26 +void jsonrpc_input_stream_read_message_async (JsonrpcInputStream *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_input_stream_read_message_finish (JsonrpcInputStream *self, + GAsyncResult *result, + GVariant **message, + GError **error); + +G_END_DECLS + +#endif /* JSONRPC_INPUT_STREAM_H */ diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-marshalers.c b/lsp/deps/jsonrpc-glib/jsonrpc-marshalers.c new file mode 100644 index 000000000..71e6790aa --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-marshalers.c @@ -0,0 +1,463 @@ +/* This file is generated by glib-genmarshal, do not modify it. This code is licensed under the same license as the containing project. Note that it links to GLib, so must comply with the LGPL linking clauses. */ +#include "jsonrpc-marshalers.h" + +#include + +#ifdef G_ENABLE_DEBUG +#define g_marshal_value_peek_boolean(v) g_value_get_boolean (v) +#define g_marshal_value_peek_char(v) g_value_get_schar (v) +#define g_marshal_value_peek_uchar(v) g_value_get_uchar (v) +#define g_marshal_value_peek_int(v) g_value_get_int (v) +#define g_marshal_value_peek_uint(v) g_value_get_uint (v) +#define g_marshal_value_peek_long(v) g_value_get_long (v) +#define g_marshal_value_peek_ulong(v) g_value_get_ulong (v) +#define g_marshal_value_peek_int64(v) g_value_get_int64 (v) +#define g_marshal_value_peek_uint64(v) g_value_get_uint64 (v) +#define g_marshal_value_peek_enum(v) g_value_get_enum (v) +#define g_marshal_value_peek_flags(v) g_value_get_flags (v) +#define g_marshal_value_peek_float(v) g_value_get_float (v) +#define g_marshal_value_peek_double(v) g_value_get_double (v) +#define g_marshal_value_peek_string(v) (char*) g_value_get_string (v) +#define g_marshal_value_peek_param(v) g_value_get_param (v) +#define g_marshal_value_peek_boxed(v) g_value_get_boxed (v) +#define g_marshal_value_peek_pointer(v) g_value_get_pointer (v) +#define g_marshal_value_peek_object(v) g_value_get_object (v) +#define g_marshal_value_peek_variant(v) g_value_get_variant (v) +#else /* !G_ENABLE_DEBUG */ +/* WARNING: This code accesses GValues directly, which is UNSUPPORTED API. + * Do not access GValues directly in your code. Instead, use the + * g_value_get_*() functions + */ +#define g_marshal_value_peek_boolean(v) (v)->data[0].v_int +#define g_marshal_value_peek_char(v) (v)->data[0].v_int +#define g_marshal_value_peek_uchar(v) (v)->data[0].v_uint +#define g_marshal_value_peek_int(v) (v)->data[0].v_int +#define g_marshal_value_peek_uint(v) (v)->data[0].v_uint +#define g_marshal_value_peek_long(v) (v)->data[0].v_long +#define g_marshal_value_peek_ulong(v) (v)->data[0].v_ulong +#define g_marshal_value_peek_int64(v) (v)->data[0].v_int64 +#define g_marshal_value_peek_uint64(v) (v)->data[0].v_uint64 +#define g_marshal_value_peek_enum(v) (v)->data[0].v_long +#define g_marshal_value_peek_flags(v) (v)->data[0].v_ulong +#define g_marshal_value_peek_float(v) (v)->data[0].v_float +#define g_marshal_value_peek_double(v) (v)->data[0].v_double +#define g_marshal_value_peek_string(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_param(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_boxed(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_pointer(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_object(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_variant(v) (v)->data[0].v_pointer +#endif /* !G_ENABLE_DEBUG */ + +/* BOOLEAN:OBJECT,STRING,VARIANT,VARIANT (../src/jsonrpc-marshalers.list:1) */ +void +_jsonrpc_marshal_BOOLEAN__OBJECT_STRING_VARIANT_VARIANT (GClosure *closure, + GValue *return_value, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint G_GNUC_UNUSED, + gpointer marshal_data) +{ + typedef gboolean (*GMarshalFunc_BOOLEAN__OBJECT_STRING_VARIANT_VARIANT) (gpointer data1, + gpointer arg1, + gpointer arg2, + gpointer arg3, + gpointer arg4, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_BOOLEAN__OBJECT_STRING_VARIANT_VARIANT callback; + gboolean v_return; + + g_return_if_fail (return_value != NULL); + g_return_if_fail (n_param_values == 5); + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = g_value_peek_pointer (param_values + 0); + } + else + { + data1 = g_value_peek_pointer (param_values + 0); + data2 = closure->data; + } + callback = (GMarshalFunc_BOOLEAN__OBJECT_STRING_VARIANT_VARIANT) (marshal_data ? marshal_data : cc->callback); + + v_return = callback (data1, + g_marshal_value_peek_object (param_values + 1), + g_marshal_value_peek_string (param_values + 2), + g_marshal_value_peek_variant (param_values + 3), + g_marshal_value_peek_variant (param_values + 4), + data2); + + g_value_set_boolean (return_value, v_return); +} + +void +_jsonrpc_marshal_BOOLEAN__OBJECT_STRING_VARIANT_VARIANTv (GClosure *closure, + GValue *return_value, + gpointer instance, + va_list args, + gpointer marshal_data, + int n_params, + GType *param_types) +{ + typedef gboolean (*GMarshalFunc_BOOLEAN__OBJECT_STRING_VARIANT_VARIANT) (gpointer data1, + gpointer arg1, + gpointer arg2, + gpointer arg3, + gpointer arg4, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_BOOLEAN__OBJECT_STRING_VARIANT_VARIANT callback; + gboolean v_return; + gpointer arg0; + gpointer arg1; + gpointer arg2; + gpointer arg3; + va_list args_copy; + + G_VA_COPY (args_copy, args); + arg0 = (gpointer) va_arg (args_copy, gpointer); + if (arg0 != NULL) + arg0 = g_object_ref (arg0); + arg1 = (gpointer) va_arg (args_copy, gpointer); + if ((param_types[1] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg1 != NULL) + arg1 = g_strdup (arg1); + arg2 = (gpointer) va_arg (args_copy, gpointer); + if ((param_types[2] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg2 != NULL) + arg2 = g_variant_ref_sink (arg2); + arg3 = (gpointer) va_arg (args_copy, gpointer); + if ((param_types[3] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg3 != NULL) + arg3 = g_variant_ref_sink (arg3); + va_end (args_copy); + + g_return_if_fail (return_value != NULL); + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = instance; + } + else + { + data1 = instance; + data2 = closure->data; + } + callback = (GMarshalFunc_BOOLEAN__OBJECT_STRING_VARIANT_VARIANT) (marshal_data ? marshal_data : cc->callback); + + v_return = callback (data1, + arg0, + arg1, + arg2, + arg3, + data2); + if (arg0 != NULL) + g_object_unref (arg0); + if ((param_types[1] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg1 != NULL) + g_free (arg1); + if ((param_types[2] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg2 != NULL) + g_variant_unref (arg2); + if ((param_types[3] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg3 != NULL) + g_variant_unref (arg3); + + g_value_set_boolean (return_value, v_return); +} + +/* BOOLEAN:STRING,VARIANT,VARIANT (../src/jsonrpc-marshalers.list:2) */ +void +_jsonrpc_marshal_BOOLEAN__STRING_VARIANT_VARIANT (GClosure *closure, + GValue *return_value, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint G_GNUC_UNUSED, + gpointer marshal_data) +{ + typedef gboolean (*GMarshalFunc_BOOLEAN__STRING_VARIANT_VARIANT) (gpointer data1, + gpointer arg1, + gpointer arg2, + gpointer arg3, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_BOOLEAN__STRING_VARIANT_VARIANT callback; + gboolean v_return; + + g_return_if_fail (return_value != NULL); + g_return_if_fail (n_param_values == 4); + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = g_value_peek_pointer (param_values + 0); + } + else + { + data1 = g_value_peek_pointer (param_values + 0); + data2 = closure->data; + } + callback = (GMarshalFunc_BOOLEAN__STRING_VARIANT_VARIANT) (marshal_data ? marshal_data : cc->callback); + + v_return = callback (data1, + g_marshal_value_peek_string (param_values + 1), + g_marshal_value_peek_variant (param_values + 2), + g_marshal_value_peek_variant (param_values + 3), + data2); + + g_value_set_boolean (return_value, v_return); +} + +void +_jsonrpc_marshal_BOOLEAN__STRING_VARIANT_VARIANTv (GClosure *closure, + GValue *return_value, + gpointer instance, + va_list args, + gpointer marshal_data, + int n_params, + GType *param_types) +{ + typedef gboolean (*GMarshalFunc_BOOLEAN__STRING_VARIANT_VARIANT) (gpointer data1, + gpointer arg1, + gpointer arg2, + gpointer arg3, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_BOOLEAN__STRING_VARIANT_VARIANT callback; + gboolean v_return; + gpointer arg0; + gpointer arg1; + gpointer arg2; + va_list args_copy; + + G_VA_COPY (args_copy, args); + arg0 = (gpointer) va_arg (args_copy, gpointer); + if ((param_types[0] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg0 != NULL) + arg0 = g_strdup (arg0); + arg1 = (gpointer) va_arg (args_copy, gpointer); + if ((param_types[1] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg1 != NULL) + arg1 = g_variant_ref_sink (arg1); + arg2 = (gpointer) va_arg (args_copy, gpointer); + if ((param_types[2] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg2 != NULL) + arg2 = g_variant_ref_sink (arg2); + va_end (args_copy); + + g_return_if_fail (return_value != NULL); + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = instance; + } + else + { + data1 = instance; + data2 = closure->data; + } + callback = (GMarshalFunc_BOOLEAN__STRING_VARIANT_VARIANT) (marshal_data ? marshal_data : cc->callback); + + v_return = callback (data1, + arg0, + arg1, + arg2, + data2); + if ((param_types[0] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg0 != NULL) + g_free (arg0); + if ((param_types[1] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg1 != NULL) + g_variant_unref (arg1); + if ((param_types[2] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg2 != NULL) + g_variant_unref (arg2); + + g_value_set_boolean (return_value, v_return); +} + +/* VOID:OBJECT,STRING,VARIANT (../src/jsonrpc-marshalers.list:3) */ +void +_jsonrpc_marshal_VOID__OBJECT_STRING_VARIANT (GClosure *closure, + GValue *return_value G_GNUC_UNUSED, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint G_GNUC_UNUSED, + gpointer marshal_data) +{ + typedef void (*GMarshalFunc_VOID__OBJECT_STRING_VARIANT) (gpointer data1, + gpointer arg1, + gpointer arg2, + gpointer arg3, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_VOID__OBJECT_STRING_VARIANT callback; + + g_return_if_fail (n_param_values == 4); + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = g_value_peek_pointer (param_values + 0); + } + else + { + data1 = g_value_peek_pointer (param_values + 0); + data2 = closure->data; + } + callback = (GMarshalFunc_VOID__OBJECT_STRING_VARIANT) (marshal_data ? marshal_data : cc->callback); + + callback (data1, + g_marshal_value_peek_object (param_values + 1), + g_marshal_value_peek_string (param_values + 2), + g_marshal_value_peek_variant (param_values + 3), + data2); +} + +void +_jsonrpc_marshal_VOID__OBJECT_STRING_VARIANTv (GClosure *closure, + GValue *return_value G_GNUC_UNUSED, + gpointer instance, + va_list args, + gpointer marshal_data, + int n_params, + GType *param_types) +{ + typedef void (*GMarshalFunc_VOID__OBJECT_STRING_VARIANT) (gpointer data1, + gpointer arg1, + gpointer arg2, + gpointer arg3, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_VOID__OBJECT_STRING_VARIANT callback; + gpointer arg0; + gpointer arg1; + gpointer arg2; + va_list args_copy; + + G_VA_COPY (args_copy, args); + arg0 = (gpointer) va_arg (args_copy, gpointer); + if (arg0 != NULL) + arg0 = g_object_ref (arg0); + arg1 = (gpointer) va_arg (args_copy, gpointer); + if ((param_types[1] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg1 != NULL) + arg1 = g_strdup (arg1); + arg2 = (gpointer) va_arg (args_copy, gpointer); + if ((param_types[2] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg2 != NULL) + arg2 = g_variant_ref_sink (arg2); + va_end (args_copy); + + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = instance; + } + else + { + data1 = instance; + data2 = closure->data; + } + callback = (GMarshalFunc_VOID__OBJECT_STRING_VARIANT) (marshal_data ? marshal_data : cc->callback); + + callback (data1, + arg0, + arg1, + arg2, + data2); + if (arg0 != NULL) + g_object_unref (arg0); + if ((param_types[1] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg1 != NULL) + g_free (arg1); + if ((param_types[2] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg2 != NULL) + g_variant_unref (arg2); +} + +/* VOID:STRING,VARIANT (../src/jsonrpc-marshalers.list:4) */ +void +_jsonrpc_marshal_VOID__STRING_VARIANT (GClosure *closure, + GValue *return_value G_GNUC_UNUSED, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint G_GNUC_UNUSED, + gpointer marshal_data) +{ + typedef void (*GMarshalFunc_VOID__STRING_VARIANT) (gpointer data1, + gpointer arg1, + gpointer arg2, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_VOID__STRING_VARIANT callback; + + g_return_if_fail (n_param_values == 3); + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = g_value_peek_pointer (param_values + 0); + } + else + { + data1 = g_value_peek_pointer (param_values + 0); + data2 = closure->data; + } + callback = (GMarshalFunc_VOID__STRING_VARIANT) (marshal_data ? marshal_data : cc->callback); + + callback (data1, + g_marshal_value_peek_string (param_values + 1), + g_marshal_value_peek_variant (param_values + 2), + data2); +} + +void +_jsonrpc_marshal_VOID__STRING_VARIANTv (GClosure *closure, + GValue *return_value G_GNUC_UNUSED, + gpointer instance, + va_list args, + gpointer marshal_data, + int n_params, + GType *param_types) +{ + typedef void (*GMarshalFunc_VOID__STRING_VARIANT) (gpointer data1, + gpointer arg1, + gpointer arg2, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_VOID__STRING_VARIANT callback; + gpointer arg0; + gpointer arg1; + va_list args_copy; + + G_VA_COPY (args_copy, args); + arg0 = (gpointer) va_arg (args_copy, gpointer); + if ((param_types[0] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg0 != NULL) + arg0 = g_strdup (arg0); + arg1 = (gpointer) va_arg (args_copy, gpointer); + if ((param_types[1] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg1 != NULL) + arg1 = g_variant_ref_sink (arg1); + va_end (args_copy); + + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = instance; + } + else + { + data1 = instance; + data2 = closure->data; + } + callback = (GMarshalFunc_VOID__STRING_VARIANT) (marshal_data ? marshal_data : cc->callback); + + callback (data1, + arg0, + arg1, + data2); + if ((param_types[0] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg0 != NULL) + g_free (arg0); + if ((param_types[1] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && arg1 != NULL) + g_variant_unref (arg1); +} + diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-marshalers.h b/lsp/deps/jsonrpc-glib/jsonrpc-marshalers.h new file mode 100644 index 000000000..63c60372d --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-marshalers.h @@ -0,0 +1,77 @@ +/* This file is generated by glib-genmarshal, do not modify it. This code is licensed under the same license as the containing project. Note that it links to GLib, so must comply with the LGPL linking clauses. */ +#pragma once + +#include + +G_BEGIN_DECLS + +/* BOOLEAN:OBJECT,STRING,VARIANT,VARIANT (../src/jsonrpc-marshalers.list:1) */ +G_GNUC_INTERNAL +void _jsonrpc_marshal_BOOLEAN__OBJECT_STRING_VARIANT_VARIANT (GClosure *closure, + GValue *return_value, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint, + gpointer marshal_data); +G_GNUC_INTERNAL +void _jsonrpc_marshal_BOOLEAN__OBJECT_STRING_VARIANT_VARIANTv (GClosure *closure, + GValue *return_value, + gpointer instance, + va_list args, + gpointer marshal_data, + int n_params, + GType *param_types); + +/* BOOLEAN:STRING,VARIANT,VARIANT (../src/jsonrpc-marshalers.list:2) */ +G_GNUC_INTERNAL +void _jsonrpc_marshal_BOOLEAN__STRING_VARIANT_VARIANT (GClosure *closure, + GValue *return_value, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint, + gpointer marshal_data); +G_GNUC_INTERNAL +void _jsonrpc_marshal_BOOLEAN__STRING_VARIANT_VARIANTv (GClosure *closure, + GValue *return_value, + gpointer instance, + va_list args, + gpointer marshal_data, + int n_params, + GType *param_types); + +/* VOID:OBJECT,STRING,VARIANT (../src/jsonrpc-marshalers.list:3) */ +G_GNUC_INTERNAL +void _jsonrpc_marshal_VOID__OBJECT_STRING_VARIANT (GClosure *closure, + GValue *return_value, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint, + gpointer marshal_data); +G_GNUC_INTERNAL +void _jsonrpc_marshal_VOID__OBJECT_STRING_VARIANTv (GClosure *closure, + GValue *return_value, + gpointer instance, + va_list args, + gpointer marshal_data, + int n_params, + GType *param_types); + +/* VOID:STRING,VARIANT (../src/jsonrpc-marshalers.list:4) */ +G_GNUC_INTERNAL +void _jsonrpc_marshal_VOID__STRING_VARIANT (GClosure *closure, + GValue *return_value, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint, + gpointer marshal_data); +G_GNUC_INTERNAL +void _jsonrpc_marshal_VOID__STRING_VARIANTv (GClosure *closure, + GValue *return_value, + gpointer instance, + va_list args, + gpointer marshal_data, + int n_params, + GType *param_types); + + +G_END_DECLS diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-message.c b/lsp/deps/jsonrpc-glib/jsonrpc-message.c new file mode 100644 index 000000000..fa7c3c645 --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-message.c @@ -0,0 +1,677 @@ +/* jsonrpc-message.c + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +//#define G_LOG_DOMAIN "jsonrpc-message" + +#include "config.h" + +#include + +#include "jsonrpc-message.h" + +#if 0 +# define ENTRY do { g_print (" ENTRY: %s(): %d\n", G_STRFUNC, __LINE__); } while (0) +# define RETURN(r) do { g_print ("RETURN: %s(): %d\n", G_STRFUNC, __LINE__); return r; } while (0) +# define EXIT do { g_print (" EXIT: %s(): %d\n", G_STRFUNC, __LINE__); return; } while (0) +#else +# define ENTRY do { } while (0) +# define RETURN(r) do { return r; } while (0) +# define EXIT do { return; } while (0) +#endif + +#define COMPARE_MAGIC(_any,_magic) \ + (strncmp ((_any)->magic.bytes, \ + _JSONRPC_MESSAGE_##_magic##_MAGIC, \ + sizeof ((_any)->magic.bytes)) == 0) + +#define IS_PUT_STRING(_any) COMPARE_MAGIC(_any, PUT_STRING) +#define IS_PUT_STRV(_any) COMPARE_MAGIC(_any, PUT_STRV) +#define IS_PUT_INT32(_any) COMPARE_MAGIC(_any, PUT_INT32) +#define IS_PUT_INT64(_any) COMPARE_MAGIC(_any, PUT_INT64) +#define IS_PUT_BOOLEAN(_any) COMPARE_MAGIC(_any, PUT_BOOLEAN) +#define IS_PUT_DOUBLE(_any) COMPARE_MAGIC(_any, PUT_DOUBLE) +#define IS_PUT_VARIANT(_any) COMPARE_MAGIC(_any, PUT_VARIANT) + +#define IS_GET_STRING(_any) COMPARE_MAGIC(_any, GET_STRING) +#define IS_GET_STRV(_any) COMPARE_MAGIC(_any, GET_STRV) +#define IS_GET_INT32(_any) COMPARE_MAGIC(_any, GET_INT32) +#define IS_GET_INT64(_any) COMPARE_MAGIC(_any, GET_INT64) +#define IS_GET_BOOLEAN(_any) COMPARE_MAGIC(_any, GET_BOOLEAN) +#define IS_GET_DOUBLE(_any) COMPARE_MAGIC(_any, GET_DOUBLE) +#define IS_GET_ITER(_any) COMPARE_MAGIC(_any, GET_ITER) +#define IS_GET_DICT(_any) COMPARE_MAGIC(_any, GET_DICT) +#define IS_GET_VARIANT(_any) COMPARE_MAGIC(_any, GET_VARIANT) + +static void jsonrpc_message_build_object (GVariantBuilder *builder, + gconstpointer param, + va_list *args); +static void jsonrpc_message_build_array (GVariantBuilder *builder, + gconstpointer param, + va_list *args); +static gboolean jsonrpc_message_parse_object (GVariantDict *dict, + gpointer param, + va_list *args); +static gboolean jsonrpc_message_parse_array_va (GVariantIter *iter, + gpointer param, + va_list *args); + +static void +jsonrpc_message_build_object (GVariantBuilder *builder, + gconstpointer param, + va_list *args) +{ + const JsonrpcMessageAny *keyptr = param; + JsonrpcMessageAny *valptr; + const char *key; + + ENTRY; + + /* + * First we need to get the key for this item. If we have reached + * the '}', then we either have an empty {} or got here via + * recursion to read the next key/val pair. + */ + + if (!keyptr || keyptr->magic.bytes[0] == '}') + EXIT; + + g_assert (!IS_PUT_VARIANT (keyptr)); + + /* + * Either this is a string wrapped in JSONRPC_MESSAGE_PUT_STRING() or + * we assume it is a raw key name like "foo". + */ + if (IS_PUT_STRING (keyptr)) + key = ((const JsonrpcMessagePutString *)keyptr)->val; + else + key = (const char *)keyptr; + + /* + * Now try to read the value for the key/val pair. + */ + valptr = va_arg (*args, gpointer); + + g_variant_builder_open (builder, G_VARIANT_TYPE ("{sv}")); + g_variant_builder_add (builder, "s", key); + g_variant_builder_open (builder, G_VARIANT_TYPE ("v")); + + switch (valptr->magic.bytes[0]) + { + case '{': + param = va_arg (*args, gconstpointer); + /* + * Peek ahead if a possible GVariant will be injected + */ + if (IS_PUT_VARIANT ((JsonrpcMessageAny *)param) && + ((JsonrpcMessagePutVariant *)param)->val != NULL) + { + if (g_variant_is_of_type (((JsonrpcMessagePutVariant *)param)->val, G_VARIANT_TYPE_VARDICT)) + { + g_variant_builder_add (builder, "v", ((JsonrpcMessagePutVariant *)param)->val); + } + else + { + g_warning ("Attempt to add variant of type %s but expected a{sv}", + g_variant_get_type_string (((JsonrpcMessagePutVariant *)param)->val)); + g_variant_builder_open (builder, G_VARIANT_TYPE ("mav")); + } + } + else if (IS_PUT_VARIANT ((JsonrpcMessageAny *)param)) + { + g_variant_builder_open (builder, G_VARIANT_TYPE ("mav")); + g_variant_builder_close (builder); + } + else + { + g_variant_builder_open (builder, G_VARIANT_TYPE ("a{sv}")); + jsonrpc_message_build_object (builder, param, args); + g_variant_builder_close (builder); + } + + break; + + case '[': + g_variant_builder_open (builder, G_VARIANT_TYPE ("av")); + param = va_arg (*args, gconstpointer); + jsonrpc_message_build_array (builder, param, args); + g_variant_builder_close (builder); + break; + + case ']': + case '}': + g_return_if_reached (); + break; + + default: + if (IS_PUT_STRING (valptr)) + { + if (((JsonrpcMessagePutString *)valptr)->val != NULL) + g_variant_builder_add (builder, "s", ((JsonrpcMessagePutString *)valptr)->val); + else + g_variant_builder_add (builder, "ms", NULL); + } + else if (IS_PUT_STRV (valptr)) + { + if (((JsonrpcMessagePutStrv *)valptr)->val != NULL) + g_variant_builder_add (builder, "^as", ((JsonrpcMessagePutStrv *)valptr)->val); + else + g_variant_builder_add (builder, "mas", NULL); + } + else if (IS_PUT_INT32 (valptr)) + g_variant_builder_add (builder, "i", ((JsonrpcMessagePutInt32 *)valptr)->val); + else if (IS_PUT_INT64 (valptr)) + g_variant_builder_add (builder, "x", ((JsonrpcMessagePutInt64 *)valptr)->val); + else if (IS_PUT_BOOLEAN (valptr)) + g_variant_builder_add (builder, "b", ((JsonrpcMessagePutBoolean *)valptr)->val); + else if (IS_PUT_DOUBLE (valptr)) + g_variant_builder_add (builder, "d", ((JsonrpcMessagePutDouble *)valptr)->val); + else + g_variant_builder_add (builder, "s", (const char *)valptr); + break; + } + + g_variant_builder_close (builder); + g_variant_builder_close (builder); + + /* + * Try to build the next field in the object if there is one. + */ + param = va_arg (*args, gconstpointer); + jsonrpc_message_build_object (builder, param, args); + + EXIT; +} + +static void +jsonrpc_message_build_array (GVariantBuilder *builder, + gconstpointer param, + va_list *args) +{ + const JsonrpcMessageAny *valptr = param; + + ENTRY; + + /* If we have the end of the array, we're done */ + if (valptr->magic.bytes[0] == ']') + EXIT; + + g_variant_builder_open (builder, G_VARIANT_TYPE ("v")); + + switch (valptr->magic.bytes[0]) + { + case '{': + g_variant_builder_open (builder, G_VARIANT_TYPE ("a{sv}")); + param = va_arg (*args, gconstpointer); + jsonrpc_message_build_object (builder, param, args); + g_variant_builder_close (builder); + break; + + case '[': + g_variant_builder_open (builder, G_VARIANT_TYPE ("av")); + param = va_arg (*args, gconstpointer); + jsonrpc_message_build_array (builder, param, args); + g_variant_builder_close (builder); + break; + + case ']': + case '}': + g_return_if_reached (); + break; + + default: + if (IS_PUT_STRING (valptr)) + { + if (((JsonrpcMessagePutString *)valptr)->val != NULL) + g_variant_builder_add (builder, "s", ((JsonrpcMessagePutString *)valptr)->val); + else + g_variant_builder_add (builder, "ms", NULL); + } + else if (IS_PUT_INT32 (valptr)) + g_variant_builder_add (builder, "i", ((JsonrpcMessagePutInt32 *)valptr)->val); + else if (IS_PUT_INT64 (valptr)) + g_variant_builder_add (builder, "x", ((JsonrpcMessagePutInt64 *)valptr)->val); + else if (IS_PUT_BOOLEAN (valptr)) + g_variant_builder_add (builder, "b", ((JsonrpcMessagePutBoolean *)valptr)->val); + else if (IS_PUT_DOUBLE (valptr)) + g_variant_builder_add (builder, "d", ((JsonrpcMessagePutDouble *)valptr)->val); + else + g_variant_builder_add (builder, "s", (const char *)valptr); + break; + } + + g_variant_builder_close (builder); + + param = va_arg (*args, gconstpointer); + if (param != NULL) + jsonrpc_message_build_array (builder, param, args); + + EXIT; +} + +static GVariant * +jsonrpc_message_new_valist (gconstpointer first_param, + va_list *args) +{ + GVariantBuilder builder; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + + if (first_param != NULL) + jsonrpc_message_build_object (&builder, first_param, args); + + return g_variant_builder_end (&builder); +} + +GVariant * +jsonrpc_message_new (gconstpointer first_param, + ...) +{ + GVariant *ret; + va_list args; + + g_return_val_if_fail (first_param != NULL, NULL); + + va_start (args, first_param); + ret = jsonrpc_message_new_valist (first_param, &args); + va_end (args); + + if (g_variant_is_floating (ret)) + g_variant_take_ref (ret); + + return ret; +} + +static GVariant * +jsonrpc_message_new_array_valist (gconstpointer first_param, + va_list *args) +{ + GVariantBuilder builder; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("av")); + + if (first_param != NULL) + jsonrpc_message_build_array (&builder, first_param, args); + + return g_variant_builder_end (&builder); +} + +GVariant * +jsonrpc_message_new_array (gconstpointer first_param, + ...) +{ + GVariant *ret; + va_list args; + + g_return_val_if_fail (first_param != NULL, NULL); + + va_start (args, first_param); + ret = jsonrpc_message_new_array_valist (first_param, &args); + va_end (args); + + if (g_variant_is_floating (ret)) + g_variant_take_ref (ret); + + return ret; +} + +static gboolean +jsonrpc_message_parse_object (GVariantDict *dict, + gpointer param, + va_list *args) +{ + JsonrpcMessageAny *valptr; + const char *key = param; + gboolean ret = FALSE; + + ENTRY; + + g_assert (dict != NULL); + + if (key == NULL || key[0] == '}') + RETURN (TRUE); + + valptr = va_arg (*args, gpointer); + + if (valptr == NULL) + g_error ("got unexpected NULL for key %s", key); + + if (valptr->magic.bytes[0] == '{' || IS_GET_DICT (valptr)) + { + g_autoptr(GVariant) value = NULL; + + if (NULL != (value = g_variant_dict_lookup_value (dict, key, G_VARIANT_TYPE ("a{sv}")))) + { + g_autoptr(GVariantDict) subdict = NULL; + + subdict = g_variant_dict_new (value); + + g_assert (subdict != NULL); + + if (IS_GET_DICT (valptr)) + ret = !!(*((JsonrpcMessageGetDict *)valptr)->dictptr = g_steal_pointer (&subdict)); + else + { + param = va_arg (*args, gpointer); + ret = jsonrpc_message_parse_object (subdict, param, args); + } + } + } + else if (valptr->magic.bytes[0] == '[' || IS_GET_ITER (valptr)) + { + g_autoptr(GVariantIter) subiter = NULL; + g_autoptr(GVariant) subvalue = NULL; + + if (g_variant_dict_lookup (dict, key, "av", &subiter)) + { + if (IS_GET_ITER (valptr)) + ret = !!(*((JsonrpcMessageGetIter *)valptr)->iterptr = g_steal_pointer (&subiter)); + else + { + param = va_arg (*args, gpointer); + ret = jsonrpc_message_parse_array_va (subiter, param, args); + } + } + else if (NULL != (subvalue = g_variant_dict_lookup_value (dict, key, G_VARIANT_TYPE ("a{sv}")))) + { + if (IS_GET_ITER (valptr) && NULL != (subiter = g_variant_iter_new (subvalue))) + ret = !!(*((JsonrpcMessageGetIter *)valptr)->iterptr = g_steal_pointer (&subiter)); + } + } + else if (IS_GET_VARIANT (valptr)) + { + GVariant *lookup = g_variant_dict_lookup_value (dict, key, NULL); + GVariant *child = NULL; + + if (lookup != NULL && + g_variant_is_of_type (lookup, G_VARIANT_TYPE_VARIANT) && + g_variant_n_children (lookup) == 1 && + (child = g_variant_get_child_value (lookup, 0)) && + g_variant_is_of_type (child, G_VARIANT_TYPE ("a{sv}"))) + *((JsonrpcMessageGetVariant *)valptr)->variantptr = g_steal_pointer (&child); + else + *((JsonrpcMessageGetVariant *)valptr)->variantptr = g_steal_pointer (&lookup); + + ret = !!(*((JsonrpcMessageGetVariant *)valptr)->variantptr); + + g_clear_pointer (&lookup, g_variant_unref); + g_clear_pointer (&child, g_variant_unref); + } + else if (IS_GET_STRING (valptr)) + { + g_autoptr(GVariant) v = g_variant_dict_lookup_value (dict, key, NULL); + + if (v != NULL) + { + /* Safe to get data pointer because @v is a sub-variant of the + * larger buffer and therefore shares raw data */ + if (g_variant_is_of_type (v, G_VARIANT_TYPE ("s"))) + { + *((JsonrpcMessageGetString *)valptr)->valptr = g_variant_get_string (v, NULL); + ret = TRUE; + } + else if (g_variant_is_of_type (v, G_VARIANT_TYPE ("mv")) || + g_variant_is_of_type (v, G_VARIANT_TYPE ("ms"))) + { + *((JsonrpcMessageGetString *)valptr)->valptr = NULL; + ret = TRUE; + } + } + } + else if (IS_GET_STRV (valptr)) + { + g_autoptr(GVariant) v = g_variant_dict_lookup_value (dict, key, NULL); + + if (v != NULL) + { + if (g_variant_is_of_type (v, G_VARIANT_TYPE ("as"))) + { + *((JsonrpcMessageGetStrv *)valptr)->valptr = g_variant_dup_strv (v, NULL); + ret = TRUE; + } + else if (g_variant_is_of_type (v, G_VARIANT_TYPE ("av"))) + { + GPtrArray *ar = g_ptr_array_new (); + GVariantIter iter; + GVariant *item; + + g_variant_iter_init (&iter, v); + while ((item = g_variant_iter_next_value (&iter))) + { + GVariant *unwrap = g_variant_get_variant (item); + + if (g_variant_is_of_type (unwrap, G_VARIANT_TYPE_STRING)) + g_ptr_array_add (ar, g_variant_dup_string (unwrap, NULL)); + + g_variant_unref (unwrap); + g_variant_unref (item); + } + g_ptr_array_add (ar, NULL); + + *((JsonrpcMessageGetStrv *)valptr)->valptr = (gchar **)g_ptr_array_free (ar, FALSE); + ret = TRUE; + } + else if (g_variant_is_of_type (v, G_VARIANT_TYPE ("mav")) || + g_variant_is_of_type (v, G_VARIANT_TYPE ("mas")) || + g_variant_is_of_type (v, G_VARIANT_TYPE ("mv"))) + { + *((JsonrpcMessageGetStrv *)valptr)->valptr = NULL; + ret = TRUE; + } + } + } + else if (IS_GET_INT32 (valptr)) + ret = g_variant_dict_lookup (dict, key, "i", ((JsonrpcMessageGetInt32 *)valptr)->valptr); + else if (IS_GET_INT64 (valptr)) + ret = g_variant_dict_lookup (dict, key, "x", ((JsonrpcMessageGetInt64 *)valptr)->valptr); + else if (IS_GET_BOOLEAN (valptr)) + ret = g_variant_dict_lookup (dict, key, "b", ((JsonrpcMessageGetBoolean *)valptr)->valptr); + else if (IS_GET_DOUBLE (valptr)) + ret = g_variant_dict_lookup (dict, key, "d", ((JsonrpcMessageGetDouble *)valptr)->valptr); + else + { + /* Assume the string is a raw string, so compare for equality */ + const gchar *valstr = NULL; + ret = g_variant_dict_lookup (dict, key, "&s", &valstr) || + g_variant_dict_lookup (dict, key, "m&s", &valstr); + if (ret) + ret = g_strcmp0 (valstr, (const gchar *)valptr) == 0; + } + + if (!ret || !param) + RETURN (ret); + + /* If we succeeded, try to read the next field */ + param = va_arg (*args, gpointer); + ret = jsonrpc_message_parse_object (dict, param, args); + + RETURN (ret); +} + +static gboolean +jsonrpc_message_parse_array_va (GVariantIter *iter, + gpointer param, + va_list *args) +{ + JsonrpcMessageAny *valptr = param; + g_autoptr(GVariant) value = NULL; + gboolean ret = FALSE; + + ENTRY; + + g_assert (iter != NULL); + + if (valptr == NULL || valptr->magic.bytes[0] == ']') + RETURN (TRUE); + + if (!g_variant_iter_next (iter, "v", &value)) + RETURN (FALSE); + + if (valptr->magic.bytes[0] == '{' || IS_GET_DICT (valptr)) + { + if (g_variant_is_of_type (value, G_VARIANT_TYPE ("a{sv}"))) + { + g_autoptr(GVariantDict) subdict = NULL; + + subdict = g_variant_dict_new (value); + + if (IS_GET_ITER (valptr)) + ret = !!(*((JsonrpcMessageGetDict *)valptr)->dictptr = g_steal_pointer (&subdict)); + else + { + param = va_arg (*args, gpointer); + ret = jsonrpc_message_parse_object (subdict, param, args); + } + } + } + else if (valptr->magic.bytes[0] == '[' || IS_GET_ITER (valptr)) + { + if (g_variant_is_of_type (value, G_VARIANT_TYPE ("av"))) + { + g_autoptr(GVariantIter) subiter = NULL; + + subiter = g_variant_iter_new (value); + + if (IS_GET_ITER (valptr)) + ret = !!(*((JsonrpcMessageGetIter *)valptr)->iterptr = g_steal_pointer (&subiter)); + else + { + param = va_arg (*args, gpointer); + ret = jsonrpc_message_parse_array_va (subiter, param, args); + } + } + } + else if (IS_GET_VARIANT (valptr)) + { + ret = !!(*((JsonrpcMessageGetVariant *)valptr)->variantptr = g_steal_pointer (&value)); + } + else if (IS_GET_STRING (valptr)) + { + if ((ret = g_variant_is_of_type (value, G_VARIANT_TYPE_STRING))) + *((JsonrpcMessageGetString *)valptr)->valptr = g_variant_get_string (value, NULL); + else if ((ret = g_variant_is_of_type (value, G_VARIANT_TYPE ("ms")))) + g_variant_get (value, "m&s", ((JsonrpcMessageGetString *)valptr)->valptr); + } + else if (IS_GET_INT32 (valptr)) + { + if ((ret = g_variant_is_of_type (value, G_VARIANT_TYPE_INT32))) + *((JsonrpcMessageGetInt32 *)valptr)->valptr = g_variant_get_int32 (value); + } + else if (IS_GET_INT64 (valptr)) + { + if ((ret = g_variant_is_of_type (value, G_VARIANT_TYPE_INT64))) + *((JsonrpcMessageGetInt64 *)valptr)->valptr = g_variant_get_int64 (value); + } + else if (IS_GET_BOOLEAN (valptr)) + { + if ((ret = g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))) + *((JsonrpcMessageGetBoolean *)valptr)->valptr = g_variant_get_boolean (value); + } + else if (IS_GET_DOUBLE (valptr)) + { + if ((ret = g_variant_is_of_type (value, G_VARIANT_TYPE_DOUBLE))) + *((JsonrpcMessageGetDouble *)valptr)->valptr = g_variant_get_double (value); + } + else + { + const gchar *val = NULL; + + /* Assume the string is a raw string, so compare for equality */ + if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) + val = g_variant_get_string (value, NULL); + else if (g_variant_is_of_type (value, G_VARIANT_TYPE ("ms"))) + g_variant_get (value, "m&s", &val); + + ret = g_strcmp0 (val, (const gchar *)valptr) == 0; + } + + if (!ret || !param) + RETURN (ret); + + /* If we succeeded, try to read the next element */ + param = va_arg (*args, gpointer); + ret = jsonrpc_message_parse_array_va (iter, param, args); + + RETURN (ret); +} + +gboolean +jsonrpc_message_parse_array (GVariantIter *iter, + ...) +{ + va_list args; + gpointer param; + gboolean ret = FALSE; + + g_return_val_if_fail (iter != NULL, FALSE); + + va_start (args, iter); + param = va_arg (args, gpointer); + if (param) + ret = jsonrpc_message_parse_array_va (iter, param, &args); + va_end (args); + + return ret; +} + +static gboolean +jsonrpc_message_parse_valist (GVariant *message, + va_list *args) +{ + gpointer param; + gboolean ret = FALSE; + + g_assert (message != NULL); + g_assert (g_variant_is_of_type (message, G_VARIANT_TYPE ("a{sv}"))); + + param = va_arg (*args, gpointer); + + if (param != NULL) + { + GVariantDict dict; + + g_variant_dict_init (&dict, message); + ret = jsonrpc_message_parse_object (&dict, param, args); + g_variant_dict_clear (&dict); + } + + return ret; +} + +gboolean +jsonrpc_message_parse (GVariant *message, + ...) +{ + g_autoptr(GVariant) unboxed = NULL; + gboolean ret; + va_list args; + + if (message == NULL) + return FALSE; + + if (g_variant_is_of_type (message, G_VARIANT_TYPE_VARIANT)) + message = unboxed = g_variant_get_variant (message); + + if (!g_variant_is_of_type (message, G_VARIANT_TYPE ("a{sv}"))) + return FALSE; + + va_start (args, message); + ret = jsonrpc_message_parse_valist (message, &args); + va_end (args); + + return ret; +} diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-message.h b/lsp/deps/jsonrpc-glib/jsonrpc-message.h new file mode 100644 index 000000000..bf6b2abef --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-message.h @@ -0,0 +1,235 @@ +/* jsonrpc-message.h + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef JSONRPC_MESSAGE_H +#define JSONRPC_MESSAGE_H + +#include + +#include "jsonrpc-version-macros.h" + +G_BEGIN_DECLS + +typedef struct +{ + char bytes[8]; +} JsonrpcMessageMagic; + +typedef struct +{ + JsonrpcMessageMagic magic; +} JsonrpcMessageAny __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + const char *val; +} JsonrpcMessagePutString __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + const char * const *val; +} JsonrpcMessagePutStrv __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + const char **valptr; +} JsonrpcMessageGetString __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + char ***valptr; +} JsonrpcMessageGetStrv __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + gint32 val; +} JsonrpcMessagePutInt32 __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + gint32 *valptr; +} JsonrpcMessageGetInt32 __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + gint64 val; +} JsonrpcMessagePutInt64 __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + gint64 *valptr; +} JsonrpcMessageGetInt64 __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + gboolean val; +} JsonrpcMessagePutBoolean __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + gboolean *valptr; +} JsonrpcMessageGetBoolean __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + double val; +} JsonrpcMessagePutDouble __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + double *valptr; +} JsonrpcMessageGetDouble __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + GVariantIter **iterptr; +} JsonrpcMessageGetIter __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + GVariantDict **dictptr; +} JsonrpcMessageGetDict __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + GVariant *val; +} JsonrpcMessagePutVariant __attribute__((aligned (8))); + +typedef struct +{ + JsonrpcMessageMagic magic; + GVariant **variantptr; +} JsonrpcMessageGetVariant __attribute__((aligned (8))); + +#define _JSONRPC_MAGIC(s) ("@!^%" s) +#define _JSONRPC_MAGIC_C(a,b,c,d) {'@','!','^','%',a,b,c,d} + +#define _JSONRPC_MESSAGE_PUT_STRING_MAGIC _JSONRPC_MAGIC("PUTS") +#define _JSONRPC_MESSAGE_GET_STRING_MAGIC _JSONRPC_MAGIC("GETS") +#define _JSONRPC_MESSAGE_PUT_STRV_MAGIC _JSONRPC_MAGIC("PUTZ") +#define _JSONRPC_MESSAGE_GET_STRV_MAGIC _JSONRPC_MAGIC("GETZ") +#define _JSONRPC_MESSAGE_PUT_INT32_MAGIC _JSONRPC_MAGIC("PUTI") +#define _JSONRPC_MESSAGE_GET_INT32_MAGIC _JSONRPC_MAGIC("GETI") +#define _JSONRPC_MESSAGE_PUT_INT64_MAGIC _JSONRPC_MAGIC("PUTX") +#define _JSONRPC_MESSAGE_GET_INT64_MAGIC _JSONRPC_MAGIC("GETX") +#define _JSONRPC_MESSAGE_PUT_BOOLEAN_MAGIC _JSONRPC_MAGIC("PUTB") +#define _JSONRPC_MESSAGE_GET_BOOLEAN_MAGIC _JSONRPC_MAGIC("GETB") +#define _JSONRPC_MESSAGE_PUT_DOUBLE_MAGIC _JSONRPC_MAGIC("PUTD") +#define _JSONRPC_MESSAGE_GET_DOUBLE_MAGIC _JSONRPC_MAGIC("GETD") +#define _JSONRPC_MESSAGE_GET_ITER_MAGIC _JSONRPC_MAGIC("GETT") +#define _JSONRPC_MESSAGE_GET_DICT_MAGIC _JSONRPC_MAGIC("GETC") +#define _JSONRPC_MESSAGE_PUT_VARIANT_MAGIC _JSONRPC_MAGIC("PUTV") +#define _JSONRPC_MESSAGE_GET_VARIANT_MAGIC _JSONRPC_MAGIC("GETV") + +#define _JSONRPC_MESSAGE_PUT_STRING_MAGIC_C _JSONRPC_MAGIC_C('P','U','T','S') +#define _JSONRPC_MESSAGE_GET_STRING_MAGIC_C _JSONRPC_MAGIC_C('G','E','T','S') +#define _JSONRPC_MESSAGE_PUT_STRV_MAGIC_C _JSONRPC_MAGIC_C('P','U','T','Z') +#define _JSONRPC_MESSAGE_GET_STRV_MAGIC_C _JSONRPC_MAGIC_C('G','E','T','Z') +#define _JSONRPC_MESSAGE_PUT_INT32_MAGIC_C _JSONRPC_MAGIC_C('P','U','T','I') +#define _JSONRPC_MESSAGE_GET_INT32_MAGIC_C _JSONRPC_MAGIC_C('G','E','T','I') +#define _JSONRPC_MESSAGE_PUT_INT64_MAGIC_C _JSONRPC_MAGIC_C('P','U','T','X') +#define _JSONRPC_MESSAGE_GET_INT64_MAGIC_C _JSONRPC_MAGIC_C('G','E','T','X') +#define _JSONRPC_MESSAGE_PUT_BOOLEAN_MAGIC_C _JSONRPC_MAGIC_C('P','U','T','B') +#define _JSONRPC_MESSAGE_GET_BOOLEAN_MAGIC_C _JSONRPC_MAGIC_C('G','E','T','B') +#define _JSONRPC_MESSAGE_PUT_DOUBLE_MAGIC_C _JSONRPC_MAGIC_C('P','U','T','D') +#define _JSONRPC_MESSAGE_GET_DOUBLE_MAGIC_C _JSONRPC_MAGIC_C('G','E','T','D') +#define _JSONRPC_MESSAGE_GET_ITER_MAGIC_C _JSONRPC_MAGIC_C('G','E','T','T') +#define _JSONRPC_MESSAGE_GET_DICT_MAGIC_C _JSONRPC_MAGIC_C('G','E','T','C') +#define _JSONRPC_MESSAGE_PUT_VARIANT_MAGIC_C _JSONRPC_MAGIC_C('P','U','T','V') +#define _JSONRPC_MESSAGE_GET_VARIANT_MAGIC_C _JSONRPC_MAGIC_C('G','E','T','V') + +#define JSONRPC_MESSAGE_NEW(first_, ...) \ + jsonrpc_message_new(first_, __VA_ARGS__, NULL) +#define JSONRPC_MESSAGE_NEW_ARRAY(first_, ...) \ + jsonrpc_message_new_array(first_, __VA_ARGS__, NULL) +#define JSONRPC_MESSAGE_PARSE(message, ...) \ + jsonrpc_message_parse(message, __VA_ARGS__, NULL) +#define JSONRPC_MESSAGE_PARSE_ARRAY(iter, ...) \ + jsonrpc_message_parse_array(iter, __VA_ARGS__, NULL) + +#define JSONRPC_MESSAGE_PUT_STRING(_val) \ + (&((JsonrpcMessagePutString) { .magic = {_JSONRPC_MESSAGE_PUT_STRING_MAGIC_C}, .val = _val })) +#define JSONRPC_MESSAGE_GET_STRING(_valptr) \ + (&((JsonrpcMessageGetString) { .magic = {_JSONRPC_MESSAGE_GET_STRING_MAGIC_C}, .valptr = _valptr })) + +#define JSONRPC_MESSAGE_PUT_STRV(_val) \ + (&((JsonrpcMessagePutStrv) { .magic = {_JSONRPC_MESSAGE_PUT_STRV_MAGIC_C}, .val = _val })) +#define JSONRPC_MESSAGE_GET_STRV(_valptr) \ + (&((JsonrpcMessageGetStrv) { .magic = {_JSONRPC_MESSAGE_GET_STRV_MAGIC_C}, .valptr = _valptr })) + +#define JSONRPC_MESSAGE_PUT_INT32(_val) \ + (&((JsonrpcMessagePutInt32) { .magic = {_JSONRPC_MESSAGE_PUT_INT32_MAGIC_C}, .val = _val })) +#define JSONRPC_MESSAGE_GET_INT32(_valptr) \ + (&((JsonrpcMessageGetInt32) { .magic = {_JSONRPC_MESSAGE_GET_INT32_MAGIC_C}, .valptr = _valptr })) + +#define JSONRPC_MESSAGE_PUT_INT64(_val) \ + (&((JsonrpcMessagePutInt64) { .magic = {_JSONRPC_MESSAGE_PUT_INT64_MAGIC_C}, .val = _val })) +#define JSONRPC_MESSAGE_GET_INT64(_valptr) \ + (&((JsonrpcMessageGetInt64) { .magic = {_JSONRPC_MESSAGE_GET_INT64_MAGIC_C}, .valptr = _valptr })) + +#define JSONRPC_MESSAGE_PUT_BOOLEAN(_val) \ + (&((JsonrpcMessagePutBoolean) { .magic = {_JSONRPC_MESSAGE_PUT_BOOLEAN_MAGIC_C}, .val = _val })) +#define JSONRPC_MESSAGE_GET_BOOLEAN(_valptr) \ + (&((JsonrpcMessageGetBoolean) { .magic = {_JSONRPC_MESSAGE_GET_BOOLEAN_MAGIC_C}, .valptr = _valptr })) + +#define JSONRPC_MESSAGE_PUT_DOUBLE(_val) \ + (&((JsonrpcMessagePutDouble) { .magic = {_JSONRPC_MESSAGE_PUT_DOUBLE_MAGIC_C}, .val = _val })) +#define JSONRPC_MESSAGE_GET_DOUBLE(_valptr) \ + (&((JsonrpcMessageGetDouble) { .magic = {_JSONRPC_MESSAGE_GET_DOUBLE_MAGIC_C}, .valptr = _valptr })) + +#define JSONRPC_MESSAGE_GET_ITER(_valptr) \ + (&((JsonrpcMessageGetIter) { .magic = {_JSONRPC_MESSAGE_GET_ITER_MAGIC_C}, .iterptr = _valptr })) + +#define JSONRPC_MESSAGE_GET_DICT(_valptr) \ + (&((JsonrpcMessageGetDict) { .magic = {_JSONRPC_MESSAGE_GET_DICT_MAGIC_C}, .dictptr = _valptr })) + +#define JSONRPC_MESSAGE_PUT_VARIANT(_val) \ + (&((JsonrpcMessagePutVariant) { .magic = {_JSONRPC_MESSAGE_PUT_VARIANT_MAGIC_C}, .val = _val })) +#define JSONRPC_MESSAGE_GET_VARIANT(_valptr) \ + (&((JsonrpcMessageGetVariant) { .magic = {_JSONRPC_MESSAGE_GET_VARIANT_MAGIC_C}, .variantptr = _valptr })) + +JSONRPC_AVAILABLE_IN_3_26 +GVariant *jsonrpc_message_new (gconstpointer first_param, ...) G_GNUC_NULL_TERMINATED; + +JSONRPC_AVAILABLE_IN_3_26 +GVariant *jsonrpc_message_new_array (gconstpointer first_param, ...) G_GNUC_NULL_TERMINATED; + +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_message_parse (GVariant *message, ...) G_GNUC_NULL_TERMINATED; + +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_message_parse_array (GVariantIter *iter, ...) G_GNUC_NULL_TERMINATED; + +G_END_DECLS + +#endif /* JSONRPC_MESSAGE_H */ diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-output-stream.c b/lsp/deps/jsonrpc-glib/jsonrpc-output-stream.c new file mode 100644 index 000000000..9cddb197c --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-output-stream.c @@ -0,0 +1,478 @@ +/* jsonrpc-output-stream.c + * + * Copyright (C) 2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +//#define G_LOG_DOMAIN "jsonrpc-output-stream" + +#include "config.h" + +#include + +#include "jsonrpc-output-stream.h" +#include "jsonrpc-version.h" + +/** + * SECTION:jsonrpc-output-stream + * @title: JsonrpcOutputStream + * @short_description: A JSON-RPC output stream + * + * The #JsonrpcOutputStream is resonsible for serializing messages onto + * the underlying stream. + * + * Optionally, if jsonrpc_output_stream_set_use_gvariant() has been called, + * the messages will be encoded directly in the #GVariant format instead of + * JSON along with setting a "Content-Type" header to "application/gvariant". + * This is useful for situations where you control both sides of the RPC server + * using jsonrpc-glib as you can reduce the overhead of parsing JSON nodes due + * to #GVariant not requiring parsing or allocation overhead to the same degree + * as JSON. + * + * For example, if you need a large message, which is encoded in JSON, you need + * to decode the entire message up front which avoids performing lazy operations. + * When using GVariant encoding, you have a single allocation created for the + * #GVariant which means you reduce the memory pressure caused by lots of small + * allocations. + */ + +typedef struct +{ + GQueue queue; + guint use_gvariant : 1; + guint processing : 1; +} JsonrpcOutputStreamPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (JsonrpcOutputStream, jsonrpc_output_stream, G_TYPE_DATA_OUTPUT_STREAM) + +static void jsonrpc_output_stream_write_message_async_cb (GObject *object, + GAsyncResult *result, + gpointer user_data); + +enum { + PROP_0, + PROP_USE_GVARIANT, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; +static gboolean jsonrpc_output_stream_debug; + +static void +jsonrpc_output_stream_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + JsonrpcOutputStream *self = JSONRPC_OUTPUT_STREAM (object); + + switch (prop_id) + { + case PROP_USE_GVARIANT: + g_value_set_boolean (value, jsonrpc_output_stream_get_use_gvariant (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +jsonrpc_output_stream_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + JsonrpcOutputStream *self = JSONRPC_OUTPUT_STREAM (object); + + switch (prop_id) + { + case PROP_USE_GVARIANT: + jsonrpc_output_stream_set_use_gvariant (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +jsonrpc_output_stream_dispose (GObject *object) +{ + JsonrpcOutputStream *self = (JsonrpcOutputStream *)object; + JsonrpcOutputStreamPrivate *priv = jsonrpc_output_stream_get_instance_private (self); + + g_queue_foreach (&priv->queue, (GFunc)g_object_unref, NULL); + g_queue_clear (&priv->queue); + + G_OBJECT_CLASS (jsonrpc_output_stream_parent_class)->dispose (object); +} + +static void +jsonrpc_output_stream_class_init (JsonrpcOutputStreamClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = jsonrpc_output_stream_dispose; + object_class->get_property = jsonrpc_output_stream_get_property; + object_class->set_property = jsonrpc_output_stream_set_property; + + properties [PROP_USE_GVARIANT] = + g_param_spec_boolean ("use-gvariant", + "Use GVariant", + "If GVariant encoding should be used", + FALSE, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + jsonrpc_output_stream_debug = !!g_getenv ("JSONRPC_DEBUG"); +} + +static void +jsonrpc_output_stream_init (JsonrpcOutputStream *self) +{ + JsonrpcOutputStreamPrivate *priv = jsonrpc_output_stream_get_instance_private (self); + + g_queue_init (&priv->queue); +} + +static GBytes * +jsonrpc_output_stream_create_bytes (JsonrpcOutputStream *self, + GVariant *message, + GError **error) +{ + JsonrpcOutputStreamPrivate *priv = jsonrpc_output_stream_get_instance_private (self); + g_autoptr(GByteArray) buffer = NULL; + g_autofree gchar *message_freeme = NULL; + gconstpointer message_data = NULL; + gsize message_len = 0; + gchar header[256]; + gsize len; + + g_assert (JSONRPC_IS_OUTPUT_STREAM (self)); + g_assert (message != NULL); + + buffer = g_byte_array_sized_new (g_variant_get_size (message) + 128); + + if G_UNLIKELY (jsonrpc_output_stream_debug) + { + g_autofree gchar *str = g_variant_print (message, TRUE); + g_message (">>> %s", str); + } + + if (priv->use_gvariant) + { + message_data = g_variant_get_data (message); + message_len = g_variant_get_size (message); + } + else + { + message_freeme = json_gvariant_serialize_data (message, &message_len); + message_data = message_freeme; + } + + /* Add Content-Length header */ + len = g_snprintf (header, sizeof header, "Content-Length: %"G_GSIZE_FORMAT"\r\n", message_len); + g_byte_array_append (buffer, (const guint8 *)header, len); + + if (priv->use_gvariant) + { + /* Add Content-Type header */ + len = g_snprintf (header, sizeof header, "Content-Type: application/%s\r\n", + priv->use_gvariant ? "gvariant" : "json"); + g_byte_array_append (buffer, (const guint8 *)header, len); + + /* Add our GVariantType for the peer to decode */ + len = g_snprintf (header, sizeof header, "X-GVariant-Type: %s\r\n", + (const gchar *)g_variant_get_type_string (message)); + g_byte_array_append (buffer, (const guint8 *)header, len); + } + + g_byte_array_append (buffer, (const guint8 *)"\r\n", 2); + + /* Add serialized message data */ + g_byte_array_append (buffer, (const guint8 *)message_data, message_len); + + return g_byte_array_free_to_bytes (g_steal_pointer (&buffer)); +} + +JsonrpcOutputStream * +jsonrpc_output_stream_new (GOutputStream *base_stream) +{ + return g_object_new (JSONRPC_TYPE_OUTPUT_STREAM, + "base-stream", base_stream, + NULL); +} + +static void +jsonrpc_output_stream_fail_pending (JsonrpcOutputStream *self) +{ + JsonrpcOutputStreamPrivate *priv = jsonrpc_output_stream_get_instance_private (self); + const GList *iter; + GList *list; + + g_assert (JSONRPC_IS_OUTPUT_STREAM (self)); + + list = priv->queue.head; + + priv->queue.head = NULL; + priv->queue.tail = NULL; + priv->queue.length = 0; + + for (iter = list; iter != NULL; iter = iter->next) + { + g_autoptr(GTask) task = iter->data; + + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Task failed due to stream failure"); + } + + g_list_free (list); +} + +static void +jsonrpc_output_stream_pump (JsonrpcOutputStream *self) +{ + JsonrpcOutputStreamPrivate *priv = jsonrpc_output_stream_get_instance_private (self); + g_autoptr(GTask) task = NULL; + const guint8 *data; + GCancellable *cancellable; + GBytes *bytes; + gsize len; + + g_assert (JSONRPC_IS_OUTPUT_STREAM (self)); + + if (priv->queue.length == 0) + return; + + if (priv->processing) + return; + + task = g_queue_pop_head (&priv->queue); + bytes = g_task_get_task_data (task); + data = g_bytes_get_data (bytes, &len); + cancellable = g_task_get_cancellable (task); + + if (g_output_stream_is_closed (G_OUTPUT_STREAM (self))) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_CLOSED, + "Stream has been closed"); + return; + } + + priv->processing = TRUE; + + g_output_stream_write_all_async (G_OUTPUT_STREAM (self), + data, + len, + G_PRIORITY_DEFAULT, + cancellable, + jsonrpc_output_stream_write_message_async_cb, + g_steal_pointer (&task)); +} + +static void +jsonrpc_output_stream_write_message_async_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + JsonrpcOutputStream *self = (JsonrpcOutputStream *)object; + JsonrpcOutputStreamPrivate *priv = jsonrpc_output_stream_get_instance_private (self); + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = user_data; + GBytes *bytes; + gsize n_written; + + g_assert (JSONRPC_IS_OUTPUT_STREAM (self)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (G_IS_TASK (task)); + + priv->processing = FALSE; + + if (!g_output_stream_write_all_finish (G_OUTPUT_STREAM (self), result, &n_written, &error)) + { + g_task_return_error (task, g_steal_pointer (&error)); + jsonrpc_output_stream_fail_pending (self); + return; + } + + bytes = g_task_get_task_data (task); + + if (g_bytes_get_size (bytes) != n_written) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_CLOSED, + "Failed to write all bytes to peer"); + jsonrpc_output_stream_fail_pending (self); + return; + } + + g_task_return_boolean (task, TRUE); + + jsonrpc_output_stream_pump (self); +} + +/** + * jsonrpc_output_stream_write_message_async: + * @self: a #JsonrpcOutputStream + * @message: (transfer none): a #GVariant + * @cancellable: (nullable): a #GCancellable or %NULL + * @callback: (nullable): a #GAsyncReadyCallback or %NULL + * @user_data: closure data for @callback + * + * Asynchronously sends a message to the peer. + * + * This asynchronous operation will complete once the message has + * been buffered, and there is no guarantee the peer received it. + * + * Since: 3.26 + */ +void +jsonrpc_output_stream_write_message_async (JsonrpcOutputStream *self, + GVariant *message, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + JsonrpcOutputStreamPrivate *priv = jsonrpc_output_stream_get_instance_private (self); + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GTask) task = NULL; + g_autoptr(GError) error = NULL; + + g_return_if_fail (JSONRPC_IS_OUTPUT_STREAM (self)); + g_return_if_fail (message != NULL); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, jsonrpc_output_stream_write_message_async); + g_task_set_priority (task, G_PRIORITY_LOW); + + if (NULL == (bytes = jsonrpc_output_stream_create_bytes (self, message, &error))) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + g_task_set_task_data (task, g_steal_pointer (&bytes), (GDestroyNotify)g_bytes_unref); + g_queue_push_tail (&priv->queue, g_steal_pointer (&task)); + jsonrpc_output_stream_pump (self); +} + +gboolean +jsonrpc_output_stream_write_message_finish (JsonrpcOutputStream *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (JSONRPC_IS_OUTPUT_STREAM (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +jsonrpc_output_stream_write_message_sync_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + JsonrpcOutputStream *self = (JsonrpcOutputStream *)object; + GTask *task = user_data; + g_autoptr(GError) error = NULL; + + g_assert (JSONRPC_IS_OUTPUT_STREAM (self)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (G_IS_TASK (task)); + + if (!jsonrpc_output_stream_write_message_finish (self, result, &error)) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_boolean (task, TRUE); +} + +/** + * jsonrpc_output_stream_write_message: + * @self: a #JsonrpcOutputStream + * @message: (transfer none): a #GVariant + * @cancellable: (nullable): a #GCancellable or %NULL + * @error: a location for a #GError, or %NULL + * + * Synchronously sends a message to the peer. + * + * This operation will complete once the message has been buffered. There + * is no guarantee the peer received it. + * + * Since: 3.26 + */ +gboolean +jsonrpc_output_stream_write_message (JsonrpcOutputStream *self, + GVariant *message, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GMainContext) main_context = NULL; + + g_return_val_if_fail (JSONRPC_IS_OUTPUT_STREAM (self), FALSE); + g_return_val_if_fail (message != NULL, FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + main_context = g_main_context_ref_thread_default (); + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_set_source_tag (task, jsonrpc_output_stream_write_message); + + jsonrpc_output_stream_write_message_async (self, + message, + cancellable, + jsonrpc_output_stream_write_message_sync_cb, + task); + + while (!g_task_get_completed (task)) + g_main_context_iteration (main_context, TRUE); + + return g_task_propagate_boolean (task, error); +} + +gboolean +jsonrpc_output_stream_get_use_gvariant (JsonrpcOutputStream *self) +{ + JsonrpcOutputStreamPrivate *priv = jsonrpc_output_stream_get_instance_private (self); + + g_return_val_if_fail (JSONRPC_IS_OUTPUT_STREAM (self), FALSE); + + return priv->use_gvariant; +} + +void +jsonrpc_output_stream_set_use_gvariant (JsonrpcOutputStream *self, + gboolean use_gvariant) +{ + JsonrpcOutputStreamPrivate *priv = jsonrpc_output_stream_get_instance_private (self); + + g_return_if_fail (JSONRPC_IS_OUTPUT_STREAM (self)); + + use_gvariant = !!use_gvariant; + + if (priv->use_gvariant != use_gvariant) + { + priv->use_gvariant = use_gvariant; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USE_GVARIANT]); + } +} diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-output-stream.h b/lsp/deps/jsonrpc-glib/jsonrpc-output-stream.h new file mode 100644 index 000000000..cf8f2ef11 --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-output-stream.h @@ -0,0 +1,77 @@ +/* jsonrpc-output-stream.h + * + * Copyright (C) 2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef JSONRPC_OUTPUT_STREAM_H +#define JSONRPC_OUTPUT_STREAM_H + +#include +#include + +#include "jsonrpc-version-macros.h" + +G_BEGIN_DECLS + +#define JSONRPC_TYPE_OUTPUT_STREAM (jsonrpc_output_stream_get_type()) + +JSONRPC_AVAILABLE_IN_3_26 +G_DECLARE_DERIVABLE_TYPE (JsonrpcOutputStream, jsonrpc_output_stream, JSONRPC, OUTPUT_STREAM, GDataOutputStream) + +struct _JsonrpcOutputStreamClass +{ + GDataOutputStreamClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; + gpointer _reserved9; + gpointer _reserved10; + gpointer _reserved11; + gpointer _reserved12; +}; + +JSONRPC_AVAILABLE_IN_3_26 +JsonrpcOutputStream *jsonrpc_output_stream_new (GOutputStream *base_stream); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_output_stream_get_use_gvariant (JsonrpcOutputStream *self); +JSONRPC_AVAILABLE_IN_3_26 +void jsonrpc_output_stream_set_use_gvariant (JsonrpcOutputStream *self, + gboolean use_gvariant); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_output_stream_write_message (JsonrpcOutputStream *self, + GVariant *message, + GCancellable *cancellable, + GError **error); +JSONRPC_AVAILABLE_IN_3_26 +void jsonrpc_output_stream_write_message_async (JsonrpcOutputStream *self, + GVariant *message, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +JSONRPC_AVAILABLE_IN_3_26 +gboolean jsonrpc_output_stream_write_message_finish (JsonrpcOutputStream *self, + GAsyncResult *result, + GError **error); + +G_END_DECLS + +#endif /* JSONRPC_OUTPUT_STREAM_H */ diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-server.c b/lsp/deps/jsonrpc-glib/jsonrpc-server.c new file mode 100644 index 000000000..a7761559a --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-server.c @@ -0,0 +1,483 @@ +/* jsonrpc-server.c + * + * Copyright (C) 2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. + * + * This file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +//#define G_LOG_DOMAIN "jsonrpc-server" + +#include "config.h" + +#include + +#include "jsonrpc-input-stream.h" +#include "jsonrpc-marshalers.h" +#include "jsonrpc-output-stream.h" +#include "jsonrpc-server.h" + +/** + * JsonrpcServer: + * + * A server for JSON-RPC communication + * + * The #JsonrpcServer class can help you implement a JSON-RPC server. You can + * accept connections and then communicate with clients using the + * [class@Client] API. + */ + +typedef struct +{ + GHashTable *clients; + GArray *handlers; + guint last_handler_id; +} JsonrpcServerPrivate; + +typedef struct +{ + const gchar *method; + JsonrpcServerHandler handler; + gpointer handler_data; + GDestroyNotify handler_data_destroy; + guint handler_id; +} JsonrpcServerHandlerData; + +G_DEFINE_TYPE_WITH_PRIVATE (JsonrpcServer, jsonrpc_server, G_TYPE_OBJECT) + +enum { + HANDLE_CALL, + NOTIFICATION, + CLIENT_ACCEPTED, + CLIENT_CLOSED, + N_SIGNALS +}; + +static guint signals [N_SIGNALS]; + +static void +jsonrpc_server_clear_handler_data (JsonrpcServerHandlerData *data) +{ + if (data->handler_data_destroy) + data->handler_data_destroy (data->handler_data); +} + +static gint +locate_handler_by_method (const void *key, + const void *element) +{ + const gchar *method = key; + const JsonrpcServerHandlerData *data = element; + + return g_strcmp0 (method, data->method); +} + +static gboolean +jsonrpc_server_real_handle_call (JsonrpcServer *self, + JsonrpcClient *client, + const gchar *method, + GVariant *id, + GVariant *params) +{ + JsonrpcServerPrivate *priv = jsonrpc_server_get_instance_private (self); + JsonrpcServerHandlerData *data; + + g_assert (JSONRPC_IS_SERVER (self)); + g_assert (JSONRPC_IS_CLIENT (client)); + g_assert (method != NULL); + g_assert (id != NULL); + + data = bsearch (method, (gpointer)priv->handlers->data, + priv->handlers->len, sizeof (JsonrpcServerHandlerData), + locate_handler_by_method); + + if (data != NULL) + { + data->handler (self, client, method, id, params, data->handler_data); + return TRUE; + } + + return FALSE; +} + +static void +jsonrpc_server_dispose (GObject *object) +{ + JsonrpcServer *self = (JsonrpcServer *)object; + JsonrpcServerPrivate *priv = jsonrpc_server_get_instance_private (self); + + g_clear_pointer (&priv->clients, g_hash_table_unref); + g_clear_pointer (&priv->handlers, g_array_unref); + + G_OBJECT_CLASS (jsonrpc_server_parent_class)->dispose (object); +} + +static void +jsonrpc_server_class_init (JsonrpcServerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = jsonrpc_server_dispose; + + klass->handle_call = jsonrpc_server_real_handle_call; + + /** + * JsonrpcServer::handle-call: + * @self: A #JsonrpcServer + * @client: A #JsonrpcClient + * @method: The method that was called + * @id: The identifier of the method call + * @params: The parameters of the method call + * + * This method is emitted when the client requests a method call. + * + * If you return %TRUE from this function, you should reply to it (even upon + * failure), using [method@Client.reply] or [method@Client.reply_async]. + * + * Returns: %TRUE if the request was handled. + * + * Since: 3.26 + */ + signals [HANDLE_CALL] = + g_signal_new ("handle-call", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonrpcServerClass, handle_call), + g_signal_accumulator_true_handled, NULL, + _jsonrpc_marshal_BOOLEAN__OBJECT_STRING_VARIANT_VARIANT, + G_TYPE_BOOLEAN, + 4, + JSONRPC_TYPE_CLIENT, + G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE, + G_TYPE_VARIANT, + G_TYPE_VARIANT); + g_signal_set_va_marshaller (signals [HANDLE_CALL], + G_TYPE_FROM_CLASS (klass), + _jsonrpc_marshal_BOOLEAN__OBJECT_STRING_VARIANT_VARIANTv); + + /** + * JsonrpcServer::notification: + * @self: A #JsonrpcServer + * @client: A #JsonrpcClient + * @method: The notification name + * @id: The params for the notification + * + * This signal is emitted when the client has sent a notification to us. + * + * Since: 3.26 + */ + signals [NOTIFICATION] = + g_signal_new ("notification", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonrpcServerClass, notification), + NULL, NULL, + _jsonrpc_marshal_VOID__OBJECT_STRING_VARIANT, + G_TYPE_NONE, + 3, + JSONRPC_TYPE_CLIENT, + G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE, + G_TYPE_VARIANT); + g_signal_set_va_marshaller (signals [NOTIFICATION], + G_TYPE_FROM_CLASS (klass), + _jsonrpc_marshal_VOID__OBJECT_STRING_VARIANTv); + + /** + * JsonrpcServer::client-accepted: + * @self: A #JsonrpcServer + * @client: A #JsonrpcClient + * + * This signal is emitted when a new client has been accepted. + * + * Since: 3.28 + */ + signals [CLIENT_ACCEPTED] = + g_signal_new ("client-accepted", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonrpcServerClass, client_accepted), + NULL, NULL, + NULL, + G_TYPE_NONE, + 1, + JSONRPC_TYPE_CLIENT); + + /** + * JsonrpcServer::client-closed: + * @self: A #JsonrpcServer + * @client: A #JsonrpcClient + * + * This signal is emitted when a new client has been lost. + * + * Since: 3.30 + */ + signals [CLIENT_CLOSED] = + g_signal_new ("client-closed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (JsonrpcServerClass, client_closed), + NULL, NULL, + NULL, + G_TYPE_NONE, + 1, + JSONRPC_TYPE_CLIENT); +} + +static void +jsonrpc_server_init (JsonrpcServer *self) +{ + JsonrpcServerPrivate *priv = jsonrpc_server_get_instance_private (self); + + priv->clients = g_hash_table_new_full (NULL, NULL, g_object_unref, NULL); + + priv->handlers = g_array_new (FALSE, FALSE, sizeof (JsonrpcServerHandlerData)); + g_array_set_clear_func (priv->handlers, (GDestroyNotify)jsonrpc_server_clear_handler_data); +} + +/** + * jsonrpc_server_new: + * + * Creates a new #JsonrpcServer. + * + * Returns: (transfer full): A newly created #JsonrpcServer instance. + * + * Since: 3.26 + */ +JsonrpcServer * +jsonrpc_server_new (void) +{ + return g_object_new (JSONRPC_TYPE_SERVER, NULL); +} + +static gboolean +dummy_func (gpointer data) +{ + return G_SOURCE_REMOVE; +} + +static void +jsonrpc_server_client_failed (JsonrpcServer *self, + JsonrpcClient *client) +{ + JsonrpcServerPrivate *priv = jsonrpc_server_get_instance_private (self); + + g_assert (JSONRPC_IS_SERVER (self)); + g_assert (JSONRPC_IS_CLIENT (client)); + + if (priv->clients != NULL && + g_hash_table_contains (priv->clients, client)) + { + /* Release instance from main thread to ensure callers return + * safely without having to be careful about incrementing ref + */ + g_debug ("Lost connection to client [%p]", client); + g_hash_table_steal (priv->clients, client); + g_signal_emit (self, signals [CLIENT_CLOSED], 0, client); + g_idle_add_full (G_MAXINT, dummy_func, client, g_object_unref); + } +} + +static gboolean +jsonrpc_server_client_handle_call (JsonrpcServer *self, + const gchar *method, + JsonNode *id, + JsonNode *params, + JsonrpcClient *client) +{ + gboolean ret; + + g_assert (JSONRPC_IS_SERVER (self)); + g_assert (method != NULL); + g_assert (id != NULL); + g_assert (params != NULL); + g_assert (JSONRPC_IS_CLIENT (client)); + + g_signal_emit (self, signals [HANDLE_CALL], 0, client, method, id, params, &ret); + + return ret; +} + +static void +jsonrpc_server_client_notification (JsonrpcServer *self, + const gchar *method, + JsonNode *params, + JsonrpcClient *client) +{ + g_assert (JSONRPC_IS_SERVER (self)); + g_assert (method != NULL); + g_assert (params != NULL); + g_assert (JSONRPC_IS_CLIENT (client)); + + g_signal_emit (self, signals [NOTIFICATION], 0, client, method, params); +} + +/** + * jsonrpc_server_accept_io_stream: + * @self: A #JsonrpcServer + * @io_stream: A #GIOStream + * + * This function accepts @io_stream as a new client to the #JsonrpcServer + * by wrapping it in a #JsonrpcClient and starting the message accept + * loop. + * + * Since: 3.26 + */ +void +jsonrpc_server_accept_io_stream (JsonrpcServer *self, + GIOStream *io_stream) +{ + JsonrpcServerPrivate *priv = jsonrpc_server_get_instance_private (self); + JsonrpcClient *client; + + g_return_if_fail (JSONRPC_IS_SERVER (self)); + g_return_if_fail (G_IS_IO_STREAM (io_stream)); + + client = jsonrpc_client_new (io_stream); + + g_signal_connect_object (client, + "failed", + G_CALLBACK (jsonrpc_server_client_failed), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (client, + "handle-call", + G_CALLBACK (jsonrpc_server_client_handle_call), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (client, + "notification", + G_CALLBACK (jsonrpc_server_client_notification), + self, + G_CONNECT_SWAPPED); + + g_hash_table_insert (priv->clients, client, NULL); + + jsonrpc_client_start_listening (client); + + g_signal_emit (self, signals [CLIENT_ACCEPTED], 0, client); +} + +static gint +sort_by_method (gconstpointer a, + gconstpointer b) +{ + const JsonrpcServerHandlerData *data_a = a; + const JsonrpcServerHandlerData *data_b = b; + + return g_strcmp0 (data_a->method, data_b->method); +} + +/** + * jsonrpc_server_add_handler: + * @self: A #JsonrpcServer + * @method: A method to handle + * @handler: (closure handler_data) (destroy handler_data_destroy): A handler to + * execute when an incoming method matches @methods + * @handler_data: User data for @handler + * @handler_data_destroy: A destroy callback for @handler_data + * + * Adds a new handler that will be dispatched when a matching @method arrives. + * + * Returns: A handler id that can be used to remove the handler with + * [method@Server.remove_handler]. + * + * Since: 3.26 + */ +guint +jsonrpc_server_add_handler (JsonrpcServer *self, + const gchar *method, + JsonrpcServerHandler handler, + gpointer handler_data, + GDestroyNotify handler_data_destroy) +{ + JsonrpcServerPrivate *priv = jsonrpc_server_get_instance_private (self); + JsonrpcServerHandlerData data; + + g_return_val_if_fail (JSONRPC_IS_SERVER (self), 0); + g_return_val_if_fail (handler != NULL, 0); + + data.method = g_intern_string (method); + data.handler = handler; + data.handler_data = handler_data; + data.handler_data_destroy = handler_data_destroy; + data.handler_id = ++priv->last_handler_id; + + g_array_append_val (priv->handlers, data); + g_array_sort (priv->handlers, sort_by_method); + + return data.handler_id; +} + +/** + * jsonrpc_server_remove_handler: + * @self: A #JsonrpcServer + * @handler_id: A handler returned from [method@Server.add_handler] + * + * Removes a handler that was previously registered with [method@Server.add_handler]. + * + * Since: 3.26 + */ +void +jsonrpc_server_remove_handler (JsonrpcServer *self, + guint handler_id) +{ + JsonrpcServerPrivate *priv = jsonrpc_server_get_instance_private (self); + + g_return_if_fail (JSONRPC_IS_SERVER (self)); + g_return_if_fail (handler_id != 0); + + for (guint i = 0; i < priv->handlers->len; i++) + { + const JsonrpcServerHandlerData *data = &g_array_index (priv->handlers, JsonrpcServerHandlerData, i); + + if (data->handler_id == handler_id) + { + g_array_remove_index (priv->handlers, i); + break; + } + } +} + +/** + * jsonrpc_server_foreach: + * @self: A #JsonrpcServer + * @foreach_func: (scope call): A callback for each client + * @user_data: Closure data for @foreach_func + * + * Calls @foreach_func for every client connected. + * + * Since: 3.28 + */ +void +jsonrpc_server_foreach (JsonrpcServer *self, + GFunc foreach_func, + gpointer user_data) +{ + JsonrpcServerPrivate *priv = jsonrpc_server_get_instance_private (self); + g_autofree gpointer *keys = NULL; + guint len; + + g_return_if_fail (JSONRPC_IS_SERVER (self)); + g_return_if_fail (foreach_func != NULL); + + keys = g_hash_table_get_keys_as_array (priv->clients, &len); + + for (guint i = 0; i < len; i++) + { + JsonrpcClient *client = keys[i]; + g_assert (JSONRPC_IS_CLIENT (client)); + foreach_func (client, user_data); + } +} diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-server.h b/lsp/deps/jsonrpc-glib/jsonrpc-server.h new file mode 100644 index 000000000..23ce4c6a2 --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-server.h @@ -0,0 +1,89 @@ +/* jsonrpc-server.h + * + * Copyright (C) 2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. + * + * This file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef JSONRPC_SERVER_H +#define JSONRPC_SERVER_H + +#include + +#include "jsonrpc-client.h" +#include "jsonrpc-version-macros.h" + +G_BEGIN_DECLS + +#define JSONRPC_TYPE_SERVER (jsonrpc_server_get_type()) + +JSONRPC_AVAILABLE_IN_3_26 +G_DECLARE_DERIVABLE_TYPE (JsonrpcServer, jsonrpc_server, JSONRPC, SERVER, GObject) + +struct _JsonrpcServerClass +{ + GObjectClass parent_class; + + gboolean (*handle_call) (JsonrpcServer *self, + JsonrpcClient *client, + const gchar *method, + GVariant *id, + GVariant *params); + void (*notification) (JsonrpcServer *self, + JsonrpcClient *client, + const gchar *method, + GVariant *params); + void (*client_accepted) (JsonrpcServer *self, + JsonrpcClient *client); + void (*client_closed) (JsonrpcServer *self, + JsonrpcClient *client); + + /*< private >*/ + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; +}; + +typedef void (*JsonrpcServerHandler) (JsonrpcServer *self, + JsonrpcClient *client, + const gchar *method, + GVariant *id, + GVariant *params, + gpointer user_data); + +JSONRPC_AVAILABLE_IN_3_26 +JsonrpcServer *jsonrpc_server_new (void); +JSONRPC_AVAILABLE_IN_3_26 +void jsonrpc_server_accept_io_stream (JsonrpcServer *self, + GIOStream *io_stream); +JSONRPC_AVAILABLE_IN_3_26 +guint jsonrpc_server_add_handler (JsonrpcServer *self, + const gchar *method, + JsonrpcServerHandler handler, + gpointer handler_data, + GDestroyNotify handler_data_destroy); +JSONRPC_AVAILABLE_IN_3_26 +void jsonrpc_server_remove_handler (JsonrpcServer *self, + guint handler_id); +JSONRPC_AVAILABLE_IN_3_28 +void jsonrpc_server_foreach (JsonrpcServer *self, + GFunc foreach_func, + gpointer user_data); + +G_END_DECLS + +#endif /* JSONRPC_SERVER_H */ diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-version-macros.h b/lsp/deps/jsonrpc-glib/jsonrpc-version-macros.h new file mode 100644 index 000000000..9e90d3a35 --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-version-macros.h @@ -0,0 +1,188 @@ +/* jsonrpc-version-macros.h + * + * Copyright © 2014 Emmanuele Bassi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef JSONRPC_VERSION_MACROS_H +#define JSONRPC_VERSION_MACROS_H + +#if !defined(JSONRPC_GLIB_INSIDE) && !defined(JSONRPC_GLIB_COMPILATION) +# error "Only can be included directly." +#endif + +#include "jsonrpc-version.h" + +#ifndef _JSONRPC_EXTERN +#define _JSONRPC_EXTERN extern +#endif + +#ifdef JSONRPC_DISABLE_DEPRECATION_WARNINGS +#define JSONRPC_DEPRECATED _JSONRPC_EXTERN +#define JSONRPC_DEPRECATED_FOR(f) _JSONRPC_EXTERN +#define JSONRPC_UNAVAILABLE(maj,min) _JSONRPC_EXTERN +#else +#define JSONRPC_DEPRECATED G_DEPRECATED _JSONRPC_EXTERN +#define JSONRPC_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f) _JSONRPC_EXTERN +#define JSONRPC_UNAVAILABLE(maj,min) G_UNAVAILABLE(maj,min) _JSONRPC_EXTERN +#endif + +#define JSONRPC_VERSION_3_26 (G_ENCODE_VERSION (3, 26)) +#define JSONRPC_VERSION_3_28 (G_ENCODE_VERSION (3, 28)) +#define JSONRPC_VERSION_3_30 (G_ENCODE_VERSION (3, 30)) +#define JSONRPC_VERSION_3_40 (G_ENCODE_VERSION (3, 40)) +#define JSONRPC_VERSION_3_44 (G_ENCODE_VERSION (3, 44)) + +#if (JSONRPC_MINOR_VERSION == 99) +# define JSONRPC_VERSION_CUR_STABLE (G_ENCODE_VERSION (JSONRPC_MAJOR_VERSION + 1, 0)) +#elif (JSONRPC_MINOR_VERSION % 2) +# define JSONRPC_VERSION_CUR_STABLE (G_ENCODE_VERSION (JSONRPC_MAJOR_VERSION, JSONRPC_MINOR_VERSION + 1)) +#else +# define JSONRPC_VERSION_CUR_STABLE (G_ENCODE_VERSION (JSONRPC_MAJOR_VERSION, JSONRPC_MINOR_VERSION)) +#endif + +#if (JSONRPC_MINOR_VERSION == 99) +# define JSONRPC_VERSION_PREV_STABLE (G_ENCODE_VERSION (JSONRPC_MAJOR_VERSION + 1, 0)) +#elif (JSONRPC_MINOR_VERSION % 2) +# define JSONRPC_VERSION_PREV_STABLE (G_ENCODE_VERSION (JSONRPC_MAJOR_VERSION, JSONRPC_MINOR_VERSION - 1)) +#else +# define JSONRPC_VERSION_PREV_STABLE (G_ENCODE_VERSION (JSONRPC_MAJOR_VERSION, JSONRPC_MINOR_VERSION - 2)) +#endif + +/** + * JSONRPC_VERSION_MIN_REQUIRED: + * + * A macro that should be defined by the user prior to including + * the jsonrpc-glib.h header. + * + * The definition should be one of the predefined JSONRPC version + * macros: %JSONRPC_VERSION_3_26, JSONRPC_VERSION_3_28, ... + * + * This macro defines the lower bound for the JSONRPC-GLib API to use. + * + * If a function has been deprecated in a newer version of JSONRPC-GLib, + * it is possible to use this symbol to avoid the compiler warnings + * without disabling warning for every deprecated function. + * + * Since: 3.28 + */ +#ifndef JSONRPC_VERSION_MIN_REQUIRED +# define JSONRPC_VERSION_MIN_REQUIRED (JSONRPC_VERSION_CUR_STABLE) +#endif + +/** + * JSONRPC_VERSION_MAX_ALLOWED: + * + * A macro that should be defined by the user prior to including + * the jsonrpc-glib.h header. + + * The definition should be one of the predefined JSONRPC-GLib version + * macros: %JSONRPC_VERSION_1_0, %JSONRPC_VERSION_1_2,... + * + * This macro defines the upper bound for the JSONRPC API to use. + * + * If a function has been introduced in a newer version of JSONRPC-GLib, + * it is possible to use this symbol to get compiler warnings when + * trying to use that function. + * + * Since: 3.26 + */ +#ifndef JSONRPC_VERSION_MAX_ALLOWED +# if JSONRPC_VERSION_MIN_REQUIRED > JSONRPC_VERSION_PREV_STABLE +# define JSONRPC_VERSION_MAX_ALLOWED (JSONRPC_VERSION_MIN_REQUIRED) +# else +# define JSONRPC_VERSION_MAX_ALLOWED (JSONRPC_VERSION_CUR_STABLE) +# endif +#endif + +#if JSONRPC_VERSION_MAX_ALLOWED < JSONRPC_VERSION_MIN_REQUIRED +#error "JSONRPC_VERSION_MAX_ALLOWED must be >= JSONRPC_VERSION_MIN_REQUIRED" +#endif +#if JSONRPC_VERSION_MIN_REQUIRED < JSONRPC_VERSION_3_26 +#error "JSONRPC_VERSION_MIN_REQUIRED must be >= JSONRPC_VERSION_3_26" +#endif + +#if JSONRPC_VERSION_MIN_REQUIRED >= JSONRPC_VERSION_3_26 +# define JSONRPC_DEPRECATED_IN_3_26 JSONRPC_DEPRECATED +# define JSONRPC_DEPRECATED_IN_3_26_FOR(f) JSONRPC_DEPRECATED_FOR(f) +#else +# define JSONRPC_DEPRECATED_IN_3_26 _JSONRPC_EXTERN +# define JSONRPC_DEPRECATED_IN_3_26_FOR(f) _JSONRPC_EXTERN +#endif + +#if JSONRPC_VERSION_MAX_ALLOWED < JSONRPC_VERSION_3_26 +# define JSONRPC_AVAILABLE_IN_3_26 JSONRPC_UNAVAILABLE(3, 26) +#else +# define JSONRPC_AVAILABLE_IN_3_26 _JSONRPC_EXTERN +#endif + +#if JSONRPC_VERSION_MIN_REQUIRED >= JSONRPC_VERSION_3_28 +# define JSONRPC_DEPRECATED_IN_3_28 JSONRPC_DEPRECATED +# define JSONRPC_DEPRECATED_IN_3_28_FOR(f) JSONRPC_DEPRECATED_FOR(f) +#else +# define JSONRPC_DEPRECATED_IN_3_28 _JSONRPC_EXTERN +# define JSONRPC_DEPRECATED_IN_3_28_FOR(f) _JSONRPC_EXTERN +#endif + +#if JSONRPC_VERSION_MAX_ALLOWED < JSONRPC_VERSION_3_28 +# define JSONRPC_AVAILABLE_IN_3_28 JSONRPC_UNAVAILABLE(3, 28) +#else +# define JSONRPC_AVAILABLE_IN_3_28 _JSONRPC_EXTERN +#endif + +#if JSONRPC_VERSION_MIN_REQUIRED >= JSONRPC_VERSION_3_30 +# define JSONRPC_DEPRECATED_IN_3_30 JSONRPC_DEPRECATED +# define JSONRPC_DEPRECATED_IN_3_30_FOR(f) JSONRPC_DEPRECATED_FOR(f) +#else +# define JSONRPC_DEPRECATED_IN_3_30 _JSONRPC_EXTERN +# define JSONRPC_DEPRECATED_IN_3_30_FOR(f) _JSONRPC_EXTERN +#endif + +#if JSONRPC_VERSION_MAX_ALLOWED < JSONRPC_VERSION_3_30 +# define JSONRPC_AVAILABLE_IN_3_30 JSONRPC_UNAVAILABLE(3, 30) +#else +# define JSONRPC_AVAILABLE_IN_3_30 _JSONRPC_EXTERN +#endif + +#if JSONRPC_VERSION_MIN_REQUIRED >= JSONRPC_VERSION_3_40 +# define JSONRPC_DEPRECATED_IN_3_40 JSONRPC_DEPRECATED +# define JSONRPC_DEPRECATED_IN_3_40_FOR(f) JSONRPC_DEPRECATED_FOR(f) +#else +# define JSONRPC_DEPRECATED_IN_3_40 _JSONRPC_EXTERN +# define JSONRPC_DEPRECATED_IN_3_40_FOR(f) _JSONRPC_EXTERN +#endif + +#if JSONRPC_VERSION_MAX_ALLOWED < JSONRPC_VERSION_3_40 +# define JSONRPC_AVAILABLE_IN_3_40 JSONRPC_UNAVAILABLE(3, 40) +#else +# define JSONRPC_AVAILABLE_IN_3_40 _JSONRPC_EXTERN +#endif + +#if JSONRPC_VERSION_MIN_REQUIRED >= JSONRPC_VERSION_3_44 +# define JSONRPC_DEPRECATED_IN_3_44 JSONRPC_DEPRECATED +# define JSONRPC_DEPRECATED_IN_3_44_FOR(f) JSONRPC_DEPRECATED_FOR(f) +#else +# define JSONRPC_DEPRECATED_IN_3_44 _JSONRPC_EXTERN +# define JSONRPC_DEPRECATED_IN_3_44_FOR(f) _JSONRPC_EXTERN +#endif + +#if JSONRPC_VERSION_MAX_ALLOWED < JSONRPC_VERSION_3_44 +# define JSONRPC_AVAILABLE_IN_3_44 JSONRPC_UNAVAILABLE(3, 44) +#else +# define JSONRPC_AVAILABLE_IN_3_44 _JSONRPC_EXTERN +#endif + +#endif /* JSONRPC_VERSION_MACROS_H */ diff --git a/lsp/deps/jsonrpc-glib/jsonrpc-version.h b/lsp/deps/jsonrpc-glib/jsonrpc-version.h new file mode 100644 index 000000000..504c5d694 --- /dev/null +++ b/lsp/deps/jsonrpc-glib/jsonrpc-version.h @@ -0,0 +1,98 @@ +/* jsonrpc-version.h.in + * + * Copyright (C) 2016 Christian Hergert + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef JSONRPC_GLIB_VERSION_H +#define JSONRPC_GLIB_VERSION_H + +#if !defined(JSONRPC_GLIB_INSIDE) && !defined(JSONRPC_GLIB_COMPILATION) +# error "Only can be included directly." +#endif + +/** + * SECTION:jsonrpc-version + * @short_description: jsonrpc-glib version checking + * + * jsonrpc-glib provides macros to check the version of the library + * at compile-time + */ + +/** + * JSONRPC_MAJOR_VERSION: + * + * jsonrpc-glib major version component (e.g. 1 if %JSONRPC_VERSION is 1.2.3) + */ +#define JSONRPC_MAJOR_VERSION (3) + +/** + * JSONRPC_MINOR_VERSION: + * + * jsonrpc-glib minor version component (e.g. 2 if %JSONRPC_VERSION is 1.2.3) + */ +#define JSONRPC_MINOR_VERSION (44) + +/** + * JSONRPC_MICRO_VERSION: + * + * jsonrpc-glib micro version component (e.g. 3 if %JSONRPC_VERSION is 1.2.3) + */ +#define JSONRPC_MICRO_VERSION (1) + +/** + * JSONRPC_VERSION + * + * jsonrpc-glib version. + */ +#define JSONRPC_VERSION (3.44.1) + +/** + * JSONRPC_VERSION_S: + * + * jsonrpc-glib version, encoded as a string, useful for printing and + * concatenation. + */ +#define JSONRPC_VERSION_S "3.44.1" + +#define JSONRPC_ENCODE_VERSION(major,minor,micro) \ + ((major) << 24 | (minor) << 16 | (micro) << 8) + +/** + * JSONRPC_VERSION_HEX: + * + * jsonrpc-glib version, encoded as an hexadecimal number, useful for + * integer comparisons. + */ +#define JSONRPC_VERSION_HEX \ + (JSONRPC_ENCODE_VERSION (JSONRPC_MAJOR_VERSION, JSONRPC_MINOR_VERSION, JSONRPC_MICRO_VERSION)) + +/** + * JSONRPC_CHECK_VERSION: + * @major: required major version + * @minor: required minor version + * @micro: required micro version + * + * Compile-time version checking. Evaluates to %TRUE if the version + * of jsonrpc-glib is greater than the required one. + */ +#define JSONRPC_CHECK_VERSION(major,minor,micro) \ + (JSONRPC_MAJOR_VERSION > (major) || \ + (JSONRPC_MAJOR_VERSION == (major) && JSONRPC_MINOR_VERSION > (minor)) || \ + (JSONRPC_MAJOR_VERSION == (major) && JSONRPC_MINOR_VERSION == (minor) && \ + JSONRPC_MICRO_VERSION >= (micro))) + +#endif /* JSONRPC_GLIB_VERSION_H */ diff --git a/lsp/src/Makefile.am b/lsp/src/Makefile.am new file mode 100644 index 000000000..e35efedc5 --- /dev/null +++ b/lsp/src/Makefile.am @@ -0,0 +1,67 @@ +include $(top_srcdir)/build/vars.build.mk +plugin = lsp + +geanyplugins_LTLIBRARIES = lsp.la + +lsp_la_SOURCES = \ + lsp-autocomplete.c \ + lsp-autocomplete.h \ + lsp-code-lens.c \ + lsp-code-lens.h \ + lsp-command.c \ + lsp-command.h \ + lsp-diagnostics.c \ + lsp-diagnostics.h \ + lsp-extension.c \ + lsp-extension.h \ + lsp-format.c \ + lsp-format.h \ + lsp-goto-anywhere.c \ + lsp-goto-anywhere.h \ + lsp-goto.c \ + lsp-goto.h \ + lsp-goto-panel.c \ + lsp-goto-panel.h \ + lsp-highlight.c \ + lsp-highlight.h \ + lsp-hover.c \ + lsp-hover.h \ + lsp-log.c \ + lsp-log.h \ + lsp-main.c \ + lsp-progress.c \ + lsp-progress.h \ + lsp-rename.c \ + lsp-rename.h \ + lsp-rpc.c \ + lsp-rpc.h \ + lsp-semtokens.c \ + lsp-semtokens.h \ + lsp-server.c \ + lsp-server.h \ + lsp-signature.c \ + lsp-signature.h \ + lsp-symbol.c \ + lsp-symbol.h \ + lsp-symbols.c \ + lsp-symbols.h \ + lsp-symbol-kinds.c \ + lsp-symbol-kinds.h \ + lsp-symbol-tree.c \ + lsp-symbol-tree.h \ + lsp-sync.c \ + lsp-sync.h \ + lsp-utils.c \ + lsp-utils.h \ + lsp-workspace-folders.c \ + lsp-workspace-folders.h + +lsp_la_CPPFLAGS = $(AM_CPPFLAGS) \ + -DG_LOG_DOMAIN=\"LSP\" \ + -I$(top_srcdir)/lsp/deps/jsonrpc-glib \ + -I$(top_srcdir)/lsp/deps \ + -I$(top_srcdir)/lsp/src +lsp_la_CFLAGS = $(AM_CFLAGS) +lsp_la_LIBADD = $(COMMONLIBS) $(top_builddir)/lsp/deps/libjsonrpc.la + +include $(top_srcdir)/build/cppcheck.mk diff --git a/lsp/src/lsp-autocomplete.c b/lsp/src/lsp-autocomplete.c new file mode 100644 index 000000000..322c41584 --- /dev/null +++ b/lsp/src/lsp-autocomplete.c @@ -0,0 +1,677 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-autocomplete.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" +#include "lsp-server.h" +#include "lsp-symbol-kinds.h" + +#include +#include +#include + + +typedef struct +{ + gchar *label; + LspCompletionKind kind; + gchar *sort_text; + gchar *insert_text; + gchar *detail; + LspTextEdit *text_edit; + GPtrArray * additional_edits; + gboolean is_snippet; +} LspAutocompleteSymbol; + + +typedef struct +{ + GeanyDocument *doc; + gint request_id; +} LspAutocompleteAsyncData; + + +typedef struct +{ + gint pass; + gchar *prefix; + gboolean use_label; + const gchar *word_chars; +} SortData; + + +static GPtrArray *displayed_autocomplete_symbols = NULL; +static gint sent_request_id = 0; +static gint received_request_id = 0; +static gint discard_up_to_request_id = 0; + + +void lsp_autocomplete_discard_pending_requests() +{ + discard_up_to_request_id = sent_request_id; +} + + +void lsp_autocomplete_set_displayed_symbols(GPtrArray *symbols) +{ + if (displayed_autocomplete_symbols) + g_ptr_array_free(displayed_autocomplete_symbols, TRUE); + displayed_autocomplete_symbols = symbols; +} + + +static void free_autocomplete_symbol(gpointer data) +{ + LspAutocompleteSymbol *sym = data; + g_free(sym->label); + g_free(sym->sort_text); + g_free(sym->insert_text); + g_free(sym->detail); + lsp_utils_free_lsp_text_edit(sym->text_edit); + if (sym->additional_edits) + g_ptr_array_free(sym->additional_edits, TRUE); + g_free(sym); +} + + +static const gchar *get_label(LspAutocompleteSymbol *sym, gboolean use_label) +{ + if (use_label && sym->label) + return sym->label; + + if (sym->text_edit && sym->text_edit->new_text) + return sym->text_edit->new_text; + if (sym->insert_text) + return sym->insert_text; + if (sym->label) + return sym->label; + + return ""; +} + + +static gchar *get_symbol_label(LspServer *server, LspAutocompleteSymbol *sym) +{ + gchar *label = g_strdup(get_label(sym, server->config.autocomplete_use_label)); + gchar *pos; + + // remove stuff after newlines (we don't want them in the popup plus \n + // is used as the autocompletion list separator) + pos = strchr(label, '\n'); + if (pos) + *pos = '\0'; + pos = strchr(label, '\r'); + if (pos) + *pos = '\0'; + + // ? used by Scintilla for icon specification + pos = strchr(label, '?'); + if (pos) + *pos = ' '; + pos = strchr(label, '\t'); + if (pos) + *pos = ' '; + + return label; +} + + +static guint get_ident_prefixlen(const gchar *word_chars, GeanyDocument *doc, gint pos) +{ + ScintillaObject *sci = doc->editor->sci; + gint num = 0; + + while (pos > 0) + { + gint new_pos = SSM(sci, SCI_POSITIONBEFORE, pos, 0); + if (pos - new_pos == 1) + { + gchar c = sci_get_char_at(sci, new_pos); + if (!strchr(word_chars, c)) + break; + } + num++; + pos = new_pos; + } + + return num; +} + + +void lsp_autocomplete_item_selected(LspServer *server, GeanyDocument *doc, guint index) +{ + ScintillaObject *sci = doc->editor->sci; + gint sel_num = SSM(sci, SCI_GETSELECTIONS, 0, 0); + LspAutocompleteSymbol *sym; + + if (!displayed_autocomplete_symbols || index >= displayed_autocomplete_symbols->len) + return; + + sym = displayed_autocomplete_symbols->pdata[index]; + /* The sent_request_id == received_request_id detects the condition when + * user typed a character and pressed enter immediately afterwards in which + * case the autocompletion list doesn't contain up-to-date text edits. + * In this case we have to fall back to insert text based autocompletion + * below. */ + if (sel_num == 1 && sym->text_edit && sent_request_id == received_request_id) + { + if (server->config.autocomplete_apply_additional_edits && sym->additional_edits) + lsp_utils_apply_text_edits(sci, sym->text_edit, sym->additional_edits, sym->is_snippet); + else + lsp_utils_apply_text_edit(sci, sym->text_edit, sym->is_snippet); + } + else + { + gchar *insert_text = sym->insert_text ? sym->insert_text : sym->label; + + if (insert_text) + { + gint i; + + for (i = 0; i < sel_num; i++) + { + gint pos = SSM(sci, SCI_GETSELECTIONNCARET, i, 0); + guint rootlen = get_ident_prefixlen(server->config.word_chars, doc, pos); + LspTextEdit text_edit; + + text_edit.new_text = insert_text; + text_edit.range.start = lsp_utils_scintilla_pos_to_lsp(sci, pos - rootlen); + text_edit.range.end = lsp_utils_scintilla_pos_to_lsp(sci, pos); + + lsp_utils_apply_text_edit(sci, &text_edit, sym->is_snippet); + } + } + + /* See comment above, prevents re-showing the autocompletion popup + * in this case. */ + if (sent_request_id != received_request_id) + lsp_autocomplete_discard_pending_requests(); + } +} + + +void lsp_autocomplete_style_init(GeanyDocument *doc) +{ + ScintillaObject *sci = doc->editor->sci; + LspServer *srv = lsp_server_get_if_running(doc); + + if (!srv) + return; + + SSM(sci, SCI_AUTOCSETORDER, SC_ORDER_CUSTOM, 0); + SSM(sci, SCI_AUTOCSETMULTI, SC_MULTIAUTOC_EACH, 0); + SSM(sci, SCI_AUTOCSETAUTOHIDE, FALSE, 0); + SSM(sci, SCI_AUTOCSETMAXHEIGHT, srv->config.autocomplete_window_max_displayed, 0); + SSM(sci, SCI_AUTOCSETMAXWIDTH, srv->config.autocomplete_window_max_width, 0); + SSM(sci, SCI_SETMULTIPASTE, TRUE, 0); +// TODO: remove eventually +#ifdef SC_AUTOCOMPLETE_SELECT_FIRST_ITEM + SSM(sci, SCI_AUTOCSETOPTIONS, SC_AUTOCOMPLETE_SELECT_FIRST_ITEM, 0); +#endif +} + + +static void show_tags_list(LspServer *server, GeanyDocument *doc, GPtrArray *symbols) +{ + guint i; + ScintillaObject *sci = doc->editor->sci; + gint pos = sci_get_current_position(sci); + GString *words = g_string_sized_new(2000); + gchar *label; + + for (i = 0; i < symbols->len; i++) + { + LspAutocompleteSymbol *symbol = symbols->pdata[i]; + guint icon_id = lsp_symbol_kinds_get_completion_icon(symbol->kind); + gchar buf[10]; + + if (i > server->config.autocomplete_window_max_entries) + break; + + if (i > 0) + g_string_append_c(words, '\n'); + + label = get_symbol_label(server, symbol); + g_string_append(words, label); + + sprintf(buf, "?%u", icon_id + 1); + g_string_append(words, buf); + + g_free(label); + } + + lsp_autocomplete_set_displayed_symbols(symbols); + SSM(sci, SCI_AUTOCSHOW, get_ident_prefixlen(server->config.word_chars, doc, pos), (sptr_t) words->str); + +// TODO: remove eventually +#ifndef SC_AUTOCOMPLETE_SELECT_FIRST_ITEM + if (SSM(sci, SCI_AUTOCGETCURRENT, 0, 0) != 0) + { + //make sure Scintilla selects the first item - see https://sourceforge.net/p/scintilla/bugs/2403/ + label = get_symbol_label(server, symbols->pdata[0]); + SSM(sci, SCI_AUTOCSELECT, 0, (sptr_t)label); + g_free(label); + } +#endif + + g_string_free(words, TRUE); +} + + +static gint strstr_delta(const gchar *s1, const gchar *s2) +{ + const gchar *pos = strstr(s1, s2); + if (!pos) + return -1; + return pos - s1; +} + + +static gboolean has_identifier_chars(const gchar *s, const gchar *word_chars) +{ + gint i; + + for (i = 0; s[i]; i++) + { + if (!strchr(word_chars, s[i])) + return FALSE; + } + return TRUE; +} + + +static gint sort_autocomplete_symbols(gconstpointer a, gconstpointer b, gpointer user_data) +{ + LspAutocompleteSymbol *sym1 = *((LspAutocompleteSymbol **)a); + LspAutocompleteSymbol *sym2 = *((LspAutocompleteSymbol **)b); + SortData *sort_data = user_data; + gchar *label1 = NULL; + gchar *label2 = NULL; + + if (sort_data->use_label && sym1->label) + label1 = sym1->label; + else if (sym1->text_edit && sym1->text_edit->new_text) + label1 = sym1->text_edit->new_text; + else if (sym1->insert_text) + label1 = sym1->insert_text; + else if (sym1->label) + label1 = sym1->label; + + if (sort_data->use_label && sym2->label) + label2 = sym2->label; + else if (sym2->text_edit && sym2->text_edit->new_text) + label2 = sym2->text_edit->new_text; + else if (sym2->insert_text) + label2 = sym2->insert_text; + else if (sym2->label) + label2 = sym2->label; + + if (sort_data->pass == 2 && label1 && label2 && sort_data->prefix) + { + const gchar *wc = sort_data->word_chars; + gint diff1, diff2; + + if (g_strcmp0(label1, sort_data->prefix) == 0 && g_strcmp0(label2, sort_data->prefix) != 0) + return -1; + if (g_strcmp0(label1, sort_data->prefix) != 0 && g_strcmp0(label2, sort_data->prefix) == 0) + return 1; + + if (g_str_has_prefix(label1, sort_data->prefix) && !g_str_has_prefix(label2, sort_data->prefix)) + return -1; + if (!g_str_has_prefix(label1, sort_data->prefix) && g_str_has_prefix(label2, sort_data->prefix)) + return 1; + + // case insensitive variants + if (utils_str_casecmp(label1, sort_data->prefix) == 0 && utils_str_casecmp(label2, sort_data->prefix) != 0) + return -1; + if (utils_str_casecmp(label1, sort_data->prefix) != 0 && utils_str_casecmp(label2, sort_data->prefix) == 0) + return 1; + + if (lsp_utils_lowercase_cmp((LspUtilsCmpFn)g_str_has_prefix, label1, sort_data->prefix) && + !lsp_utils_lowercase_cmp((LspUtilsCmpFn)g_str_has_prefix, label2, sort_data->prefix)) + return -1; + if (!lsp_utils_lowercase_cmp((LspUtilsCmpFn)g_str_has_prefix, label1, sort_data->prefix) && + lsp_utils_lowercase_cmp((LspUtilsCmpFn)g_str_has_prefix, label2, sort_data->prefix)) + return 1; + + // anywhere within string, any case, earlier occurrence wins + diff1 = GPOINTER_TO_INT(lsp_utils_lowercase_cmp( + (LspUtilsCmpFn)strstr_delta, label1, sort_data->prefix)); + diff2 = GPOINTER_TO_INT(lsp_utils_lowercase_cmp( + (LspUtilsCmpFn)strstr_delta, label2, sort_data->prefix)); + if (diff1 != -1 && diff2 == -1) + return -1; + if (diff1 == -1 && diff2 != -1) + return 1; + if (diff1 != -1 && diff2 != -1 && diff1 != diff2) + return diff1 - diff2; + + if (has_identifier_chars(label1, wc) && !has_identifier_chars(label2, wc)) + return -1; + if (!has_identifier_chars(label1, wc) && has_identifier_chars(label2, wc)) + return 1; + } + + if (sym1->sort_text && sym2->sort_text) + return g_strcmp0(sym1->sort_text, sym2->sort_text); + + if (label1 && label2) + return utils_str_casecmp(label1, label2); + + return 0; +} + + +static gboolean should_add(GPtrArray *symbols, const gchar *prefix) +{ + LspAutocompleteSymbol *sym; + const gchar *label; + + if (symbols->len == 0) + return FALSE; + + if (symbols->len > 1) + return TRUE; + + // don't single value with what's already typed unless it's a snippet + sym = symbols->pdata[0]; + label = get_label(sym, FALSE); + if (g_strcmp0(label, prefix) != 0) + return TRUE; + + return sym->is_snippet || sym->kind == LspCompletionKindSnippet; +} + + +static void process_response(LspServer *server, GVariant *response, GeanyDocument *doc) +{ + //gboolean is_incomplete = FALSE; + GVariantIter *iter = NULL; + GVariant *member = NULL; + ScintillaObject *sci = doc->editor->sci; + gint pos = sci_get_current_position(sci); + gint prefixlen = get_ident_prefixlen(server->config.word_chars, doc, pos); + SortData sort_data = { 1, NULL, server->config.autocomplete_use_label, server->config.word_chars }; + GPtrArray *symbols, *symbols_filtered; + GHashTable *entry_set; + gint i; + + JSONRPC_MESSAGE_PARSE(response, + //"isIncomplete", JSONRPC_MESSAGE_GET_BOOLEAN(&is_incomplete), + "items", JSONRPC_MESSAGE_GET_ITER(&iter)); + + if (!iter && g_variant_is_of_type(response, G_VARIANT_TYPE_ARRAY)) + iter = g_variant_iter_new(response); + + if (!iter) + return; + + symbols = g_ptr_array_new_full(0, NULL); // not freeing symbols here + + while (g_variant_iter_loop(iter, "v", &member)) + { + LspAutocompleteSymbol *sym; + GVariant *text_edit = NULL; + GVariantIter *additional_edits = NULL; + const gchar *label = NULL; + const gchar *insert_text = NULL; + const gchar *sort_text = NULL; + const gchar *detail = NULL; + gint64 kind = 0; + gint64 format = 0; + + JSONRPC_MESSAGE_PARSE(member, "kind", JSONRPC_MESSAGE_GET_INT64(&kind)); + + if (kind == LspCompletionKindSnippet && !server->config.autocomplete_use_snippets) + continue; + + JSONRPC_MESSAGE_PARSE(member, "insertText", JSONRPC_MESSAGE_GET_STRING(&insert_text)); + JSONRPC_MESSAGE_PARSE(member, "insertTextFormat", JSONRPC_MESSAGE_GET_INT64(&format)); + + if (!server->config.autocomplete_use_snippets && format == 2 && + // Lua server flags as snippet without actually being a snippet + insert_text && strchr(insert_text, '$')) + { + continue; + } + + JSONRPC_MESSAGE_PARSE(member, "label", JSONRPC_MESSAGE_GET_STRING(&label)); + JSONRPC_MESSAGE_PARSE(member, "sortText", JSONRPC_MESSAGE_GET_STRING(&sort_text)); + JSONRPC_MESSAGE_PARSE(member, "detail", JSONRPC_MESSAGE_GET_STRING(&detail)); + JSONRPC_MESSAGE_PARSE(member, "textEdit", JSONRPC_MESSAGE_GET_VARIANT(&text_edit)); + JSONRPC_MESSAGE_PARSE(member, "additionalTextEdits", JSONRPC_MESSAGE_GET_ITER(&additional_edits)); + + sym = g_new0(LspAutocompleteSymbol, 1); + sym->label = g_strdup(label); + sym->insert_text = g_strdup(insert_text); + sym->sort_text = g_strdup(sort_text); + sym->detail = g_strdup(detail); + sym->kind = kind; + sym->text_edit = lsp_utils_parse_text_edit(text_edit); + sym->additional_edits = lsp_utils_parse_text_edits(additional_edits); + sym->is_snippet = (format == 2); + + g_ptr_array_add(symbols, sym); + + if (text_edit) + g_variant_unref(text_edit); + + if (additional_edits) + g_variant_iter_free(additional_edits); + } + + /* sort based on sorting provided by LSP server */ + g_ptr_array_sort_with_data(symbols, sort_autocomplete_symbols, &sort_data); + + symbols_filtered = g_ptr_array_new_full(symbols->len, free_autocomplete_symbol); + entry_set = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + + /* remove duplicates */ + for (i = 0; i < symbols->len; i++) + { + LspAutocompleteSymbol *sym = symbols->pdata[i]; + gchar *display_label = get_symbol_label(server, sym); + + if (g_hash_table_contains(entry_set, display_label)) + { + free_autocomplete_symbol(sym); + g_free(display_label); + } + else + { + g_ptr_array_add(symbols_filtered, sym); + g_hash_table_insert(entry_set, display_label, NULL); + } + } + + g_ptr_array_free(symbols, TRUE); + symbols = symbols_filtered; + + if (prefixlen > 0) + sort_data.prefix = sci_get_contents_range(sci, pos - prefixlen, pos); + sort_data.pass = 2; + /* sort with symbols matching the typed prefix first */ + g_ptr_array_sort_with_data(symbols, sort_autocomplete_symbols, &sort_data); + + if (should_add(symbols, sort_data.prefix)) + show_tags_list(server, doc, symbols); + else + { + g_ptr_array_free(symbols, TRUE); + SSM(doc->editor->sci, SCI_AUTOCCANCEL, 0, 0); + } + + g_variant_iter_free(iter); + g_hash_table_destroy(entry_set); + g_free(sort_data.prefix); +} + + +static void autocomplete_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + if (!error) + { + GeanyDocument *current_doc = document_get_current(); + LspAutocompleteAsyncData *data = user_data; + GeanyDocument *doc = data->doc; + + if (current_doc == doc && data->request_id > received_request_id && + data->request_id > discard_up_to_request_id) + { + LspServer *srv = lsp_server_get(doc); + received_request_id = data->request_id; + process_response(srv, return_value, doc); + //printf("%s\n", lsp_utils_json_pretty_print(return_value)); + } + } + + g_free(user_data); +} + + +static gboolean ends_with_sequence(ScintillaObject *sci, gchar** seqs) +{ + gint pos = sci_get_current_position(sci); + guint max = 0; + gchar **str; + gchar *prev_str; + gboolean ret = FALSE; + + if (!seqs) + return FALSE; + + foreach_strv(str, seqs) + max = MAX(max, strlen(*str)); + + prev_str = sci_get_contents_range(sci, pos - max > 0 ? pos - max : 0, pos); + + foreach_strv(str, seqs) + { + if (g_str_has_suffix(prev_str, *str)) + { + ret = TRUE; + break; + } + } + + g_free(prev_str); + return ret; +} + + +void lsp_autocomplete_completion(LspServer *server, GeanyDocument *doc, gboolean force) +{ + GVariant *node; + gchar *doc_uri; + LspAutocompleteAsyncData *data; + ScintillaObject *sci = doc->editor->sci; + gint pos = sci_get_current_position(sci); + LspPosition lsp_pos = lsp_utils_scintilla_pos_to_lsp(sci, pos); + gboolean is_trigger_char = FALSE; + gchar c = pos > 0 ? sci_get_char_at(sci, SSM(sci, SCI_POSITIONBEFORE, pos, 0)) : '\0'; + gchar c_str[2] = {c, '\0'}; + gint prefixlen = get_ident_prefixlen(server->config.word_chars, doc, pos); + + if (prefixlen == 0) + { + if (!EMPTY(server->config.autocomplete_trigger_sequences) && + !ends_with_sequence(sci, server->config.autocomplete_trigger_sequences)) + { + SSM(doc->editor->sci, SCI_AUTOCCANCEL, 0, 0); + return; + } + + if (EMPTY(server->autocomplete_trigger_chars) || + !strchr(server->autocomplete_trigger_chars, c) || + c == ' ') // Lua LSP has the stupid idea of putting ' ' into trigger chars + { + SSM(doc->editor->sci, SCI_AUTOCCANCEL, 0, 0); + return; + } + else + is_trigger_char = !force; + } + else + { + gint next_pos = SSM(sci, SCI_POSITIONAFTER, pos, 0); + /* if we are inside an identifier also after the next char */ + if (pos != next_pos && // not at EOF + (prefixlen + (next_pos - pos) == get_ident_prefixlen(server->config.word_chars, doc, next_pos))) + { + SSM(doc->editor->sci, SCI_AUTOCCANCEL, 0, 0); + return; /* avoid autocompletion in the middle of a word */ + } + + if (!EMPTY(server->config.autocomplete_hide_after_words)) + { + gchar **comps = g_strsplit(server->config.autocomplete_hide_after_words, ";", -1); + gchar *prefix = sci_get_contents_range(sci, pos - prefixlen, pos); + gboolean found = FALSE; + gchar **comp; + + foreach_strv(comp, comps) + { + if (utils_str_casecmp(*comp, prefix) == 0) + { + found = TRUE; + break; + } + } + g_free(prefix); + g_strfreev(comps); + + if (found) + { + SSM(doc->editor->sci, SCI_AUTOCCANCEL, 0, 0); + return; + } + } + } + + doc_uri = lsp_utils_get_doc_uri(doc); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}", + "position", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.line), + "character", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.character), + "}", + "context", "{", + "triggerKind", JSONRPC_MESSAGE_PUT_INT32(is_trigger_char ? 2 : 1), + "triggerCharacter", JSONRPC_MESSAGE_PUT_STRING(c_str), + "}" + ); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + data = g_new0(LspAutocompleteAsyncData, 1); + data->doc = doc; + data->request_id = ++sent_request_id; + + lsp_rpc_call(server, "textDocument/completion", node, + autocomplete_cb, data); + + g_free(doc_uri); + g_variant_unref(node); +} diff --git a/lsp/src/lsp-autocomplete.h b/lsp/src/lsp-autocomplete.h new file mode 100644 index 000000000..b43d34475 --- /dev/null +++ b/lsp/src/lsp-autocomplete.h @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_AUTOCOMPLETE_H +#define LSP_AUTOCOMPLETE_H 1 + +#include "lsp-server.h" + +void lsp_autocomplete_style_init(GeanyDocument *doc); + +void lsp_autocomplete_completion(LspServer *server, GeanyDocument *doc, gboolean force); + +void lsp_autocomplete_set_displayed_symbols(GPtrArray *symbols); +void lsp_autocomplete_item_selected(LspServer *server, GeanyDocument *doc, guint index); +void lsp_autocomplete_discard_pending_requests(); + +#endif /* LSP_AUTOCOMPLETE_H */ diff --git a/lsp/src/lsp-code-lens.c b/lsp/src/lsp-code-lens.c new file mode 100644 index 000000000..8b6922296 --- /dev/null +++ b/lsp/src/lsp-code-lens.c @@ -0,0 +1,249 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-code-lens.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" +#include "lsp-sync.h" +#include "lsp-command.h" +#include "lsp-utils.h" + +#include + + +extern GeanyPlugin *geany_plugin; +extern GeanyData *geany_data; + +static GPtrArray *commands; + + +static void set_color(LspServer *srv, GeanyDocument *doc) +{ + GdkRGBA bg_color, fg_color, color; + ScintillaObject *sci; + gint style_offset; + gint i = 0; + gchar **comps; + + sci = doc->editor->sci; + + style_offset = SSM(sci, SCI_EOLANNOTATIONGETSTYLEOFFSET, 0, 0); + + gdk_rgba_parse(&bg_color, "yellow"); + gdk_rgba_parse(&fg_color, "black"); + + comps = g_strsplit(srv->config.code_lens_style, ";", -1); + + for (i = 0; comps && comps[i]; i++) + { + switch (i) + { + case 0: + if (!gdk_rgba_parse(&color, comps[i])) + color = fg_color; + SSM(sci, SCI_STYLESETFORE, style_offset, + ((unsigned char)(color.red * 255)) | + ((unsigned char)(color.green * 255) << 8) | + ((unsigned char)(color.blue * 255) << 16)); + break; + case 1: + { + if (!gdk_rgba_parse(&color, comps[i])) + color = bg_color; + SSM(sci, SCI_STYLESETBACK, style_offset, + ((unsigned char)(color.red * 255)) | + ((unsigned char)(color.green * 255) << 8) | + ((unsigned char)(color.blue * 255) << 16)); + break; + } + } + } + + g_strfreev(comps); +} + + +void lsp_code_lens_style_init(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get_if_running(doc); + ScintillaObject *sci; + + if (!srv) + return; + + sci = doc->editor->sci; + + if (SSM(sci, SCI_EOLANNOTATIONGETSTYLEOFFSET, 0, 0) == 0) + { + gint style_offset = SSM(sci, SCI_ALLOCATEEXTENDEDSTYLES, 1, 0); + + SSM(sci, SCI_EOLANNOTATIONSETSTYLEOFFSET, style_offset, 0); + set_color(srv, doc); + } + + if (!commands) + commands = g_ptr_array_new_full(0, (GDestroyNotify)lsp_command_free); +} + + +static void add_annotation(ScintillaObject *sci, gint line, const gchar *text) +{ + SSM(sci, SCI_EOLANNOTATIONSETSTYLE, line, 0); + SSM(sci, SCI_EOLANNOTATIONSETVISIBLE, EOLANNOTATION_ANGLE_CIRCLE, 0); + SSM(sci, SCI_EOLANNOTATIONSETTEXT, line, (sptr_t)text); +} + + +static void code_lens_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + GeanyDocument *doc = user_data; + LspServer *srv; + + srv = DOC_VALID(doc) ? lsp_server_get(doc) : NULL; + + if (!error && srv && g_variant_is_of_type(return_value, G_VARIANT_TYPE_ARRAY)) + { + GVariant *code_action = NULL; + gint last_line = 0; + GVariantIter iter; + GString *str; + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + + SSM(doc->editor->sci, SCI_EOLANNOTATIONCLEARALL, 0, 0); + + str = g_string_new(NULL); + + g_variant_iter_init(&iter, return_value); + + while (g_variant_iter_loop(&iter, "v", &code_action)) + { + const gchar *title = NULL; + const gchar *command = NULL; + GVariant *arguments = NULL; + GVariant *loc_variant = NULL; + LspCommand *cmd; + gint line_num = 0; + + JSONRPC_MESSAGE_PARSE(code_action, + "range", JSONRPC_MESSAGE_GET_VARIANT(&loc_variant) + ); + + if (loc_variant) + { + LspRange range = lsp_utils_parse_range(loc_variant); + line_num = range.start.line; + g_variant_unref(loc_variant); + } + + if (!JSONRPC_MESSAGE_PARSE(code_action, + "command", "{", + "title", JSONRPC_MESSAGE_GET_STRING(&title), + "command", JSONRPC_MESSAGE_GET_STRING(&command), + "}")) + { + continue; + } + + JSONRPC_MESSAGE_PARSE (code_action, + "command", "{", + "arguments", JSONRPC_MESSAGE_GET_VARIANT(&arguments), + "}" + ); + + cmd = g_new0(LspCommand, 1); + cmd->line = line_num; + cmd->title = g_strdup(title); + cmd->command = g_strdup(command); + cmd->arguments = arguments; + + if (line_num != last_line && str->len > 0) + { + add_annotation(doc->editor->sci, last_line, str->str); + g_string_set_size(str, 0); + } + if (str->len == 0) + g_string_append(str, _("LSP Commands: ")); + else + g_string_append(str, " | "); + g_string_append(str, cmd->title); + last_line = line_num; + + g_ptr_array_add(commands, cmd); + } + + if (str->len > 0) + add_annotation(doc->editor->sci, last_line, str->str); + + g_string_free(str, TRUE); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + } +} + + +GPtrArray *lsp_code_lens_get_commands(void) +{ + return commands; +} + + +void lsp_code_lens_send_request(GeanyDocument *doc) +{ + LspServer *server = lsp_server_get(doc); + gchar *doc_uri; + GVariant *node; + + if (!doc || !doc->real_path || !server) + return; + + if (!server->config.code_lens_enable) + return; + + /* set annotation colors every time - Geany doesn't provide any notification + * when color theme changes which also resets colors to some defaults. Even + * though we set colors here, it isn't a perfect solution as it needs a modification + * of the document for the update and in the meantime the color is wrong. */ + lsp_code_lens_style_init(doc); + + g_ptr_array_set_size(commands, 0); + + doc_uri = lsp_utils_get_doc_uri(doc); + + /* Geany requests symbols before firing "document-activate" signal so we may + * need to request document opening here */ + if (!lsp_sync_is_document_open(doc)) + lsp_sync_text_document_did_open(server, doc); + + node = JSONRPC_MESSAGE_NEW( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}" + ); + lsp_rpc_call(server, "textDocument/codeLens", node, + code_lens_cb, doc); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + g_free(doc_uri); + g_variant_unref(node); +} diff --git a/lsp/src/lsp-code-lens.h b/lsp/src/lsp-code-lens.h new file mode 100644 index 000000000..c1e62d421 --- /dev/null +++ b/lsp/src/lsp-code-lens.h @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_CODE_LENS_H +#define LSP_CODE_LENS_H 1 + +#include "lsp-server.h" + +#include + +void lsp_code_lens_send_request(GeanyDocument *doc); +void lsp_code_lens_style_init(GeanyDocument *doc); + +GPtrArray *lsp_code_lens_get_commands(void); + +#endif /* LSP_CODE_LENS_H */ diff --git a/lsp/src/lsp-command.c b/lsp/src/lsp-command.c new file mode 100644 index 000000000..cc82b1be0 --- /dev/null +++ b/lsp/src/lsp-command.c @@ -0,0 +1,274 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-command.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" +#include "lsp-diagnostics.h" + +#include + + +typedef struct +{ + LspCallback callback; + gpointer user_data; +} CommandData; + + +typedef struct +{ + CodeActionCallback callback; + gpointer user_data; +} CodeActionData; + + +void lsp_command_free(LspCommand *cmd) +{ + g_free(cmd->title); + g_free(cmd->command); + if (cmd->arguments) + g_variant_unref(cmd->arguments); + if (cmd->edit) + g_variant_unref(cmd->edit); + g_free(cmd); +} + + +static void command_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + CommandData *data = user_data; + + if (data->callback) + data->callback(data->user_data); + g_free(data); +} + + +void lsp_command_perform(LspServer *server, LspCommand *cmd, LspCallback callback, gpointer user_data) +{ + if (cmd->edit) + lsp_utils_apply_workspace_edit(cmd->edit); + + if (cmd->command) + { + GVariant *node; + CommandData *data; + + if (cmd->arguments) + { + GVariantDict dict; + + g_variant_dict_init(&dict, NULL); + g_variant_dict_insert_value(&dict, "command", g_variant_new_string(cmd->command)); + g_variant_dict_insert_value(&dict, "arguments", cmd->arguments); + node = g_variant_take_ref(g_variant_dict_end(&dict)); + } + else + { + node = JSONRPC_MESSAGE_NEW( + "command", JSONRPC_MESSAGE_PUT_STRING(cmd->command) + ); + } + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + data = g_new0(CommandData, 1); + data->callback = callback; + data->user_data = user_data; + lsp_rpc_call(server, "workspace/executeCommand", node, + command_cb, data); + + g_variant_unref(node); + } + else if (callback) + callback(user_data); // this was just an edit without command execution +} + + +static void code_action_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + GPtrArray *code_actions = g_ptr_array_new_full(1, (GDestroyNotify)lsp_command_free); + CodeActionData *data = user_data; + + if (!error) + { + if (g_variant_is_of_type(return_value, G_VARIANT_TYPE_ARRAY)) + { + GVariant *code_action = NULL; + GVariantIter iter; + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + + g_variant_iter_init(&iter, return_value); + + while (g_variant_iter_loop(&iter, "v", &code_action)) + { + const gchar *title = NULL; + const gchar *command = NULL; + GVariant *edit = NULL; + LspCommand *cmd; + gboolean is_command; + + // Can either be Command or CodeAction: + // Command {title: string; command: string; arguments?: LSPAny[];} + // CodeAction {title: string; edit?: WorkspaceEdit; command?: Command;} + + JSONRPC_MESSAGE_PARSE(code_action, + "title", JSONRPC_MESSAGE_GET_STRING(&title) + ); + + is_command = JSONRPC_MESSAGE_PARSE(code_action, + "command", JSONRPC_MESSAGE_GET_STRING(&command) + ); + + if (!is_command) + { + JSONRPC_MESSAGE_PARSE(code_action, + "command", "{", + "command", JSONRPC_MESSAGE_GET_STRING(&command), + "}" + ); + + JSONRPC_MESSAGE_PARSE(code_action, + "edit", JSONRPC_MESSAGE_GET_VARIANT(&edit) + ); + } + + if (title && (command || edit)) + { + GVariant *arguments = NULL; + + if (is_command) + { + JSONRPC_MESSAGE_PARSE(code_action, + "arguments", JSONRPC_MESSAGE_GET_VARIANT(&arguments) + ); + } + else + { + JSONRPC_MESSAGE_PARSE(code_action, + "command", "{", + "arguments", JSONRPC_MESSAGE_GET_VARIANT(&arguments), + "}" + ); + } + + cmd = g_new0(LspCommand, 1); + cmd->title = g_strdup(title); + cmd->command = g_strdup(command); + cmd->arguments = arguments; + cmd->edit = edit; + + g_ptr_array_add(code_actions, cmd); + } + else + { + if (edit) + g_variant_unref(edit); + } + } + } + } + + if (data->callback(code_actions, data->user_data)) + g_ptr_array_free(code_actions, TRUE); + + g_free(data); +} + + +void lsp_command_send_code_action_request(GeanyDocument *doc, gint pos, CodeActionCallback actions_resolved_cb, gpointer user_data) +{ + LspServer *srv = lsp_server_get_if_running(doc); + GVariant *diag_raw = lsp_diagnostics_get_diag_raw(pos); + GVariant *node, *diagnostics, *diags_dict; + LspPosition lsp_pos_start, lsp_pos_end; + gint pos_start, pos_end; + ScintillaObject *sci; + GVariantDict dict; + GPtrArray *arr; + gchar *doc_uri; + CodeActionData *data; + + if (!srv) + { + GPtrArray *empty = g_ptr_array_new_full(0, (GDestroyNotify)lsp_command_free); + if (actions_resolved_cb(empty, user_data)) + g_ptr_array_free(empty, TRUE); + return; + } + + sci = doc->editor->sci; + + pos_start = sci_get_selection_start(sci); + pos_end = sci_get_selection_end(sci); + + if (pos_start == pos_end) + pos_start = pos_end = pos; + + lsp_pos_start = lsp_utils_scintilla_pos_to_lsp(sci, pos_start); + lsp_pos_end = lsp_utils_scintilla_pos_to_lsp(sci, pos_end); + + arr = g_ptr_array_new_full(1, (GDestroyNotify) g_variant_unref); + if (diag_raw) + g_ptr_array_add(arr, g_variant_ref(diag_raw)); + diagnostics = g_variant_new_array(G_VARIANT_TYPE_VARDICT, + (GVariant **)arr->pdata, arr->len); + + g_variant_dict_init(&dict, NULL); + g_variant_dict_insert_value(&dict, "diagnostics", diagnostics); + diags_dict = g_variant_take_ref(g_variant_dict_end(&dict)); + + doc_uri = lsp_utils_get_doc_uri(doc); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}", + "range", "{", + "start", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(lsp_pos_start.line), + "character", JSONRPC_MESSAGE_PUT_INT32(lsp_pos_start.character), + "}", + "end", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(lsp_pos_end.line), + "character", JSONRPC_MESSAGE_PUT_INT32(lsp_pos_end.character), + "}", + "}", + "context", "{", + JSONRPC_MESSAGE_PUT_VARIANT(diags_dict), + "}" + ); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + data = g_new0(CodeActionData, 1); + data->user_data = user_data; + data->callback = actions_resolved_cb; + lsp_rpc_call(srv, "textDocument/codeAction", node, code_action_cb, data); + + g_variant_unref(node); + g_variant_unref(diags_dict); + g_free(doc_uri); + g_ptr_array_free(arr, TRUE); +} diff --git a/lsp/src/lsp-command.h b/lsp/src/lsp-command.h new file mode 100644 index 000000000..7a834a333 --- /dev/null +++ b/lsp/src/lsp-command.h @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_COMMAND_H +#define LSP_COMMAND_H 1 + +#include "lsp-server.h" + +#include + +typedef struct +{ + guint line; + gchar *title; + gchar *command; + GVariant *arguments; + GVariant *edit; +} LspCommand; + +typedef gboolean (*CodeActionCallback) (GPtrArray *actions, gpointer user_data); + +void lsp_command_free(LspCommand *cmd); + +void lsp_command_perform(LspServer *server, LspCommand *cmd, LspCallback callback, gpointer user_data); + +// Careful! Returning TRUE from actions_resolved_cb frees the actions array, FALSE passes the +// ownership to the caller +void lsp_command_send_code_action_request(GeanyDocument *doc, gint pos, CodeActionCallback actions_resolved_cb, gpointer user_data); + +#endif /* LSP_COMMAND_H */ diff --git a/lsp/src/lsp-diagnostics.c b/lsp/src/lsp-diagnostics.c new file mode 100644 index 000000000..e8c2456a1 --- /dev/null +++ b/lsp/src/lsp-diagnostics.c @@ -0,0 +1,509 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-diagnostics.h" +#include "lsp-utils.h" + +#include + +extern GeanyData *geany_data; + + +typedef struct { + LspRange range; + gchar *code; + gchar *source; + gchar *message; + gint severity; + GVariant *diag_raw; +} LspDiag; + + +typedef enum { + LSP_DIAG_SEVERITY_MIN = 1, + LspError = 1, + LspWarning, + LspInfo, + LspHint, + LSP_DIAG_SEVERITY_MAX +} LspDiagSeverity; + + +static gint style_indices[LSP_DIAG_SEVERITY_MAX]; + +static GHashTable *diag_table = NULL; +static ScintillaObject *calltip_sci; +static GtkWidget *issue_label; + + +static void diag_free(LspDiag *diag) +{ + g_free(diag->code); + g_free(diag->source); + g_free(diag->message); + g_variant_unref(diag->diag_raw); + g_free(diag); +} + + +static void array_free(GPtrArray *arr) +{ + g_ptr_array_free(arr, TRUE); +} + + +void lsp_diagnostics_init(void) +{ + GtkWidget *geany_statusbar; + + if (!diag_table) + diag_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)array_free); + g_hash_table_remove_all(diag_table); + + issue_label = gtk_label_new(""); + geany_statusbar = ui_lookup_widget(geany_data->main_widgets->window, "statusbar"); + gtk_box_pack_start(GTK_BOX(geany_statusbar), issue_label, FALSE, FALSE, 4); + gtk_widget_show_all(issue_label); +} + + +void lsp_diagnostics_destroy(void) +{ + if (diag_table) + g_hash_table_destroy(diag_table); + if (issue_label) + gtk_widget_destroy(issue_label); + diag_table = NULL; + calltip_sci = NULL; + issue_label = NULL; +} + + +static LspDiag *get_diag(gint pos, gint where) +{ + GeanyDocument *doc = document_get_current(); + LspDiag *previous_diag = NULL; + GPtrArray *diags; + gint i; + + if (!doc || !doc->real_path) + return NULL; + + diags = g_hash_table_lookup(diag_table, doc->real_path); + if (!diags) + return NULL; + + for (i = 0; i < diags->len; i++) + { + ScintillaObject *sci = doc->editor->sci; + LspDiag *diag = diags->pdata[i]; + gint start_pos = lsp_utils_lsp_pos_to_scintilla(sci, diag->range.start); + gint end_pos = lsp_utils_lsp_pos_to_scintilla(sci, diag->range.end); + gint index = style_indices[diag->severity]; + + if (index == 0) + continue; + + if (start_pos == end_pos) + { + start_pos = SSM(sci, SCI_POSITIONBEFORE, start_pos, 0); + end_pos = SSM(sci, SCI_POSITIONAFTER, end_pos, 0); + } + + if (where == 0) // at the position + { + if (pos >= start_pos && pos <= end_pos) + return diag; + } + else if (where == 1) // after position + { + if (start_pos > pos) + return diag; + } + else if (where == -1) // before position + { + if (end_pos < pos) + previous_diag = diag; + else + break; + } + } + + if (previous_diag) + return previous_diag; + + return NULL; +} + + +gboolean lsp_diagnostics_has_diag(gint pos) +{ + return get_diag(pos, 0) != NULL; +} + + +GVariant *lsp_diagnostics_get_diag_raw(gint pos) +{ + LspDiag *diag = get_diag(pos, 0); + + if (diag) + return diag->diag_raw; + return NULL; +} + + +void lsp_diagnostics_goto_next_diag(gint pos) +{ + GeanyDocument *doc = document_get_current(); + LspDiag *diag = get_diag(pos, 1); + + if (doc && diag) + { + gint start_pos = lsp_utils_lsp_pos_to_scintilla(doc->editor->sci, diag->range.start); + sci_set_current_position(doc->editor->sci, start_pos, TRUE); + } +} + + +void lsp_diagnostics_goto_prev_diag(gint pos) +{ + GeanyDocument *doc = document_get_current(); + LspDiag *diag = get_diag(pos, -1); + + if (doc && diag) + { + gint start_pos = lsp_utils_lsp_pos_to_scintilla(doc->editor->sci, diag->range.start); + sci_set_current_position(doc->editor->sci, start_pos, TRUE); + } +} + + +static gboolean is_diagnostics_disabled_for(GeanyDocument *doc, LspServerConfig *cfg) +{ + gboolean is_disabled = FALSE; + gint i = 0; + gchar **comps; + gchar *fname; + + if (!cfg || !cfg->diagnostics_enable) + return TRUE; + + if (EMPTY(cfg->diagnostics_disable_for)) + return FALSE; + + comps = g_strsplit(cfg->diagnostics_disable_for, ";", -1); + fname = utils_get_utf8_from_locale(doc->real_path); + + for (i = 0; comps && comps[i] && !is_disabled; i++) + { + // TODO: possibly precompile the glob and store somewhere if performance is a problem + if (g_pattern_match_simple(comps[i], fname)) + is_disabled = TRUE; + } + + g_strfreev(comps); + g_free(fname); + + return is_disabled; +} + + +void lsp_diagnostics_show_calltip(gint pos) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get_if_running(doc); + LspDiag *diag = get_diag(pos, 0); + gchar *first = NULL; + gchar *second; + + if (!srv || !diag || is_diagnostics_disabled_for(doc, &srv->config)) + return; + + second = diag->message; + + if (diag->code && diag->source) + first = g_strconcat(diag->code, " (", diag->source, ")", NULL); + else if (diag->code) + first = g_strdup(diag->code); + else if (diag->source) + first = g_strdup(diag->source); + + if (first || second) + { + ScintillaObject *sci = doc->editor->sci; + gchar *msg; + + if (first && second) + msg = g_strconcat(first, "\n---\n", second, NULL); + else if (first) + msg = g_strdup(first); + else + msg = g_strdup(second); + + lsp_utils_wrap_string(msg, -1); + + calltip_sci = sci; + SSM(sci, SCI_CALLTIPSHOW, pos, (sptr_t) msg); + g_free(msg); + } + + g_free(first); +} + + +static void clear_indicators(ScintillaObject *sci) +{ + gint severity; + + for (severity = LSP_DIAG_SEVERITY_MIN; severity < LSP_DIAG_SEVERITY_MAX; severity++) + { + gint index = style_indices[severity]; + if (index > 0) + sci_indicator_set(sci, index); + sci_indicator_clear(sci, 0, sci_get_length(sci)); + } +} + + +static void set_statusbar_issue_num(gint num) +{ + gchar *issue_str; + + if (!issue_label) + return; + + issue_str = num >= 0 ? g_strdup_printf(_("issues: %d"), num) : g_strdup(""); + gtk_label_set_text(GTK_LABEL(issue_label), issue_str); + g_free(issue_str); +} + + +static void refresh_issue_statusbar(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get_if_running(doc); + gint num = 0; + + if (srv && doc->real_path && !is_diagnostics_disabled_for(doc, &srv->config)) + { + GPtrArray *diags = g_hash_table_lookup(diag_table, doc->real_path); + gint i; + + for (i = 0; diags && i < diags->len; i++) + { + LspDiag *diag = diags->pdata[i]; + + if (diag->severity <= srv->config.diagnostics_statusbar_severity) + num++; + } + } + + set_statusbar_issue_num(num); +} + + +void lsp_diagnostics_redraw(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get_if_running(doc); + ScintillaObject *sci; + GPtrArray *diags; + gint last_start_pos = 0, last_end_pos = 0; + gint i; + + if (!srv || !doc || !doc->real_path || is_diagnostics_disabled_for(doc, &srv->config)) + { + set_statusbar_issue_num(-1); + return; + } + + sci = doc->editor->sci; + + clear_indicators(sci); + + diags = g_hash_table_lookup(diag_table, doc->real_path); + if (!diags) + { + set_statusbar_issue_num(0); + return; + } + + for (i = 0; i < diags->len; i++) + { + LspDiag *diag = diags->pdata[i]; + gint start_pos = lsp_utils_lsp_pos_to_scintilla(sci, diag->range.start); + gint end_pos = lsp_utils_lsp_pos_to_scintilla(sci, diag->range.end); + gint next_pos = SSM(sci, SCI_POSITIONAFTER, start_pos, 0); + + if (start_pos == end_pos) + { + start_pos = SSM(sci, SCI_POSITIONBEFORE, start_pos, 0); + end_pos = SSM(sci, SCI_POSITIONAFTER, end_pos, 0); + } + + // if the error range spans from the last character on line to the + // first character on the next line (e.g. missing ':' in Python after else), + // it won't get drawn by Scintilla + if (end_pos == next_pos && + sci_get_line_from_position(sci, start_pos) + 1 == sci_get_line_from_position(sci, end_pos)) + { + start_pos = SSM(sci, SCI_POSITIONBEFORE, start_pos, 0); + } + + if (start_pos != last_start_pos || end_pos != last_end_pos) + { + gint index = style_indices[diag->severity]; + if (index > 0) + editor_indicator_set_on_range(doc->editor, index, start_pos, end_pos); + last_start_pos = start_pos; + last_end_pos = end_pos; + } + } + + refresh_issue_statusbar(doc); +} + + +void lsp_diagnostics_style_init(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get_if_running(doc); + ScintillaObject *sci; + + if (!srv) + return; + + sci = doc->editor->sci; + + style_indices[LspError] = lsp_utils_set_indicator_style(sci, srv->config.diagnostics_error_style); + style_indices[LspWarning] = lsp_utils_set_indicator_style(sci, srv->config.diagnostics_warning_style); + style_indices[LspInfo] = lsp_utils_set_indicator_style(sci, srv->config.diagnostics_info_style); + style_indices[LspHint] = lsp_utils_set_indicator_style(sci, srv->config.diagnostics_hint_style); + + SSM(sci, SCI_SETMOUSEDWELLTIME, 500, 0); +} + + +static gint sort_diags(gconstpointer a, gconstpointer b) +{ + LspDiag *d1 = *((LspDiag **)a); + LspDiag *d2 = *((LspDiag **)b); + + if (d2->range.start.line > d1->range.start.line) + return -1; + if (d2->range.start.line < d1->range.start.line) + return 1; + + if (d2->range.start.character > d1->range.start.character) + return -1; + if (d2->range.start.character < d1->range.start.character) + return 1; + + return d1->severity - d2->severity; +} + + +void lsp_diagnostics_received(GVariant* diags) +{ + GeanyDocument *doc = document_get_current();; + GVariantIter *iter = NULL; + const gchar *uri = NULL; + gchar *real_path; + GVariant *diag = NULL; + GPtrArray *arr; + + JSONRPC_MESSAGE_PARSE(diags, + "uri", JSONRPC_MESSAGE_GET_STRING(&uri), + "diagnostics", JSONRPC_MESSAGE_GET_ITER(&iter) + ); + + if (!iter) + return; + + real_path = lsp_utils_get_real_path_from_uri_locale(uri); + + if (!real_path) + { + g_variant_iter_free(iter); + return; + } + + arr = g_ptr_array_new_full(10, (GDestroyNotify)diag_free); + + while (g_variant_iter_next(iter, "v", &diag)) + { + GVariant *range = NULL; + const gchar *code = NULL; + const gchar *source = NULL; + const gchar *message = NULL; + gint64 severity = 0; + LspDiag *lsp_diag; + + JSONRPC_MESSAGE_PARSE(diag, "code", JSONRPC_MESSAGE_GET_STRING(&code)); + JSONRPC_MESSAGE_PARSE(diag, "source", JSONRPC_MESSAGE_GET_STRING(&source)); + JSONRPC_MESSAGE_PARSE(diag, "message", JSONRPC_MESSAGE_GET_STRING(&message)); + JSONRPC_MESSAGE_PARSE(diag, "severity", JSONRPC_MESSAGE_GET_INT64(&severity)); + JSONRPC_MESSAGE_PARSE(diag, "range", JSONRPC_MESSAGE_GET_VARIANT(&range)); + + lsp_diag = g_new0(LspDiag, 1); + lsp_diag->code = g_strdup(code); + lsp_diag->source = g_strdup(source); + lsp_diag->message = g_strdup(message); + lsp_diag->severity = severity; + lsp_diag->range = lsp_utils_parse_range(range); + lsp_diag->diag_raw = diag; + + g_ptr_array_add(arr, lsp_diag); + + if (range) + g_variant_unref(range); + } + + g_ptr_array_sort(arr, sort_diags); + + g_hash_table_insert(diag_table, g_strdup(real_path), arr); + + if (doc && doc->real_path && g_strcmp0(doc->real_path, real_path) == 0) + lsp_diagnostics_redraw(doc); + + g_variant_iter_free(iter); + g_free(real_path); +} + + +void lsp_diagnostics_clear(GeanyDocument *doc) +{ + if (doc && doc->real_path) + { + g_hash_table_remove(diag_table, doc->real_path); + lsp_diagnostics_redraw(doc); + } + + refresh_issue_statusbar(doc); +} + + +void lsp_diagnostics_hide_calltip(GeanyDocument *doc) +{ + if (doc->editor->sci == calltip_sci) + { + SSM(doc->editor->sci, SCI_CALLTIPCANCEL, 0, 0); + calltip_sci = NULL; + } +} diff --git a/lsp/src/lsp-diagnostics.h b/lsp/src/lsp-diagnostics.h new file mode 100644 index 000000000..981a145ad --- /dev/null +++ b/lsp/src/lsp-diagnostics.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_DIAGNOSTICS_H +#define LSP_DIAGNOSTICS_H 1 + +#include "lsp-server.h" + +#include + +void lsp_diagnostics_init(void); +void lsp_diagnostics_destroy(void); + +void lsp_diagnostics_show_calltip(gint pos); +void lsp_diagnostics_hide_calltip(GeanyDocument *doc); + +void lsp_diagnostics_received(GVariant* diags); +void lsp_diagnostics_redraw(GeanyDocument *doc); +void lsp_diagnostics_clear(GeanyDocument *doc); + +void lsp_diagnostics_style_init(GeanyDocument *doc); + +gboolean lsp_diagnostics_has_diag(gint pos); +GVariant *lsp_diagnostics_get_diag_raw(gint pos); + +void lsp_diagnostics_goto_next_diag(gint pos); +void lsp_diagnostics_goto_prev_diag(gint pos); + +#endif /* LSP_DIAGNOSTICS_H */ diff --git a/lsp/src/lsp-extension.c b/lsp/src/lsp-extension.c new file mode 100644 index 000000000..1b0b30d0e --- /dev/null +++ b/lsp/src/lsp-extension.c @@ -0,0 +1,68 @@ +/* + * Copyright 2024 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include + +#include "lsp-extension.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" +#include "lsp-server.h" + + +static void goto_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + if (!error) + { + const gchar *str = g_variant_get_string(return_value, NULL); + + if (str && strlen(str) > 0) + { + gchar *fname = lsp_utils_get_real_path_from_uri_locale(str); + + if (fname) + document_open_file(fname, FALSE, NULL, NULL); + g_free(fname); + } + } +} + + +void lsp_extension_clangd_switch_source_header(void) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get(doc); + GVariant *node; + gchar *doc_uri; + + if (!doc || !srv) + return; + + doc_uri = lsp_utils_get_doc_uri(doc); + + node = g_variant_new("{sv}", "uri", g_variant_new_string(doc_uri)); + g_variant_ref_sink(node); + + lsp_rpc_call(srv, "textDocument/switchSourceHeader", node, goto_cb, doc); + + g_free(doc_uri); + g_variant_unref(node); +} diff --git a/lsp/src/lsp-extension.h b/lsp/src/lsp-extension.h new file mode 100644 index 000000000..562d0edb6 --- /dev/null +++ b/lsp/src/lsp-extension.h @@ -0,0 +1,26 @@ +/* + * Copyright 2024 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_EXTENSION_H +#define LSP_EXTENSION_H 1 + +#include + +void lsp_extension_clangd_switch_source_header(void); + +#endif /* LSP_EXTENSION_H */ diff --git a/lsp/src/lsp-format.c b/lsp/src/lsp-format.c new file mode 100644 index 000000000..e1cc4f63b --- /dev/null +++ b/lsp/src/lsp-format.c @@ -0,0 +1,154 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-format.h" +#include "lsp-server.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" + +#include + + +typedef struct { + GeanyDocument *doc; + LspCallback callback; + gpointer user_data; +} FormatData; + + +static void format_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + FormatData *data = user_data; + GeanyDocument *doc = data->doc; + + if (!error && DOC_VALID(doc) && g_variant_is_of_type(return_value, G_VARIANT_TYPE_ARRAY)) + { + GPtrArray *edits; + GVariantIter iter; + + g_variant_iter_init(&iter, return_value); + edits = lsp_utils_parse_text_edits(&iter); + + sci_start_undo_action(doc->editor->sci); + lsp_utils_apply_text_edits(doc->editor->sci, NULL, edits, FALSE); + sci_end_undo_action(doc->editor->sci); + + g_ptr_array_free(edits, TRUE); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + } + + if (data->callback) + data->callback(data->user_data); + + g_free(data); +} + + +void lsp_format_perform(GeanyDocument *doc, gboolean force_whole_doc, LspCallback callback, gpointer user_data) +{ + LspServer *srv = lsp_server_get(doc); + ScintillaObject *sci; + const gchar *method; + GVariant *node = NULL; + gchar *doc_uri; + FormatData *data; + GVariant *options; + + if (!srv) + return; + + sci = doc->editor->sci; + doc_uri = lsp_utils_get_doc_uri(doc); + + options = lsp_utils_parse_json_file_as_variant(srv->config.formatting_options_file, srv->config.formatting_options); + + if ((sci_has_selection(sci) || !srv->config.document_formatting_enable) && + srv->config.range_formatting_enable) + { + LspRange range; + gint sel_start; + gint sel_end; + + if (!sci_has_selection(sci) || force_whole_doc) + { + sel_start = 0; + sel_end = sci_get_length(sci); + } + else + { + sel_start = sci_get_selection_start(sci); + sel_end = sci_get_selection_end(sci); + } + + range.start = lsp_utils_scintilla_pos_to_lsp(sci, sel_start); + range.end = lsp_utils_scintilla_pos_to_lsp(sci, sel_end); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}", + "range", "{", + "start", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(range.start.line), + "character", JSONRPC_MESSAGE_PUT_INT32(range.start.character), + "}", + "end", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(range.end.line), + "character", JSONRPC_MESSAGE_PUT_INT32(range.end.character), + "}", + "}", + "options", "{", + JSONRPC_MESSAGE_PUT_VARIANT(options), + "}" + ); + method = "textDocument/rangeFormatting"; + } + else if (srv->config.document_formatting_enable) + { + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}", + "options", "{", + JSONRPC_MESSAGE_PUT_VARIANT(options), + "}" + ); + method = "textDocument/formatting"; + } + + if (node) + { + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + data = g_new0(FormatData, 1); + data->doc = doc; + data->callback = callback; + data->user_data = user_data; + + lsp_rpc_call(srv, method, node, format_cb, data); + + g_variant_unref(node); + } + + g_free(doc_uri); +} diff --git a/lsp/src/lsp-format.h b/lsp/src/lsp-format.h new file mode 100644 index 000000000..c4b142560 --- /dev/null +++ b/lsp/src/lsp-format.h @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_FORMAT_H +#define LSP_FORMAT_H 1 + +#include "lsp-server.h" + + +void lsp_format_perform(GeanyDocument *doc, gboolean force_whole_doc, LspCallback callback, gpointer user_data); + +#endif /* LSP_FORMAT_H */ diff --git a/lsp/src/lsp-goto-anywhere.c b/lsp/src/lsp-goto-anywhere.c new file mode 100644 index 000000000..c656b4527 --- /dev/null +++ b/lsp/src/lsp-goto-anywhere.c @@ -0,0 +1,289 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-goto-anywhere.h" +#include "lsp-goto-panel.h" +#include "lsp-symbols.h" +#include "lsp-utils.h" +#include "lsp-symbol.h" + +#include +#include + + +typedef struct +{ + GeanyDocument *doc; + gchar *query; +} DocQueryData; + + +extern GeanyData *geany_data; + + +static void workspace_symbol_cb(GPtrArray *symbols, gpointer user_data) +{ + lsp_goto_panel_fill(symbols); +} + + +static void doc_symbol_cb(gpointer user_data) +{ + DocQueryData *data = user_data; + GeanyDocument *doc = document_get_current(); + gchar *text = data->query; + GPtrArray *symbols; + GPtrArray *filtered; + + if (doc != data->doc) + return; + + symbols = lsp_symbols_doc_get_cached(doc); + + filtered = lsp_goto_panel_filter(symbols, text[0] ? text + 1 : text); + lsp_goto_panel_fill(filtered); + + g_ptr_array_free(filtered, TRUE); + g_free(data->query); + g_free(data); +} + + +static void goto_line(GeanyDocument *doc, const gchar *line_str) +{ + GPtrArray *arr = g_ptr_array_new_full(0, (GDestroyNotify)lsp_symbol_unref); + gint lineno = atoi(line_str); + gint linenum = sci_get_line_count(doc->editor->sci); + guint i; + + for (i = 0; i < 4; i++) + { + LspSymbol *sym; + gchar *file_name = utils_get_utf8_from_locale(doc->real_path); + TMIcon icon = TM_ICON_OTHER; + const gchar *name = ""; + gint line = 0; + + switch (i) + { + case 0: + /* For translators: Item in a list which, when selected, navigates + * to the line typed in the entry above the list */ + name = _("line typed above"); + if (lineno == 0) + line = sci_get_current_line(doc->editor->sci) + 1; + else if (lineno > linenum) + line = linenum; + else + line = lineno; + break; + + case 1: + /* For translators: Item in a list which, when selected, navigates + * to the beginning of the current document */ + name = _("beginning"); + line = 1; + break; + + case 2: + /* For translators: Item in a list which, when selected, navigates + * to the middle of the current document */ + name = _("middle"); + line = linenum / 2; + break; + + case 3: + /* For translators: Item in a list which, when selected, navigates + * to the end of the current document */ + name = _("end"); + line = linenum; + break; + } + + sym = lsp_symbol_new(name, "", "", file_name, 0, 0, line, 0, icon); + + g_ptr_array_add(arr, sym); + + g_free(file_name); + } + + lsp_goto_panel_fill(arr); + + g_ptr_array_free(arr, TRUE); +} + + +static void goto_file(const gchar *file_str) +{ + GPtrArray *arr = g_ptr_array_new_full(0, (GDestroyNotify)lsp_symbol_unref); + GPtrArray *filtered; + guint i; + + foreach_document(i) + { + GeanyDocument *doc = documents[i]; + gchar *file_name, *name; + LspSymbol *sym; + + if (!doc->real_path) + continue; + + name = g_path_get_basename(doc->real_path); + file_name = utils_get_utf8_from_locale(doc->real_path); + sym = lsp_symbol_new(name, "", "", file_name, 0, 0, 0, 0, TM_ICON_OTHER); + + g_ptr_array_add(arr, sym); + + g_free(name); + g_free(file_name); + } + + filtered = lsp_goto_panel_filter(arr, file_str); + lsp_goto_panel_fill(filtered); + + g_ptr_array_free(filtered, TRUE); + g_ptr_array_free(arr, TRUE); +} + + +static void goto_tm_symbol(const gchar *query, GPtrArray *tags, TMParserType lang) +{ + GPtrArray *converted = g_ptr_array_new_full(0, (GDestroyNotify)lsp_symbol_unref); + GPtrArray *filtered; + TMTag *tag; + guint i; + + if (tags) + { + foreach_ptr_array(tag, i, tags) + { + if (tag->lang == lang && tag->type != tm_tag_local_var_t && tag->file) + { + gchar *file_name, *name; + LspSymbol *sym; + + name = g_strdup(tag->name); + file_name = utils_get_utf8_from_locale(tag->file->file_name); + + sym = lsp_symbol_new(name, "", "", file_name, 0, 0, tag->line, 0, + lsp_symbol_kinds_get_symbol_icon(lsp_symbol_kinds_tm_to_lsp(tag->type))); + + g_ptr_array_add(converted, sym); + + g_free(name); + g_free(file_name); + } + } + } + + filtered = lsp_goto_panel_filter(converted, query); + lsp_goto_panel_fill(filtered); + + g_ptr_array_free(filtered, TRUE); + g_ptr_array_free(converted, TRUE); +} + + +static void perform_lookup(const gchar *query) +{ + GeanyDocument *doc = document_get_current(); + const gchar *query_str = query ? query : ""; + LspServer *srv = lsp_server_get(doc); + + if (g_str_has_prefix(query_str, "#")) + { + if (srv && srv->supports_workspace_symbols) + lsp_symbols_workspace_request(doc, query_str+1, workspace_symbol_cb, NULL); + else if (doc) + // TODO: possibly improve performance by binary searching the start and the end point + goto_tm_symbol(query_str+1, geany_data->app->tm_workspace->tags_array, doc->file_type->lang); + } + else if (g_str_has_prefix(query_str, "@")) + { + if (srv && srv->config.document_symbols_available) + { + DocQueryData *data = g_new0(DocQueryData, 1); + data->query = g_strdup(query_str); + data->doc = doc; + lsp_symbols_doc_request(doc, doc_symbol_cb, data); + } + else if (doc) + { + GPtrArray *tags = doc->tm_file ? doc->tm_file->tags_array : g_ptr_array_new(); + goto_tm_symbol(query_str+1, tags, doc->file_type->lang); + if (!doc->tm_file) + g_ptr_array_free(tags, TRUE); + } + } + else if (g_str_has_prefix(query_str, ":")) + { + if (doc && doc->real_path) + goto_line(doc, query_str+1); + } + else + goto_file(query_str); +} + + +static void goto_panel_query(const gchar *query_type, gboolean prefill) +{ + GeanyDocument *doc = document_get_current(); + gchar *query = NULL; + + if (prefill && doc) + { + LspServer *srv = lsp_server_get_if_running(doc); + gint pos = sci_get_current_position(doc->editor->sci); + query = lsp_utils_get_current_iden(doc, pos, srv ? srv->config.word_chars : GEANY_WORDCHARS); + } + if (!query) + query = g_strdup(""); + SETPTR(query, g_strconcat(query_type, query, NULL)); + + lsp_goto_panel_show(query, perform_lookup); + + g_free(query); +} + + +void lsp_goto_anywhere_for_workspace(void) +{ + goto_panel_query("#", TRUE); +} + + +void lsp_goto_anywhere_for_doc(void) +{ + goto_panel_query("@", TRUE); +} + + +void lsp_goto_anywhere_for_line(void) +{ + goto_panel_query(":", FALSE); +} + + +void lsp_goto_anywhere_for_file(void) +{ + goto_panel_query("", FALSE); +} diff --git a/lsp/src/lsp-goto-anywhere.h b/lsp/src/lsp-goto-anywhere.h new file mode 100644 index 000000000..f2a2aead2 --- /dev/null +++ b/lsp/src/lsp-goto-anywhere.h @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_GOTO_ANYWHERE_H +#define LSP_GOTO_ANYWHERE_H 1 + + +void lsp_goto_anywhere_for_workspace(void); +void lsp_goto_anywhere_for_doc(void); +void lsp_goto_anywhere_for_line(void); +void lsp_goto_anywhere_for_file(void); + +#endif /* LSP_GOTO_ANYWHERE_H */ diff --git a/lsp/src/lsp-goto-panel.c b/lsp/src/lsp-goto-panel.c new file mode 100644 index 000000000..14ae639e8 --- /dev/null +++ b/lsp/src/lsp-goto-panel.c @@ -0,0 +1,440 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/* This file contains mostly stolen code from the Colomban Wendling's Commander + * plugin. Thanks! */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-goto-panel.h" +#include "lsp-server.h" +#include "lsp-symbols.h" +#include "lsp-symbol-kinds.h" +#include "lsp-utils.h" +#include "lsp-symbol.h" + +#include +#include + + +enum { + COL_ICON, + COL_LABEL, + COL_PATH, + COL_LINENO, + COL_COUNT +}; + + +//TODO: free on plugin unload +struct { + GtkWidget *panel; + GtkWidget *entry; + GtkWidget *tree_view; + GtkListStore *store; +} panel_data = { + NULL, NULL, NULL, NULL +}; + + +static LspGotoPanelLookupFunction lookup_function; + + +extern GeanyData *geany_data; + + +static void tree_view_set_cursor_from_iter(GtkTreeView *view, GtkTreeIter *iter) +{ + GtkTreePath *path; + + path = gtk_tree_model_get_path(gtk_tree_view_get_model(view), iter); + gtk_tree_view_set_cursor(view, path, NULL, FALSE); + gtk_tree_path_free(path); +} + + +static void tree_view_move_focus(GtkTreeView *view, GtkMovementStep step, gint amount) +{ + GtkTreeIter iter; + GtkTreePath *path; + GtkTreeModel *model = gtk_tree_view_get_model(view); + gboolean valid = FALSE; + + gtk_tree_view_get_cursor(view, &path, NULL); + if (!path) + valid = gtk_tree_model_get_iter_first(model, &iter); + else + { + switch (step) { + case GTK_MOVEMENT_BUFFER_ENDS: + valid = gtk_tree_model_get_iter_first(model, &iter); + if (valid && amount > 0) + { + GtkTreeIter prev; + + do { + prev = iter; + } while (gtk_tree_model_iter_next(model, &iter)); + iter = prev; + } + break; + + case GTK_MOVEMENT_PAGES: + /* FIXME: move by page */ + case GTK_MOVEMENT_DISPLAY_LINES: + gtk_tree_model_get_iter(model, &iter, path); + if (amount > 0) + { + while ((valid = gtk_tree_model_iter_next(model, &iter)) && --amount > 0) + ; + } + else if (amount < 0) + { + while ((valid = gtk_tree_path_prev(path)) && --amount > 0) + ; + + if (valid) + gtk_tree_model_get_iter(model, &iter, path); + } + break; + + default: + g_assert_not_reached(); + } + gtk_tree_path_free(path); + } + + if (valid) + tree_view_set_cursor_from_iter(view, &iter); + else + gtk_widget_error_bell(GTK_WIDGET(view)); +} + + +static void tree_view_activate_focused_row(GtkTreeView *view) +{ + GtkTreePath *path; + GtkTreeViewColumn *column; + + gtk_tree_view_get_cursor(view, &path, &column); + if (path) + { + gtk_tree_view_row_activated(view, path, column); + gtk_tree_path_free(path); + } +} + + +void lsp_goto_panel_fill(GPtrArray *symbols) +{ + GtkTreeView *view = GTK_TREE_VIEW(panel_data.tree_view); + GtkTreeIter iter; + LspSymbol *sym; + guint i; + + gtk_list_store_clear(panel_data.store); + + foreach_ptr_array(sym, i, symbols) + { + gchar *label; + + if (!lsp_symbol_get_file(sym)) + continue; + + if (lsp_symbol_get_line(sym) > 0) + label = g_markup_printf_escaped("%s\n%s:%lu", + lsp_symbol_get_name(sym), lsp_symbol_get_file(sym), lsp_symbol_get_line(sym)); + else + label = g_markup_printf_escaped("%s\n%s", + lsp_symbol_get_name(sym), lsp_symbol_get_file(sym)); + + gtk_list_store_insert_with_values(panel_data.store, NULL, -1, + COL_ICON, symbols_get_icon_pixbuf(lsp_symbol_get_icon(sym)), + COL_LABEL, label, + COL_PATH, lsp_symbol_get_file(sym), + COL_LINENO, lsp_symbol_get_line(sym), + -1); + + g_free(label); + } + + if (gtk_tree_model_get_iter_first(gtk_tree_view_get_model(view), &iter)) + tree_view_set_cursor_from_iter(GTK_TREE_VIEW(panel_data.tree_view), &iter); +} + + +static gboolean on_panel_key_press_event(GtkWidget *widget, GdkEventKey *event, + gpointer dummy) +{ + switch (event->keyval) { + case GDK_KEY_Escape: + gtk_widget_hide(widget); + return TRUE; + + case GDK_KEY_Tab: + /* avoid leaving the entry */ + return TRUE; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + case GDK_KEY_ISO_Enter: + tree_view_activate_focused_row(GTK_TREE_VIEW(panel_data.tree_view)); + return TRUE; + + case GDK_KEY_Page_Up: + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Up: + case GDK_KEY_KP_Page_Down: + { + gboolean up = event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_KP_Page_Up; + tree_view_move_focus(GTK_TREE_VIEW(panel_data.tree_view), + GTK_MOVEMENT_PAGES, up ? -1 : 1); + return TRUE; + } + + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + { + gboolean up = event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_KP_Up; + tree_view_move_focus(GTK_TREE_VIEW(panel_data.tree_view), + GTK_MOVEMENT_DISPLAY_LINES, up ? -1 : 1); + return TRUE; + } + } + + return FALSE; +} + + +static void on_entry_text_notify(GObject *object, GParamSpec *pspec, gpointer dummy) +{ + GtkTreeIter iter; + GtkTreeView *view = GTK_TREE_VIEW(panel_data.tree_view); + GtkTreeModel *model = gtk_tree_view_get_model(view); + const gchar *text = gtk_entry_get_text(GTK_ENTRY(panel_data.entry)); + + lookup_function(text); + + if (gtk_tree_model_get_iter_first(model, &iter)) + tree_view_set_cursor_from_iter(view, &iter); +} + + +static void on_entry_activate(GtkEntry *entry, gpointer dummy) +{ + tree_view_activate_focused_row(GTK_TREE_VIEW(panel_data.tree_view)); +} + + +static void on_panel_hide(GtkWidget *widget, gpointer dummy) +{ + gtk_list_store_clear(panel_data.store); +} + + +static void on_panel_show(GtkWidget *widget, gpointer dummy) +{ + const gchar *text = gtk_entry_get_text(GTK_ENTRY(panel_data.entry)); + gboolean select_first = TRUE; + + if (text && (text[0] == ':' || text[0] == '#' || text[0] == '@')) + select_first = FALSE; + + gtk_widget_grab_focus(panel_data.entry); + gtk_editable_select_region(GTK_EDITABLE(panel_data.entry), select_first ? 0 : 1, -1); +} + + +static void on_view_row_activated(GtkTreeView *view, GtkTreePath *path, + GtkTreeViewColumn *column, gpointer dummy) +{ + GtkTreeModel *model = gtk_tree_view_get_model(view); + GtkTreeIter iter; + + if (gtk_tree_model_get_iter(model, &iter, path)) + { + GeanyDocument *doc; + gchar *file_path; + gint line; + + gtk_tree_model_get(model, &iter, + COL_PATH, &file_path, + COL_LINENO, &line, + -1); + + SETPTR(file_path, utils_get_locale_from_utf8(file_path)); + doc = document_open_file(file_path, FALSE, NULL, NULL); + + if (doc && line > 0) + navqueue_goto_line(document_get_current(), doc, line); + + g_free(file_path); + } + + gtk_widget_hide(panel_data.panel); +} + + +static void create_panel(void) +{ + GtkWidget *frame, *box, *scroll; + GtkTreeViewColumn *col; + GtkCellRenderer *renderer; + + panel_data.panel = g_object_new(GTK_TYPE_WINDOW, + "decorated", FALSE, + "default-width", 500, + "default-height", 350, + "transient-for", geany_data->main_widgets->window, + "window-position", GTK_WIN_POS_CENTER_ON_PARENT, + "type-hint", GDK_WINDOW_TYPE_HINT_DIALOG, + "skip-taskbar-hint", TRUE, + "skip-pager-hint", TRUE, + NULL); + g_signal_connect(panel_data.panel, "focus-out-event", + G_CALLBACK(gtk_widget_hide), NULL); + g_signal_connect(panel_data.panel, "show", + G_CALLBACK(on_panel_show), NULL); + g_signal_connect(panel_data.panel, "hide", + G_CALLBACK(on_panel_hide), NULL); + g_signal_connect(panel_data.panel, "key-press-event", + G_CALLBACK(on_panel_key_press_event), NULL); + + frame = gtk_frame_new(NULL); + gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN); + gtk_container_add(GTK_CONTAINER(panel_data.panel), frame); + + box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add(GTK_CONTAINER(frame), box); + + panel_data.entry = gtk_entry_new(); + gtk_box_pack_start(GTK_BOX(box), panel_data.entry, FALSE, TRUE, 0); + + scroll = g_object_new(GTK_TYPE_SCROLLED_WINDOW, + "hscrollbar-policy", GTK_POLICY_AUTOMATIC, + "vscrollbar-policy", GTK_POLICY_AUTOMATIC, + NULL); + gtk_box_pack_start(GTK_BOX(box), scroll, TRUE, TRUE, 0); + + panel_data.tree_view = gtk_tree_view_new(); + gtk_widget_set_can_focus(panel_data.tree_view, FALSE); + gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(panel_data.tree_view), FALSE); + + panel_data.store = gtk_list_store_new(COL_COUNT, + GDK_TYPE_PIXBUF, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_INT); + gtk_tree_view_set_model(GTK_TREE_VIEW(panel_data.tree_view), GTK_TREE_MODEL(panel_data.store)); + g_object_unref(panel_data.store); + + renderer = gtk_cell_renderer_pixbuf_new(); + col = gtk_tree_view_column_new(); + gtk_tree_view_column_pack_start(col, renderer, FALSE); + gtk_tree_view_column_set_attributes(col, renderer, "pixbuf", COL_ICON, NULL); + g_object_set(renderer, "xalign", 0.0, NULL); + gtk_tree_view_append_column(GTK_TREE_VIEW(panel_data.tree_view), col); + + renderer = gtk_cell_renderer_text_new(); + g_object_set(renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); + col = gtk_tree_view_column_new_with_attributes(NULL, renderer, + "markup", COL_LABEL, + NULL); + gtk_tree_view_append_column(GTK_TREE_VIEW(panel_data.tree_view), col); + + g_signal_connect(panel_data.tree_view, "row-activated", + G_CALLBACK(on_view_row_activated), NULL); + gtk_container_add(GTK_CONTAINER(scroll), panel_data.tree_view); + + /* connect entry signals after the view is created as they use it */ + g_signal_connect(panel_data.entry, "notify::text", + G_CALLBACK(on_entry_text_notify), NULL); + g_signal_connect(panel_data.entry, "activate", + G_CALLBACK(on_entry_activate), NULL); + + gtk_widget_show_all(frame); +} + + +void lsp_goto_panel_show(const gchar *query, LspGotoPanelLookupFunction func) +{ + if (!panel_data.panel) + create_panel(); + + lookup_function = func; + + gtk_entry_set_text(GTK_ENTRY(panel_data.entry), query); + gtk_list_store_clear(panel_data.store); + gtk_widget_show(panel_data.panel); + + lookup_function(query); +} + + +GPtrArray *lsp_goto_panel_filter(GPtrArray *symbols, const gchar *filter) +{ + GPtrArray *ret = g_ptr_array_new(); + gchar *case_normalized_filter; + gchar **tf_strv; + guint i; + guint j = 0; + + if (!symbols) + return ret; + + case_normalized_filter = g_utf8_normalize(filter, -1, G_NORMALIZE_ALL); + SETPTR(case_normalized_filter, g_utf8_casefold(case_normalized_filter, -1)); + + tf_strv = g_strsplit_set(case_normalized_filter, " ", -1); + + for (i = 0; i < symbols->len && j < 20; i++) + { + LspSymbol *symbol = symbols->pdata[i]; + gboolean filtered = FALSE; + gchar *case_normalized_name; + gchar **val; + + case_normalized_name = g_utf8_normalize(lsp_symbol_get_name(symbol), -1, G_NORMALIZE_ALL); + SETPTR(case_normalized_name, g_utf8_casefold(case_normalized_name, -1)); + + foreach_strv(val, tf_strv) + { + if (case_normalized_name != NULL && *val != NULL) + filtered = strstr(case_normalized_name, *val) == NULL; + + if (filtered) + break; + } + if (!filtered) + { + g_ptr_array_add(ret, symbol); + j++; + } + + g_free(case_normalized_name); + } + + g_strfreev(tf_strv); + g_free(case_normalized_filter); + + return ret; +} diff --git a/lsp/src/lsp-goto-panel.h b/lsp/src/lsp-goto-panel.h new file mode 100644 index 000000000..a3d24e49f --- /dev/null +++ b/lsp/src/lsp-goto-panel.h @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_LOOKUP_PANEL_H +#define LSP_LOOKUP_PANEL_H 1 + +#include + +typedef void (*LspGotoPanelLookupFunction) (const char *); + +void lsp_goto_panel_show(const gchar *query, LspGotoPanelLookupFunction func); +void lsp_goto_panel_fill(GPtrArray *symbols); +GPtrArray *lsp_goto_panel_filter(GPtrArray *symbols, const gchar *filter); + +#endif /* LSP_LOOKUP_PANEL_H */ diff --git a/lsp/src/lsp-goto.c b/lsp/src/lsp-goto.c new file mode 100644 index 000000000..125c4a002 --- /dev/null +++ b/lsp/src/lsp-goto.c @@ -0,0 +1,322 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-goto.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" +#include "lsp-goto-panel.h" +#include "lsp-symbol.h" + +#include + + +typedef struct { + GeanyDocument *doc; + gboolean show_in_msgwin; +} GotoData; + + +extern GeanyData *geany_data; + + +// TODO: free on plugin unload +GPtrArray *last_result; + + +static void goto_location(GeanyDocument *old_doc, LspLocation *loc) +{ + gchar *fname = lsp_utils_get_real_path_from_uri_locale(loc->uri); + GeanyDocument *doc = NULL; + + if (fname) + doc = document_open_file(fname, FALSE, NULL, NULL); + + if (doc) + navqueue_goto_line(old_doc, doc, loc->range.start.line + 1); + + g_free(fname); +} + + +static void filter_symbols(const gchar *filter) +{ + GPtrArray *filtered; + + if (!last_result) + return; + + filtered = lsp_goto_panel_filter(last_result, filter); + lsp_goto_panel_fill(filtered); + + g_ptr_array_free(filtered, TRUE); +} + + +static void show_in_msgwin(LspLocation *loc, GHashTable *sci_table) +{ + ScintillaObject *sci = NULL; + gint lineno = loc->range.start.line; + gchar *fname, *base_path; + GeanyDocument *doc; + gchar *line_str; + + fname = lsp_utils_get_real_path_from_uri_utf8(loc->uri); + if (!fname) + return; + + doc = document_find_by_filename(fname); + base_path = lsp_utils_get_project_base_path(); + + if (doc) + sci = doc->editor->sci; + else + { + if (sci_table) + sci = g_hash_table_lookup(sci_table, fname); + if (!sci) + { + sci = lsp_utils_new_sci_from_file(fname); + if (sci && sci_table) + g_hash_table_insert(sci_table, g_strdup(fname), g_object_ref_sink(sci)); + } + } + + line_str = sci ? sci_get_line(sci, lineno) : g_strdup(""); + g_strstrip(line_str); + + if (base_path) + { + gchar *rel_path = lsp_utils_get_relative_path(base_path, fname); + gchar *locale_base_path = utils_get_locale_from_utf8(base_path); + + if (rel_path && !g_str_has_prefix(rel_path, "..")) + SETPTR(fname, g_strdup(rel_path)); + + msgwin_set_messages_dir(locale_base_path); + + g_free(locale_base_path); + g_free(rel_path); + } + msgwin_msg_add(COLOR_BLACK, -1, NULL, "%s:%d: %s", fname, lineno + 1, line_str); + + g_free(line_str); + g_free(fname); + g_free(base_path); + if (!sci_table && !doc) + { + g_object_ref_sink(sci); + g_object_unref(sci); + } +} + + +static void goto_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + if (!error) + { + GotoData *data = user_data; + + if (DOC_VALID(data->doc)) + { + if (data->show_in_msgwin) + { + msgwin_clear_tab(MSG_MESSAGE); + msgwin_switch_tab(MSG_MESSAGE, TRUE); + } + + // array of locations + if (g_variant_is_of_type(return_value, G_VARIANT_TYPE_ARRAY)) + { + GPtrArray *locations = NULL; + GVariantIter iter; + + g_variant_iter_init(&iter, return_value); + + locations = lsp_utils_parse_locations(&iter); + + if (locations && locations->len > 0) + { + if (data->show_in_msgwin) + { + GHashTable *sci_table = g_hash_table_new_full(g_str_hash, + g_str_equal, g_free, (GDestroyNotify)g_object_unref); + LspLocation *loc; + guint j; + + foreach_ptr_array(loc, j, locations) + { + show_in_msgwin(loc, sci_table); + } + + g_hash_table_destroy(sci_table); + } + else if (locations->len == 1) + goto_location(data->doc, locations->pdata[0]); + else + { + LspLocation *loc; + guint j; + + if (last_result) + g_ptr_array_free(last_result, TRUE); + + last_result = g_ptr_array_new_full(0, (GDestroyNotify)lsp_symbol_unref); + + foreach_ptr_array(loc, j, locations) + { + gchar *file_name, *name; + LspSymbol *sym; + + file_name = lsp_utils_get_real_path_from_uri_utf8(loc->uri); + if (!file_name) + continue; + + name = g_path_get_basename(file_name); + + sym = lsp_symbol_new(name, "", "", file_name, 0, 0, loc->range.start.line + 1, 0, + TM_ICON_OTHER); + + g_ptr_array_add(last_result, sym); + + g_free(name); + g_free(file_name); + } + + lsp_goto_panel_show("", filter_symbols); + } + } + + g_ptr_array_free(locations, TRUE); + } + //single location + else if (g_variant_is_of_type(return_value, G_VARIANT_TYPE_DICTIONARY)) + { + LspLocation *loc = lsp_utils_parse_location(return_value); + + if (loc) + { + if (data->show_in_msgwin) + show_in_msgwin(loc, NULL); + else + goto_location(data->doc, loc); + } + + lsp_utils_free_lsp_location(loc); + } + } + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + } + + g_free(user_data); +} + + +static void perform_goto(LspServer *server, GeanyDocument *doc, gint pos, const gchar *request, + gboolean show_in_msgwin) +{ + GVariant *node; + ScintillaObject *sci = doc->editor->sci; + LspPosition lsp_pos = lsp_utils_scintilla_pos_to_lsp(sci, pos); + gchar *doc_uri = lsp_utils_get_doc_uri(doc); + GotoData *data = g_new0(GotoData, 1); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}", + "position", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.line), + "character", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.character), + "}", + "context", "{", // only for textDocument/references + "includeDeclaration", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "}" + ); + + data->doc = doc; + data->show_in_msgwin = show_in_msgwin; + lsp_rpc_call(server, request, node, goto_cb, data); + + g_free(doc_uri); + g_variant_unref(node); +} + + +void lsp_goto_definition(gint pos) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get(doc); + + if (!doc || !srv) + return; + + perform_goto(srv, doc, pos, "textDocument/definition", FALSE); +} + + +void lsp_goto_declaration(gint pos) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get(doc); + + if (!doc || !srv) + return; + + perform_goto(srv, doc, pos, "textDocument/declaration", FALSE); +} + + +void lsp_goto_type_definition(gint pos) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get(doc); + + if (!doc || !srv) + return; + + perform_goto(srv, doc, pos, "textDocument/typeDefinition", FALSE); +} + + +void lsp_goto_implementations(gint pos) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get(doc); + + if (!doc || !srv) + return; + + perform_goto(srv, doc, pos, "textDocument/implementation", TRUE); +} + + +void lsp_goto_references(gint pos) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get(doc); + + if (!doc || !srv) + return; + + perform_goto(srv, doc, pos, "textDocument/references", TRUE); +} diff --git a/lsp/src/lsp-goto.h b/lsp/src/lsp-goto.h new file mode 100644 index 000000000..4133c6635 --- /dev/null +++ b/lsp/src/lsp-goto.h @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_GOTO_H +#define LSP_GOTO_H 1 + +#include "lsp-server.h" + +#include + + +void lsp_goto_definition(gint pos); +void lsp_goto_declaration(gint pos); +void lsp_goto_type_definition(gint pos); +void lsp_goto_implementations(gint pos); +void lsp_goto_references(gint pos); + +#endif /* LSP_GOTO_H */ diff --git a/lsp/src/lsp-highlight.c b/lsp/src/lsp-highlight.c new file mode 100644 index 000000000..1e5440aea --- /dev/null +++ b/lsp/src/lsp-highlight.c @@ -0,0 +1,227 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-highlight.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" + +#include + + +typedef struct { + GeanyDocument *doc; + gint pos; + gchar *identifier; + gboolean highlight; +} LspHighlightData; + + +static gint indicator; +static gboolean dirty; + + +void lsp_highlight_clear(GeanyDocument *doc) +{ + if (dirty) + { + ScintillaObject *sci = doc->editor->sci; + + if (indicator > 0) + sci_indicator_set(sci, indicator); + sci_indicator_clear(sci, 0, sci_get_length(sci)); + dirty = FALSE; + } +} + + +void lsp_highlight_style_init(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get_if_running(doc); + ScintillaObject *sci; + + if (!srv) + return; + + sci = doc->editor->sci; + + if (indicator > 0) + { + dirty = TRUE; + lsp_highlight_clear(doc); + } + indicator = lsp_utils_set_indicator_style(sci, srv->config.highlighting_style); + if (indicator > 0) + SSM(sci, SCI_INDICSETUNDER, indicator, TRUE); +} + + +static void highlight_range(GeanyDocument *doc, LspRange range) +{ + ScintillaObject *sci = doc->editor->sci; + gint start_pos = lsp_utils_lsp_pos_to_scintilla(sci, range.start); + gint end_pos = lsp_utils_lsp_pos_to_scintilla(sci, range.end); + + if (indicator > 0) + editor_indicator_set_on_range(doc->editor, indicator, start_pos, end_pos); + dirty = TRUE; +} + + +static void highlight_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + LspHighlightData *data = user_data; + + if (!error) + { + GeanyDocument *doc = document_get_current(); + + if (doc == data->doc) + lsp_highlight_clear(doc); + + if (doc == data->doc && g_variant_is_of_type(return_value, G_VARIANT_TYPE_ARRAY)) + { + GVariant *member = NULL; + GVariantIter iter; + gint sel_id = 0; + gint main_sel_id = 0; + gboolean first_sel = TRUE; + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + + g_variant_iter_init(&iter, return_value); + + while (g_variant_iter_loop(&iter, "v", &member)) + { + GVariant *range = NULL; + + JSONRPC_MESSAGE_PARSE(member, + "range", JSONRPC_MESSAGE_GET_VARIANT(&range) + ); + + if (range) + { + LspRange r = lsp_utils_parse_range(range); + gint start_pos = lsp_utils_lsp_pos_to_scintilla(doc->editor->sci, r.start); + gint end_pos = lsp_utils_lsp_pos_to_scintilla(doc->editor->sci, r.end); + gchar *ident = sci_get_contents_range(doc->editor->sci, start_pos, end_pos); + + //clangd returns highlight for 'editor' in 'doc-|>editor' where + //'|' is the caret position and also other cases, which is strange + //restrict to identifiers only + if (g_strcmp0(ident, data->identifier) == 0) + { + if (data->highlight) + highlight_range(doc, r); + else + { + SSM(doc->editor->sci, first_sel ? SCI_SETSELECTION : SCI_ADDSELECTION, + start_pos, end_pos); + if (data->pos >= start_pos && data->pos <= end_pos) + main_sel_id = sel_id; + + first_sel = FALSE; + sel_id++; + } + } + + g_free(ident); + g_variant_unref(range); + } + } + + if (!data->highlight) + SSM(doc->editor->sci, SCI_SETMAINSELECTION, main_sel_id, 0); + } + } + + g_free(data->identifier); + g_free(user_data); +} + + +static void send_request(LspServer *server, GeanyDocument *doc, gint pos, gboolean highlight) +{ + GVariant *node; + ScintillaObject *sci = doc->editor->sci; + LspPosition lsp_pos = lsp_utils_scintilla_pos_to_lsp(sci, pos); + gchar *doc_uri = lsp_utils_get_doc_uri(doc); + gchar *iden = lsp_utils_get_current_iden(doc, pos, server->config.word_chars); + gchar *selection = sci_get_selection_contents(sci); + gboolean valid_rename; + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}", + "position", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.line), + "character", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.character), + "}" + ); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + valid_rename = (!sci_has_selection(sci) && iden) || + (sci_has_selection(sci) && g_strcmp0(iden, selection) == 0); + + if ((highlight && !sci_has_selection(sci) && iden) || (!highlight && valid_rename)) + { + LspHighlightData *data = g_new0(LspHighlightData, 1); + + data->doc = doc; + data->pos = pos; + data->identifier = g_strdup(iden); + data->highlight = highlight; + lsp_rpc_call(server, "textDocument/documentHighlight", node, + highlight_cb, data); + } + else + lsp_highlight_clear(doc); + + g_free(selection); + g_free(iden); + g_free(doc_uri); + g_variant_unref(node); +} + + +void lsp_highlight_send_request(LspServer *server, GeanyDocument *doc) +{ + gint pos = sci_get_current_position(doc->editor->sci); + + if (!doc || !doc->real_path) + return; + + send_request(server, doc, pos, TRUE); +} + + +void lsp_highlight_rename(gint pos) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get(doc); + + if (!srv || !doc->real_path) + return; + + send_request(srv, doc, pos, FALSE); +} diff --git a/lsp/src/lsp-highlight.h b/lsp/src/lsp-highlight.h new file mode 100644 index 000000000..dbd7fc1fd --- /dev/null +++ b/lsp/src/lsp-highlight.h @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_HIGHLIGHT_H +#define LSP_HIGHLIGHT_H 1 + +#include "lsp-server.h" + +#include + +void lsp_highlight_style_init(GeanyDocument *doc); + +void lsp_highlight_send_request(LspServer *server, GeanyDocument *doc); + +void lsp_highlight_rename(gint pos); + +void lsp_highlight_clear(GeanyDocument *doc); + +#endif /* LSP_HIGHLIGHT_H */ diff --git a/lsp/src/lsp-hover.c b/lsp/src/lsp-hover.c new file mode 100644 index 000000000..2c73597b1 --- /dev/null +++ b/lsp/src/lsp-hover.c @@ -0,0 +1,162 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-hover.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" + +#include + + +typedef struct { + GeanyDocument *doc; + gint pos; +} LspHoverData; + + +static ScintillaObject *calltip_sci; + + +static void show_calltip(GeanyDocument *doc, gint pos, const gchar *calltip) +{ + LspServer *srv = lsp_server_get_if_running(doc); + gchar *s, *p; + gboolean quit = FALSE; + gboolean start = TRUE; + gint paragraph_no = 0; + guint i; + + if (!srv) + return; + + s = g_strdup(calltip); + p = s; + + lsp_utils_wrap_string(s, -1); + for (i = 0; p && !quit && i < srv->config.hover_popup_max_lines; i++) + { + gchar *q; + + if (!start) + p++; + start = FALSE; + + q = strchr(p, '\n'); + + if (q) + { + gchar *line; + + *q = '\0'; + line = g_strdup(p); + g_strstrip(line); + if (line[0] == '\0') + paragraph_no++; + if (paragraph_no == srv->config.hover_popup_max_paragraphs) + quit = TRUE; + *q = '\n'; + + g_free(line); + } + + p = q; + } + + if (p) + { + *p = '\0'; + g_strstrip(s); + SETPTR(s, g_strconcat(s, "\n...", NULL)); + } + + calltip_sci = doc->editor->sci; + SSM(calltip_sci, SCI_CALLTIPSHOW, pos, (sptr_t) s); + g_free(s); +} + + +static void hover_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + if (!error) + { + GeanyDocument *doc = document_get_current(); + LspHoverData *data = user_data; + + if (doc == data->doc && gtk_widget_has_focus(GTK_WIDGET(doc->editor->sci))) + { + const gchar *str = NULL; + + JSONRPC_MESSAGE_PARSE(return_value, + "contents", "{", + "value", JSONRPC_MESSAGE_GET_STRING(&str), + "}"); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + + if (str && strlen(str) > 0) + show_calltip(doc, data->pos, str); + } + } + + g_free(user_data); +} + + +void lsp_hover_send_request(LspServer *server, GeanyDocument *doc, gint pos) +{ + GVariant *node; + ScintillaObject *sci = doc->editor->sci; + LspPosition lsp_pos = lsp_utils_scintilla_pos_to_lsp(sci, pos); + gchar *doc_uri = lsp_utils_get_doc_uri(doc); + LspHoverData *data = g_new0(LspHoverData, 1); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}", + "position", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.line), + "character", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.character), + "}" + ); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + data->doc = doc; + data->pos = pos; + + lsp_rpc_call(server, "textDocument/hover", node, + hover_cb, data); + + g_free(doc_uri); + g_variant_unref(node); +} + + +void lsp_hover_hide_calltip(GeanyDocument *doc) +{ + if (doc->editor->sci == calltip_sci) + { + SSM(doc->editor->sci, SCI_CALLTIPCANCEL, 0, 0); + calltip_sci = NULL; + } +} diff --git a/lsp/src/lsp-hover.h b/lsp/src/lsp-hover.h new file mode 100644 index 000000000..7cdcb9c36 --- /dev/null +++ b/lsp/src/lsp-hover.h @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_HOVER_H +#define LSP_HOVER_H 1 + +#include "lsp-server.h" + +#include + +void lsp_hover_send_request(LspServer *server, GeanyDocument *doc, gint pos); + +void lsp_hover_hide_calltip(GeanyDocument *doc); + +#endif /* LSP_HOVER_H */ diff --git a/lsp/src/lsp-log.c b/lsp/src/lsp-log.c new file mode 100644 index 000000000..5351c2063 --- /dev/null +++ b/lsp/src/lsp-log.c @@ -0,0 +1,163 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-log.h" +#include "lsp-utils.h" + +#include + + +static void log_print(LspLogInfo log, const gchar *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + + if (log.type == STDOUT_FILENO) + vprintf(fmt, args); + else if (log.type == STDERR_FILENO) + vfprintf(stderr, fmt, args); + else + g_output_stream_vprintf(G_OUTPUT_STREAM(log.stream), NULL, NULL, NULL, fmt, args); + + va_end(args); +} + + +LspLogInfo lsp_log_start(LspServerConfig *config) +{ + LspLogInfo info = {0, TRUE, NULL}; + GFile *fp; + + if (!config->rpc_log) + return info; + + info.full = config->rpc_log_full; + + if (g_strcmp0(config->rpc_log, "stdout") == 0) + info.type = STDOUT_FILENO; + else if (g_strcmp0(config->rpc_log, "stderr") == 0) + info.type = STDERR_FILENO; + else + { + fp = g_file_new_for_path(config->rpc_log); + g_file_delete(fp, NULL, NULL); + info.stream = g_file_create(fp, G_FILE_CREATE_NONE, NULL, NULL); + + if (!info.stream) + msgwin_status_add(_("Failed to create log file: %s"), config->rpc_log); + + g_object_unref(fp); + } + + if (info.full) + log_print(info, "{\n"); + + return info; +} + + +void lsp_log_stop(LspLogInfo log) +{ + if (log.type == 0 && !log.stream) + return; + + if (log.full) + log_print(log, "\n\n\"log end\": \"\"\n}\n"); + + if (log.stream) + g_output_stream_close(G_OUTPUT_STREAM(log.stream), NULL, NULL); + log.stream = NULL; +} + + +void lsp_log(LspLogInfo log, LspLogType type, const gchar *method, GVariant *params, + GError *error, GDateTime *req_time) +{ + gchar *json_msg, *time_str; + const gchar *title = ""; + GDateTime *time; + gint time_str_len; + gchar *delta_str = NULL; + gchar *err_msg; + + if (log.type == 0 && !log.stream) + return; + + err_msg = error ? g_strdup_printf("\n ^-- %s", error->message) : g_strdup(""); + + time = g_date_time_new_now_local(); + if (req_time) + { + GTimeSpan delta = g_date_time_difference(time, req_time); + delta_str = g_strdup_printf(" (%ld ms)", delta / 1000); + } + else + delta_str = g_strdup(""); + time_str = g_date_time_format(time, "\%H:\%M:\%S.\%f"); + time_str_len = strlen(time_str); + if (time_str_len > 3) + time_str[time_str_len-3] = '\0'; + g_date_time_unref(time); + + if (!method) + method = ""; + + switch (type) + { + case LspLogClientMessageSent: + title = "C --> S req: "; + break; + case LspLogClientMessageReceived: + title = "C <-- S resp: "; + break; + case LspLogClientNotificationSent: + title = "C --> S notif:"; + break; + case LspLogServerMessageSent: + title = "C <-- S req: "; + break; + case LspLogServerMessageReceived: + title = "C --> S resp: "; + break; + case LspLogServerNotificationSent: + title = "C <-- S notif:"; + break; + } + + if (log.full) + { + if (!params) + json_msg = g_strdup("null"); + else + json_msg = lsp_utils_json_pretty_print(params); + + log_print(log, "\n\n\"[%s] %s %s%s\":\n%s,\n", time_str, title, method, delta_str, json_msg); + g_free(json_msg); + } + else + log_print(log, "[%s] %s %s%s%s\n", time_str, title, method, delta_str, err_msg); + + g_free(time_str); + g_free(err_msg); + g_free(delta_str); +} diff --git a/lsp/src/lsp-log.h b/lsp/src/lsp-log.h new file mode 100644 index 000000000..6d251df6e --- /dev/null +++ b/lsp/src/lsp-log.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_LOG_H +#define LSP_LOG_H 1 + +#include +#include "lsp-server.h" + +typedef enum +{ + LspLogClientMessageSent, + LspLogClientMessageReceived, + LspLogClientNotificationSent, + + LspLogServerMessageSent, + LspLogServerMessageReceived, + LspLogServerNotificationSent +} LspLogType; + + +LspLogInfo lsp_log_start(LspServerConfig *config); +void lsp_log_stop(LspLogInfo log); + +void lsp_log(LspLogInfo log, LspLogType type, const gchar *method, GVariant *params, + GError *error, GDateTime *req_time); + + +#endif /* LSP_LOG_H */ diff --git a/lsp/src/lsp-main.c b/lsp/src/lsp-main.c new file mode 100644 index 000000000..e5ee166a4 --- /dev/null +++ b/lsp/src/lsp-main.c @@ -0,0 +1,1685 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-server.h" +#include "lsp-sync.h" +#include "lsp-utils.h" +#include "lsp-autocomplete.h" +#include "lsp-diagnostics.h" +#include "lsp-hover.h" +#include "lsp-semtokens.h" +#include "lsp-signature.h" +#include "lsp-goto.h" +#include "lsp-symbols.h" +#include "lsp-goto-anywhere.h" +#include "lsp-format.h" +#include "lsp-highlight.h" +#include "lsp-rename.h" +#include "lsp-command.h" +#include "lsp-code-lens.h" +#include "lsp-symbol.h" +#include "lsp-extension.h" +#include "lsp-workspace-folders.h" +#include "lsp-symbol-tree.h" + +#include +#include + +#include + +#include + + +// https://github.com/microsoft/language-server-protocol/blob/main/versions/protocol-1-x.md +// https://github.com/microsoft/language-server-protocol/blob/main/versions/protocol-2-x.md + + +GeanyPlugin *geany_plugin; +GeanyData *geany_data; + +LspProjectConfiguration project_configuration = UnconfiguredConfiguration; +LspProjectConfigurationType project_configuration_type = UserConfigurationType; +gchar *project_configuration_file; + +static gint last_click_pos; + + +PLUGIN_VERSION_CHECK(248) +PLUGIN_SET_TRANSLATABLE_INFO( + LOCALEDIR, + GETTEXT_PACKAGE, + _("LSP Client"), + _("Language server protocol client for Geany"), + VERSION, + "Jiri Techet ") + +#define UPDATE_SOURCE_DOC_DATA "lsp_update_source" +#define CODE_ACTIONS_PERFORMED "lsp_code_actions_performed" + +enum { + KB_GOTO_DEFINITION, + KB_GOTO_DECLARATION, + KB_GOTO_TYPE_DEFINITION, + + KB_GOTO_ANYWHERE, + KB_GOTO_DOC_SYMBOL, + KB_GOTO_WORKSPACE_SYMBOL, + KB_GOTO_LINE, + + KB_GOTO_NEXT_DIAG, + KB_GOTO_PREV_DIAG, + KB_SHOW_DIAG, + + KB_FIND_IMPLEMENTATIONS, + KB_FIND_REFERENCES, + + KB_SHOW_HOVER_POPUP, + KB_SWAP_HEADER_SOURCE, + + KB_RENAME_IN_FILE, + KB_RENAME_IN_PROJECT, + KB_FORMAT_CODE, + + KB_RESTART_SERVERS, + + KB_COUNT +}; + + +struct +{ + GtkWidget *parent_item; + + GtkWidget *project_config; + GtkWidget *user_config; + + GtkWidget *goto_def; + GtkWidget *goto_decl; + GtkWidget *goto_type_def; + + GtkWidget *goto_next_diag; + GtkWidget *goto_prev_diag; + GtkWidget *show_diag; + + GtkWidget *goto_ref; + GtkWidget *goto_impl; + + GtkWidget *rename_in_file; + GtkWidget *rename_in_project; + GtkWidget *format_code; + + GtkWidget *hover_popup; + GtkWidget *header_source; +} menu_items; + + +struct +{ + GtkWidget *command_item; + GtkWidget *goto_type_def; + GtkWidget *goto_def; + GtkWidget *goto_ref; + GtkWidget *rename_in_file; + GtkWidget *rename_in_project; + GtkWidget *format_code; + GtkWidget *separator1; + GtkWidget *separator2; +} context_menu_items; + + +struct +{ + GtkWidget *enable_check_button; + GtkWidget *settings_type_combo; + GtkWidget *config_file_entry; + GtkWidget *path_box; + GtkWidget *properties_tab; +} project_dialog; + + +static void on_save_finish(GeanyDocument *doc); +static gboolean on_code_actions_received(GPtrArray *actions, gpointer user_data); + + +static gboolean autocomplete_provided(GeanyDocument *doc, gpointer user_data) +{ + LspServer *srv = lsp_server_get(doc); + + if (!srv) + return FALSE; + + return lsp_server_is_usable(doc) && srv->config.autocomplete_enable; +} + + +static void autocomplete_perform(GeanyDocument *doc, gboolean force, gpointer user_data) +{ + LspServer *srv = lsp_server_get(doc); + + if (!srv) + return; + + lsp_autocomplete_completion(srv, doc, force); +} + + +static gboolean calltips_provided(GeanyDocument *doc, gpointer user_data) +{ + LspServer *srv = lsp_server_get(doc); + + if (!srv) + return FALSE; + + return lsp_server_is_usable(doc) && srv->config.signature_enable; +} + + +static void calltips_show(GeanyDocument *doc, gboolean force, gpointer user_data) +{ + LspServer *srv = lsp_server_get(doc); + + if (!srv) + return; + + lsp_signature_send_request(srv, doc, force); +} + + +static gboolean goto_provided(GeanyDocument *doc, gpointer user_data) +{ + LspServer *srv = lsp_server_get(doc); + + if (!srv) + return FALSE; + + return lsp_server_is_usable(doc) && srv->config.goto_enable; +} + + +static gboolean goto_perform(GeanyDocument *doc, gint pos, gboolean definition, gpointer user_data) +{ + if (definition) + lsp_goto_definition(pos); + else + lsp_goto_declaration(pos); + + return TRUE; //TODO - possibly return TRUE only when cursor on identifier +} + + +static gboolean symbol_highlight_provided(GeanyDocument *doc, gpointer user_data) +{ + LspServer *srv = lsp_server_get(doc); + + if (!srv) + return FALSE; + + return lsp_server_is_usable(doc) && srv->config.semantic_tokens_enable; +} + + +static PluginExtension extension = { + .autocomplete_provided = autocomplete_provided, + .autocomplete_perform = autocomplete_perform, + + .calltips_provided = calltips_provided, + .calltips_show = calltips_show, + + .goto_provided = goto_provided, + .goto_perform = goto_perform, + + .symbol_highlight_provided = symbol_highlight_provided, +}; + + +static void lsp_symbol_request_cb(gpointer user_data) +{ + GeanyDocument *doc = user_data; + + if (doc == document_get_current()) + lsp_symbol_tree_refresh(); +} + + +static void on_document_new(G_GNUC_UNUSED GObject *obj, GeanyDocument *doc, + G_GNUC_UNUSED gpointer user_data) +{ + // we don't know the filename yet - nothing for the LSP server +} + + +static void update_menu(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get_if_running(doc); + gboolean goto_definition_enable = srv && srv->config.goto_definition_enable; + gboolean goto_references_enable = srv && srv->config.goto_references_enable; + gboolean goto_type_definition_enable = srv && srv->config.goto_type_definition_enable; + gboolean document_formatting_enable = srv && srv->config.document_formatting_enable; + gboolean range_formatting_enable = srv && srv->config.range_formatting_enable; + gboolean rename_enable = srv && srv->config.rename_enable; + gboolean highlighting_enable = srv && srv->config.highlighting_enable; + gboolean goto_declaration_enable = srv && srv->config.goto_declaration_enable; + gboolean goto_implementation_enable = srv && srv->config.goto_implementation_enable; + gboolean diagnostics_enable = srv && srv->config.diagnostics_enable; + gboolean hover_popup_enable = srv && srv->config.hover_available; + + if (!menu_items.parent_item) + return; + + gtk_widget_set_sensitive(menu_items.goto_def, goto_definition_enable); + gtk_widget_set_sensitive(menu_items.goto_decl, goto_declaration_enable); + gtk_widget_set_sensitive(menu_items.goto_type_def, goto_type_definition_enable); + + gtk_widget_set_sensitive(menu_items.goto_next_diag, diagnostics_enable); + gtk_widget_set_sensitive(menu_items.goto_prev_diag, diagnostics_enable); + gtk_widget_set_sensitive(menu_items.show_diag, diagnostics_enable); + + gtk_widget_set_sensitive(menu_items.goto_ref, goto_references_enable); + gtk_widget_set_sensitive(menu_items.goto_impl, goto_implementation_enable); + + gtk_widget_set_sensitive(menu_items.rename_in_file, highlighting_enable); + gtk_widget_set_sensitive(menu_items.rename_in_project, rename_enable); + gtk_widget_set_sensitive(menu_items.format_code, document_formatting_enable || range_formatting_enable); + + gtk_widget_set_sensitive(menu_items.hover_popup, hover_popup_enable); +} + + +static gboolean on_update_idle(gpointer data) +{ + GeanyDocument *doc = data; + LspServer *srv; + + if (!DOC_VALID(doc)) + return G_SOURCE_REMOVE; + + plugin_set_document_data(geany_plugin, doc, UPDATE_SOURCE_DOC_DATA, GUINT_TO_POINTER(0)); + + srv = lsp_server_get_if_running(doc); + if (!srv) + return G_SOURCE_REMOVE; + + lsp_code_lens_send_request(doc); + if (symbol_highlight_provided(doc, NULL)) + lsp_semtokens_send_request(doc); + if (srv->config.document_symbols_enable) + lsp_symbols_doc_request(doc, lsp_symbol_request_cb, doc); + + return G_SOURCE_REMOVE; +} + + +static void on_document_visible(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get(doc); + + update_menu(doc); + + // quick synchronous refresh with the last value without server request + lsp_symbol_tree_refresh(); + + if (!srv) + return; + + lsp_diagnostics_style_init(doc); + lsp_diagnostics_redraw(doc); + lsp_highlight_style_init(doc); + lsp_semtokens_style_init(doc); + lsp_code_lens_style_init(doc); + lsp_autocomplete_style_init(doc); + + // just in case we didn't get some callback from the server + on_save_finish(doc); + + // this might not get called for the first time when server gets started because + // lsp_server_get() returns NULL. However, we also "open" current and modified + // documents after successful server handshake inside on_server_initialized() + if (!lsp_sync_is_document_open(doc)) + lsp_sync_text_document_did_open(srv, doc); + + on_update_idle(doc); +} + + +static gboolean on_doc_close_idle(gpointer user_data) +{ + if (!document_get_current() && menu_items.parent_item) + update_menu(NULL); // the last open document was closed + + return G_SOURCE_REMOVE; +} + + +static void on_document_close(G_GNUC_UNUSED GObject * obj, GeanyDocument *doc, + G_GNUC_UNUSED gpointer user_data) +{ + LspServer *srv = lsp_server_get_if_running(doc); + + plugin_idle_add(geany_plugin, on_doc_close_idle, NULL); + + if (!srv) + return; + + lsp_diagnostics_clear(doc); + lsp_semtokens_clear(doc); + lsp_sync_text_document_did_close(srv, doc); +} + + +static void destroy_all(void) +{ + lsp_diagnostics_destroy(); + lsp_semtokens_destroy(); + lsp_symbols_destroy(); + lsp_workspace_folders_destroy(); +} + + +static void stop_and_init_all_servers(void) +{ + lsp_server_stop_all(FALSE); + lsp_server_init_all(); + + destroy_all(); + + lsp_sync_init(); + lsp_diagnostics_init(); + lsp_workspace_folders_init(); + lsp_symbol_tree_init(); +} + + +static void restart_all_servers(void) +{ + GeanyDocument *doc = document_get_current(); + + stop_and_init_all_servers(); + + if (doc) + on_document_visible(doc); +} + + +static void on_document_save(G_GNUC_UNUSED GObject *obj, GeanyDocument *doc, + G_GNUC_UNUSED gpointer user_data) +{ + LspServer *srv; + + if (g_strcmp0(doc->real_path, lsp_utils_get_config_filename()) == 0) + { + stop_and_init_all_servers(); + return; + } + + if (lsp_server_uses_init_file(doc->real_path)) + { + stop_and_init_all_servers(); + return; + } + + srv = lsp_server_get(doc); + if (!srv) + return; + + if (!lsp_sync_is_document_open(doc)) + { + // "new" documents without filename saved for the first time or + // "save as" performed + on_document_visible(doc); + } + + lsp_sync_text_document_did_save(srv, doc); +} + + +static gboolean code_action_was_performed(LspCommand *cmd, GeanyDocument *doc) +{ + GPtrArray *code_actions_performed = plugin_get_document_data(geany_plugin, doc, CODE_ACTIONS_PERFORMED); + gchar *name; + guint i; + + if (!code_actions_performed) + return TRUE; + + foreach_ptr_array(name, i, code_actions_performed) + { + if (g_strcmp0(cmd->title, name) == 0) + return TRUE; + } + + return FALSE; +} + + +static void on_save_finish(GeanyDocument *doc) +{ + if (!DOC_VALID(doc)) + return; + + if (plugin_get_document_data(geany_plugin, doc, CODE_ACTIONS_PERFORMED)) + { + // save file at the end because the intermediate updates modified the file + document_save_file(doc, FALSE); + plugin_set_document_data(geany_plugin, doc, CODE_ACTIONS_PERFORMED, NULL); + } +} + + +static void on_command_performed(GeanyDocument *doc) +{ + if (DOC_VALID(doc)) + { + // re-request code actions on the now modified document + lsp_command_send_code_action_request(doc, sci_get_current_position(doc->editor->sci), + on_code_actions_received, doc); + } +} + + +static gboolean on_code_actions_received(GPtrArray *actions, gpointer user_data) +{ + GeanyDocument *doc = user_data; + LspCommand *cmd; + LspServer *srv; + guint i; + + if (!DOC_VALID(doc)) + return TRUE; + + srv = lsp_server_get_if_running(doc); + + if (!srv) + return TRUE; + + foreach_ptr_array(cmd, i, actions) + { + if (!code_action_was_performed(cmd, doc) && + g_regex_match_simple(srv->config.command_on_save_regex, cmd->title, G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY)) + { + GPtrArray *code_actions_performed = plugin_get_document_data(geany_plugin, doc, CODE_ACTIONS_PERFORMED); + + // save the performed title so it isn't performed in the next iteration + g_ptr_array_add(code_actions_performed, g_strdup(cmd->title)); + // perform the code action and re-request code actions in its + // callback + lsp_command_perform(srv, cmd, (LspCallback)on_command_performed, doc); + // this isn't the final call - return now + return TRUE; + } + } + + // we get here only when nothing can be performed above - this is the last + // code action call + if (srv->config.document_formatting_enable && srv->config.format_on_save) + lsp_format_perform(doc, TRUE, (LspCallback)on_save_finish, doc); + else + on_save_finish(doc); + + return TRUE; +} + + +static void free_ptrarray(gpointer data) +{ + g_ptr_array_free((GPtrArray *)data, TRUE); +} + + +static void on_document_before_save(G_GNUC_UNUSED GObject *obj, GeanyDocument *doc, + G_GNUC_UNUSED gpointer user_data) +{ + GPtrArray *code_actions_performed = plugin_get_document_data(geany_plugin, doc, CODE_ACTIONS_PERFORMED); + LspServer *srv = lsp_server_get(doc); + + // code_actions_performed non-NULL when performing save and applying code actions + if (!srv || code_actions_performed) + return; + + code_actions_performed = g_ptr_array_new_full(1, g_free); + plugin_set_document_data_full(geany_plugin, doc, CODE_ACTIONS_PERFORMED, code_actions_performed, free_ptrarray); + + if (srv->config.code_action_enable && !EMPTY(srv->config.command_on_save_regex)) + lsp_command_send_code_action_request(doc, sci_get_current_position(doc->editor->sci), + on_code_actions_received, doc); + else if (srv->config.document_formatting_enable && srv->config.format_on_save) + lsp_format_perform(doc, TRUE, (LspCallback)on_save_finish, doc); +} + + +static void on_document_before_save_as(G_GNUC_UNUSED GObject *obj, GeanyDocument *doc, + G_GNUC_UNUSED gpointer user_data) +{ + LspServer *srv = lsp_server_get(doc); + + if (!srv) + return; + + lsp_sync_text_document_did_close(srv, doc); +} + + +static void on_document_filetype_set(G_GNUC_UNUSED GObject *obj, GeanyDocument *doc, + GeanyFiletype *filetype_old, G_GNUC_UNUSED gpointer user_data) +{ + LspServer *srv_old; + LspServer *srv_new; + + // called also when opening documents - without this it would start servers + // unnecessarily + if (!lsp_sync_is_document_open(doc)) + return; + + srv_old = lsp_server_get_for_ft(filetype_old); + srv_new = lsp_server_get(doc); + + if (srv_old == srv_new) + return; + + if (srv_old) + { + // only uses URI/path so no problem we are using the "new" doc here + lsp_diagnostics_clear(doc); + lsp_semtokens_clear(doc); + lsp_sync_text_document_did_close(srv_old, doc); + } + + // might not succeed because lsp_server_get() just launched new server but should + // be opened once the new server starts + on_document_visible(doc); +} + + +static void on_document_reload(G_GNUC_UNUSED GObject *obj, GeanyDocument *doc, + G_GNUC_UNUSED gpointer user_data) +{ + // do nothing - reload behaves like a normal edit where the original text is + // removed from Scintilla and the new one inserted +} + + +static void on_document_activate(G_GNUC_UNUSED GObject *obj, GeanyDocument *doc, + G_GNUC_UNUSED gpointer user_data) +{ + on_document_visible(doc); +} + + +static gboolean on_editor_notify(G_GNUC_UNUSED GObject *obj, GeanyEditor *editor, SCNotification *nt, + G_GNUC_UNUSED gpointer user_data) +{ + static gboolean ignore_selection_change = FALSE; // static! + GeanyDocument *doc = editor->document; + ScintillaObject *sci = editor->sci; + + if (nt->nmhdr.code == SCN_PAINTED) // e.g. caret blinking + return FALSE; + + if (nt->nmhdr.code == SCN_AUTOCSELECTION && + plugin_extension_autocomplete_provided(doc, &extension)) + { + LspServer *srv = lsp_server_get_if_running(doc); + + if (!srv || !srv->config.autocomplete_enable) + return FALSE; + + // ignore cursor position change as a result of autocomplete (for highlighting) + ignore_selection_change = TRUE; + + sci_start_undo_action(editor->sci); + lsp_autocomplete_item_selected(srv, doc, SSM(sci, SCI_AUTOCGETCURRENT, 0, 0)); + sci_end_undo_action(editor->sci); + + sci_send_command(sci, SCI_AUTOCCANCEL); + + lsp_autocomplete_set_displayed_symbols(NULL); + return FALSE; + } + else if (nt->nmhdr.code == SCN_AUTOCCANCELLED) + { + lsp_autocomplete_set_displayed_symbols(NULL); + lsp_autocomplete_discard_pending_requests(); + return FALSE; + } + else if (nt->nmhdr.code == SCN_CALLTIPCLICK && + plugin_extension_calltips_provided(doc, &extension)) + { + LspServer *srv = lsp_server_get_if_running(doc); + + if (!srv) + return FALSE; + + if (srv->config.signature_enable) + { + if (nt->position == 1) /* up arrow */ + lsp_signature_show_prev(); + if (nt->position == 2) /* down arrow */ + lsp_signature_show_next(); + } + } + + if (nt->nmhdr.code == SCN_DWELLSTART) + { + LspServer *srv = lsp_server_get_if_running(doc); + if (!srv) + return FALSE; + + // also delivered when other window has focus + if (!gtk_widget_has_focus(GTK_WIDGET(sci))) + return FALSE; + + // the event is also delivered for the margin with numbers where position + // is -1. In addition, at the top of Scintilla window, the event is delivered + // when mouse is at the menubar place, with y = 0 + if (nt->position < 0 || nt->y == 0) + return FALSE; + + if (lsp_signature_showing_calltip(doc)) + ; /* don't cancel signature calltips by accidental hovers */ + else if (srv->config.diagnostics_enable && lsp_diagnostics_has_diag(nt->position)) + lsp_diagnostics_show_calltip(nt->position); + else if (srv->config.hover_enable) + lsp_hover_send_request(srv, doc, nt->position); + + return FALSE; + } + else if (nt->nmhdr.code == SCN_DWELLEND) + { + LspServer *srv = lsp_server_get_if_running(doc); + if (!srv) + return FALSE; + + if (srv->config.diagnostics_enable) + lsp_diagnostics_hide_calltip(doc); + if (srv->config.hover_enable) + lsp_hover_hide_calltip(doc); + + return FALSE; + } + else if (nt->nmhdr.code == SCN_MODIFIED) + { + LspServer *srv; + + // lots of SCN_MODIFIED notifications, filter-out those we are not interested in + if (!(nt->modificationType & (SC_MOD_BEFOREINSERT | SC_MOD_INSERTTEXT | SC_MOD_BEFOREDELETE | SC_MOD_DELETETEXT))) + return FALSE; + + srv = lsp_server_get(doc); + + if (!srv || !doc->real_path) + return FALSE; + + // BEFORE insert, BEFORE delete - send the original document + if (!lsp_sync_is_document_open(doc) && + nt->modificationType & (SC_MOD_BEFOREINSERT | SC_MOD_BEFOREDELETE)) + { + // might happen when the server just started and no interaction with it was + // possible before + lsp_sync_text_document_did_open(srv, doc); + } + + if (nt->modificationType & SC_MOD_INSERTTEXT) // after insert + { + LspPosition pos_start = lsp_utils_scintilla_pos_to_lsp(sci, nt->position); + LspPosition pos_end = pos_start; + gchar *text; + + if (srv->use_incremental_sync) + { + text = g_malloc(nt->length + 1); + memcpy(text, nt->text, nt->length); + text[nt->length] = '\0'; + } + else + text = sci_get_contents(doc->editor->sci, -1); + + lsp_sync_text_document_did_change(srv, doc, pos_start, pos_end, text); + + g_free(text); + } + else if (srv->use_incremental_sync &&(nt->modificationType & SC_MOD_BEFOREDELETE)) + { + // BEFORE! delete for incremental sync + LspPosition pos_start = lsp_utils_scintilla_pos_to_lsp(sci, nt->position); + LspPosition pos_end = lsp_utils_scintilla_pos_to_lsp(sci, nt->position + nt->length); + gchar *text = g_strdup(""); + + lsp_sync_text_document_did_change(srv, doc, pos_start, pos_end, text); + g_free(text); + } + else if (!srv->use_incremental_sync &&(nt->modificationType & SC_MOD_DELETETEXT)) + { + // AFTER! delete for full document sync + LspPosition dummy_start = lsp_utils_scintilla_pos_to_lsp(sci, 0); + LspPosition dummy_end = lsp_utils_scintilla_pos_to_lsp(sci, 0); + gchar *text = sci_get_contents(doc->editor->sci, -1); + + lsp_sync_text_document_did_change(srv, doc, dummy_start, dummy_end, text); + g_free(text); + } + + if (nt->modificationType & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT)) + { + guint update_source = GPOINTER_TO_UINT(plugin_get_document_data(geany_plugin, doc, UPDATE_SOURCE_DOC_DATA)); + + if (update_source != 0) + g_source_remove(update_source); + + // perform expensive queries only after some minimum delay + update_source = plugin_timeout_add(geany_plugin, 300, on_update_idle, doc); + plugin_set_document_data(geany_plugin, doc, UPDATE_SOURCE_DOC_DATA, GUINT_TO_POINTER(update_source)); + } + } + else if (nt->nmhdr.code == SCN_UPDATEUI) + { + LspServer *srv = lsp_server_get_if_running(doc); + + if (!srv) + return FALSE; + + if (nt->updated & (SC_UPDATE_H_SCROLL | SC_UPDATE_V_SCROLL | SC_UPDATE_SELECTION /* when caret moves */)) + { + lsp_signature_hide_calltip(doc); + lsp_hover_hide_calltip(doc); + lsp_diagnostics_hide_calltip(doc); + + SSM(sci, SCI_AUTOCCANCEL, 0, 0); + } + + if (srv->config.highlighting_enable && !ignore_selection_change && + (nt->updated & SC_UPDATE_SELECTION)) + { + lsp_highlight_send_request(srv, doc); + } + ignore_selection_change = FALSE; + } + else if (nt->nmhdr.code == SCN_CHARADDED) + { + // don't hightlight while typing + lsp_highlight_clear(doc); + + } + + return FALSE; +} + + +static void on_project_open(G_GNUC_UNUSED GObject *obj, GKeyFile *kf, + G_GNUC_UNUSED gpointer user_data) +{ + gboolean have_project_config; + + project_configuration = utils_get_setting_boolean(kf, "lsp", "enabled", UnconfiguredConfiguration); + project_configuration_type = utils_get_setting_integer(kf, "lsp", "settings_type", UserConfigurationType); + project_configuration_file = g_key_file_get_string(kf, "lsp", "config_file", NULL); + + have_project_config = lsp_utils_get_project_config_filename() != NULL; + gtk_widget_set_sensitive(menu_items.project_config, have_project_config); + gtk_widget_set_sensitive(menu_items.user_config, !have_project_config); + + stop_and_init_all_servers(); +} + + +static void on_project_close(G_GNUC_UNUSED GObject *obj, G_GNUC_UNUSED gpointer user_data) +{ + project_configuration = UnconfiguredConfiguration; + project_configuration_type = UserConfigurationType; + g_free(project_configuration_file); + project_configuration_file = NULL; + + gtk_widget_set_sensitive(menu_items.project_config, FALSE); + gtk_widget_set_sensitive(menu_items.user_config, TRUE); + + stop_and_init_all_servers(); +} + + +static void on_project_dialog_confirmed(G_GNUC_UNUSED GObject *obj, GtkWidget *notebook, + G_GNUC_UNUSED gpointer user_data) +{ + const gchar *config_file; + gboolean have_project_config; + + project_configuration = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(project_dialog.enable_check_button)); + project_configuration_type = gtk_combo_box_get_active(GTK_COMBO_BOX(project_dialog.settings_type_combo)); + config_file = gtk_entry_get_text(GTK_ENTRY(project_dialog.config_file_entry)); + SETPTR(project_configuration_file, g_strdup(config_file)); + + have_project_config = lsp_utils_get_project_config_filename() != NULL; + gtk_widget_set_sensitive(menu_items.project_config, have_project_config); + gtk_widget_set_sensitive(menu_items.user_config, !have_project_config); + + restart_all_servers(); +} + + +static void update_sensitivity(gboolean checkbox_enabled, LspProjectConfigurationType combo_state) +{ + gtk_widget_set_sensitive(project_dialog.settings_type_combo, checkbox_enabled); + gtk_widget_set_sensitive(project_dialog.path_box, checkbox_enabled && combo_state == ProjectConfigurationType); +} + + +static void on_config_changed(void) +{ + LspProjectConfigurationType combo_state = gtk_combo_box_get_active(GTK_COMBO_BOX(project_dialog.settings_type_combo)); + gboolean checkbox_enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(project_dialog.enable_check_button)); + + update_sensitivity(checkbox_enabled, combo_state); +} + + +static void add_project_properties_tab(GtkWidget *notebook) +{ + LspServerConfig *all_cfg = lsp_server_get_all_section_config(); + GtkWidget *vbox, *hbox, *ebox, *table_box; + GtkWidget *label; + GtkSizeGroup *size_group; + gint combo_value; + gboolean project_enabled; + + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + project_dialog.enable_check_button = gtk_check_button_new_with_label(_("Enable LSP client for project")); + + hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start(GTK_BOX(hbox), project_dialog.enable_check_button, TRUE, TRUE, 12); + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 12); + + table_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_box_set_spacing(GTK_BOX(table_box), 6); + + size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + label = gtk_label_new(_("Configuration type:")); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); + gtk_size_group_add_widget(size_group, label); + + project_dialog.settings_type_combo = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(project_dialog.settings_type_combo), _("Use user configuration file")); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(project_dialog.settings_type_combo), _("Use project configuration file")); + + if (project_configuration == UnconfiguredConfiguration) + { + project_enabled = all_cfg->enable_by_default; + combo_value = UserConfigurationType; + } + else + { + project_enabled = project_configuration; + combo_value = project_configuration_type; + } + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(project_dialog.enable_check_button), project_enabled); + gtk_combo_box_set_active(GTK_COMBO_BOX(project_dialog.settings_type_combo), combo_value); + g_signal_connect(project_dialog.settings_type_combo, "changed", on_config_changed, NULL); + g_signal_connect(project_dialog.enable_check_button, "toggled", on_config_changed, NULL); + + ebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start(GTK_BOX(ebox), label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(ebox), project_dialog.settings_type_combo, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(table_box), ebox, TRUE, FALSE, 0); + + label = gtk_label_new(_("Configuration file:")); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); + gtk_size_group_add_widget(size_group, label); + + project_dialog.config_file_entry = gtk_entry_new(); + ui_entry_add_clear_icon(GTK_ENTRY(project_dialog.config_file_entry)); + project_dialog.path_box = ui_path_box_new(_("Choose LSP Configuration File"), + GTK_FILE_CHOOSER_ACTION_OPEN, GTK_ENTRY(project_dialog.config_file_entry)); + gtk_entry_set_text(GTK_ENTRY(project_dialog.config_file_entry), + project_configuration_file ? project_configuration_file : ""); + + update_sensitivity(project_enabled, combo_value); + + ebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start(GTK_BOX(ebox), label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(ebox), project_dialog.path_box, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(table_box), ebox, TRUE, FALSE, 0); + + hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start(GTK_BOX(hbox), table_box, TRUE, TRUE, 12); + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 6); + + label = gtk_label_new(_("LSP Client")); + + project_dialog.properties_tab = vbox; + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), vbox, label); +} + + +static void on_project_dialog_open(G_GNUC_UNUSED GObject * obj, GtkWidget * notebook, + G_GNUC_UNUSED gpointer user_data) +{ + if (!project_dialog.properties_tab) + add_project_properties_tab(notebook); +} + + +static void on_project_dialog_close(G_GNUC_UNUSED GObject * obj, GtkWidget * notebook, + G_GNUC_UNUSED gpointer user_data) +{ + if (project_dialog.properties_tab) + { + gtk_widget_destroy(project_dialog.properties_tab); + project_dialog.properties_tab = NULL; + } +} + + +static void on_project_save(G_GNUC_UNUSED GObject *obj, GKeyFile *kf, + G_GNUC_UNUSED gpointer user_data) +{ + if (project_configuration != UnconfiguredConfiguration) + { + g_key_file_set_boolean(kf, "lsp", "enabled", project_configuration); + g_key_file_set_integer(kf, "lsp", "settings_type", project_configuration_type); + g_key_file_set_string(kf, "lsp", "config_file", + project_configuration_file ? project_configuration_file : ""); + } +} + + +static void code_action_cb(GtkWidget *widget, gpointer user_data) +{ + LspCommand *cmd = user_data; + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get_if_running(doc); + + lsp_command_perform(srv, cmd, NULL, NULL); +} + + +static void remove_item(GtkWidget *widget, gpointer data) +{ + gtk_container_remove(GTK_CONTAINER(data), widget); +} + + +static gboolean update_command_menu_items(GPtrArray *code_action_commands, gpointer user_data) +{ + static GPtrArray *action_commands = NULL; // static! + GeanyDocument *doc = user_data; + GtkWidget *menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(context_menu_items.command_item)); + GPtrArray *code_lens_commands = lsp_code_lens_get_commands(); + gboolean command_added = FALSE; + LspCommand *cmd; + guint i; + + gtk_container_foreach(GTK_CONTAINER(menu), remove_item, menu); + + if (action_commands) + g_ptr_array_free(action_commands, TRUE); + action_commands = code_action_commands; + + foreach_ptr_array(cmd, i, code_action_commands) + { + GtkWidget *item = gtk_menu_item_new_with_label(cmd->title); + + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(code_action_cb), code_action_commands->pdata[i]); + command_added = TRUE; + } + + foreach_ptr_array(cmd, i, code_lens_commands) + { + guint line = sci_get_line_from_position(doc->editor->sci, last_click_pos); + + if (cmd->line == line) + { + GtkWidget *item = gtk_menu_item_new_with_label(cmd->title); + + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(code_action_cb), code_lens_commands->pdata[i]); + command_added = TRUE; + } + } + + gtk_widget_show_all(context_menu_items.command_item); + gtk_widget_set_sensitive(context_menu_items.command_item, command_added); + + // code_action_commands are not freed now but preserved until the next call + return FALSE; +} + + +static gboolean on_update_editor_menu(G_GNUC_UNUSED GObject *obj, + const gchar *word, gint pos, GeanyDocument *doc, gpointer user_data) +{ + LspServer *srv = lsp_server_get_if_running(doc); + gboolean goto_definition_enable = srv && srv->config.goto_definition_enable; + gboolean goto_references_enable = srv && srv->config.goto_references_enable; + gboolean goto_type_definition_enable = srv && srv->config.goto_type_definition_enable; + gboolean code_action_enable = srv && srv->config.code_action_enable; + gboolean document_formatting_enable = srv && srv->config.document_formatting_enable; + gboolean range_formatting_enable = srv && srv->config.range_formatting_enable; + gboolean rename_enable = srv && srv->config.rename_enable; + gboolean highlighting_enable = srv && srv->config.highlighting_enable; + + gtk_widget_set_sensitive(context_menu_items.goto_ref, goto_references_enable); + gtk_widget_set_sensitive(context_menu_items.goto_def, goto_definition_enable); + gtk_widget_set_sensitive(context_menu_items.goto_type_def, goto_type_definition_enable); + + gtk_widget_set_sensitive(context_menu_items.rename_in_file, highlighting_enable); + gtk_widget_set_sensitive(context_menu_items.rename_in_project, rename_enable); + gtk_widget_set_sensitive(context_menu_items.format_code, document_formatting_enable || range_formatting_enable); + + gtk_widget_set_sensitive(context_menu_items.command_item, code_action_enable); + + last_click_pos = pos; + + if (code_action_enable) + lsp_command_send_code_action_request(doc, pos, update_command_menu_items, doc); + + return FALSE; +} + + +static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data) +{ + GeanyDocument *doc = document_get_current(); + ScintillaObject *sci = doc ? doc->editor->sci : NULL; + + if (!sci || gtk_window_get_focus(GTK_WINDOW(geany->main_widgets->window)) != GTK_WIDGET(sci)) + return FALSE; + + switch (event->keyval) + { + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + case GDK_KEY_uparrow: + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + case GDK_KEY_downarrow: + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: + if (SSM(sci, SCI_GETSELECTIONS, 0, 0) > 1 && !SSM(sci, SCI_AUTOCACTIVE, 0, 0)) + SSM(sci, SCI_CANCEL, 0, 0); // drop multiple carets + break; + default: + break; + } + + return FALSE; +} + + +PluginCallback plugin_callbacks[] = { + {"document-new", (GCallback) &on_document_new, FALSE, NULL}, + {"document-close", (GCallback) &on_document_close, FALSE, NULL}, + {"document-reload", (GCallback) &on_document_reload, FALSE, NULL}, + {"document-activate", (GCallback) &on_document_activate, FALSE, NULL}, + {"document-save", (GCallback) &on_document_save, FALSE, NULL}, + {"document-before-save", (GCallback) &on_document_before_save, FALSE, NULL}, + {"document-before-save-as", (GCallback) &on_document_before_save_as, TRUE, NULL}, + {"document-filetype-set", (GCallback) &on_document_filetype_set, FALSE, NULL}, + {"editor-notify", (GCallback) &on_editor_notify, FALSE, NULL}, + {"update-editor-menu", (GCallback) &on_update_editor_menu, FALSE, NULL}, + {"project-open", (GCallback) &on_project_open, FALSE, NULL}, + {"project-close", (GCallback) &on_project_close, FALSE, NULL}, + {"project-save", (GCallback) &on_project_save, FALSE, NULL}, + {"project-dialog-open", (GCallback) &on_project_dialog_open, FALSE, NULL}, + {"project-dialog-confirmed", (GCallback) &on_project_dialog_confirmed, FALSE, NULL}, + {"project-dialog-close", (GCallback) &on_project_dialog_close, FALSE, NULL}, + {"key-press", (GCallback) &on_key_press, FALSE, NULL}, + {NULL, NULL, FALSE, NULL} +}; + + +static void on_open_project_config(void) +{ + gchar *utf8_filename = utils_get_utf8_from_locale(lsp_utils_get_project_config_filename()); + if (utf8_filename) + document_open_file(utf8_filename, FALSE, NULL, NULL); + g_free(utf8_filename); +} + + +static void on_open_user_config(void) +{ + gchar *utf8_filename = utils_get_utf8_from_locale(lsp_utils_get_user_config_filename()); + document_open_file(utf8_filename, FALSE, NULL, NULL); + g_free(utf8_filename); +} + + +static void on_open_global_config(void) +{ + gchar *utf8_filename = utils_get_utf8_from_locale(lsp_utils_get_global_config_filename()); + document_open_file(utf8_filename, TRUE, NULL, NULL); + g_free(utf8_filename); +} + + +static void on_show_initialize_responses(void) +{ + gchar *resps = lsp_server_get_initialize_responses(); + document_new_file(NULL, filetypes_lookup_by_name("JSON"), resps); + g_free(resps); +} + + +static void show_hover_popup(void) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get(doc); + + if (srv) + lsp_hover_send_request(srv, doc, sci_get_current_position(doc->editor->sci)); +} + + +static void on_rename_done(void) +{ + GeanyDocument *doc = document_get_current(); + + if (!doc) + return; + + if (doc->file_type->id == GEANY_FILETYPES_C || doc->file_type->id == GEANY_FILETYPES_CPP) + { + // workaround strange behavior of clangd: it doesn't seem to reflect changes + // in non-open files unless all files are saved and the server is restarted + lsp_utils_save_all_docs(); + restart_all_servers(); + } +} + + +static gboolean on_code_actions_received_kb(GPtrArray *code_action_commands, gpointer user_data) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get_if_running(doc); + + if (srv) + { + GPtrArray *code_lens_commands = lsp_code_lens_get_commands(); + gint cmd_id = GPOINTER_TO_INT(user_data); + gchar *cmd_str = srv->config.command_regexes->pdata[cmd_id]; + gint line = sci_get_current_line(doc->editor->sci); + LspCommand *cmd; + guint i; + + foreach_ptr_array(cmd, i, code_action_commands) + { + if (g_regex_match_simple(cmd_str, cmd->title, G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY)) + { + lsp_command_perform(srv, cmd, NULL, NULL); + // perform only the first matching command + return TRUE; + } + } + + foreach_ptr_array(cmd, i, code_lens_commands) + { + if (cmd->line == line && + g_regex_match_simple(cmd_str, cmd->title, G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY)) + { + lsp_command_perform(srv, cmd, NULL, NULL); + // perform only the first matching command + return TRUE; + } + } + } + + return TRUE; +} + + +static void invoke_command_kb(guint key_id, gint pos) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get(doc); + + if (!srv) + return; + + if (key_id >= KB_COUNT + srv->config.command_keybinding_num) + return; + + lsp_command_send_code_action_request(doc, pos, on_code_actions_received_kb, GINT_TO_POINTER(key_id - KB_COUNT)); +} + + +static void invoke_kb(guint key_id, gint pos) +{ + GeanyDocument *doc = document_get_current(); + + if (pos < 0) + pos = doc ? sci_get_current_position(doc->editor->sci) : 0; + + if (key_id >= KB_COUNT) + { + invoke_command_kb(key_id , pos); + return; + } + + switch (key_id) + { + case KB_GOTO_DEFINITION: + lsp_goto_definition(pos); + break; + case KB_GOTO_DECLARATION: + lsp_goto_declaration(pos); + break; + case KB_GOTO_TYPE_DEFINITION: + lsp_goto_type_definition(pos); + break; + + case KB_GOTO_ANYWHERE: + lsp_goto_anywhere_for_file(); + break; + case KB_GOTO_DOC_SYMBOL: + lsp_goto_anywhere_for_doc(); + break; + case KB_GOTO_WORKSPACE_SYMBOL: + lsp_goto_anywhere_for_workspace(); + break; + case KB_GOTO_LINE: + lsp_goto_anywhere_for_line(); + break; + + case KB_GOTO_NEXT_DIAG: + lsp_diagnostics_goto_next_diag(pos); + break; + case KB_GOTO_PREV_DIAG: + lsp_diagnostics_goto_prev_diag(pos); + break; + case KB_SHOW_DIAG: + lsp_diagnostics_show_calltip(pos); + break; + + case KB_FIND_REFERENCES: + lsp_goto_references(pos); + break; + case KB_FIND_IMPLEMENTATIONS: + lsp_goto_implementations(pos); + break; + + case KB_SHOW_HOVER_POPUP: + show_hover_popup(); + break; + + case KB_SWAP_HEADER_SOURCE: + lsp_extension_clangd_switch_source_header(); + break; + + case KB_RENAME_IN_FILE: + lsp_highlight_rename(pos); + break; + + case KB_RENAME_IN_PROJECT: + lsp_rename_send_request(pos, on_rename_done); + break; + + case KB_FORMAT_CODE: + lsp_format_perform(doc, FALSE, NULL, NULL); + break; + + case KB_RESTART_SERVERS: + restart_all_servers(); + break; + + default: + break; + } +} + + +static gboolean on_kb_invoked(guint key_id) +{ + invoke_kb(key_id, -1); + return TRUE; +} + + +static void on_menu_invoked(GtkWidget *widget, gpointer user_data) +{ + guint key_id = GPOINTER_TO_UINT(user_data); + invoke_kb(key_id, -1); +} + + +static void on_context_menu_invoked(GtkWidget *widget, gpointer user_data) +{ + guint key_id = GPOINTER_TO_UINT(user_data); + invoke_kb(key_id, last_click_pos); +} + + +static void create_menu_items() +{ + LspServerConfig *all_cfg = lsp_server_get_all_section_config(); + GtkWidget *menu, *item, *command_submenu; + GeanyKeyGroup *group; + gint i; + + group = plugin_set_key_group(geany_plugin, "lsp", KB_COUNT + all_cfg->command_keybinding_num, on_kb_invoked); + + menu_items.parent_item = gtk_menu_item_new_with_mnemonic(_("_LSP Client")); + gtk_container_add(GTK_CONTAINER(geany->main_widgets->tools_menu), menu_items.parent_item); + + menu = gtk_menu_new (); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_items.parent_item), menu); + + menu_items.goto_def = gtk_menu_item_new_with_mnemonic(_("Go to _Definition")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.goto_def); + g_signal_connect(menu_items.goto_def, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_GOTO_DEFINITION)); + keybindings_set_item(group, KB_GOTO_DEFINITION, NULL, 0, 0, "goto_definition", + _("Go to definition"), menu_items.goto_def); + + menu_items.goto_decl = gtk_menu_item_new_with_mnemonic(_("Go to D_eclaration")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.goto_decl); + g_signal_connect(menu_items.goto_decl, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_GOTO_DECLARATION)); + keybindings_set_item(group, KB_GOTO_DECLARATION, NULL, 0, 0, "goto_declaration", + _("Go to declaration"), menu_items.goto_decl); + + menu_items.goto_type_def = gtk_menu_item_new_with_mnemonic(_("Go to _Type Definition")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.goto_type_def); + g_signal_connect(menu_items.goto_type_def, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_GOTO_TYPE_DEFINITION)); + keybindings_set_item(group, KB_GOTO_TYPE_DEFINITION, NULL, 0, 0, "goto_type_definition", + _("Go to type definition"), menu_items.goto_type_def); + + gtk_container_add(GTK_CONTAINER(menu), gtk_separator_menu_item_new()); + + item = gtk_menu_item_new_with_mnemonic(_("Go to _Anywhere...")); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_GOTO_ANYWHERE)); + keybindings_set_item(group, KB_GOTO_ANYWHERE, NULL, 0, 0, "goto_anywhere", + _("Go to anywhere"), item); + + item = gtk_menu_item_new_with_mnemonic(_("Go to _Document Symbol...")); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_GOTO_DOC_SYMBOL)); + keybindings_set_item(group, KB_GOTO_DOC_SYMBOL, NULL, 0, 0, "goto_doc_symbol", + _("Go to document symbol"), item); + + item = gtk_menu_item_new_with_mnemonic(_("Go to _Workspace Symbol...")); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_GOTO_WORKSPACE_SYMBOL)); + keybindings_set_item(group, KB_GOTO_WORKSPACE_SYMBOL, NULL, 0, 0, "goto_workspace_symbol", + _("Go to workspace symbol"), item); + + item = gtk_menu_item_new_with_mnemonic(_("Go to _Line...")); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_GOTO_LINE)); + keybindings_set_item(group, KB_GOTO_LINE, NULL, 0, 0, "goto_line", + _("Go to line"), item); + + gtk_container_add(GTK_CONTAINER(menu), gtk_separator_menu_item_new()); + + menu_items.goto_next_diag = gtk_menu_item_new_with_mnemonic(_("Go to _Next Diagnostic")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.goto_next_diag); + g_signal_connect(menu_items.goto_next_diag, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_GOTO_NEXT_DIAG)); + keybindings_set_item(group, KB_GOTO_NEXT_DIAG, NULL, 0, 0, "goto_next_diag", + _("Go to next diagnostic"), menu_items.goto_next_diag); + + menu_items.goto_prev_diag = gtk_menu_item_new_with_mnemonic(_("Go to _Previous Diagnostic")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.goto_prev_diag); + g_signal_connect(menu_items.goto_prev_diag, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_GOTO_PREV_DIAG)); + keybindings_set_item(group, KB_GOTO_PREV_DIAG, NULL, 0, 0, "goto_prev_diag", + _("Go to previous diagnostic"), menu_items.goto_prev_diag); + + menu_items.show_diag = gtk_menu_item_new_with_mnemonic(_("_Show Diagnostic")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.show_diag); + g_signal_connect(menu_items.show_diag, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_SHOW_DIAG)); + keybindings_set_item(group, KB_SHOW_DIAG, NULL, 0, 0, "show_diag", + _("Show diagnostic"), menu_items.show_diag); + + gtk_container_add(GTK_CONTAINER(menu), gtk_separator_menu_item_new()); + + menu_items.goto_ref = gtk_menu_item_new_with_mnemonic(_("Find _References")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.goto_ref); + g_signal_connect(menu_items.goto_ref, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_FIND_REFERENCES)); + keybindings_set_item(group, KB_FIND_REFERENCES, NULL, 0, 0, "find_references", + _("Find references"), menu_items.goto_ref); + + menu_items.goto_impl = gtk_menu_item_new_with_mnemonic(_("Find _Implementations")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.goto_impl); + g_signal_connect(menu_items.goto_impl, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_FIND_IMPLEMENTATIONS)); + keybindings_set_item(group, KB_FIND_IMPLEMENTATIONS, NULL, 0, 0, "find_implementations", + _("Find implementations"), menu_items.goto_impl); + + gtk_container_add(GTK_CONTAINER(menu), gtk_separator_menu_item_new()); + + menu_items.rename_in_file = gtk_menu_item_new_with_mnemonic(_("_Rename Highlighted")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.rename_in_file); + g_signal_connect(menu_items.rename_in_file, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_RENAME_IN_FILE)); + keybindings_set_item(group, KB_RENAME_IN_FILE, NULL, 0, 0, "rename_in_file", + _("Rename highlighted"), menu_items.rename_in_file); + + menu_items.rename_in_project = gtk_menu_item_new_with_mnemonic(_("Rename in _Project...")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.rename_in_project); + g_signal_connect(menu_items.rename_in_project, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_RENAME_IN_PROJECT)); + keybindings_set_item(group, KB_RENAME_IN_PROJECT, NULL, 0, 0, "rename_in_project", + _("Rename in project"), menu_items.rename_in_project); + + menu_items.format_code = gtk_menu_item_new_with_mnemonic(_("_Format Code")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.format_code); + g_signal_connect(menu_items.format_code, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_FORMAT_CODE)); + keybindings_set_item(group, KB_FORMAT_CODE, NULL, 0, 0, "format_code", + _("Format code"), menu_items.format_code); + + gtk_container_add(GTK_CONTAINER(menu), gtk_separator_menu_item_new()); + + menu_items.hover_popup = gtk_menu_item_new_with_mnemonic(_("Show _Hover Popup")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.hover_popup); + g_signal_connect(menu_items.hover_popup, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_SHOW_HOVER_POPUP)); + keybindings_set_item(group, KB_SHOW_HOVER_POPUP, NULL, 0, 0, "show_hover_popup", + _("Show hover popup"), menu_items.hover_popup); + + gtk_container_add(GTK_CONTAINER(menu), gtk_separator_menu_item_new()); + + menu_items.header_source = gtk_menu_item_new_with_mnemonic(_("Swap Header/Source")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.header_source); + g_signal_connect(menu_items.header_source, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_SWAP_HEADER_SOURCE)); + keybindings_set_item(group, KB_SWAP_HEADER_SOURCE, NULL, 0, 0, "swap_header_source", + _("Swap header/source"), menu_items.header_source); + + gtk_container_add(GTK_CONTAINER(menu), gtk_separator_menu_item_new()); + + menu_items.project_config = gtk_menu_item_new_with_mnemonic(_("_Project Configuration")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.project_config); + g_signal_connect(menu_items.project_config, "activate", G_CALLBACK(on_open_project_config), NULL); + + menu_items.user_config = gtk_menu_item_new_with_mnemonic(_("_User Configuration")); + gtk_container_add(GTK_CONTAINER(menu), menu_items.user_config); + g_signal_connect(menu_items.user_config, "activate", G_CALLBACK(on_open_user_config), NULL); + + item = gtk_menu_item_new_with_mnemonic(_("_Global Configuration")); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_open_global_config), NULL); + + gtk_container_add(GTK_CONTAINER(menu), gtk_separator_menu_item_new()); + + item = gtk_menu_item_new_with_mnemonic(_("_Server Initialize Responses")); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_show_initialize_responses), NULL); + + gtk_container_add(GTK_CONTAINER(menu), gtk_separator_menu_item_new()); + + item = gtk_menu_item_new_with_mnemonic(_("_Restart All Servers")); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_menu_invoked), + GUINT_TO_POINTER(KB_RESTART_SERVERS)); + keybindings_set_item(group, KB_RESTART_SERVERS, NULL, 0, 0, "restart_all_servers", + _("Restart all servers"), item); + + gtk_widget_show_all(menu_items.parent_item); + + for (i = 0; i < all_cfg->command_keybinding_num; i++) + { + gchar *kb_name = g_strdup_printf("lsp_command_%d", i + 1); + gchar *kb_display_name = g_strdup_printf(_("Command %d"), i + 1); + + keybindings_set_item(group, KB_COUNT + i, NULL, 0, 0, kb_name, kb_display_name, NULL); + + g_free(kb_display_name); + g_free(kb_name); + } + + /* context menu */ + context_menu_items.separator1 = gtk_separator_menu_item_new(); + gtk_widget_show(context_menu_items.separator1); + gtk_menu_shell_prepend(GTK_MENU_SHELL(geany->main_widgets->editor_menu), context_menu_items.separator1); + + context_menu_items.command_item = gtk_menu_item_new_with_mnemonic(_("_Commands (LSP)")); + command_submenu = gtk_menu_new (); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(context_menu_items.command_item), command_submenu); + gtk_widget_show_all(context_menu_items.command_item); + gtk_menu_shell_prepend(GTK_MENU_SHELL(geany->main_widgets->editor_menu), context_menu_items.command_item); + + context_menu_items.format_code = gtk_menu_item_new_with_mnemonic(_("_Format Code (LSP)")); + gtk_widget_show(context_menu_items.format_code); + gtk_menu_shell_prepend(GTK_MENU_SHELL(geany->main_widgets->editor_menu), context_menu_items.format_code); + g_signal_connect(context_menu_items.format_code, "activate", G_CALLBACK(on_context_menu_invoked), + GUINT_TO_POINTER(KB_FORMAT_CODE)); + + context_menu_items.rename_in_project = gtk_menu_item_new_with_mnemonic(_("Rename in _Project (LSP)...")); + gtk_widget_show(context_menu_items.rename_in_project); + gtk_menu_shell_prepend(GTK_MENU_SHELL(geany->main_widgets->editor_menu), context_menu_items.rename_in_project); + g_signal_connect(context_menu_items.rename_in_project, "activate", G_CALLBACK(on_context_menu_invoked), + GUINT_TO_POINTER(KB_RENAME_IN_PROJECT)); + + context_menu_items.rename_in_file = gtk_menu_item_new_with_mnemonic(_("_Rename Highlighted (LSP)")); + gtk_widget_show(context_menu_items.rename_in_file); + gtk_menu_shell_prepend(GTK_MENU_SHELL(geany->main_widgets->editor_menu), context_menu_items.rename_in_file); + g_signal_connect(context_menu_items.rename_in_file, "activate", G_CALLBACK(on_context_menu_invoked), + GUINT_TO_POINTER(KB_RENAME_IN_FILE)); + + context_menu_items.separator2 = gtk_separator_menu_item_new(); + gtk_widget_show(context_menu_items.separator2); + gtk_menu_shell_prepend(GTK_MENU_SHELL(geany->main_widgets->editor_menu), context_menu_items.separator2); + + context_menu_items.goto_type_def = gtk_menu_item_new_with_mnemonic(_("Go to _Type Definition (LSP)")); + gtk_widget_show(context_menu_items.goto_type_def); + gtk_menu_shell_prepend(GTK_MENU_SHELL(geany->main_widgets->editor_menu), context_menu_items.goto_type_def); + g_signal_connect(context_menu_items.goto_type_def, "activate", G_CALLBACK(on_context_menu_invoked), + GUINT_TO_POINTER(KB_GOTO_TYPE_DEFINITION)); + + context_menu_items.goto_def = gtk_menu_item_new_with_mnemonic(_("Go to _Definition (LSP)")); + gtk_widget_show(context_menu_items.goto_def); + gtk_menu_shell_prepend(GTK_MENU_SHELL(geany->main_widgets->editor_menu), context_menu_items.goto_def); + g_signal_connect(context_menu_items.goto_def, "activate", G_CALLBACK(on_context_menu_invoked), + GUINT_TO_POINTER(KB_GOTO_DEFINITION)); + + context_menu_items.goto_ref = gtk_menu_item_new_with_mnemonic(_("Find _References (LSP)")); + gtk_widget_show(context_menu_items.goto_ref); + gtk_menu_shell_prepend(GTK_MENU_SHELL(geany->main_widgets->editor_menu), context_menu_items.goto_ref); + g_signal_connect(context_menu_items.goto_ref, "activate", G_CALLBACK(on_context_menu_invoked), + GUINT_TO_POINTER(KB_FIND_REFERENCES)); + + update_menu(NULL); +} + + +static void on_server_initialized(LspServer *srv) +{ + GeanyDocument *current_doc = document_get_current(); + guint i; + + update_menu(current_doc); + + foreach_document(i) + { + GeanyDocument *doc = documents[i]; + LspServer *s2 = lsp_server_get_if_running(doc); + + // see on_document_visible() for detailed comment + if (s2 == srv && (doc->changed || doc == current_doc)) + { + if (doc == current_doc) + on_document_visible(doc); + else // unsaved open file + lsp_sync_text_document_did_open(srv, doc); + } + } +} + + +void plugin_init(G_GNUC_UNUSED GeanyData * data) +{ + GeanyDocument *doc = document_get_current(); + + plugin_module_make_resident(geany_plugin); + + lsp_server_set_initialized_cb(on_server_initialized); + + stop_and_init_all_servers(); + + plugin_extension_register(&extension, "LSP", 100, NULL); + + create_menu_items(); + + if (doc) + on_document_visible(doc); +} + + +void plugin_cleanup(void) +{ + gtk_widget_destroy(menu_items.parent_item); + menu_items.parent_item = NULL; + + gtk_widget_destroy(context_menu_items.goto_type_def); + gtk_widget_destroy(context_menu_items.goto_def); + gtk_widget_destroy(context_menu_items.format_code); + gtk_widget_destroy(context_menu_items.rename_in_file); + gtk_widget_destroy(context_menu_items.rename_in_project); + gtk_widget_destroy(context_menu_items.goto_ref); + gtk_widget_destroy(context_menu_items.command_item); + gtk_widget_destroy(context_menu_items.separator1); + gtk_widget_destroy(context_menu_items.separator2); + + lsp_symbol_tree_destroy(); + + plugin_extension_unregister(&extension); + + lsp_server_set_initialized_cb(NULL); + lsp_server_stop_all(TRUE); + destroy_all(); +} + + +void plugin_help(void) +{ + utils_open_browser("https://plugins.geany.org/lsp.html"); +} diff --git a/lsp/src/lsp-progress.c b/lsp/src/lsp-progress.c new file mode 100644 index 000000000..242fe6927 --- /dev/null +++ b/lsp/src/lsp-progress.c @@ -0,0 +1,205 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-progress.h" + +#include + + +typedef struct +{ + LspProgressToken token; + gchar *title; +} LspProgress; + + +static gint progress_num = 0; + + +static void progress_free(LspProgress *p) +{ + g_free(p->token.token_str); + g_free(p->title); + g_free(p); +} + + +void lsp_progress_create(LspServer *server, LspProgressToken token) +{ + LspProgress *p; + + p = g_new0(LspProgress, 1); + + p->token.token_str = g_strdup(token.token_str); + p->token.token_int = token.token_int; + + server->progress_ops = g_slist_prepend(server->progress_ops, p); +} + + +static gboolean token_equal(LspProgressToken t1, LspProgressToken t2) +{ + if (t1.token_str != NULL || t2.token_str != NULL) + return g_strcmp0(t1.token_str, t2.token_str) == 0; + return t1.token_int == t2.token_int; +} + + +static void progress_begin(LspServer *server, LspProgressToken token, const gchar *title, const gchar *message) +{ + GSList *node; + + foreach_slist(node, server->progress_ops) + { + LspProgress *p = node->data; + if (token_equal(p->token, token)) + { + p->title = g_strdup(title); + ui_set_statusbar(FALSE, "%s: %s", p->title, message ? message : ""); + if (progress_num == 0) + { + if (server->config.progress_bar_enable) + ui_progress_bar_start(""); + } + progress_num++; + break; + } + } +} + + +static void progress_report(LspServer *server, LspProgressToken token, const gchar *message) +{ + GSList *node; + + foreach_slist(node, server->progress_ops) + { + LspProgress *p = node->data; + if (token_equal(p->token, token)) + { + ui_set_statusbar(FALSE, "%s: %s", p->title, message ? message : ""); + break; + } + } +} + + +static void progress_end(LspServer *server, LspProgressToken token, const gchar *message) +{ + GSList *node; + + foreach_slist(node, server->progress_ops) + { + LspProgress *p = node->data; + if (token_equal(p->token, token)) + { + if (progress_num > 0) + progress_num--; + if (progress_num == 0) + ui_progress_bar_stop(); + + if (message) + ui_set_statusbar(FALSE, "%s: %s", p->title, message ? message : ""); + else + ui_set_statusbar(FALSE, "%s", ""); + + server->progress_ops = g_slist_remove_link(server->progress_ops, node); + g_slist_free_full(node, (GDestroyNotify)progress_free); + break; + } + } +} + + +void lsp_progress_free_all(LspServer *server) +{ + guint len = g_slist_length(server->progress_ops); + + g_slist_free_full(server->progress_ops, (GDestroyNotify)progress_free); + server->progress_ops = 0; + progress_num = MAX(0, progress_num - len); + if (progress_num == 0) + ui_progress_bar_stop(); +} + + +void lsp_progress_process_notification(LspServer *srv, GVariant *params) +{ + gboolean have_token = FALSE; + gint64 token_int = 0; + const gchar *token_str = NULL; + const gchar *kind = NULL; + const gchar *title = NULL; + const gchar *message = NULL; + gchar buf[50]; + + have_token = JSONRPC_MESSAGE_PARSE(params, + "token", JSONRPC_MESSAGE_GET_STRING(&token_str) + ); + if (!have_token) + { + have_token = JSONRPC_MESSAGE_PARSE(params, + "token", JSONRPC_MESSAGE_GET_INT64(&token_int) + ); + } + JSONRPC_MESSAGE_PARSE(params, + "value", "{", + "kind", JSONRPC_MESSAGE_GET_STRING(&kind), + "}" + ); + JSONRPC_MESSAGE_PARSE(params, + "value", "{", + "title", JSONRPC_MESSAGE_GET_STRING(&title), + "}" + ); + JSONRPC_MESSAGE_PARSE(params, + "value", "{", + "message", JSONRPC_MESSAGE_GET_STRING(&message), + "}" + ); + + if (!message) + { + gint64 percentage; + gboolean have_percentage = JSONRPC_MESSAGE_PARSE(params, + "value", "{", + "percentage", JSONRPC_MESSAGE_GET_INT64(&percentage), + "}" + ); + if (have_percentage) + { + g_snprintf(buf, 30, "%ld%%", percentage); + message = buf; + } + } + + if (srv && have_token && kind) + { + LspProgressToken token = {token_int, (gchar *)token_str}; + if (g_strcmp0(kind, "begin") == 0) + progress_begin(srv, token, title, message); + else if (g_strcmp0(kind, "report") == 0) + progress_report(srv, token, message); + else if (g_strcmp0(kind, "end") == 0) + progress_end(srv, token, message); + } +} diff --git a/lsp/src/lsp-progress.h b/lsp/src/lsp-progress.h new file mode 100644 index 000000000..534de6a42 --- /dev/null +++ b/lsp/src/lsp-progress.h @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_PROGRESS_H +#define LSP_PROGRESS_H 1 + +#include "lsp-server.h" + +#include + + +typedef struct +{ + gint token_int; + gchar *token_str; +} LspProgressToken; + + +void lsp_progress_create(LspServer *server, LspProgressToken token); + +void lsp_progress_process_notification(LspServer *srv, GVariant *params); + +void lsp_progress_free_all(LspServer *server); + +#endif /* LSP_PROGRESS_H */ diff --git a/lsp/src/lsp-rename.c b/lsp/src/lsp-rename.c new file mode 100644 index 000000000..964e64edf --- /dev/null +++ b/lsp/src/lsp-rename.c @@ -0,0 +1,202 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-rename.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" + +#include + + +static struct +{ + GtkWidget *widget; + GtkWidget *old_label; + GtkWidget *combo; +} rename_dialog = {NULL, NULL, NULL}; + + +extern GeanyData *geany_data; + + +static gchar *show_dialog_rename(const gchar *old_name) +{ + gint res; + GtkWidget *entry; + GtkSizeGroup *size_group; + const gchar *str = NULL; + gchar *old_name_str; + + if (!rename_dialog.widget) + { + GtkWidget *label, *vbox, *ebox; + + rename_dialog.widget = gtk_dialog_new_with_buttons( + _("Rename in Project"), GTK_WINDOW(geany->main_widgets->window), + GTK_DIALOG_DESTROY_WITH_PARENT, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Rename"), GTK_RESPONSE_ACCEPT, NULL); + gtk_window_set_default_size(GTK_WINDOW(rename_dialog.widget), 600, -1); + gtk_dialog_set_default_response(GTK_DIALOG(rename_dialog.widget), GTK_RESPONSE_CANCEL); + + vbox = ui_dialog_vbox_new(GTK_DIALOG(rename_dialog.widget)); + gtk_box_set_spacing(GTK_BOX(vbox), 6); + + label = gtk_label_new(_("Warning")); + gtk_label_set_use_markup(GTK_LABEL(label), TRUE); + gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, FALSE, 0); + + label = gtk_label_new(_("By pressing the Rename button below, you are going to replace Old name with New name in the whole project. There is no further confirmation or change review after this step.")); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); + gtk_label_set_use_markup(GTK_LABEL(label), TRUE); + gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); + gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, FALSE, 0); + + label = gtk_label_new(_("Since this operation cannot be undone easily, it is highly recommended to perform this action only after committing all modified files into VCS in case something goes wrong.")); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); + gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); + gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, FALSE, 0); + + size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + label = gtk_label_new(_("New name:")); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); + gtk_size_group_add_widget(size_group, label); + rename_dialog.combo = gtk_combo_box_text_new_with_entry(); + entry = gtk_bin_get_child(GTK_BIN(rename_dialog.combo)); + gtk_entry_set_width_chars(GTK_ENTRY(entry), 30); + gtk_label_set_mnemonic_widget(GTK_LABEL(label), entry); + ui_entry_add_clear_icon(GTK_ENTRY(entry)); + gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); + + ebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start(GTK_BOX(ebox), label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(ebox), rename_dialog.combo, TRUE, TRUE, 0); + + gtk_box_pack_start(GTK_BOX(vbox), ebox, TRUE, FALSE, 0); + + label = gtk_label_new(_("Old name:")); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); + gtk_size_group_add_widget(size_group, label); + rename_dialog.old_label = gtk_label_new(""); + gtk_label_set_use_markup(GTK_LABEL(rename_dialog.old_label), TRUE); + gtk_label_set_xalign(GTK_LABEL(rename_dialog.old_label), 0.0); + + ebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start(GTK_BOX(ebox), label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(ebox), rename_dialog.old_label, TRUE, TRUE, 0); + + gtk_box_pack_start(GTK_BOX(vbox), ebox, TRUE, FALSE, 0); + + gtk_widget_show_all(vbox); + } + + old_name_str = g_markup_printf_escaped("%s", old_name); + gtk_label_set_markup(GTK_LABEL(rename_dialog.old_label), old_name_str); + g_free(old_name_str); + + entry = gtk_bin_get_child(GTK_BIN(rename_dialog.combo)); + gtk_entry_set_text(GTK_ENTRY(entry), old_name); + gtk_widget_grab_focus(entry); + + res = gtk_dialog_run(GTK_DIALOG(rename_dialog.widget)); + + if (res == GTK_RESPONSE_ACCEPT) + { + str = gtk_entry_get_text(GTK_ENTRY(entry)); + ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(rename_dialog.combo), str, 0); + } + + gtk_widget_hide(rename_dialog.widget); + + return g_strdup(str); +} + + +static void rename_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + GCallback on_rename_done = user_data; + + if (!error) + { + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + + if (lsp_utils_apply_workspace_edit(return_value)) + on_rename_done(); + } + else + dialogs_show_msgbox(GTK_MESSAGE_ERROR, "%s", error->message); +} + + +void lsp_rename_send_request(gint pos, GCallback on_rename_done) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get(doc); + ScintillaObject *sci; + LspPosition lsp_pos; + GVariant *node; + gchar *selection; + gchar *iden; + + if (!srv) + return; + + sci = doc->editor->sci; + lsp_pos = lsp_utils_scintilla_pos_to_lsp(sci, pos); + + iden = lsp_utils_get_current_iden(doc, pos, srv->config.word_chars); + selection = sci_get_selection_contents(sci); + if ((!sci_has_selection(sci) && iden) || (sci_has_selection(sci) && g_strcmp0(iden, selection) == 0)) + { + gchar *new_name = show_dialog_rename(iden); + + if (new_name && new_name[0]) + { + gchar *doc_uri = lsp_utils_get_doc_uri(doc); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}", + "position", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.line), + "character", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.character), + "}", + "newName", JSONRPC_MESSAGE_PUT_STRING(new_name) + ); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + lsp_rpc_call(srv, "textDocument/rename", node, + rename_cb, on_rename_done); + + g_free(doc_uri); + g_variant_unref(node); + } + + g_free(new_name); + } + + g_free(iden); + g_free(selection); +} diff --git a/lsp/src/lsp-rename.h b/lsp/src/lsp-rename.h new file mode 100644 index 000000000..0aa16f87c --- /dev/null +++ b/lsp/src/lsp-rename.h @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_RENAME_H +#define LSP_RENAME_H 1 + +#include "lsp-server.h" + +#include + + +void lsp_rename_send_request(gint pos, GCallback on_rename_done); + + +#endif /* LSP_RENAME_H */ diff --git a/lsp/src/lsp-rpc.c b/lsp/src/lsp-rpc.c new file mode 100644 index 000000000..c26d74bfe --- /dev/null +++ b/lsp/src/lsp-rpc.c @@ -0,0 +1,539 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-rpc.h" +#include "lsp-server.h" +#include "lsp-diagnostics.h" +#include "lsp-progress.h" +#include "lsp-log.h" +#include "lsp-utils.h" +#include "lsp-workspace-folders.h" + +#include +#include + + +typedef struct +{ + gchar *method_name; + gpointer user_data; + LspRpcCallback callback; + GDateTime *req_time; + gboolean cb_on_startup_shutdown; +} CallbackData; + + +struct LspRpc +{ + JsonrpcClient *client; +}; + + +extern GeanyData *geany_data; + +GHashTable *client_table; + + +static void log_message(GVariant *params) +{ + gint64 type; + const gchar *msg; + gboolean success; + + success = JSONRPC_MESSAGE_PARSE(params, + "type", JSONRPC_MESSAGE_GET_INT64(&type), + "message", JSONRPC_MESSAGE_GET_STRING(&msg)); + + if (success) + { + const gchar *type_str; + gchar *stripped_msg = g_strdup(msg); + + switch (type) + { + case 1: + type_str = "Error"; + break; + case 2: + type_str = "Warning"; + break; + case 3: + type_str = "Info"; + break; + case 4: + type_str = "Log"; + break; + default: + type_str = "Debug"; + break; + } + + g_strstrip(stripped_msg); + msgwin_status_add("%s: %s", type_str, stripped_msg); + g_free(stripped_msg); + } +} + + +static void handle_notification(JsonrpcClient *client, gchar *method, GVariant *params, + gpointer user_data) +{ + LspServer *srv = g_hash_table_lookup(client_table, client); + + if (!srv) + return; + + lsp_log(srv->log, LspLogServerNotificationSent, method, params, NULL, NULL); + + if (g_strcmp0(method, "textDocument/publishDiagnostics") == 0) + lsp_diagnostics_received(params); + else if (g_strcmp0(method, "window/logMessage") == 0 || + g_strcmp0(method, "window/showMessage") == 0) + { + log_message(params); + } + else if (g_strcmp0(method, "$/progress") == 0) + { + lsp_progress_process_notification(srv, params); + } + else + { + //printf("\n\nNOTIFICATION FROM SERVER: %s\n", method); + //printf("params:\n%s\n\n\n", lsp_utils_json_pretty_print(params)); + } +} + + +static void reply_async(LspServer *srv, const gchar *method, JsonrpcClient *client, + GVariant *id, GVariant *result) +{ + jsonrpc_client_reply_async(client, id, result, NULL, NULL, NULL); + lsp_log(srv->log, LspLogServerMessageReceived, method, result, NULL, NULL); +} + + +static GVariant *create_progress(LspServer *srv, GVariant *params) +{ + gboolean have_token = FALSE; + const gchar *token_str = NULL; + gint64 token_int = 0; + + have_token = JSONRPC_MESSAGE_PARSE(params, + "token", JSONRPC_MESSAGE_GET_STRING(&token_str) + ); + if (!have_token) + { + have_token = JSONRPC_MESSAGE_PARSE(params, + "token", JSONRPC_MESSAGE_GET_INT64(&token_int) + ); + } + + if (srv && have_token) + { + LspProgressToken token = {token_int, (gchar *)token_str}; + lsp_progress_create(srv, token); + } + + return NULL; +} + + +static GVariant *apply_edit(LspServer *srv, GVariant *params) +{ + GVariant *edit, *msg; + gboolean success; + + success = JSONRPC_MESSAGE_PARSE(params, + "edit", JSONRPC_MESSAGE_GET_VARIANT(&edit) + ); + + if (success) + success = lsp_utils_apply_workspace_edit(edit); + + msg = JSONRPC_MESSAGE_NEW( + "applied", JSONRPC_MESSAGE_PUT_BOOLEAN(success) + ); + + if (edit) + g_variant_unref(edit); + + return msg; +} + + +static GVariant *workspace_configuration(LspServer *srv, GVariant *params) +{ + GVariantIter *iter = NULL; + GVariant *res = NULL; + + JSONRPC_MESSAGE_PARSE(params, + "items", JSONRPC_MESSAGE_GET_ITER(&iter) + ); + + if (iter) + { + JsonNode *settings = lsp_utils_parse_json_file(srv->config.initialization_options_file, + srv->config.initialization_options); + JsonNode *empty_elem = json_from_string("{}", NULL); + JsonBuilder *builder = json_builder_new(); + GVariant *member = NULL; + JsonNode *res_json = NULL; + + json_builder_begin_array(builder); + + while (g_variant_iter_loop(iter, "v", &member)) + { + const gchar *section = NULL; + gboolean added = FALSE; + + if (JSONRPC_MESSAGE_PARSE(member, "section", JSONRPC_MESSAGE_GET_STRING(§ion))) + { + gchar *path = g_strconcat("$.", section, NULL); + JsonNode *matched = json_path_query(path, settings, NULL); + + if (matched) + { + JsonArray *arr = json_node_get_array(matched); + if (arr && json_array_get_length(arr) > 0) + { + json_builder_add_value(builder, json_array_dup_element(arr, 0)); + added = TRUE; + } + json_node_unref(matched); + } + + g_free(path); + } + + if (!added) + json_builder_add_value(builder, json_node_ref(empty_elem)); + } + + json_builder_end_array(builder); + res_json = json_builder_get_root(builder); + res = g_variant_take_ref(json_gvariant_deserialize(res_json, NULL, NULL)); + + g_variant_iter_free(iter); + json_node_free(settings); + json_node_free(res_json); + g_object_unref(builder); + json_node_unref(empty_elem); + } + + return res; +} + + +static GVariant *show_document(LspServer *srv, GVariant *params) +{ + const gchar *uri = NULL; + gboolean external = FALSE; + gboolean success = FALSE; + GVariant *msg; + + JSONRPC_MESSAGE_PARSE(params, + "uri", JSONRPC_MESSAGE_GET_STRING(&uri) + ); + + JSONRPC_MESSAGE_PARSE(params, + "external", JSONRPC_MESSAGE_GET_BOOLEAN(&external) + ); + + if (uri) + { + if (external || !g_str_has_prefix(uri, "file://")) + { + utils_open_browser(uri); + success = TRUE; + } + else if (g_str_has_prefix(uri, "file://")) + { + gchar *fname = lsp_utils_get_real_path_from_uri_locale(uri); + if (fname) + { + document_open_file(fname, FALSE, NULL, NULL); + g_free(fname); + success = TRUE; + } + } + } + + msg = JSONRPC_MESSAGE_NEW( + "success", JSONRPC_MESSAGE_PUT_BOOLEAN(success) + ); + + return msg; +} + + +static GVariant *workspace_folders(LspServer *srv, GVariant *params) +{ + GtkNotebook *notebook = GTK_NOTEBOOK(geany_data->main_widgets->sidebar_notebook); + gint num = gtk_notebook_get_n_pages(notebook); + GPtrArray *folders = lsp_workspace_folders_get(); + GVariant *msg = NULL; + + if (num > 1) // non-single-open document variant + { + GPtrArray *arr = g_ptr_array_new_full(1, (GDestroyNotify) g_variant_unref); + gchar *folder; + guint i; + + foreach_ptr_array(folder, i, folders) + { + gchar *uri = g_filename_to_uri(folder, NULL, NULL); + GVariant *folder_variant; + + folder_variant = JSONRPC_MESSAGE_NEW( + "uri", JSONRPC_MESSAGE_PUT_STRING(uri), + "name", JSONRPC_MESSAGE_PUT_STRING(folder) + ); + g_ptr_array_add(arr, folder_variant); + + g_free(uri); + } + + msg = g_variant_take_ref(g_variant_new_array(G_VARIANT_TYPE_VARDICT, + (GVariant **)arr->pdata, arr->len)); + + g_ptr_array_free(arr, TRUE); + } + + g_ptr_array_free(folders, TRUE); + + return msg; +} + + +static gboolean handle_call(JsonrpcClient *client, gchar* method, GVariant *id, GVariant *params, + gpointer user_data) +{ + LspServer *srv = g_hash_table_lookup(client_table, client); + gboolean handled = FALSE; + GVariant *msg = NULL; + + if (!srv) + return FALSE; + + lsp_log(srv->log, LspLogServerMessageSent, method, params, NULL, NULL); + + //printf("\n\nREQUEST FROM SERVER: %s\n", method); + //printf("params:\n%s\n\n\n", lsp_utils_json_pretty_print(params)); + + if (g_strcmp0(method, "window/workDoneProgress/create") == 0) + { + msg = create_progress(srv, params); + handled = TRUE; + } + else if (g_strcmp0(method, "workspace/applyEdit") == 0) + { + msg = apply_edit(srv, params); + handled = TRUE; + } + // we officially don't support this (not advertised in initialize's + // workspace/configuration) but some servers ask for it anyway so do our + // best in this case + else if (g_strcmp0(method, "workspace/configuration") == 0) + { + msg = workspace_configuration(srv, params); + handled = TRUE; + } + else if (g_strcmp0(method, "window/showDocument") == 0) + { + msg = show_document(srv, params); + handled = TRUE; + } + else if (g_strcmp0(method, "workspace/workspaceFolders") == 0) + { + msg = workspace_folders(srv, params); + handled = TRUE; + } + + if (handled) + { + reply_async(srv, method, client, id, msg); + if (msg) + g_variant_unref(msg); + return TRUE; + } + else + { + GVariant *variant; + JsonNode *node; + + node = json_from_string("{}", NULL); + variant = json_gvariant_deserialize(node, NULL, NULL); + lsp_log(srv->log, LspLogServerMessageReceived, method, variant, NULL, NULL); + g_variant_unref(variant); + json_node_free(node); + } + + return FALSE; +} + + +static void call_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + JsonrpcClient *client = (JsonrpcClient *)source_object; + LspServer *srv = g_hash_table_lookup(client_table, client); + CallbackData *data = user_data; + GVariant *return_value = NULL; + GError *error = NULL; + gboolean is_startup_shutdown = TRUE; + + jsonrpc_client_call_finish(client, res, &return_value, &error); + + if (srv) + { + lsp_log(srv->log, LspLogClientMessageReceived, data->method_name, + return_value, error, data->req_time); + is_startup_shutdown = srv->startup_shutdown; + } + + if (data->callback && (!is_startup_shutdown || data->cb_on_startup_shutdown)) + data->callback(return_value, error, data->user_data); + + if (return_value) + g_variant_unref(return_value); + + if (error) + g_error_free(error); + + g_date_time_unref(data->req_time); + g_free(data->method_name); + g_free(data); +} + + +static void call_full(LspServer *srv, const gchar *method, GVariant *params, + LspRpcCallback callback, gboolean cb_on_startup_shutdown, gpointer user_data) +{ + CallbackData *data = g_new0(CallbackData, 1); + + data->method_name = g_strdup(method); + data->user_data = user_data; + data->callback = callback; + data->req_time = g_date_time_new_now_local(); + data->cb_on_startup_shutdown = cb_on_startup_shutdown; + + lsp_log(srv->log, LspLogClientMessageSent, method, params, NULL, NULL); + + jsonrpc_client_call_async(srv->rpc->client, method, params, NULL, call_cb, data); +} + + +void lsp_rpc_call(LspServer *srv, const gchar *method, GVariant *params, + LspRpcCallback callback, gpointer user_data) +{ + call_full(srv, method, params, callback, FALSE, user_data); +} + + +void lsp_rpc_call_startup_shutdown(LspServer *srv, const gchar *method, GVariant *params, + LspRpcCallback callback, gpointer user_data) +{ + call_full(srv, method, params, callback, TRUE, user_data); +} + + +static void notify_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + JsonrpcClient *client = (JsonrpcClient *)source_object; + CallbackData *data = user_data; + GVariant *return_value = NULL; + GError *error = NULL; + + jsonrpc_client_send_notification_finish(client, res, &error); + + if (data->callback) + data->callback(return_value, error, data->user_data); + + if (return_value) + g_variant_unref(return_value); + + if (error) + g_error_free(error); + + g_free(data); +} + + +void lsp_rpc_notify(LspServer *srv, const gchar *method, GVariant *params, + LspRpcCallback callback, gpointer user_data) +{ + gboolean params_added = FALSE; + CallbackData *data = g_new0(CallbackData, 1); + + data->user_data = user_data; + data->callback = callback; + + lsp_log(srv->log, LspLogClientNotificationSent, + method, params, NULL, NULL); + + /* Two hacks in one: + * 1. some servers (e.g. gopls) require that the params member is present + * (jsonrpc-glib removes it when there are no parameters which is jsonrpc + * compliant) + * 2. haskell-language-server or nil require that the "exit" notification + * has no params member + */ + if (!params && g_strcmp0(method, "exit") != 0) + { + GVariantDict dict; + + g_variant_dict_init(&dict, NULL); + params = g_variant_take_ref(g_variant_dict_end(&dict)); + + params_added = TRUE; + } + + jsonrpc_client_send_notification_async(srv->rpc->client, method, params, NULL, notify_cb, data); + + if (params_added) + g_variant_unref(params); +} + + +LspRpc *lsp_rpc_new(LspServer *srv, GIOStream *stream) +{ + LspRpc *c = g_new0(LspRpc, 1); + + if (!client_table) + client_table = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, NULL); + + c->client = jsonrpc_client_new(stream); + g_hash_table_insert(client_table, c->client, srv); + g_signal_connect(c->client, "handle-call", G_CALLBACK(handle_call), NULL); + g_signal_connect(c->client, "notification", G_CALLBACK(handle_notification), NULL); + jsonrpc_client_start_listening(c->client); + + return c; +} + + +void lsp_rpc_destroy(LspRpc *rpc) +{ + g_hash_table_remove(client_table, rpc->client); + jsonrpc_client_close(rpc->client, NULL, NULL); + g_object_unref(rpc->client); + g_free(rpc); +} diff --git a/lsp/src/lsp-rpc.h b/lsp/src/lsp-rpc.h new file mode 100644 index 000000000..7d24c78f9 --- /dev/null +++ b/lsp/src/lsp-rpc.h @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_RPC_H +#define LSP_RPC_H 1 + + +#include "lsp-server.h" + + +typedef void (*LspRpcCallback) (GVariant *return_value, GError *error, gpointer user_data); + + +struct LspRpc; +typedef struct LspRpc LspRpc; + + +LspRpc *lsp_rpc_new(LspServer *srv, GIOStream *stream); +void lsp_rpc_destroy(LspRpc *rpc); + +void lsp_rpc_call(LspServer *srv, const gchar *method, GVariant *params, + LspRpcCallback callback, gpointer user_data); + +void lsp_rpc_call_startup_shutdown(LspServer *srv, const gchar *method, GVariant *params, + LspRpcCallback callback, gpointer user_data); + +void lsp_rpc_notify(LspServer *srv, const gchar *method, GVariant *params, + LspRpcCallback callback, gpointer user_data); + + +#endif /* LSP_RPC_H */ diff --git a/lsp/src/lsp-semtokens.c b/lsp/src/lsp-semtokens.c new file mode 100644 index 000000000..6715b5ae9 --- /dev/null +++ b/lsp/src/lsp-semtokens.c @@ -0,0 +1,475 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-semtokens.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" +#include "lsp-sync.h" + +#include + +#define CACHE_KEY "lsp_semtokens_key" + +typedef struct { + guint start; + guint delete_count; + GArray *data; +} SemanticTokensEdit; + + +extern GeanyPlugin *geany_plugin; + +extern GeanyData *geany_data; + +typedef struct { + GArray *tokens; + gchar *tokens_str; + gchar *result_id; +} CachedData; + + +static gint style_index; + +static guint keyword_hash = 0; + + +static void cached_data_free(CachedData *data) +{ + g_array_free(data->tokens, TRUE); + g_free(data->tokens_str); + g_free(data->result_id); + g_free(data); +} + + +void lsp_semtokens_init(gint ft_id) +{ + guint i; + foreach_document(i) + { + GeanyDocument *doc = documents[i]; + if (doc->file_type->id == ft_id) + plugin_set_document_data(geany_plugin, doc, CACHE_KEY, NULL); + } +} + + +void lsp_semtokens_style_init(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get_if_running(doc); + ScintillaObject *sci; + + if (!srv) + return; + + sci = doc->editor->sci; + + style_index = 0; + if (!EMPTY(srv->config.semantic_tokens_type_style)) + style_index = lsp_utils_set_indicator_style(sci, srv->config.semantic_tokens_type_style); +} + + +void lsp_semtokens_destroy(void) +{ + guint i; + foreach_document(i) + { + plugin_set_document_data(geany_plugin, documents[i], CACHE_KEY, NULL); + } +} + + +static SemanticTokensEdit *sem_tokens_edit_new(void) +{ + SemanticTokensEdit *edit = g_new0(SemanticTokensEdit, 1); + edit->data = g_array_sized_new(FALSE, FALSE, sizeof(guint), 20); + return edit; +} + + +static void sem_tokens_edit_free(SemanticTokensEdit *edit) +{ + g_array_free(edit->data, TRUE); + g_free(edit); +} + + +static void sem_tokens_edit_apply(CachedData *data, SemanticTokensEdit *edit) +{ + g_return_if_fail(edit->start + edit->delete_count <= data->tokens->len); + + g_array_remove_range(data->tokens, edit->start, edit->delete_count); + g_array_insert_vals(data->tokens, edit->start, edit->data->data, edit->data->len); +} + + +static const gchar *get_cached(GeanyDocument *doc) +{ + CachedData *data; + + if (style_index > 0) + return ""; + + data = plugin_get_document_data(geany_plugin, doc, CACHE_KEY); + if (!data || !data->tokens_str) + return ""; + + return data->tokens_str; +} + + +static void highlight_keywords(LspServer *srv, GeanyDocument *doc) +{ + const gchar *keywords = get_cached(doc); + guint new_hash; + + keywords = keywords != NULL ? keywords : ""; + new_hash = g_str_hash(keywords); + + if (keyword_hash != new_hash) + { + SSM(doc->editor->sci, SCI_SETKEYWORDS, srv->config.semantic_tokens_lexer_kw_index, (sptr_t) keywords); + SSM(doc->editor->sci, SCI_COLOURISE, (uptr_t) 0, -1); + keyword_hash = new_hash; + } +} + + +static gchar *process_tokens(GArray *tokens, GeanyDocument *doc, guint64 token_mask) +{ + GHashTable *type_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + ScintillaObject *sci = doc->editor->sci; + guint delta_line = 0; + guint delta_char = 0; + guint len = 0; + guint token_type = 0; + LspPosition last_pos = {0, 0}; + gboolean first = TRUE; + GList *keys, *item; + GString *type_str; + gint i; + + if (style_index > 0) + { + sci_indicator_set(doc->editor->sci, style_index); + sci_indicator_clear(doc->editor->sci, 0, sci_get_length(doc->editor->sci)); + } + + for (i = 0; i < tokens->len; i++) + { + guint v = g_array_index(tokens, guint, i); + + switch (i % 5) + { + case 0: + delta_line = v; + break; + case 1: + delta_char = v; + break; + case 2: + len = v; + break; + case 3: + token_type = 1 << v; + break; + } + + if (i % 5 == 4) + { + last_pos.line += delta_line; + if (delta_line == 0) + last_pos.character += delta_char; + else + last_pos.character = delta_char; + + if (token_type & token_mask) + { + LspPosition end_pos = last_pos; + gint sci_pos_start, sci_pos_end; + gchar *str; + + end_pos.character += len; + sci_pos_start = lsp_utils_lsp_pos_to_scintilla(sci, last_pos); + sci_pos_end = lsp_utils_lsp_pos_to_scintilla(sci, end_pos); + + if (style_index > 0) + editor_indicator_set_on_range(doc->editor, style_index, sci_pos_start, sci_pos_end); + + str = sci_get_contents_range(sci, sci_pos_start, sci_pos_end); + if (str) + g_hash_table_insert(type_table, str, NULL); + } + } + } + + keys = g_hash_table_get_keys(type_table); + type_str = g_string_new(""); + + foreach_list(item, keys) + { + if (!first) + g_string_append_c(type_str, ' '); + g_string_append(type_str, item->data); + first = FALSE; + } + + g_list_free(keys); + g_hash_table_destroy(type_table); + + return g_string_free(type_str, FALSE); +} + + +static void process_full_result(GeanyDocument *doc, GVariant *result, guint64 token_mask) +{ + GVariantIter *iter = NULL; + const gchar *result_id = NULL; + + JSONRPC_MESSAGE_PARSE(result, + "resultId", JSONRPC_MESSAGE_GET_STRING(&result_id) + ); + JSONRPC_MESSAGE_PARSE(result, + "data", JSONRPC_MESSAGE_GET_ITER(&iter) + ); + + if (iter) + { + GVariant *val = NULL; + CachedData *data = plugin_get_document_data(geany_plugin, doc, CACHE_KEY); + + if (data == NULL) + { + data = g_new0(CachedData, 1); + data->tokens = g_array_sized_new(FALSE, FALSE, sizeof(guint), 1000); + plugin_set_document_data_full(geany_plugin, doc, CACHE_KEY, data, (GDestroyNotify)cached_data_free); + } + + g_free(data->result_id); + data->result_id = g_strdup(result_id); + data->tokens->len = 0; + + while (g_variant_iter_loop(iter, "v", &val)) + { + guint v = g_variant_get_int64(val); + g_array_append_val(data->tokens, v); + } + + g_free(data->tokens_str); + data->tokens_str = process_tokens(data->tokens, doc, token_mask); + + g_variant_iter_free(iter); + } +} + + +static gint sort_edits(gconstpointer a, gconstpointer b) +{ + const SemanticTokensEdit *e1 = *((SemanticTokensEdit **) a); + const SemanticTokensEdit *e2 = *((SemanticTokensEdit **) b); + + return e2->start - e1->start; +} + + +static gboolean process_delta_result(GeanyDocument *doc, GVariant *result, guint64 token_mask) +{ + GVariantIter *iter = NULL; + const gchar *result_id = NULL; + CachedData *data = NULL; + gboolean ret = FALSE; + + JSONRPC_MESSAGE_PARSE(result, + "resultId", JSONRPC_MESSAGE_GET_STRING(&result_id), + "edits", JSONRPC_MESSAGE_GET_ITER(&iter) + ); + + data = plugin_get_document_data(geany_plugin, doc, CACHE_KEY); + + if (data && (!iter || !result_id)) + { + // something got wrong - let's delete our cached result so the next request + // is full instead of delta which may be out of sync + plugin_set_document_data(geany_plugin, doc, CACHE_KEY, NULL); + } + else if (data && iter && result_id) + { + GPtrArray *edits = g_ptr_array_new_full(4, (GDestroyNotify)sem_tokens_edit_free); + SemanticTokensEdit *edit; + GVariant *val = NULL; + guint i; + + while (g_variant_iter_loop(iter, "v", &val)) + { + GVariantIter *iter2 = NULL; + GVariant *val2 = NULL; + gint64 delete_count = 0; + gint64 start = 0; + gboolean success; + + success = JSONRPC_MESSAGE_PARSE(val, + "start", JSONRPC_MESSAGE_GET_INT64(&start), + "deleteCount", JSONRPC_MESSAGE_GET_INT64(&delete_count), + "data", JSONRPC_MESSAGE_GET_ITER(&iter2) + ); + + if (success) + { + edit = sem_tokens_edit_new(); + edit->start = start; + edit->delete_count = delete_count; + + while (g_variant_iter_loop(iter2, "v", &val2)) + { + guint v = g_variant_get_int64(val2); + g_array_append_val(edit->data, v); + } + + g_ptr_array_add(edits, edit); + } + + if (iter2) + g_variant_iter_free(iter2); + } + + g_ptr_array_sort(edits, sort_edits); + + foreach_ptr_array(edit, i, edits) + sem_tokens_edit_apply(data, edit); + + g_free(data->tokens_str); + data->tokens_str = process_tokens(data->tokens, doc, token_mask); + g_free(data->result_id); + data->result_id = g_strdup(result_id); + + g_ptr_array_free(edits, TRUE); + + ret = TRUE; + } + + if (iter) + g_variant_iter_free(iter); + + return ret; +} + + +static void semtokens_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + if (!error) + { + GeanyDocument *doc = user_data; + LspServer *srv; + + srv = DOC_VALID(doc) ? lsp_server_get(doc) : NULL; + + if (srv) + { + gboolean success = TRUE; + GVariantIter *iter = NULL; + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + + JSONRPC_MESSAGE_PARSE(return_value, + "data", JSONRPC_MESSAGE_GET_ITER(&iter) + ); + + if (iter) + { + process_full_result(doc, return_value, srv->semantic_token_mask); + g_variant_iter_free(iter); + } + else + success = process_delta_result(doc, return_value, srv->semantic_token_mask); + + if (success) + highlight_keywords(srv, doc); + } + } +} + + +void lsp_semtokens_send_request(GeanyDocument *doc) +{ + LspServer *server = lsp_server_get(doc); + gchar *doc_uri; + GVariant *node; + CachedData *cached_data; + gboolean delta; + + if (!doc || !server) + return; + + doc_uri = lsp_utils_get_doc_uri(doc); + + /* Geany requests symbols before firing "document-activate" signal so we may + * need to request document opening here */ + if (!lsp_sync_is_document_open(doc)) + lsp_sync_text_document_did_open(server, doc); + + cached_data = plugin_get_document_data(geany_plugin, doc, CACHE_KEY); + delta = cached_data != NULL && cached_data->result_id && + server->config.semantic_tokens_supports_delta && + !server->config.semantic_tokens_force_full; + + if (delta) + { + node = JSONRPC_MESSAGE_NEW( + "previousResultId", JSONRPC_MESSAGE_PUT_STRING(cached_data->result_id), + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}" + ); + lsp_rpc_call(server, "textDocument/semanticTokens/full/delta", node, + semtokens_cb, doc); + } + else + { + node = JSONRPC_MESSAGE_NEW( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}" + ); + lsp_rpc_call(server, "textDocument/semanticTokens/full", node, + semtokens_cb, doc); + } + + g_free(doc_uri); + g_variant_unref(node); +} + + +void lsp_semtokens_clear(GeanyDocument *doc) +{ + if (!doc) + return; + + plugin_set_document_data(geany_plugin, doc, CACHE_KEY, NULL); + keyword_hash = 0; + + if (style_index > 0) + { + sci_indicator_set(doc->editor->sci, style_index); + sci_indicator_clear(doc->editor->sci, 0, sci_get_length(doc->editor->sci)); + } +} diff --git a/lsp/src/lsp-semtokens.h b/lsp/src/lsp-semtokens.h new file mode 100644 index 000000000..df40abd79 --- /dev/null +++ b/lsp/src/lsp-semtokens.h @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_SEMTOKENS_H +#define LSP_SEMTOKENS_H 1 + +#include "lsp-server.h" + +#include + +void lsp_semtokens_send_request(GeanyDocument *doc); +void lsp_semtokens_clear(GeanyDocument *doc); + +void lsp_semtokens_style_init(GeanyDocument *doc); + +void lsp_semtokens_init(gint ft_id); +void lsp_semtokens_destroy(void); + +#endif /* LSP_SEMTOKENS_H */ diff --git a/lsp/src/lsp-server.c b/lsp/src/lsp-server.c new file mode 100644 index 000000000..4e02397a8 --- /dev/null +++ b/lsp/src/lsp-server.c @@ -0,0 +1,1391 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-server.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" +#include "lsp-sync.h" +#include "lsp-diagnostics.h" +#include "lsp-log.h" +#include "lsp-semtokens.h" +#include "lsp-progress.h" +#include "lsp-symbols.h" +#include "lsp-symbol-kinds.h" +#include "lsp-highlight.h" +#include "lsp-workspace-folders.h" + +#include + + +static void start_lsp_server(LspServer *server); +static LspServer *lsp_server_init(gint ft); + + +extern GeanyData *geany_data; +extern GeanyPlugin *geany_plugin; +extern LspProjectConfigurationType project_configuration_type; + +static GPtrArray *lsp_servers = NULL; +static GPtrArray *servers_in_shutdown = NULL; + +static LspServerInitializedCallback lsp_server_initialized_cb; + + +static void free_config(LspServerConfig *cfg) +{ + g_free(cfg->cmd); + g_strfreev(cfg->env); + g_free(cfg->ref_lang); + g_strfreev(cfg->autocomplete_trigger_sequences); + g_strfreev(cfg->semantic_tokens_types); + g_free(cfg->command_on_save_regex); + g_free(cfg->semantic_tokens_type_style); + g_free(cfg->autocomplete_hide_after_words); + g_free(cfg->diagnostics_disable_for); + g_free(cfg->diagnostics_error_style); + g_free(cfg->diagnostics_warning_style); + g_free(cfg->diagnostics_info_style); + g_free(cfg->diagnostics_hint_style); + g_free(cfg->highlighting_style); + g_free(cfg->code_lens_style); + g_free(cfg->formatting_options_file); + g_free(cfg->formatting_options); + g_free(cfg->initialization_options_file); + g_free(cfg->initialization_options); + g_free(cfg->word_chars); + g_free(cfg->document_symbols_tab_label); + g_free(cfg->rpc_log); + g_strfreev(cfg->lang_id_mappings); + g_ptr_array_free(cfg->command_regexes, TRUE); + g_strfreev(cfg->project_root_marker_patterns); +} + + +static void free_server(LspServer *s) +{ + if (s->process) + { + g_object_unref(s->process); + lsp_rpc_destroy(s->rpc); + g_object_unref(s->stream); + lsp_log_stop(s->log); + } + + g_free(s->autocomplete_trigger_chars); + g_free(s->signature_trigger_chars); + g_free(s->initialize_response); + lsp_progress_free_all(s); + + free_config(&s->config); + + g_free(s); +} + + +static gboolean free_server_after_delay(gpointer user_data) +{ + free_server((LspServer *)user_data); + + return G_SOURCE_REMOVE; +} + + +static gboolean is_dead(LspServer *server) +{ + return server->restarts >= 10; +} + + +static void process_stopped(GObject *source_object, GAsyncResult *res, gpointer data) +{ + LspServer *s = data; + + // normal shutdown + if (g_ptr_array_find(servers_in_shutdown, s, NULL)) + { + msgwin_status_add(_("LSP server %s stopped"), s->config.cmd); + g_ptr_array_remove_fast(servers_in_shutdown, s); + } + else // crash + { + gint restarts = s->restarts; + gint ft = s->filetype; + + msgwin_status_add(_("LSP server %s stopped unexpectedly, restarting"), s->config.cmd); + + // it seems that calls/notifications get delivered to the plugin + // from the server even after the process is stopped in unnormal + // conditions like server crash and if we free the server immediately, + // the RPC call gets invalid server. Wait for a while until such + // calls get performed + plugin_timeout_add(geany_plugin, 300, free_server_after_delay, s); + + if (lsp_servers) // NULL on plugin unload + { + s = lsp_server_init(ft); + s->restarts = restarts + 1; + lsp_servers->pdata[ft] = s; + if (is_dead(s)) + msgwin_status_add(_("LSP server %s terminated %d times, giving up"), s->config.cmd, s->restarts); + else + start_lsp_server(s); + } + } +} + + +static gboolean kill_cb(gpointer user_data) +{ + LspServer *srv = user_data; + + if (g_ptr_array_find(servers_in_shutdown, srv, NULL)) + { + msgwin_status_add(_("Killing LSP server %s"), srv->config.cmd); + g_subprocess_force_exit(srv->process); + } + + return G_SOURCE_REMOVE; +} + + +static void shutdown_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + LspServer *srv = user_data; + + if (!g_ptr_array_find(servers_in_shutdown, srv, NULL)) + return; + + if (!error) + { + msgwin_status_add(_("Sending exit notification to LSP server %s"), srv->config.cmd); + lsp_rpc_notify(srv, "exit", NULL, NULL, srv); + } + else + { +#ifndef G_OS_WIN32 + msgwin_status_add(_("Force terminating LSP server %s"), srv->config.cmd); + g_subprocess_send_signal(srv->process, SIGTERM); +#endif + } + + plugin_timeout_add(geany_plugin, 2000, kill_cb, srv); +} + + +static void stop_process(LspServer *s) +{ + if (g_ptr_array_find(servers_in_shutdown, s, NULL)) + return; + + s->startup_shutdown = TRUE; + g_ptr_array_add(servers_in_shutdown, s); + + if (lsp_servers) // NULL on plugin unload + lsp_servers->pdata[s->filetype] = lsp_server_init(s->filetype); + + msgwin_status_add(_("Sending shutdown request to LSP server %s"), s->config.cmd); + lsp_rpc_call_startup_shutdown(s, "shutdown", NULL, shutdown_cb, s); + + // should not be performed if server behaves correctly + plugin_timeout_add(geany_plugin, 5000, kill_cb, s); +} + + +static void stop_and_free_server(LspServer *s) +{ + if (s->process) + stop_process(s); + else + free_server(s); +} + + +static gchar *get_autocomplete_trigger_chars(GVariant *node) +{ + GVariantIter *iter = NULL; + GString *str = g_string_new(""); + + JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "completionProvider", "{", + "triggerCharacters", JSONRPC_MESSAGE_GET_ITER(&iter), + "}", + "}"); + + if (iter) + { + GVariant *val = NULL; + while (g_variant_iter_loop(iter, "v", &val)) + g_string_append(str, g_variant_get_string(val, NULL)); + g_variant_iter_free(iter); + } + + return g_string_free(str, FALSE); +} + + +static gboolean supports_full_semantic_tokens(GVariant *node) +{ + gboolean val = FALSE; + + JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "semanticTokensProvider", "{", + "full", JSONRPC_MESSAGE_GET_BOOLEAN(&val), + "}", + "}"); + + return val; +} + + +static gboolean supports_delta_semantic_tokens(GVariant *node) +{ + gboolean val = FALSE; + + JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "semanticTokensProvider", "{", + "full", "{", + "delta", JSONRPC_MESSAGE_GET_BOOLEAN(&val), + "}", + "}", + "}"); + + return val; +} + + +static guint64 get_semantic_token_mask(LspServer *srv, GVariant *node) +{ + guint64 mask = 0; + guint64 index = 1; + GVariantIter *iter = NULL; + + JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "semanticTokensProvider", "{", + "legend", "{", + "tokenTypes", JSONRPC_MESSAGE_GET_ITER(&iter), + "}", + "}", + "}"); + + if (iter && srv->config.semantic_tokens_types) + { + GVariant *val = NULL; + while (g_variant_iter_loop(iter, "v", &val)) + { + const gchar *str = g_variant_get_string(val, NULL); + gchar **token_ptr; + + foreach_strv(token_ptr, srv->config.semantic_tokens_types) + { + if (g_strcmp0(str, *token_ptr) == 0) + { + mask |= index; + break; + } + } + + index <<= 1; + } + g_variant_iter_free(iter); + } + + return mask; +} + + +static gchar *get_signature_trigger_chars(GVariant *node) +{ + GVariantIter *iter = NULL; + GString *str = g_string_new(""); + + JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "signatureHelpProvider", "{", + "triggerCharacters", JSONRPC_MESSAGE_GET_ITER(&iter), + "}", + "}"); + + if (iter) + { + GVariant *val = NULL; + while (g_variant_iter_loop(iter, "v", &val)) + g_string_append(str, g_variant_get_string(val, NULL)); + g_variant_iter_free(iter); + } + + return g_string_free(str, FALSE); +} + + +static gboolean use_incremental_sync(GVariant *node) +{ + gint64 val; + + gboolean success = JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "textDocumentSync", "{", + "change", JSONRPC_MESSAGE_GET_INT64(&val), + "}", + "}"); + + if (!success) + { + success = JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "textDocumentSync", JSONRPC_MESSAGE_GET_INT64(&val), + "}"); + } + + // not supporting "0", i.e. no sync - not sure if any server uses it and how + // Geany could work with it + return success && val == 2; +} + + +static gboolean send_did_save(GVariant *node) +{ + gboolean val; + gboolean success = JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "textDocumentSync", "{", + "save", JSONRPC_MESSAGE_GET_BOOLEAN(&val), + "}", + "}"); + + if (!success) + { + GVariant *var = NULL; + + JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "textDocumentSync", "{", + "save", JSONRPC_MESSAGE_GET_VARIANT(&var), + "}", + "}"); + + success = val = var != NULL; + if (var) + g_variant_unref(var); + } + + return success && val; +} + + +static gboolean include_text_on_save(GVariant *node) +{ + gboolean val; + gboolean success = JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "textDocumentSync", "{", + "save", "{", + "includeText", JSONRPC_MESSAGE_GET_BOOLEAN(&val), + "}", + "}", + "}"); + + return success && val; +} + + +static gboolean use_workspace_folders(GVariant *node) +{ + gboolean change_notifications = FALSE; + const gchar *notif_id = NULL; + gboolean supported = FALSE; + gboolean success; + + JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "workspace", "{", + "workspaceFolders", "{", + "supported", JSONRPC_MESSAGE_GET_BOOLEAN(&supported), + "}", + "}", + "}"); + + if (!supported) + return FALSE; + + success = JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "workspace", "{", + "workspaceFolders", "{", + "changeNotifications", JSONRPC_MESSAGE_GET_BOOLEAN(&change_notifications), + "}", + "}", + "}"); + + if (!success) // can also be string + { + JSONRPC_MESSAGE_PARSE(node, + "capabilities", "{", + "workspace", "{", + "workspaceFolders", "{", + "changeNotifications", JSONRPC_MESSAGE_GET_STRING(¬if_id), + "}", + "}", + "}"); + } + + return change_notifications || notif_id; +} + + +static void update_config(GVariant *variant, gboolean *option, const gchar *key) +{ + gboolean val = FALSE; + gboolean success = JSONRPC_MESSAGE_PARSE(variant, + "capabilities", "{", + key, JSONRPC_MESSAGE_GET_BOOLEAN(&val), + "}"); + + if (success) // explicit TRUE, FALSE + { + if (!val) + *option = FALSE; + } + else // dict (possibly just empty) which also indicates TRUE + { + GVariant *var = NULL; + + JSONRPC_MESSAGE_PARSE(variant, + "capabilities", "{", + key, JSONRPC_MESSAGE_GET_VARIANT(&var), + "}"); + + if (var) + g_variant_unref(var); + else + *option = FALSE; + } +} + + +static void send_did_change_configuration(LspServer *srv) +{ + JsonNode *settings = lsp_utils_parse_json_file(srv->config.initialization_options_file, + srv->config.initialization_options); + GVariant *res = g_variant_take_ref(json_gvariant_deserialize(settings, NULL, NULL)); + GVariant *msg = JSONRPC_MESSAGE_NEW( "settings", "{", + JSONRPC_MESSAGE_PUT_VARIANT(res), + "}"); + + lsp_rpc_notify(srv, "workspace/didChangeConfiguration", msg, NULL, NULL); + + json_node_free(settings); + g_variant_unref(res); + g_variant_unref(msg); +} + + +static void initialize_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + LspServer *s = user_data; + + if (!error) + { + g_free(s->autocomplete_trigger_chars); + s->autocomplete_trigger_chars = get_autocomplete_trigger_chars(return_value); + + g_free(s->signature_trigger_chars); + s->signature_trigger_chars = get_signature_trigger_chars(return_value); + if (EMPTY(s->signature_trigger_chars)) + s->config.signature_enable = FALSE; + + update_config(return_value, &s->config.hover_enable, "hoverProvider"); + update_config(return_value, &s->config.hover_available, "hoverProvider"); + update_config(return_value, &s->config.goto_enable, "definitionProvider"); + update_config(return_value, &s->config.document_symbols_enable, "documentSymbolProvider"); + update_config(return_value, &s->config.document_symbols_available, "documentSymbolProvider"); + update_config(return_value, &s->config.highlighting_enable, "documentHighlightProvider"); + update_config(return_value, &s->config.code_lens_enable, "codeLensProvider"); + update_config(return_value, &s->config.goto_declaration_enable, "declarationProvider"); + update_config(return_value, &s->config.goto_definition_enable, "definitionProvider"); + update_config(return_value, &s->config.goto_implementation_enable, "implementationProvider"); + update_config(return_value, &s->config.goto_references_enable, "referencesProvider"); + update_config(return_value, &s->config.goto_type_definition_enable, "typeDefinitionProvider"); + update_config(return_value, &s->config.document_formatting_enable, "documentFormattingProvider"); + update_config(return_value, &s->config.range_formatting_enable, "documentRangeFormattingProvider"); + update_config(return_value, &s->config.execute_command_enable, "executeCommandProvider"); + update_config(return_value, &s->config.code_action_enable, "codeActionProvider"); + update_config(return_value, &s->config.rename_enable, "renameProvider"); + + s->supports_workspace_symbols = TRUE; + update_config(return_value, &s->supports_workspace_symbols, "workspaceSymbolProvider"); + + s->use_incremental_sync = use_incremental_sync(return_value); + s->send_did_save = send_did_save(return_value); + s->include_text_on_save = include_text_on_save(return_value); + s->use_workspace_folders = use_workspace_folders(return_value); + + s->initialize_response = lsp_utils_json_pretty_print(return_value); + + s->config.semantic_tokens_supports_delta = supports_delta_semantic_tokens(return_value); + if (!supports_full_semantic_tokens(return_value) && !s->config.semantic_tokens_supports_delta) + s->config.semantic_tokens_enable = FALSE; + s->semantic_token_mask = get_semantic_token_mask(s, return_value); + + msgwin_status_add(_("LSP server %s initialized"), s->config.cmd); + + lsp_rpc_notify(s, "initialized", NULL, NULL, NULL); + s->startup_shutdown = FALSE; + + lsp_semtokens_init(s->filetype); + + // Duplicate request to add project root to workspace folders - this + // was already done in the initialize request but the pyright server + // requires adding them dynamically - hopefully alright for other servers + // too + lsp_workspace_folders_add_project_root(s); + + // e.g. vscode-json-languageserver requires this instead of static + // configuration during initialize + if (s->config.send_did_change_configuration) + send_did_change_configuration(s); + + if (lsp_server_initialized_cb) + lsp_server_initialized_cb(s); + } + else + { + msgwin_status_add(_("LSP initialize request failed for LSP server, killing %s"), s->config.cmd); + + // force exit the server - since the handshake didn't perform, the + // server may be in some strange state and normal "exit" may not work + // (happens with haskell server) + g_subprocess_force_exit(s->process); + } +} + + +static void perform_initialize(LspServer *server) +{ + gchar *project_base = lsp_utils_get_project_base_path(); + GVariant *workspace_folders = NULL; + GVariant *node, *capabilities, *info; + gchar *project_base_uri = NULL; + GVariantDict dct; + + if (project_base) + project_base_uri = g_filename_to_uri(project_base, NULL, NULL); + + capabilities = JSONRPC_MESSAGE_NEW( + "window", "{", + "workDoneProgress", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "showDocument", "{", + "support", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "}", + "}", + "textDocument", "{", + "synchronization", "{", + "didSave", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "}", + "completion", "{", + "completionItem", "{", + "snippetSupport", JSONRPC_MESSAGE_PUT_BOOLEAN(server->config.autocomplete_use_snippets), + "documentationFormat", "[", + "plaintext", + "]", + "}", + "completionItemKind", "{", + "valueSet", "[", + LSP_COMPLETION_KINDS, + "]", + "}", + "contxtSupport", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "}", + "hover", "{", + "contentFormat", "[", + "plaintext", + "]", + "}", + "documentSymbol", "{", + "symbolKind", "{", + "valueSet", "[", + LSP_SYMBOL_KINDS, + "]", + "}", + "hierarchicalDocumentSymbolSupport", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "}", + "semanticTokens", "{", + "requests", "{", + "full", "{", + "delta", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "}", + "}", + "tokenTypes", "[", + // specify all possible token types - gopls returns incorrect offsets without it + // TODO: investigate more and possibly report upstream + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator", + "]", + "tokenModifiers", "[", + "]", + "formats", "[", + "relative", + "]", + "augmentsSyntaxTokens", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "}", + "}", + "workspace", "{", + "applyEdit", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "symbol", "{", + "symbolKind", "{", + "valueSet", "[", + LSP_SYMBOL_KINDS, + "]", + "}", + "}", + "workspaceFolders", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + // possibly enable in the future - we have support for this + //"configuration", JSONRPC_MESSAGE_PUT_BOOLEAN(TRUE), + "}" + ); + + info = JSONRPC_MESSAGE_NEW( + "clientInfo", "{", + "name", JSONRPC_MESSAGE_PUT_STRING("Geany LSP Client Plugin"), + "version", JSONRPC_MESSAGE_PUT_STRING(VERSION), + "}", + "processId", JSONRPC_MESSAGE_PUT_INT64(getpid()), + "locale", JSONRPC_MESSAGE_PUT_STRING("en"), + "trace", JSONRPC_MESSAGE_PUT_STRING("off"), + "rootPath", JSONRPC_MESSAGE_PUT_STRING(project_base), + "rootUri", JSONRPC_MESSAGE_PUT_STRING(project_base_uri) + ); + + if (project_base) + { + workspace_folders = JSONRPC_MESSAGE_NEW_ARRAY( + "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(project_base_uri), + "name", JSONRPC_MESSAGE_PUT_STRING(project_base), + "}"); + } + + g_variant_dict_init(&dct, info); + + if (workspace_folders) + g_variant_dict_insert_value(&dct, "workspaceFolders", workspace_folders); + g_variant_dict_insert_value(&dct, "initializationOptions", + lsp_utils_parse_json_file_as_variant(server->config.initialization_options_file, server->config.initialization_options)); + g_variant_dict_insert_value(&dct, "capabilities", capabilities); + + node = g_variant_take_ref(g_variant_dict_end(&dct)); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + msgwin_status_add(_("Sending initialize request to LSP server %s"), server->config.cmd); + + server->startup_shutdown = TRUE; + lsp_rpc_call_startup_shutdown(server, "initialize", node, initialize_cb, server); + + g_free(project_base); + g_free(project_base_uri); + g_variant_unref(node); + g_variant_unref(info); + g_variant_unref(capabilities); + if (workspace_folders) + g_variant_unref(workspace_folders); +} + + +static GKeyFile *read_keyfile(const gchar *config_file) +{ + GError *error = NULL; + GKeyFile *kf = g_key_file_new(); + + if (!g_key_file_load_from_file(kf, config_file, G_KEY_FILE_NONE, &error)) + { + msgwin_status_add(_("Failed to load LSP configuration file with message %s"), error->message); + g_error_free(error); + } + + return kf; +} + + +static void start_lsp_server(LspServer *server) +{ + GInputStream *input_stream; + GOutputStream *output_stream; + GError *error = NULL; + gchar **argv, **env; + gint flags = G_SUBPROCESS_FLAGS_STDIN_PIPE | G_SUBPROCESS_FLAGS_STDOUT_PIPE; + GString *cmd; + GSubprocessLauncher *launcher; + + cmd = g_string_new(server->config.cmd); + while (utils_string_replace_all(cmd, " ", " ") > 0) + ; + if (g_str_has_prefix(cmd->str, "~/")) + utils_string_replace_first(cmd, "~", g_get_home_dir()); + argv = g_strsplit_set(cmd->str, " ", -1); + g_string_free(cmd, TRUE); + + if (!server->config.show_server_stderr) + flags |= G_SUBPROCESS_FLAGS_STDERR_SILENCE; + launcher = g_subprocess_launcher_new(flags); + + foreach_strv(env, server->config.env) + { + gchar **kv = g_strsplit_set(*env, "=", 2); + if (kv && kv[0] && kv[1]) + { + g_subprocess_launcher_setenv(launcher, kv[0], kv[1], TRUE); + } + g_strfreev(kv); + } + + msgwin_status_add(_("Starting LSP server %s"), server->config.cmd); + + server->process = g_subprocess_launcher_spawnv(launcher, (const gchar * const *)argv, &error); + + g_strfreev(argv); + g_object_unref(launcher); + + if (!server->process) + { + msgwin_status_add(_("LSP server process %s failed to start, giving up: %s"), server->config.cmd, error->message); + server->restarts = 100; // don't retry - probably missing executable + g_error_free(error); + return; + } + + g_subprocess_wait_async(server->process, NULL, process_stopped, server); + + input_stream = g_subprocess_get_stdout_pipe(server->process); + output_stream = g_subprocess_get_stdin_pipe(server->process); + server->stream = g_simple_io_stream_new(input_stream, output_stream); + + server->log = lsp_log_start(&server->config); + server->rpc = lsp_rpc_new(server, server->stream); + + perform_initialize(server); +} + + +static void get_bool(gboolean *dest, GKeyFile *kf, const gchar *section, const gchar *key) +{ + GError *error = NULL; + gboolean bool_val = g_key_file_get_boolean(kf, section, key, &error); + + if (!error) + *dest = bool_val; + else + g_error_free(error); +} + + +static void get_str(gchar **dest, GKeyFile *kf, const gchar *section, const gchar *key) +{ + gchar *str_val = g_key_file_get_string(kf, section, key, NULL); + + if (str_val) + { + g_strstrip(str_val); + g_free(*dest); + *dest = str_val; + } +} + + +static void get_strv(gchar ***dest, GKeyFile *kf, const gchar *section, const gchar *key) +{ + gchar **strv_val = g_key_file_get_string_list(kf, section, key, NULL, NULL); + + if (strv_val) + { + g_strfreev(*dest); + *dest = strv_val; + } +} + + +static void get_int(gint *dest, GKeyFile *kf, const gchar *section, const gchar *key) +{ + GError *error = NULL; + gint int_val = g_key_file_get_integer(kf, section, key, &error); + + if (!error) + *dest = int_val; + else + g_error_free(error); +} + + +static void load_config(GKeyFile *kf, const gchar *section, LspServer *s) +{ + gint i; + + get_bool(&s->config.use_outside_project_dir, kf, section, "use_outside_project_dir"); + get_bool(&s->config.use_without_project, kf, section, "use_without_project"); + get_bool(&s->config.rpc_log_full, kf, section, "rpc_log_full"); + get_str(&s->config.word_chars, kf, section, "extra_identifier_characters"); + get_bool(&s->config.send_did_change_configuration, kf, section, "send_did_change_configuration"); + + get_bool(&s->config.autocomplete_enable, kf, section, "autocomplete_enable"); + + get_strv(&s->config.autocomplete_trigger_sequences, kf, section, "autocomplete_trigger_sequences"); + get_strv(&s->config.project_root_marker_patterns, kf, section, "project_root_marker_patterns"); + + get_int(&s->config.autocomplete_window_max_entries, kf, section, "autocomplete_window_max_entries"); + get_int(&s->config.autocomplete_window_max_displayed, kf, section, "autocomplete_window_max_displayed"); + get_int(&s->config.autocomplete_window_max_width, kf, section, "autocomplete_window_max_width"); + get_str(&s->config.autocomplete_hide_after_words, kf, section, "autocomplete_hide_after_words"); + + get_bool(&s->config.autocomplete_use_label, kf, section, "autocomplete_use_label"); + get_bool(&s->config.autocomplete_apply_additional_edits, kf, section, "autocomplete_apply_additional_edits"); + get_bool(&s->config.diagnostics_enable, kf, section, "diagnostics_enable"); + get_bool(&s->config.autocomplete_use_snippets, kf, section, "autocomplete_use_snippets"); + get_int(&s->config.diagnostics_statusbar_severity, kf, section, "diagnostics_statusbar_severity"); + get_str(&s->config.diagnostics_disable_for, kf, section, "diagnostics_disable_for"); + + get_str(&s->config.diagnostics_error_style, kf, section, "diagnostics_error_style"); + get_str(&s->config.diagnostics_warning_style, kf, section, "diagnostics_warning_style"); + get_str(&s->config.diagnostics_info_style, kf, section, "diagnostics_info_style"); + get_str(&s->config.diagnostics_hint_style, kf, section, "diagnostics_hint_style"); + + get_bool(&s->config.hover_enable, kf, section, "hover_enable"); + get_int(&s->config.hover_popup_max_lines, kf, section, "hover_popup_max_lines"); + get_int(&s->config.hover_popup_max_paragraphs, kf, section, "hover_popup_max_paragraphs"); + get_bool(&s->config.signature_enable, kf, section, "signature_enable"); + get_bool(&s->config.goto_enable, kf, section, "goto_enable"); + get_bool(&s->config.show_server_stderr, kf, section, "show_server_stderr"); + + get_bool(&s->config.document_symbols_enable, kf, section, "document_symbols_enable"); + + get_bool(&s->config.semantic_tokens_enable, kf, section, "semantic_tokens_enable"); + get_bool(&s->config.semantic_tokens_force_full, kf, section, "semantic_tokens_force_full"); + get_strv(&s->config.semantic_tokens_types, kf, section, "semantic_tokens_types"); + get_int(&s->config.semantic_tokens_lexer_kw_index, kf, section, "semantic_tokens_lexer_kw_index"); + get_str(&s->config.semantic_tokens_type_style, kf, section, "semantic_tokens_type_style"); + + get_bool(&s->config.highlighting_enable, kf, section, "highlighting_enable"); + get_str(&s->config.highlighting_style, kf, section, "highlighting_style"); + + get_bool(&s->config.code_lens_enable, kf, section, "code_lens_enable"); + get_str(&s->config.code_lens_style, kf, section, "code_lens_style"); + + get_str(&s->config.formatting_options_file, kf, section, "formatting_options_file"); + get_str(&s->config.formatting_options, kf, section, "formatting_options"); + + get_bool(&s->config.format_on_save, kf, section, "format_on_save"); + get_str(&s->config.command_on_save_regex, kf, section, "command_on_save_regex"); + + get_bool(&s->config.progress_bar_enable, kf, section, "progress_bar_enable"); + + // create for the first time, then just update + if (!s->config.command_regexes) + s->config.command_regexes = g_ptr_array_new_full(s->config.command_keybinding_num, g_free); + + g_ptr_array_set_size(s->config.command_regexes, s->config.command_keybinding_num); + + for (i = 0; i < s->config.command_keybinding_num; i++) + { + gchar *key = g_strdup_printf("command_%d_regex", i + 1); + + get_str((gchar **)&s->config.command_regexes->pdata[i], kf, section, key); + if (!s->config.command_regexes->pdata[i]) + s->config.command_regexes->pdata[i] = g_strdup(""); + + g_free(key); + } + + s->config.goto_declaration_enable = TRUE; + s->config.goto_definition_enable = TRUE; + s->config.goto_implementation_enable = TRUE; + s->config.goto_references_enable = TRUE; + s->config.goto_type_definition_enable = TRUE; + s->config.document_formatting_enable = TRUE; + s->config.range_formatting_enable = TRUE; + s->config.execute_command_enable = TRUE; + s->config.code_action_enable = TRUE; + s->config.rename_enable = TRUE; + + s->config.hover_available = TRUE; + s->config.document_symbols_available = TRUE; +} + + +static void load_all_section_only_config(GKeyFile *kf, const gchar *section, LspServer *s) +{ + get_bool(&s->config.enable_by_default, kf, section, "enable_by_default"); + + get_int(&s->config.command_keybinding_num, kf, section, "command_keybinding_num"); + s->config.command_keybinding_num = CLAMP(s->config.command_keybinding_num, 1, 1000); + + get_str(&s->config.document_symbols_tab_label, kf, section, "document_symbols_tab_label"); +} + + +static void load_filetype_only_config(GKeyFile *kf, const gchar *section, LspServer *s) +{ + get_str(&s->config.cmd, kf, section, "cmd"); + get_strv(&s->config.env, kf, section, "env"); + get_str(&s->config.ref_lang, kf, section, "use"); + get_str(&s->config.rpc_log, kf, section, "rpc_log"); + get_str(&s->config.initialization_options_file, kf, section, "initialization_options_file"); + get_str(&s->config.initialization_options, kf, section, "initialization_options"); + get_strv(&s->config.lang_id_mappings, kf, section, "lang_id_mappings"); +} + + +static LspServer *server_get_or_start_for_ft(GeanyFiletype *ft, gboolean launch_server) +{ + LspServer *s, *s2 = NULL; + + if (!ft || !lsp_servers || lsp_utils_is_lsp_disabled_for_project()) + return NULL; + + s = lsp_servers->pdata[ft->id]; + if (s->referenced) + s = s->referenced; + + if (s->startup_shutdown) + return NULL; + + if (s->process) + return s; + + if (s->not_used) + return NULL; + + if (is_dead(s)) + return NULL; + + if (!launch_server) + return NULL; + + if (s->config.ref_lang) + { + GeanyFiletype *ref_ft = filetypes_lookup_by_name(s->config.ref_lang); + + if (ref_ft) + { + s2 = g_ptr_array_index(lsp_servers, ref_ft->id); + s->referenced = s2; + if (s2->process) + return s2; + } + } + + if (s2) + s = s2; + + if (s->config.cmd) + g_strstrip(s->config.cmd); + if (EMPTY(s->config.cmd)) + { + g_free(s->config.cmd); + s->config.cmd = NULL; + s->not_used = TRUE; + } + else + { + start_lsp_server(s); + } + + // the server isn't initialized when running for the first time because the async + // handshake with the server hasn't completed yet + return NULL; +} + + +LspServer *lsp_server_get_for_ft(GeanyFiletype *ft) +{ + return server_get_or_start_for_ft(ft, TRUE); +} + + +static gboolean is_lsp_valid_for_doc(LspServerConfig *cfg, GeanyDocument *doc) +{ + gchar *base_path, *real_path, *rel_path; + gboolean inside_project; + + if (!doc || !doc->real_path) + return FALSE; + + if (EMPTY(cfg->cmd)) + return FALSE; + + if (cfg->project_root_marker_patterns) + { + gchar *project_root = lsp_utils_find_project_root(doc, cfg); + if (project_root) + { + g_free(project_root); + return TRUE; + } + g_free(project_root); + } + + if (!cfg->use_without_project && !geany_data->app->project) + return FALSE; + + if (cfg->use_outside_project_dir || !geany_data->app->project) + return TRUE; + + base_path = lsp_utils_get_project_base_path(); + real_path = utils_get_utf8_from_locale(doc->real_path); + rel_path = lsp_utils_get_relative_path(base_path, real_path); + + inside_project = rel_path && !g_str_has_prefix(rel_path, ".."); + + g_free(rel_path); + g_free(real_path); + g_free(base_path); + + return inside_project; +} + + +#if ! GLIB_CHECK_VERSION(2, 70, 0) +#define g_pattern_spec_match_string g_pattern_match_string +#endif + +GeanyFiletype *lsp_server_get_ft(GeanyDocument *doc, gchar **lsp_lang_id) +{ + LspServer *srv; + guint i; + + if (!lsp_servers || (!doc->real_path || doc->file_type->id != GEANY_FILETYPES_NONE)) + { + if (lsp_lang_id) + *lsp_lang_id = lsp_utils_get_lsp_lang_id(doc); + return doc->file_type; + } + + foreach_ptr_array(srv, i, lsp_servers) + { + gint j = 0; + gchar **val; + gchar *lang_id = NULL; + + if (!srv->config.lang_id_mappings || EMPTY(srv->config.cmd)) + continue; + + foreach_strv(val, srv->config.lang_id_mappings) + { + if (j % 2 == 0) + lang_id = *val; + else + { + GPatternSpec *spec = g_pattern_spec_new(*val); + gchar *fname = g_path_get_basename(doc->file_name); + GeanyFiletype *ret = NULL; + + if (g_pattern_spec_match_string(spec, fname)) + ret = filetypes_index(i); + + g_pattern_spec_free(spec); + g_free(fname); + + if (ret) + { + if (lsp_lang_id) + *lsp_lang_id = g_strdup(lang_id); + return ret; + } + } + + j++; + } + } + + if (lsp_lang_id) + *lsp_lang_id = lsp_utils_get_lsp_lang_id(doc); + + return doc->file_type; +} + + +static LspServer *server_get_configured_for_doc(GeanyDocument *doc) +{ + GeanyFiletype *ft; + LspServer *s; + + if (!doc || !lsp_servers || lsp_utils_is_lsp_disabled_for_project()) + return NULL; + + ft = lsp_server_get_ft(doc, NULL); + s = lsp_servers->pdata[ft->id]; + + if (s->config.ref_lang) + { + ft = filetypes_lookup_by_name(s->config.ref_lang); + + if (ft) + s = lsp_servers->pdata[ft->id]; + else + return NULL; + } + + if (!s) + return NULL; + + if (!is_lsp_valid_for_doc(&s->config, doc)) + return NULL; + + return s; +} + + +static LspServer *server_get_for_doc(GeanyDocument *doc, gboolean launch_server) +{ + GeanyFiletype *ft; + + if (server_get_configured_for_doc(doc) == NULL) + return NULL; + + ft = lsp_server_get_ft(doc, NULL); + return server_get_or_start_for_ft(ft, launch_server); +} + + +LspServer *lsp_server_get(GeanyDocument *doc) +{ + return server_get_for_doc(doc, TRUE); +} + + +LspServer *lsp_server_get_if_running(GeanyDocument *doc) +{ + return server_get_for_doc(doc, FALSE); +} + + +LspServerConfig *lsp_server_get_all_section_config(void) +{ + // hack - the assumption is that nobody will ever configure anything for + // ABC so its settings are identical to the [all] section + GeanyFiletype *ft = filetypes_index(GEANY_FILETYPES_ABC); + LspServer *s = lsp_servers->pdata[ft->id]; + + return &s->config; +} + + +gboolean lsp_server_is_usable(GeanyDocument *doc) +{ + LspServer *s = server_get_configured_for_doc(doc); + + if (!s) + return FALSE; + + return !s->not_used && !is_dead(s); +} + + +void lsp_server_stop_all(gboolean wait) +{ + GPtrArray *lsp_servers_tmp = lsp_servers; + + // set to NULL now - inside free functions of g_ptr_array_free() we need + // to check if lsp_servers is NULL in this case + lsp_servers = NULL; + if (lsp_servers_tmp) + g_ptr_array_free(lsp_servers_tmp, TRUE); + + if (wait) + { + GMainContext *main_context = g_main_context_ref_thread_default(); + + // this runs the main loop and blocks - otherwise gio won't return async results + while (servers_in_shutdown->len > 0) + g_main_context_iteration(main_context, TRUE); + + g_main_context_unref(main_context); + } +} + + +static LspServer *lsp_server_new(GKeyFile *kf_global, GKeyFile *kf, GeanyFiletype *ft) +{ + LspServer *s = g_new0(LspServer, 1); + GString *wc = g_string_new(GEANY_WORDCHARS); + guint i, word_chars_len; + + s->filetype = ft->id; + + load_all_section_only_config(kf_global, "all", s); + load_all_section_only_config(kf, "all", s); + + load_config(kf_global, "all", s); + load_config(kf, "all", s); + + load_config(kf_global, ft->name, s); + load_config(kf, ft->name, s); + + load_filetype_only_config(kf_global, ft->name, s); + load_filetype_only_config(kf, ft->name, s); + + word_chars_len = s->config.word_chars ? strlen(s->config.word_chars) : 0; + for (i = 0; i < word_chars_len; i++) + { + gchar c = s->config.word_chars[i]; + if (!strchr(wc->str, c)) + g_string_append_c(wc, c); + } + g_free(s->config.word_chars); + s->config.word_chars = g_string_free(wc, FALSE); + + return s; +} + + +static LspServer *lsp_server_init(gint ft) +{ + GKeyFile *kf_global = read_keyfile(lsp_utils_get_global_config_filename()); + GKeyFile *kf = read_keyfile(lsp_utils_get_config_filename()); + GeanyFiletype *filetype = filetypes_index(ft); + LspServer *s = lsp_server_new(kf_global, kf, filetype); + + g_key_file_free(kf); + g_key_file_free(kf_global); + + return s; +} + + +void lsp_server_init_all(void) +{ + GKeyFile *kf_global = read_keyfile(lsp_utils_get_global_config_filename()); + GKeyFile *kf = read_keyfile(lsp_utils_get_config_filename()); + GeanyFiletype *ft; + guint i; + + if (lsp_servers) + lsp_server_stop_all(FALSE); + + if (!servers_in_shutdown) + servers_in_shutdown = g_ptr_array_new_full(0, (GDestroyNotify)free_server); + + lsp_servers = g_ptr_array_new_full(0, (GDestroyNotify)stop_and_free_server); + + for (i = 0; (ft = filetypes_index(i)); i++) + { + LspServer *s = lsp_server_new(kf_global, kf, ft); + g_ptr_array_add(lsp_servers, s); + } + + g_key_file_free(kf); + g_key_file_free(kf_global); +} + + +gboolean lsp_server_uses_init_file(gchar *path) +{ + guint i; + + if (!lsp_servers) + return FALSE; + + for (i = 0; i < lsp_servers->len; i++) + { + LspServer *s = lsp_servers->pdata[i]; + + if (s->config.initialization_options_file) + { + gboolean found = FALSE; + gchar *p1 = utils_get_real_path(path); + gchar *p2 = utils_get_real_path(s->config.initialization_options_file); + + found = g_strcmp0(p1, p2) == 0; + + g_free(p1); + g_free(p2); + + if (found) + return TRUE; + } + } + + return FALSE; +} + + +gchar *lsp_server_get_initialize_responses(void) +{ + gboolean first = TRUE; + GString *str; + guint i; + + if (!lsp_servers) + return FALSE; + + str = g_string_new("{"); + + for (i = 0; i < lsp_servers->len; i++) + { + LspServer *s = lsp_servers->pdata[i]; + + if (s->config.cmd && s->initialize_response) + { + if (!first) + g_string_append(str, "\n\n\"############################################################\": \"next server\","); + first = FALSE; + g_string_append(str, "\n\n\""); + g_string_append(str, s->config.cmd); + g_string_append(str, "\":\n"); + g_string_append(str, s->initialize_response); + g_string_append_c(str, ','); + } + } + if (g_str_has_suffix(str->str, ",")) + g_string_erase(str, str->len-1, 1); + g_string_append(str, "\n}"); + + return g_string_free(str, FALSE); +} + + +void lsp_server_set_initialized_cb(LspServerInitializedCallback cb) +{ + lsp_server_initialized_cb = cb; +} diff --git a/lsp/src/lsp-server.h b/lsp/src/lsp-server.h new file mode 100644 index 000000000..e6b15e897 --- /dev/null +++ b/lsp/src/lsp-server.h @@ -0,0 +1,175 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_SERVER_H +#define LSP_SERVER_H 1 + +#include + +#include + +typedef void (*LspCallback) (gpointer user_data); + +struct LspRpc; +typedef struct LspRpc LspRpc; + + +typedef struct LspServerConfig +{ + gchar *cmd; + gchar **env; + gchar *ref_lang; + gchar **lang_id_mappings; + + gboolean show_server_stderr; + gchar *rpc_log; + gboolean rpc_log_full; + gboolean send_did_change_configuration; + gchar *initialization_options_file; + gchar *word_chars; + gchar *initialization_options; + gchar **project_root_marker_patterns; + gboolean enable_by_default; + gboolean use_outside_project_dir; + gboolean use_without_project; + + gboolean autocomplete_enable; + gchar **autocomplete_trigger_sequences; + gboolean autocomplete_use_label; + gboolean autocomplete_apply_additional_edits; + gint autocomplete_window_max_entries; + gint autocomplete_window_max_displayed; + gint autocomplete_window_max_width; + gboolean autocomplete_use_snippets; + gchar *autocomplete_hide_after_words; + + gboolean diagnostics_enable; + gint diagnostics_statusbar_severity; + gchar *diagnostics_disable_for; + gchar *diagnostics_error_style; + gchar *diagnostics_warning_style; + gchar *diagnostics_info_style; + gchar *diagnostics_hint_style; + + gchar *formatting_options_file; + gchar *formatting_options; + + gboolean hover_enable; + gboolean hover_available; + gint hover_popup_max_lines; + gint hover_popup_max_paragraphs; + + gboolean signature_enable; + + gboolean goto_enable; + + gboolean document_symbols_enable; + gchar *document_symbols_tab_label; + gboolean document_symbols_available; + + gboolean semantic_tokens_enable; + gboolean semantic_tokens_force_full; + gchar **semantic_tokens_types; + gboolean semantic_tokens_supports_delta; + gint semantic_tokens_lexer_kw_index; + gchar *semantic_tokens_type_style; + + gboolean highlighting_enable; + gchar *highlighting_style; + + gboolean code_lens_enable; + gchar *code_lens_style; + + gboolean goto_declaration_enable; + gboolean goto_definition_enable; + gboolean goto_implementation_enable; + gboolean goto_references_enable; + gboolean goto_type_definition_enable; + + gboolean document_formatting_enable; + gboolean range_formatting_enable; + gboolean format_on_save; + + gboolean progress_bar_enable; + + gboolean execute_command_enable; + gboolean code_action_enable; + gchar *command_on_save_regex; + gint command_keybinding_num; + GPtrArray *command_regexes; + + gboolean rename_enable; +} LspServerConfig; + + +typedef struct +{ + gint type; // 0: use stream, 1: stdout, 2: stderr + gboolean full; + GFileOutputStream *stream; +} LspLogInfo; + + +typedef struct LspServer +{ + LspRpc *rpc; + GSubprocess *process; + GIOStream *stream; + LspLogInfo log; + + struct LspServer *referenced; + gboolean not_used; + gboolean startup_shutdown; + guint restarts; + gint filetype; + + LspServerConfig config; + + GSList *progress_ops; + + gchar *autocomplete_trigger_chars; + gchar *signature_trigger_chars; + gchar *initialize_response; + gboolean use_incremental_sync; + gboolean send_did_save; + gboolean include_text_on_save; + gboolean use_workspace_folders; + gboolean supports_workspace_symbols; + + guint64 semantic_token_mask; +} LspServer; + +typedef void (*LspServerInitializedCallback) (LspServer *srv); + +LspServer *lsp_server_get(GeanyDocument *doc); +LspServer *lsp_server_get_for_ft(GeanyFiletype *ft); +LspServer *lsp_server_get_if_running(GeanyDocument *doc); +LspServerConfig *lsp_server_get_all_section_config(void); +gboolean lsp_server_is_usable(GeanyDocument *doc); +GeanyFiletype *lsp_server_get_ft(GeanyDocument *doc, gchar **lsp_lang_id); + +void lsp_server_stop_all(gboolean wait); +void lsp_server_init_all(void); + +void lsp_server_set_initialized_cb(LspServerInitializedCallback cb); + +gboolean lsp_server_uses_init_file(gchar *path); + +gchar *lsp_server_get_initialize_responses(void); + +#endif /* LSP_SERVER_H */ diff --git a/lsp/src/lsp-signature.c b/lsp/src/lsp-signature.c new file mode 100644 index 000000000..f8e012a79 --- /dev/null +++ b/lsp/src/lsp-signature.c @@ -0,0 +1,218 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-signature.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" + +#include + + +typedef struct { + GeanyDocument *doc; + gint pos; + gboolean force; +} LspSignatureData; + + +static GPtrArray *signatures = NULL; +static gint displayed_signature = 0; +static ScintillaObject *calltip_sci; + + +static void show_signature(ScintillaObject *sci) +{ + gboolean have_arrow = FALSE; + GString *str = g_string_new(NULL); + + if (displayed_signature > 0) + { + g_string_append_c(str, '\001'); /* up arrow */ + have_arrow = TRUE; + } + if (displayed_signature < signatures->len - 1) + { + g_string_append_c(str, '\002'); /* down arrow */ + have_arrow = TRUE; + } + if (have_arrow) + g_string_append_c(str, ' '); + g_string_append(str, signatures->pdata[displayed_signature]); + + lsp_utils_wrap_string(str->str, -1); + calltip_sci = sci; + SSM(sci, SCI_CALLTIPSHOW, sci_get_current_position(sci), (sptr_t) str->str); + + g_string_free(str, TRUE); +} + + +static void signature_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + if (!error) + { + GeanyDocument *current_doc = document_get_current(); + LspSignatureData *data = user_data; + + //printf("%s\n", lsp_utils_json_pretty_print(return_value)); + + if (current_doc == data->doc && + sci_get_current_position(current_doc->editor->sci) < data->pos + 10 && + (data->force || (!data->force && !SSM(current_doc->editor->sci, SCI_AUTOCACTIVE, 0, 0)))) + { + GVariantIter *iter = NULL; + gint64 active = 1; + + JSONRPC_MESSAGE_PARSE(return_value, "signatures", JSONRPC_MESSAGE_GET_ITER(&iter)); + JSONRPC_MESSAGE_PARSE(return_value, "activeSignature", JSONRPC_MESSAGE_GET_INT64(&active)); + + if (signatures) + g_ptr_array_free(signatures, TRUE); + signatures = g_ptr_array_new_full(1, g_free); + + if (iter) + { + GVariant *member = NULL; + + while (g_variant_iter_loop(iter, "v", &member)) + { + const gchar *label = NULL; + + JSONRPC_MESSAGE_PARSE(member, "label", JSONRPC_MESSAGE_GET_STRING(&label)); + + if (label) + g_ptr_array_add(signatures, g_strdup(label)); + } + } + + displayed_signature = CLAMP(active, 1, signatures->len) - 1; + + if (signatures->len == 0) + SSM(current_doc->editor->sci, SCI_CALLTIPCANCEL, 0, 0); + else + show_signature(current_doc->editor->sci); + + if (iter) + g_variant_iter_free(iter); + } + } + + g_free(user_data); +} + + +void lsp_signature_show_prev(void) +{ + GeanyDocument *doc = document_get_current(); + + if (!doc || !signatures) + return; + + if (displayed_signature > 0) + displayed_signature--; + show_signature(doc->editor->sci); +} + + +void lsp_signature_show_next(void) +{ + GeanyDocument *doc = document_get_current(); + + if (!doc || !signatures) + return; + + if (displayed_signature < signatures->len - 1) + displayed_signature++; + show_signature(doc->editor->sci); +} + + +void lsp_signature_send_request(LspServer *server, GeanyDocument *doc, gboolean force) +{ + GVariant *node; + gchar *doc_uri; + LspSignatureData *data; + ScintillaObject *sci = doc->editor->sci; + gint pos = sci_get_current_position(sci); + LspPosition lsp_pos = lsp_utils_scintilla_pos_to_lsp(sci, pos); + gchar c = (pos > 0 && !force) ? sci_get_char_at(sci, SSM(sci, SCI_POSITIONBEFORE, pos, 0)) : '\0'; + const gchar *trigger_chars = server->signature_trigger_chars; + + if (!force && EMPTY(trigger_chars)) + return; + + if ((c == ')' && strchr(trigger_chars, '(') && !strchr(trigger_chars, ')')) || + (c == ']' && strchr(trigger_chars, '[') && !strchr(trigger_chars, ']')) || + (c == '>' && strchr(trigger_chars, '<') && !strchr(trigger_chars, '>')) || + (c == '}' && strchr(trigger_chars, '{') && !strchr(trigger_chars, '}'))) + { + lsp_signature_hide_calltip(doc); + return; + } + + if (!force && !strchr(trigger_chars, c)) + return; + + doc_uri = lsp_utils_get_doc_uri(doc); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}", + "position", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.line), + "character", JSONRPC_MESSAGE_PUT_INT32(lsp_pos.character), + "}" + ); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + data = g_new0(LspSignatureData, 1); + data->doc = doc; + data->pos = pos; + data->force = force; + + lsp_rpc_call(server, "textDocument/signatureHelp", node, + signature_cb, data); + + g_free(doc_uri); + g_variant_unref(node); +} + + +gboolean lsp_signature_showing_calltip(GeanyDocument *doc) +{ + return SSM(doc->editor->sci, SCI_CALLTIPACTIVE, 0, 0) && + calltip_sci == doc->editor->sci && signatures && signatures->len > 0; +} + + +void lsp_signature_hide_calltip(GeanyDocument *doc) +{ + if (calltip_sci == doc->editor->sci && signatures && signatures->len > 0) + { + SSM(doc->editor->sci, SCI_CALLTIPCANCEL, 0, 0); + g_ptr_array_free(signatures, TRUE); + signatures = NULL; + calltip_sci = NULL; + } +} diff --git a/lsp/src/lsp-signature.h b/lsp/src/lsp-signature.h new file mode 100644 index 000000000..fe0f00ded --- /dev/null +++ b/lsp/src/lsp-signature.h @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_SIGNATURE_H +#define LSP_SIGNATURE_H 1 + +#include "lsp-server.h" + +#include + +void lsp_signature_send_request(LspServer *server, GeanyDocument *doc, gboolean force); + +void lsp_signature_show_prev(void); +void lsp_signature_show_next(void); + +void lsp_signature_hide_calltip(GeanyDocument *doc); +gboolean lsp_signature_showing_calltip(GeanyDocument *doc); + +#endif /* LSP_SIGNATURE_H */ diff --git a/lsp/src/lsp-symbol-kinds.c b/lsp/src/lsp-symbol-kinds.c new file mode 100644 index 000000000..33888abaa --- /dev/null +++ b/lsp/src/lsp-symbol-kinds.c @@ -0,0 +1,158 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-symbol-kinds.h" + +#include + + +static TMIcon lsp_symbol_icons[LSP_SYMBOL_KIND_NUM] = { + TM_ICON_NAMESPACE, /* LspKindFile */ + TM_ICON_NAMESPACE, /* LspKindModule */ + TM_ICON_NAMESPACE, /* LspKindNamespace */ + TM_ICON_NAMESPACE, /* LspKindPackage */ + TM_ICON_CLASS, /* LspKindClass */ + TM_ICON_METHOD, /* LspKindMethod */ + TM_ICON_MEMBER, /* LspKindProperty */ + TM_ICON_MEMBER, /* LspKindField */ + TM_ICON_METHOD, /* LspKindConstructor */ + TM_ICON_STRUCT, /* LspKindEnum */ + TM_ICON_CLASS, /* LspKindInterface */ + TM_ICON_METHOD, /* LspKindFunction */ + TM_ICON_VAR, /* LspKindVariable */ + TM_ICON_MACRO, /* LspKindConstant */ + TM_ICON_OTHER, /* LspKindString */ + TM_ICON_OTHER, /* LspKindNumber */ + TM_ICON_OTHER, /* LspKindBoolean */ + TM_ICON_OTHER, /* LspKindArray */ + TM_ICON_OTHER, /* LspKindObject */ + TM_ICON_OTHER, /* LspKindKey */ + TM_ICON_OTHER, /* LspKindNull */ + TM_ICON_MEMBER, /* LspKindEnumMember */ + TM_ICON_STRUCT, /* LspKindStruct */ + TM_ICON_OTHER, /* LspKindEvent */ + TM_ICON_METHOD, /* LspKindOperator */ + TM_ICON_OTHER /* LspKindTypeParameter */ +}; + + +static TMIcon lsp_completion_icons[LSP_COMPLETION_KIND_NUM] = { + TM_ICON_MACRO, // LspKindText - also used for macros by clangd + TM_ICON_METHOD, // LspKindMethod + TM_ICON_METHOD, // LspKindFunction + TM_ICON_METHOD, // LspKindConstructor + TM_ICON_MEMBER, // LspKindField + TM_ICON_VAR, // LspKindVariable + TM_ICON_CLASS, // LspKindClass + TM_ICON_CLASS, // LspKindInterface + TM_ICON_NAMESPACE, // LspKindModule + TM_ICON_MEMBER, // LspKindProperty + TM_ICON_NAMESPACE, // LspKindUnit + TM_ICON_MACRO, // LspKindValue + TM_ICON_STRUCT, // LspKindEnum + TM_ICON_NONE, // LspKindKeyword + TM_ICON_NONE, // LspKindSnippet + TM_ICON_OTHER, // LspKindColor + TM_ICON_OTHER, // LspKindFile + TM_ICON_OTHER, // LspKindReference + TM_ICON_OTHER, // LspKindFolder + TM_ICON_MEMBER, // LspKindEnumMember + TM_ICON_MACRO, // LspKindConstant + TM_ICON_STRUCT, // LspKindStruct + TM_ICON_OTHER, // LspKindEvent + TM_ICON_OTHER, // LspKindOperator + TM_ICON_OTHER // LspKindTypeParameter +}; + + +TMIcon lsp_symbol_kinds_get_completion_icon(LspCompletionKind kind) +{ + if (kind < 1 || kind > LSP_COMPLETION_KIND_NUM) + return TM_ICON_OTHER; + + return lsp_completion_icons[kind - 1]; +} + + +TMIcon lsp_symbol_kinds_get_symbol_icon(LspSymbolKind kind) +{ + if (kind < 1 || kind > LSP_SYMBOL_KIND_NUM) + return TM_ICON_OTHER; + + return lsp_symbol_icons[kind - 1]; +} + + +LspSymbolKind lsp_symbol_kinds_tm_to_lsp(TMTagType type) +{ + switch (type) + { + case tm_tag_undef_t: + return LspSymbolKindVariable; + case tm_tag_class_t: + return LspSymbolKindClass; + case tm_tag_enum_t: + return LspSymbolKindEnum; + case tm_tag_enumerator_t: + return LspSymbolKindEnumMember; + case tm_tag_field_t: + return LspSymbolKindField; + case tm_tag_function_t: + return LspSymbolKindFunction; + case tm_tag_interface_t: + return LspSymbolKindInterface; + case tm_tag_member_t: + return LspSymbolKindProperty; + case tm_tag_method_t: + return LspSymbolKindMethod; + case tm_tag_namespace_t: + return LspSymbolKindNamespace; + case tm_tag_package_t: + return LspSymbolKindPackage; + case tm_tag_prototype_t: + return LspSymbolKindFunction; + case tm_tag_struct_t: + return LspSymbolKindStruct; + case tm_tag_typedef_t: + return LspSymbolKindStruct; + case tm_tag_union_t: + return LspSymbolKindStruct; + case tm_tag_variable_t: + return LspSymbolKindVariable; + case tm_tag_externvar_t: + return LspSymbolKindVariable; + case tm_tag_macro_t: + return LspSymbolKindConstant; + case tm_tag_macro_with_arg_t: + return LspSymbolKindFunction; + case tm_tag_local_var_t: + return LspSymbolKindVariable; + case tm_tag_other_t: + return LspSymbolKindVariable; + case tm_tag_include_t: + return LspSymbolKindPackage; + default: + break; + } + + return LspSymbolKindVariable; +} diff --git a/lsp/src/lsp-symbol-kinds.h b/lsp/src/lsp-symbol-kinds.h new file mode 100644 index 000000000..4c0c5a5c2 --- /dev/null +++ b/lsp/src/lsp-symbol-kinds.h @@ -0,0 +1,151 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_SYMBOL_KINDS_H +#define LSP_SYMBOL_KINDS_H 1 + +#include +#include + + +typedef enum { + LspSymbolKindFile = 1, + LspSymbolKindModule, + LspSymbolKindNamespace, + LspSymbolKindPackage, + LspSymbolKindClass, + LspSymbolKindMethod, + LspSymbolKindProperty, + LspSymbolKindField, + LspSymbolKindConstructor, + LspSymbolKindEnum, + LspSymbolKindInterface, + LspSymbolKindFunction, + LspSymbolKindVariable, + LspSymbolKindConstant, + LspSymbolKindString, + LspSymbolKindNumber, + LspSymbolKindBoolean, + LspSymbolKindArray, + LspSymbolKindObject, + LspSymbolKindKey, + LspSymbolKindNull, + LspSymbolKindEnumMember, + LspSymbolKindStruct, + LspSymbolKindEvent, + LspSymbolKindOperator, + LspSymbolKindTypeParameter, // 26 + LSP_SYMBOL_KIND_NUM = LspSymbolKindTypeParameter +} LspSymbolKind; + + +// keep in sync with LspSymbolKind above +#define LSP_SYMBOL_KINDS \ + JSONRPC_MESSAGE_PUT_INT32(1),\ + JSONRPC_MESSAGE_PUT_INT32(2),\ + JSONRPC_MESSAGE_PUT_INT32(3),\ + JSONRPC_MESSAGE_PUT_INT32(4),\ + JSONRPC_MESSAGE_PUT_INT32(5),\ + JSONRPC_MESSAGE_PUT_INT32(6),\ + JSONRPC_MESSAGE_PUT_INT32(7),\ + JSONRPC_MESSAGE_PUT_INT32(8),\ + JSONRPC_MESSAGE_PUT_INT32(9),\ + JSONRPC_MESSAGE_PUT_INT32(10),\ + JSONRPC_MESSAGE_PUT_INT32(11),\ + JSONRPC_MESSAGE_PUT_INT32(12),\ + JSONRPC_MESSAGE_PUT_INT32(13),\ + JSONRPC_MESSAGE_PUT_INT32(14),\ + JSONRPC_MESSAGE_PUT_INT32(15),\ + JSONRPC_MESSAGE_PUT_INT32(16),\ + JSONRPC_MESSAGE_PUT_INT32(17),\ + JSONRPC_MESSAGE_PUT_INT32(18),\ + JSONRPC_MESSAGE_PUT_INT32(19),\ + JSONRPC_MESSAGE_PUT_INT32(20),\ + JSONRPC_MESSAGE_PUT_INT32(21),\ + JSONRPC_MESSAGE_PUT_INT32(22),\ + JSONRPC_MESSAGE_PUT_INT32(23),\ + JSONRPC_MESSAGE_PUT_INT32(24),\ + JSONRPC_MESSAGE_PUT_INT32(25),\ + JSONRPC_MESSAGE_PUT_INT32(26) + + +typedef enum { + LspCompletionKindText = 1, + LspCompletionKindMethod, + LspCompletionKindFunction, + LspCompletionKindConstructor, + LspCompletionKindField, + LspCompletionKindVariable, + LspCompletionKindClass, + LspCompletionKindInterface, + LspCompletionKindModule, + LspCompletionKindProperty, + LspCompletionKindUnit, + LspCompletionKindValue, + LspCompletionKindEnum, + LspCompletionKindKeyword, + LspCompletionKindSnippet, + LspCompletionKindColor, + LspCompletionKindFile, + LspCompletionKindReference, + LspCompletionKindFolder, + LspCompletionKindEnumMember, + LspCompletionKindConstant, + LspCompletionKindStruct, + LspCompletionKindEvent, + LspCompletionKindOperator, + LspCompletionKindTypeParameter, // 25 + LSP_COMPLETION_KIND_NUM = LspCompletionKindTypeParameter +} LspCompletionKind; + + +// keep in sync with LspCompletionKind above +#define LSP_COMPLETION_KINDS \ + JSONRPC_MESSAGE_PUT_INT32(1),\ + JSONRPC_MESSAGE_PUT_INT32(2),\ + JSONRPC_MESSAGE_PUT_INT32(3),\ + JSONRPC_MESSAGE_PUT_INT32(4),\ + JSONRPC_MESSAGE_PUT_INT32(5),\ + JSONRPC_MESSAGE_PUT_INT32(6),\ + JSONRPC_MESSAGE_PUT_INT32(7),\ + JSONRPC_MESSAGE_PUT_INT32(8),\ + JSONRPC_MESSAGE_PUT_INT32(9),\ + JSONRPC_MESSAGE_PUT_INT32(10),\ + JSONRPC_MESSAGE_PUT_INT32(11),\ + JSONRPC_MESSAGE_PUT_INT32(12),\ + JSONRPC_MESSAGE_PUT_INT32(13),\ + JSONRPC_MESSAGE_PUT_INT32(14),\ + JSONRPC_MESSAGE_PUT_INT32(15),\ + JSONRPC_MESSAGE_PUT_INT32(16),\ + JSONRPC_MESSAGE_PUT_INT32(17),\ + JSONRPC_MESSAGE_PUT_INT32(18),\ + JSONRPC_MESSAGE_PUT_INT32(19),\ + JSONRPC_MESSAGE_PUT_INT32(20),\ + JSONRPC_MESSAGE_PUT_INT32(21),\ + JSONRPC_MESSAGE_PUT_INT32(22),\ + JSONRPC_MESSAGE_PUT_INT32(23),\ + JSONRPC_MESSAGE_PUT_INT32(24),\ + JSONRPC_MESSAGE_PUT_INT32(25) + + +TMIcon lsp_symbol_kinds_get_completion_icon(LspCompletionKind kind); +TMIcon lsp_symbol_kinds_get_symbol_icon(LspSymbolKind kind); + +LspSymbolKind lsp_symbol_kinds_tm_to_lsp(TMTagType type); + +#endif /* LSP_SYMBOL_KINDS_H */ diff --git a/lsp/src/lsp-symbol-tree.c b/lsp/src/lsp-symbol-tree.c new file mode 100644 index 000000000..d23a7a3cf --- /dev/null +++ b/lsp/src/lsp-symbol-tree.c @@ -0,0 +1,1256 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// This file contains mostly stolen code from Geany's symbol tree implementation +// modified to work for LSP + +// for gtk_widget_override_font() and GTK_STOCK_* +#define GDK_DISABLE_DEPRECATION_WARNINGS + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include + +#include "lsp-symbol.h" +#include "lsp-symbols.h" +#include "lsp-symbol-tree.h" +#include "lsp-goto.h" +#include "lsp-utils.h" + +#include +#include +#include +#include +#include + +#include + + +#define SYM_TREE_KEY "lsp_symbol_tree" +#define SYM_STORE_KEY "lsp_symbol_store" +#define SYM_FILTER_KEY "lsp_symbol_filter" + + +enum +{ + SYMBOLS_COLUMN_ICON, + SYMBOLS_COLUMN_NAME, + SYMBOLS_COLUMN_SYMBOL, + SYMBOLS_COLUMN_TOOLTIP, + SYMBOLS_N_COLUMNS +}; + + +typedef struct +{ + gint found_line; /* return: the nearest line found */ + gint line; /* input: the line to look for */ + gboolean lower /* input: search only for lines with lower number than @line */; +} TreeSearchData; + + +extern GeanyData *geany_data; +extern GeanyPlugin *geany_plugin; + +static struct +{ + GtkWidget *expand_all; + GtkWidget *collapse_all; + GtkWidget *find_refs; + GtkWidget *find_impls; + GtkWidget *goto_type; + GtkWidget *goto_decl; +} +s_symbol_menu; + +static GtkWidget *s_sym_view_vbox = NULL; +static GtkWidget *s_search_entry = NULL; +static GtkWidget *s_sym_window; /* scrolled window that holds the symbol list GtkTreeView */ +static GtkWidget *s_default_sym_tree; +static GtkWidget *s_popup_sym_list; + + +/* sort by line, then scope */ +static gint compare_symbol_lines(gconstpointer a, gconstpointer b) +{ + const LspSymbol *sym_a = a; + const LspSymbol *sym_b = b; + gint ret; + + if (a == NULL || b == NULL) + return 0; + + ret = lsp_symbol_get_line(sym_a) - lsp_symbol_get_line(sym_b); + if (ret == 0) + { + if (lsp_symbol_get_scope(sym_a) == NULL) + return -(lsp_symbol_get_scope(sym_a) != lsp_symbol_get_scope(sym_b)); + if (lsp_symbol_get_scope(sym_b) == NULL) + return lsp_symbol_get_scope(sym_a) != lsp_symbol_get_scope(sym_b); + else + return strcmp(lsp_symbol_get_scope(sym_a), lsp_symbol_get_scope(sym_b)); + } + return ret; +} + + +static GList *get_symbol_list(GeanyDocument *doc, GPtrArray *lsp_symbols) +{ + GList *symbols = NULL; + guint i; + gchar **tf_strv; + + g_return_val_if_fail(doc, NULL); + + const gchar *tag_filter = plugin_get_document_data(geany_plugin, doc, SYM_FILTER_KEY); + if (!tag_filter) + tag_filter = ""; + tf_strv = g_strsplit_set(tag_filter, " ", -1); + + for (i = 0; i < lsp_symbols->len; ++i) + { + LspSymbol *sym = lsp_symbols->pdata[i]; + gboolean filtered = FALSE; + gchar **val; + gchar *full_tagname = lsp_symbol_get_symtree_name(sym, TRUE); + gchar *normalized_tagname = g_utf8_normalize(full_tagname, -1, G_NORMALIZE_ALL); + + foreach_strv(val, tf_strv) + { + gchar *normalized_val = g_utf8_normalize(*val, -1, G_NORMALIZE_ALL); + + if (normalized_tagname != NULL && normalized_val != NULL) + { + gchar *case_normalized_tagname = g_utf8_casefold(normalized_tagname, -1); + gchar *case_normalized_val = g_utf8_casefold(normalized_val, -1); + + filtered = strstr(case_normalized_tagname, case_normalized_val) == NULL; + g_free(case_normalized_tagname); + g_free(case_normalized_val); + } + g_free(normalized_val); + + if (filtered) + break; + } + + if (!filtered) + symbols = g_list_prepend(symbols, sym); + + g_free(normalized_tagname); + g_free(full_tagname); + } + symbols = g_list_sort(symbols, compare_symbol_lines); + + g_strfreev(tf_strv); + + return symbols; +} + + +static const gchar *get_parent_name(const LspSymbol *sym) +{ + return !EMPTY(lsp_symbol_get_scope(sym)) ? lsp_symbol_get_scope(sym) : NULL; +} + + +static gboolean symbols_table_equal(gconstpointer v1, gconstpointer v2) +{ + const LspSymbol *s1 = v1; + const LspSymbol *s2 = v2; + + return (lsp_symbol_get_kind(s1) == lsp_symbol_get_kind(s2) && + strcmp(lsp_symbol_get_name(s1), lsp_symbol_get_name(s2)) == 0 && + utils_str_equal(lsp_symbol_get_scope(s1), lsp_symbol_get_scope(s2)) && + /* include arglist in match to support e.g. C++ overloading */ + utils_str_equal(lsp_symbol_get_detail(s1), lsp_symbol_get_detail(s2))); +} + + +/* inspired by g_str_hash() */ +static guint symbols_table_hash(gconstpointer v) +{ + const LspSymbol *sym = v; + const gchar *p; + guint32 h = 5381; + + h = (h << 5) + h + lsp_symbol_get_kind(sym); + for (p = lsp_symbol_get_name(sym); *p != '\0'; p++) + h = (h << 5) + h + *p; + if (lsp_symbol_get_scope(sym)) + { + for (p = lsp_symbol_get_scope(sym); *p != '\0'; p++) + h = (h << 5) + h + *p; + } + /* for e.g. C++ overloading */ + if (lsp_symbol_get_detail(sym)) + { + for (p = lsp_symbol_get_detail(sym); *p != '\0'; p++) + h = (h << 5) + h + *p; + } + + return h; +} + + +/* like gtk_tree_view_expand_to_path() but with an iter */ +static void tree_view_expand_to_iter(GtkTreeView *view, GtkTreeIter *iter) +{ + GtkTreeModel *model = gtk_tree_view_get_model(view); + GtkTreePath *path = gtk_tree_model_get_path(model, iter); + + gtk_tree_view_expand_to_path(view, path); + gtk_tree_path_free(path); +} + + +static gboolean ui_tree_model_iter_any_next(GtkTreeModel *model, GtkTreeIter *iter, gboolean down) +{ + GtkTreeIter guess; + GtkTreeIter copy = *iter; + + /* go down if the item has children */ + if (down && gtk_tree_model_iter_children(model, &guess, iter)) + *iter = guess; + /* or to the next item at the same level */ + else if (gtk_tree_model_iter_next(model, ©)) + *iter = copy; + /* or to the next item at a parent level */ + else if (gtk_tree_model_iter_parent(model, &guess, iter)) + { + copy = guess; + while (TRUE) + { + if (gtk_tree_model_iter_next(model, ©)) + { + *iter = copy; + return TRUE; + } + else if (gtk_tree_model_iter_parent(model, ©, &guess)) + guess = copy; + else + return FALSE; + } + } + else + return FALSE; + + return TRUE; +} + + +/* like gtk_tree_store_remove() but finds the next iter at any level */ +static gboolean tree_store_remove_row(GtkTreeStore *store, GtkTreeIter *iter) +{ + GtkTreeIter parent; + gboolean has_parent; + gboolean cont; + + has_parent = gtk_tree_model_iter_parent(GTK_TREE_MODEL(store), &parent, iter); + cont = gtk_tree_store_remove(store, iter); + /* if there is no next at this level but there is a parent iter, continue from it */ + if (! cont && has_parent) + { + *iter = parent; + cont = ui_tree_model_iter_any_next(GTK_TREE_MODEL(store), iter, FALSE); + } + + return cont; +} + + +static gint tree_search_func(gconstpointer key, gpointer user_data) +{ + TreeSearchData *data = user_data; + gint parent_line = GPOINTER_TO_INT(key); + gboolean new_nearest; + + if (data->found_line == -1) + data->found_line = parent_line; /* initial value */ + + new_nearest = ABS(data->line - parent_line) < ABS(data->line - data->found_line); + + if (parent_line > data->line) + { + if (new_nearest && !data->lower) + data->found_line = parent_line; + return -1; + } + + if (new_nearest) + data->found_line = parent_line; + + if (parent_line < data->line) + return 1; + + return 0; +} + + +static gint tree_cmp(gconstpointer a, gconstpointer b, gpointer user_data) +{ + return GPOINTER_TO_INT(a) - GPOINTER_TO_INT(b); +} + + +static void parents_table_tree_value_free(gpointer data) +{ + g_slice_free(GtkTreeIter, data); +} + + +/* adds a new element in the parent table if its key is known. */ +static void update_parents_table(GHashTable *table, const LspSymbol *sym, const GtkTreeIter *iter) +{ + gchar *name = lsp_symbol_get_name_with_scope(sym); + GTree *tree; + + if (name && g_hash_table_lookup_extended(table, name, NULL, (gpointer *) &tree)) + { + if (!tree) + { + tree = g_tree_new_full(tree_cmp, NULL, NULL, parents_table_tree_value_free); + g_hash_table_insert(table, name, tree); + name = NULL; + } + + g_tree_insert(tree, GINT_TO_POINTER(lsp_symbol_get_line(sym)), g_slice_dup(GtkTreeIter, iter)); + } + + g_free(name); +} + + +static GtkTreeIter *parents_table_lookup(GHashTable *table, const gchar *name, guint line) +{ + GtkTreeIter *parent_search = NULL; + GTree *tree; + + tree = g_hash_table_lookup(table, name); + if (tree) + { + TreeSearchData user_data = {-1, line, TRUE}; + + /* search parent candidates for the one with the nearest + * line number which is lower than the symbol's line number */ + g_tree_search(tree, (GCompareFunc)tree_search_func, &user_data); + parent_search = g_tree_lookup(tree, GINT_TO_POINTER(user_data.found_line)); + } + + return parent_search; +} + + +static void parents_table_value_free(gpointer data) +{ + GTree *tree = data; + if (tree) + g_tree_destroy(tree); +} + + +/* inserts a @data in @table on key @sym. + * previous data is not overwritten if the key is duplicated, but rather the + * two values are kept in a list + * + * table is: GHashTable>>> */ +static void symbols_table_insert(GHashTable *table, LspSymbol *sym, GList *data) +{ + GTree *tree = g_hash_table_lookup(table, sym); + GList *list; + + if (!tree) + { + tree = g_tree_new_full(tree_cmp, NULL, NULL, NULL); + g_hash_table_insert(table, sym, tree); + } + list = g_tree_lookup(tree, GINT_TO_POINTER(lsp_symbol_get_line(sym))); + list = g_list_prepend(list, data); + g_tree_insert(tree, GINT_TO_POINTER(lsp_symbol_get_line(sym)), list); +} + + +/* looks up the entry in @table that best matches @sym. + * if there is more than one candidate, the one that has closest line position to @sym is chosen */ +static GList *symbols_table_lookup(GHashTable *table, LspSymbol *sym) +{ + TreeSearchData user_data = {-1, lsp_symbol_get_line(sym), FALSE}; + GTree *tree = g_hash_table_lookup(table, sym); + + if (tree) + { + GList *list; + + g_tree_search(tree, (GCompareFunc)tree_search_func, &user_data); + list = g_tree_lookup(tree, GINT_TO_POINTER(user_data.found_line)); + /* return the first value in the list - we don't care which of the + * symbols with identical names defined on the same line we get */ + if (list) + return list->data; + } + return NULL; +} + + +/* removes the element at @sym from @table. + * @sym must be the exact pointer used at insertion time */ +static void symbols_table_remove(GHashTable *table, LspSymbol *sym) +{ + GTree *tree = g_hash_table_lookup(table, sym); + if (tree) + { + GList *list = g_tree_lookup(tree, GINT_TO_POINTER(lsp_symbol_get_line(sym))); + if (list) + { + GList *node; + /* should always be the first element as we returned the first one in + * symbols_table_lookup() */ + foreach_list(node, list) + { + if (((GList *) node->data)->data == sym) + break; + } + list = g_list_delete_link(list, node); + if (!list) + g_tree_remove(tree, GINT_TO_POINTER(lsp_symbol_get_line(sym))); + else + g_tree_insert(tree, GINT_TO_POINTER(lsp_symbol_get_line(sym)), list); + } + } +} + + +static gboolean symbols_table_tree_value_free(gpointer key, gpointer value, gpointer data) +{ + GList *list = value; + g_list_free(list); + return FALSE; +} + + +static void symbols_table_value_free(gpointer data) +{ + GTree *tree = data; + if (tree) + { + /* free any leftover elements. note that we can't register a value_free_func when + * creating the tree because we only want to free it when destroying the tree, + * not when inserting a duplicate (we handle this manually) */ + g_tree_foreach(tree, symbols_table_tree_value_free, NULL); + g_tree_destroy(tree); + } +} + + +/* + * Updates the symbol tree for a document with the symbols in *list. + * @param doc a document + * @param symbols a pointer to a GList* holding the symbols to add/update. This + * list may be updated, removing updated elements. + * + * The update is done in two passes: + * 1) walking the current tree, update symbols that still exist and remove the + * obsolescent ones; + * 2) walking the remaining (non updated) symbols, adds them in the list. + * + * For better performances, we use 2 hash tables: + * - one containing all the symbols for lookup in the first pass (actually stores a + * reference in the symbols list for removing it efficiently), avoiding list search + * on each symbol; + * - the other holding "symbol-name":row references for symbols having children, used to + * lookup for a parent in both passes, avoiding tree traversal. + */ +static void update_symbols(GeanyDocument *doc, GList **symbols) +{ + GtkTreeStore *store = plugin_get_document_data(geany_plugin, doc, SYM_STORE_KEY); + GtkWidget *sym_tree = plugin_get_document_data(geany_plugin, doc, SYM_TREE_KEY); + GtkTreeModel *model = GTK_TREE_MODEL(store); + GHashTable *parents_table; + GHashTable *symbols_table; + GtkTreeIter iter; + gboolean cont; + GList *item; + + /* Build hash tables holding symbols and parents */ + /* parent table is GHashTable> + * where symbol_name might be a fully qualified name (with scope) if the language + * parser reports scope properly (see tm_parser_has_full_scope()). */ + parents_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, parents_table_value_free); + /* symbols table is another representation of the @symbols list, + * GHashTable>>> */ + symbols_table = g_hash_table_new_full(symbols_table_hash, symbols_table_equal, + NULL, symbols_table_value_free); + foreach_list(item, *symbols) + { + LspSymbol *symbol = item->data; + const gchar *parent_name; + + symbols_table_insert(symbols_table, symbol, item); + + parent_name = get_parent_name(symbol); + if (parent_name) + g_hash_table_insert(parents_table, g_strdup(parent_name), NULL); + } + + /* First pass, update existing rows or delete them. + * It is OK to delete them since we walk top down so we would remove + * parents before checking for their children, thus never implicitly + * deleting an updated child */ + cont = gtk_tree_model_get_iter_first(model, &iter); + while (cont) + { + LspSymbol *symbol; + + gtk_tree_model_get(model, &iter, SYMBOLS_COLUMN_SYMBOL, &symbol, -1); + if (! symbol) /* most probably a toplevel, skip it */ + cont = ui_tree_model_iter_any_next(model, &iter, TRUE); + else + { + GList *found_item; + + found_item = symbols_table_lookup(symbols_table, symbol); + if (! found_item) /* symbol doesn't exist, remove it */ + cont = tree_store_remove_row(store, &iter); + else /* symbol still exist, update it */ + { + const gchar *parent_name; + LspSymbol *found = found_item->data; + + parent_name = get_parent_name(found); + /* if parent is unknown, ignore it */ + if (parent_name && ! g_hash_table_lookup(parents_table, parent_name)) + parent_name = NULL; + + if (!lsp_symbol_equal(symbol, found)) + { + gchar *name, *tooltip; + + /* only update fields that (can) have changed (name that holds line + * number, tooltip, and the symbol itself) */ + name = lsp_symbol_get_symtree_name(found, parent_name == NULL); + tooltip = lsp_symbol_get_symtree_tooltip(found, doc->encoding); + gtk_tree_store_set(store, &iter, + SYMBOLS_COLUMN_NAME, name, + SYMBOLS_COLUMN_TOOLTIP, tooltip, + SYMBOLS_COLUMN_SYMBOL, found, + -1); + g_free(tooltip); + g_free(name); + } + + update_parents_table(parents_table, found, &iter); + + /* remove the updated symbol from the table and list */ + symbols_table_remove(symbols_table, found); + *symbols = g_list_delete_link(*symbols, found_item); + + cont = ui_tree_model_iter_any_next(model, &iter, TRUE); + } + + lsp_symbol_unref(symbol); + } + } + + /* Second pass, now we have a tree cleaned up from invalid rows, + * we simply add new ones */ + foreach_list (item, *symbols) + { + LspSymbol *symbol = item->data; + GtkTreeIter *parent = NULL; + gboolean expand = FALSE; + const gchar *parent_name; + gchar *tooltip, *name; + GdkPixbuf *icon = symbols_get_icon_pixbuf(lsp_symbol_get_icon(symbol)); + + parent_name = get_parent_name(symbol); + if (parent_name) + { + GtkTreeIter *parent_search = parents_table_lookup(parents_table, parent_name, + lsp_symbol_get_line(symbol)); + + if (parent_search) + parent = parent_search; + else + parent_name = NULL; + } + + /* only expand to the iter if the parent was empty, otherwise we let the + * folding as it was before (already expanded, or closed by the user) */ + if (parent) + expand = ! gtk_tree_model_iter_has_child(model, parent); + + /* insert the new element */ + name = lsp_symbol_get_symtree_name(symbol, parent_name == NULL); + tooltip = lsp_symbol_get_symtree_tooltip(symbol, doc->encoding); + gtk_tree_store_insert_with_values(store, &iter, parent, 0, + SYMBOLS_COLUMN_NAME, name, + SYMBOLS_COLUMN_TOOLTIP, tooltip, + SYMBOLS_COLUMN_ICON, icon, + SYMBOLS_COLUMN_SYMBOL, symbol, + -1); + g_free(tooltip); + g_free(name); + + update_parents_table(parents_table, symbol, &iter); + + if (expand) + tree_view_expand_to_iter(GTK_TREE_VIEW(sym_tree), &iter); + } + + g_hash_table_destroy(parents_table); + g_hash_table_destroy(symbols_table); +} + + +static gboolean symbol_has_missing_parent(const LspSymbol *symbol, GtkTreeStore *store, + GtkTreeIter *iter) +{ + /* if the symbol has a parent symbol, it should be at depth >= 2 */ + return !EMPTY(lsp_symbol_get_scope(symbol)) && + gtk_tree_store_iter_depth(store, iter) == 1; +} + + +static gint tree_sort_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, + gpointer user_data) +{ + LspSymbol *sym_a, *sym_b; + gint cmp; + + gtk_tree_model_get(model, a, SYMBOLS_COLUMN_SYMBOL, &sym_a, -1); + gtk_tree_model_get(model, b, SYMBOLS_COLUMN_SYMBOL, &sym_b, -1); + + /* Check if the iters can be sorted based on symbol name and line, not tree item name. + * Sort by tree name if the scope was prepended, e.g. 'ScopeNameWithNoSymbol::SymbolName'. */ + if (sym_a && !symbol_has_missing_parent(sym_a, GTK_TREE_STORE(model), a) && + sym_b && !symbol_has_missing_parent(sym_b, GTK_TREE_STORE(model), b)) + { + cmp = compare_symbol_lines(sym_a, sym_b); + } + else + { + gchar *astr, *bstr; + + gtk_tree_model_get(model, a, SYMBOLS_COLUMN_NAME, &astr, -1); + gtk_tree_model_get(model, b, SYMBOLS_COLUMN_NAME, &bstr, -1); + + { + /* this is what g_strcmp0() does */ + if (! astr) + cmp = -(astr != bstr); + else if (! bstr) + cmp = astr != bstr; + else + { + cmp = strcmp(astr, bstr); + + /* sort duplicate 'ScopeName::OverloadedSymbolName' items by line as well */ + if (sym_a && sym_b) + cmp = compare_symbol_lines(sym_a, sym_b); + } + } + g_free(astr); + g_free(bstr); + } + lsp_symbol_unref(sym_a); + lsp_symbol_unref(sym_b); + + return cmp; +} + + +static void sort_tree(GtkTreeStore *store) +{ + gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(store), SYMBOLS_COLUMN_NAME, tree_sort_func, + NULL, NULL); + + gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store), SYMBOLS_COLUMN_NAME, GTK_SORT_ASCENDING); +} + + +static void symbols_recreate_symbol_list(GeanyDocument *doc) +{ + GList *symbols; + GPtrArray *lsp_symbols; + GtkTreeStore *sym_store; + + g_return_if_fail(DOC_VALID(doc)); + + sym_store = plugin_get_document_data(geany_plugin, doc, SYM_STORE_KEY); + if (!sym_store) + return; + + lsp_symbols = lsp_symbols_doc_get_cached(doc); + symbols = get_symbol_list(doc, lsp_symbols); + if (symbols == NULL) + return; + + /* disable sorting during update because the code doesn't support correctly + * models that are currently being built */ + gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(sym_store), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, 0); + + update_symbols(doc, &symbols); + g_list_free(symbols); + + sort_tree(sym_store); +} + + +static void on_symbol_tree_menu_show(GtkWidget *widget, + gpointer user_data) +{ + GeanyDocument *doc = document_get_current(); + LspServer *srv = lsp_server_get_if_running(doc); + LspServerConfig *cfg = srv ? &srv->config : NULL; + + gtk_widget_set_sensitive(s_symbol_menu.expand_all, cfg != NULL); + gtk_widget_set_sensitive(s_symbol_menu.collapse_all, cfg != NULL); + gtk_widget_set_sensitive(s_symbol_menu.find_refs, cfg != NULL && cfg->goto_references_enable); + gtk_widget_set_sensitive(s_symbol_menu.find_impls, cfg != NULL && cfg->goto_implementation_enable); + gtk_widget_set_sensitive(s_symbol_menu.goto_type, cfg != NULL && cfg->goto_type_definition_enable); + gtk_widget_set_sensitive(s_symbol_menu.goto_decl, cfg != NULL && cfg->goto_declaration_enable); +} + + +static void on_expand_collapse(GtkWidget *widget, gpointer user_data) +{ + gboolean expand = GPOINTER_TO_INT(user_data); + GeanyDocument *doc = document_get_current(); + GtkWidget *sym_tree; + + if (! doc) + return; + + sym_tree = plugin_get_document_data(geany_plugin, doc, SYM_TREE_KEY); + + g_return_if_fail(sym_tree); + + if (expand) + gtk_tree_view_expand_all(GTK_TREE_VIEW(sym_tree)); + else + gtk_tree_view_collapse_all(GTK_TREE_VIEW(sym_tree)); +} + + +static void hide_sidebar(gpointer data) +{ + keybindings_send_command(GEANY_KEY_GROUP_VIEW, GEANY_KEYS_VIEW_SIDEBAR); +} + + +static void change_focus_to_editor(GeanyDocument *doc) +{ + if (DOC_VALID(doc)) + { + GtkWidget *sci = GTK_WIDGET(doc->editor->sci); + gtk_widget_grab_focus(sci); + } +} + + +static gboolean symlist_go_to_selection(GtkTreeSelection *selection, guint keyval, guint state) +{ + GtkTreeIter iter; + GtkTreeModel *model; + gint line = 0; + gboolean handled = TRUE; + + if (gtk_tree_selection_get_selected(selection, &model, &iter)) + { + LspSymbol *sym; + + gtk_tree_model_get(model, &iter, SYMBOLS_COLUMN_SYMBOL, &sym, -1); + if (! sym) + return FALSE; + + line = lsp_symbol_get_line(sym); + if (line > 0) + { + GeanyDocument *doc = document_get_current(); + + if (doc != NULL) + { + navqueue_goto_line(doc, doc, line); + state = keybindings_get_modifiers(state); + if (keyval != GDK_KEY_space && ! (state & GEANY_PRIMARY_MOD_MASK)) + change_focus_to_editor(doc); + else + handled = FALSE; + } + } + lsp_symbol_unref(sym); + } + return handled; +} + + +static gboolean sidebar_button_press_cb(GtkWidget *widget, GdkEventButton *event, + gpointer user_data) +{ + GtkTreeSelection *selection; + GtkWidgetClass *widget_class = GTK_WIDGET_GET_CLASS(widget); + gboolean handled = FALSE; + + /* force the TreeView handler to run before us for it to do its job (selection & stuff). + * doing so will prevent further handlers to be run in most cases, but the only one is our own, + * so guess it's fine. */ + if (widget_class->button_press_event) + handled = widget_class->button_press_event(widget, event); + + selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget)); + + if (event->type == GDK_2BUTTON_PRESS) + { /* double click on parent node(section) expands/collapses it */ + GtkTreeModel *model; + GtkTreeIter iter; + + if (gtk_tree_selection_get_selected(selection, &model, &iter)) + { + if (gtk_tree_model_iter_has_child(model, &iter)) + { + GtkTreePath *path = gtk_tree_model_get_path(model, &iter); + + if (gtk_tree_view_row_expanded(GTK_TREE_VIEW(widget), path)) + gtk_tree_view_collapse_row(GTK_TREE_VIEW(widget), path); + else + gtk_tree_view_expand_row(GTK_TREE_VIEW(widget), path, FALSE); + + gtk_tree_path_free(path); + return TRUE; + } + } + } + else if (event->button == 1) + { + handled = symlist_go_to_selection(selection, 0, event->state); + } + else if (event->button == 3) + { + gtk_menu_popup_at_pointer(GTK_MENU(s_popup_sym_list), (GdkEvent *) event); + handled = TRUE; + } + return handled; +} + + +static gboolean sidebar_key_press_cb(GtkWidget *widget, GdkEventKey *event, + gpointer user_data) +{ + if (ui_is_keyval_enter_or_return(event->keyval) || event->keyval == GDK_KEY_space) + { + GtkWidgetClass *widget_class = GTK_WIDGET_GET_CLASS(widget); + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget)); + + /* force the TreeView handler to run before us for it to do its job (selection & stuff). + * doing so will prevent further handlers to be run in most cases, but the only one is our + * own, so guess it's fine. */ + if (widget_class->key_press_event) + widget_class->key_press_event(widget, event); + + symlist_go_to_selection(selection, event->keyval, event->state); + + return TRUE; + } + return FALSE; +} + + +/* the prepare_* functions are document-related, but I think they fit better here than in document.c */ +static void prepare_symlist(GtkWidget *tree, GtkTreeStore *store) +{ + GtkCellRenderer *text_renderer, *icon_renderer; + GtkTreeViewColumn *column; + GtkTreeSelection *selection; + PangoFontDescription *pfd; + + pfd = pango_font_description_from_string(geany_data->interface_prefs->tagbar_font); + gtk_widget_override_font(tree, pfd); + pango_font_description_free(pfd); + + text_renderer = gtk_cell_renderer_text_new(); + icon_renderer = gtk_cell_renderer_pixbuf_new(); + column = gtk_tree_view_column_new(); + + gtk_tree_view_column_pack_start(column, icon_renderer, FALSE); + gtk_tree_view_column_set_attributes(column, icon_renderer, "pixbuf", SYMBOLS_COLUMN_ICON, NULL); + g_object_set(icon_renderer, "xalign", 0.0, NULL); + + gtk_tree_view_column_pack_start(column, text_renderer, TRUE); + gtk_tree_view_column_set_attributes(column, text_renderer, "text", SYMBOLS_COLUMN_NAME, NULL); + g_object_set(text_renderer, "yalign", 0.5, NULL); + gtk_tree_view_column_set_title(column, _("Symbols")); + + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree), FALSE); + + ui_widget_modify_font_from_string(tree, geany_data->interface_prefs->tagbar_font); + + gtk_tree_view_set_model(GTK_TREE_VIEW(tree), GTK_TREE_MODEL(store)); + g_object_unref(store); + + g_signal_connect(tree, "button-press-event", + G_CALLBACK(sidebar_button_press_cb), NULL); + g_signal_connect(tree, "key-press-event", + G_CALLBACK(sidebar_key_press_cb), NULL); + + gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(tree), geany_data->interface_prefs->show_symbol_list_expanders); + if (! geany_data->interface_prefs->show_symbol_list_expanders) + gtk_tree_view_set_level_indentation(GTK_TREE_VIEW(tree), 10); + /* Tooltips */ + ui_tree_view_set_tooltip_text_column(GTK_TREE_VIEW(tree), SYMBOLS_COLUMN_TOOLTIP); + + /* selection handling */ + selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE); + /* callback for changed selection not necessary, will be handled by button-press-event */ +} + + +static gboolean on_default_sym_tree_button_press_event(GtkWidget *widget, GdkEventButton *event, + gpointer user_data) +{ + if (event->button == 3) + { + gtk_menu_popup_at_pointer(GTK_MENU(s_popup_sym_list), (GdkEvent *) event); + return TRUE; + } + return FALSE; +} + + +static gint find_symbol_tab(void) +{ + GtkNotebook *notebook = GTK_NOTEBOOK(geany_data->main_widgets->sidebar_notebook); + gint pagenum = gtk_notebook_get_n_pages(notebook); + gint i; + + for (i = 0; i < pagenum; i++) + { + GtkWidget *page = gtk_notebook_get_nth_page(notebook, i); + if (page == s_sym_view_vbox) + return i; + } + + return -1; +} + + +void lsp_symbol_tree_refresh(void) +{ + GeanyDocument *doc = document_get_current(); + GtkWidget *child; + GPtrArray *symbols; + const gchar *filter; + const gchar *entry_text; + GtkTreeStore *sym_store; + GtkWidget *sym_tree; + + if (!doc || !s_sym_window) + return; + + if (gtk_notebook_get_current_page(GTK_NOTEBOOK(geany_data->main_widgets->sidebar_notebook)) != find_symbol_tab()) + return; /* don't bother updating symbol tree if we don't see it */ + + child = gtk_bin_get_child(GTK_BIN(s_sym_window)); + symbols = lsp_symbols_doc_get_cached(doc); + filter = plugin_get_document_data(geany_plugin, doc, SYM_FILTER_KEY); + filter = filter ? filter : ""; + entry_text = gtk_entry_get_text(GTK_ENTRY(s_search_entry)); + if (g_strcmp0(filter, entry_text) != 0) + { + gtk_entry_set_text(GTK_ENTRY(s_search_entry), filter); + /* on_entry_tagfilter_changed() will call lsp_symbol_tree_refresh() again */ + return; + } + + /* changes the tree view to the given one, trying not to do useless changes */ + #define CHANGE_TREE(new_child) \ + G_STMT_START { \ + /* only change the symbol tree if it's actually not the same (to avoid flickering) and if \ + * it's the one of the current document (to avoid problems when e.g. reloading \ + * configuration files */ \ + if (child != new_child) \ + { \ + if (child) \ + gtk_container_remove(GTK_CONTAINER(s_sym_window), child); \ + gtk_container_add(GTK_CONTAINER(s_sym_window), new_child); \ + } \ + } G_STMT_END + + /* show default empty symbol tree if there are no symbols */ + if (symbols == NULL || symbols->len == 0) + { + CHANGE_TREE(s_default_sym_tree); + return; + } + + sym_store = plugin_get_document_data(geany_plugin, doc, SYM_STORE_KEY); + sym_tree = plugin_get_document_data(geany_plugin, doc, SYM_TREE_KEY); + + if (sym_tree == NULL) + { + sym_store = gtk_tree_store_new( + SYMBOLS_N_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, LSP_TYPE_SYMBOL, G_TYPE_STRING); + sym_tree = gtk_tree_view_new(); + prepare_symlist(sym_tree, sym_store); + gtk_widget_show(sym_tree); + g_object_ref(sym_tree); /* to hold it after removing */ + + plugin_set_document_data_full(geany_plugin, doc, SYM_STORE_KEY, g_object_ref(sym_store), g_object_unref); + plugin_set_document_data_full(geany_plugin, doc, SYM_TREE_KEY, g_object_ref(sym_tree), g_object_unref); + } + + symbols_recreate_symbol_list(doc); + + CHANGE_TREE(sym_tree); + + #undef CHANGE_TREE +} + + +static void on_sidebar_switch_page(GtkNotebook *notebook, + gpointer page, guint page_num, gpointer user_data) +{ + if (page_num == find_symbol_tab()) + lsp_symbol_tree_refresh(); +} + + +static void on_entry_tagfilter_changed(GtkAction *action, gpointer user_data) +{ + GeanyDocument *doc = document_get_current(); + GtkTreeStore *store; + + if (!doc) + return; + + plugin_set_document_data_full(geany_plugin, doc, SYM_FILTER_KEY, + g_strdup(gtk_entry_get_text(GTK_ENTRY(s_search_entry))), g_free); + + /* make sure the tree is fully re-created so it appears correctly + * after applying filter */ + store = plugin_get_document_data(geany_plugin, doc, SYM_STORE_KEY); + if (store) + gtk_tree_store_clear(store); + + lsp_symbol_tree_refresh(); +} + + +static void on_entry_tagfilter_activate(GtkEntry *entry, gpointer user_data) +{ + GeanyDocument *doc = document_get_current(); + GtkWidget *sym_tree; + + if (!doc) + return; + + sym_tree = plugin_get_document_data(geany_plugin, doc, SYM_TREE_KEY); + + gtk_widget_grab_focus(sym_tree); +} + + +static void on_symtree_goto(GtkWidget *widget, G_GNUC_UNUSED gpointer unused) +{ + GtkTreeIter iter; + GtkTreeSelection *selection; + GtkTreeModel *model; + GeanyDocument *doc; + LspSymbol *sym = NULL; + GtkWidget *sym_tree; + + doc = document_get_current(); + if (!doc) + return; + + sym_tree = plugin_get_document_data(geany_plugin, doc, SYM_TREE_KEY); + + selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sym_tree)); + if (gtk_tree_selection_get_selected(selection, &model, &iter)) + gtk_tree_model_get(model, &iter, SYMBOLS_COLUMN_SYMBOL, &sym, -1); + + if (sym) + { + LspPosition lsp_pos = {lsp_symbol_get_line(sym) - 1, lsp_symbol_get_pos(sym)}; + gint sci_pos = lsp_utils_lsp_pos_to_scintilla(doc->editor->sci, lsp_pos); + + if (widget == s_symbol_menu.find_refs) + lsp_goto_references(sci_pos); + else if (widget == s_symbol_menu.find_impls) + lsp_goto_implementations(sci_pos); + else if (widget == s_symbol_menu.goto_type) + lsp_goto_type_definition(sci_pos); + else if (widget == s_symbol_menu.goto_decl) + lsp_goto_declaration(sci_pos); + + lsp_symbol_unref(sym); + } +} + + +static void create_symlist_popup_menu(void) +{ + GtkWidget *item, *menu; + + s_popup_sym_list = menu = gtk_menu_new(); + + s_symbol_menu.expand_all = item = ui_image_menu_item_new(GTK_STOCK_ADD, _("_Expand All")); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_expand_collapse), GINT_TO_POINTER(TRUE)); + + s_symbol_menu.collapse_all = item = ui_image_menu_item_new(GTK_STOCK_REMOVE, _("_Collapse All")); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_expand_collapse), GINT_TO_POINTER(FALSE)); + + item = gtk_separator_menu_item_new(); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(menu), item); + + s_symbol_menu.find_refs = item = ui_image_menu_item_new(GTK_STOCK_FIND, _("Find _References")); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_symtree_goto), s_symbol_menu.find_refs); + + s_symbol_menu.find_impls = item = ui_image_menu_item_new(GTK_STOCK_FIND, _("Find _Implementations")); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_symtree_goto), s_symbol_menu.find_refs); + + item = gtk_separator_menu_item_new(); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(menu), item); + + s_symbol_menu.goto_decl = item = gtk_menu_item_new_with_mnemonic(_("Go to _Declaration")); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_symtree_goto), NULL); + + s_symbol_menu.goto_type = item = gtk_menu_item_new_with_mnemonic(_("Go to _Type")); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(on_symtree_goto), NULL); + + g_signal_connect(menu, "show", G_CALLBACK(on_symbol_tree_menu_show), NULL); + + item = gtk_separator_menu_item_new(); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(menu), item); + + item = ui_image_menu_item_new(GTK_STOCK_CLOSE, _("H_ide Sidebar")); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(menu), item); + g_signal_connect(item, "activate", G_CALLBACK(hide_sidebar), NULL); +} + + +static void create_default_sym_tree(void) +{ + GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW(s_sym_window); + + /* default_sym_tree is a GtkViewPort with a GtkLabel inside it */ + s_default_sym_tree = gtk_viewport_new( + gtk_scrolled_window_get_hadjustment(scrolled_window), + gtk_scrolled_window_get_vadjustment(scrolled_window)); + gtk_viewport_set_shadow_type(GTK_VIEWPORT(s_default_sym_tree), GTK_SHADOW_NONE); + gtk_widget_show_all(s_default_sym_tree); + g_signal_connect(s_default_sym_tree, "button-press-event", + G_CALLBACK(on_default_sym_tree_button_press_event), NULL); + g_object_ref((gpointer)s_default_sym_tree); /* to hold it after removing */ +} + + +void lsp_symbol_tree_destroy(void) +{ + guint i; + + if (!s_default_sym_tree) + return; + + gtk_widget_destroy(s_default_sym_tree); /* make GTK release its references, if any... */ + g_object_unref(s_default_sym_tree); /* ...and release our own */ + s_default_sym_tree = NULL; + + gtk_widget_destroy(s_popup_sym_list); + gtk_widget_destroy(s_sym_view_vbox); + + foreach_document(i) + { + GeanyDocument *doc = documents[i]; + + plugin_set_document_data(geany_plugin, doc, SYM_TREE_KEY, NULL); + plugin_set_document_data(geany_plugin, doc, SYM_STORE_KEY, NULL); + plugin_set_document_data(geany_plugin, doc, SYM_FILTER_KEY, NULL); + } +} + + +void lsp_symbol_tree_init(void) +{ + LspServerConfig *cfg = lsp_server_get_all_section_config(); + const gchar *tab_label = cfg->document_symbols_tab_label; + const gchar *old_tab_label = NULL; + gboolean show_symtree_tab = !EMPTY(tab_label); + + if (s_default_sym_tree) + old_tab_label = gtk_notebook_get_tab_label_text(GTK_NOTEBOOK(geany_data->main_widgets->sidebar_notebook), s_sym_view_vbox); + + if (old_tab_label && g_strcmp0(old_tab_label, tab_label) != 0) + lsp_symbol_tree_destroy(); + + if ((show_symtree_tab && s_default_sym_tree) || (!show_symtree_tab && !s_default_sym_tree)) + return; + + if (!show_symtree_tab && s_default_sym_tree) + { + lsp_symbol_tree_destroy(); + return; + } + + s_sym_view_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + s_search_entry = gtk_entry_new(); + g_signal_connect(s_search_entry, "activate", + G_CALLBACK(on_entry_tagfilter_activate), NULL); + g_signal_connect(s_search_entry, "changed", + G_CALLBACK(on_entry_tagfilter_changed), NULL); + ui_entry_add_clear_icon(GTK_ENTRY(s_search_entry)); + g_object_set(s_search_entry, "primary-icon-stock", GTK_STOCK_FIND, NULL); + + gtk_box_pack_start(GTK_BOX(s_sym_view_vbox), s_search_entry, FALSE, FALSE, 0); + + s_sym_window = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(s_sym_window), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + + gtk_box_pack_start(GTK_BOX(s_sym_view_vbox), s_sym_window, TRUE, TRUE, 0); + + gtk_widget_show_all(s_sym_view_vbox); + + create_symlist_popup_menu(); + create_default_sym_tree(); + + gtk_notebook_append_page(GTK_NOTEBOOK(geany->main_widgets->sidebar_notebook), + s_sym_view_vbox, gtk_label_new(tab_label)); + g_signal_connect_after(geany_data->main_widgets->sidebar_notebook, "switch-page", + G_CALLBACK(on_sidebar_switch_page), NULL); +} diff --git a/lsp/src/lsp-symbol-tree.h b/lsp/src/lsp-symbol-tree.h new file mode 100644 index 000000000..689c48577 --- /dev/null +++ b/lsp/src/lsp-symbol-tree.h @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_SYMBOL_TREE_H +#define LSP_SYMBOL_TREE_H 1 + +void lsp_symbol_tree_init(void); +void lsp_symbol_tree_destroy(void); + +void lsp_symbol_tree_refresh(void); + +#endif /* LSP_SYMBOL_TREE_H */ diff --git a/lsp/src/lsp-symbol.c b/lsp/src/lsp-symbol.c new file mode 100644 index 000000000..ff84eedb1 --- /dev/null +++ b/lsp/src/lsp-symbol.c @@ -0,0 +1,202 @@ +/* + * Copyright 2024 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-symbol.h" + + +typedef struct LspSymbol +{ + gchar *name; + gchar *detail; + gchar *scope; + gchar *file; + GeanyFiletypeID ft_id; + gulong line; + gulong pos; + glong kind; + guint icon; + + gint refcount; /* the reference count of the symbol */ +} LspSymbol; + + +LspSymbol *lsp_symbol_new(const gchar *name, const gchar *detail, const gchar *scope, const gchar *file, + GeanyFiletypeID ft_id, glong kind, gulong line, gulong pos, guint icon) +{ + LspSymbol *sym = g_slice_new0(LspSymbol); + sym->refcount = 1; + + sym->name = g_strdup(name); + sym->detail = g_strdup(detail); + sym->scope = g_strdup(scope); + sym->file = g_strdup(file); + sym->ft_id = ft_id; + sym->kind = kind; + sym->line = line; + sym->pos = pos; + sym->icon = icon; + + return sym; +} + + +LspSymbol *lsp_symbol_new_from_tag(TMTag *tag) +{ + LspSymbol *sym = g_slice_new0(LspSymbol); + sym->refcount = 1; + return sym; +} + + +static void symbol_destroy(LspSymbol *sym) +{ + g_free(sym->name); + g_free(sym->detail); + g_free(sym->scope); + g_free(sym->file); +} + + +GType lsp_symbol_get_type(void) +{ + static GType gtype = 0; + if (G_UNLIKELY (gtype == 0)) + { + gtype = g_boxed_type_register_static("LspSymbol", (GBoxedCopyFunc)lsp_symbol_ref, + (GBoxedFreeFunc)lsp_symbol_unref); + } + return gtype; +} + + +void lsp_symbol_unref(LspSymbol *sym) +{ + if (sym && g_atomic_int_dec_and_test(&sym->refcount)) + { + symbol_destroy(sym); + g_slice_free(LspSymbol, sym); + } +} + + +LspSymbol *lsp_symbol_ref(LspSymbol *sym) +{ + g_atomic_int_inc(&sym->refcount); + return sym; +} + + +gulong lsp_symbol_get_line(const LspSymbol *sym) +{ + return sym->line; +} + + +gulong lsp_symbol_get_pos(const LspSymbol *sym) +{ + return sym->pos; +} + + +const gchar *lsp_symbol_get_scope(const LspSymbol *sym) +{ + return sym->scope; +} + + +const gchar *lsp_symbol_get_name(const LspSymbol *sym) +{ + return sym->name; +} + + +const gchar *lsp_symbol_get_file(const LspSymbol *sym) +{ + return sym->file; +} + + +const gchar *lsp_symbol_get_detail(const LspSymbol *sym) +{ + return sym->detail; +} + + +glong lsp_symbol_get_kind(const LspSymbol *sym) +{ + return sym->kind; +} + + +TMIcon lsp_symbol_get_icon(const LspSymbol *sym) +{ + return sym->icon; +} + + +gchar *lsp_symbol_get_name_with_scope(const LspSymbol *sym) +{ + gchar *name = NULL; + + if (EMPTY(sym->scope)) + name = g_strdup(sym->name); + else + name = g_strconcat(sym->scope, LSP_SCOPE_SEPARATOR, sym->name, NULL); + + return name; +} + + +gchar *lsp_symbol_get_symtree_name(const LspSymbol *sym, gboolean include_scope) +{ + GString *buffer; + + if (include_scope && !EMPTY(sym->scope)) + { + buffer = g_string_new(sym->scope); + g_string_append(buffer, LSP_SCOPE_SEPARATOR); + g_string_append(buffer, sym->name); + } + else + buffer = g_string_new(sym->name); + + g_string_append_printf(buffer, " [%lu]", sym->line); + + return g_string_free(buffer, FALSE); +} + + +gchar *lsp_symbol_get_symtree_tooltip(const LspSymbol *sym, const gchar *encoding) +{ + return g_strdup(sym->detail); +} + + +gboolean lsp_symbol_equal(const LspSymbol *a, const LspSymbol *b) +{ + return a->line == b->line && a->pos == b->pos && + a->kind == b->kind && a->ft_id == b->ft_id && + g_strcmp0(a->name, b->name) == 0 && + g_strcmp0(a->file, b->file) == 0 && + g_strcmp0(a->scope, b->scope) == 0 && + g_strcmp0(a->detail, b->detail) == 0; +} diff --git a/lsp/src/lsp-symbol.h b/lsp/src/lsp-symbol.h new file mode 100644 index 000000000..3c7cb74ca --- /dev/null +++ b/lsp/src/lsp-symbol.h @@ -0,0 +1,60 @@ +/* + * Copyright 2024 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_SYMBOL_H +#define LSP_SYMBOL_H 1 + +#include "lsp-symbol-kinds.h" + +#include +#include + +#define LSP_SCOPE_SEPARATOR "->" + +struct LspSymbol; +typedef struct LspSymbol LspSymbol; + +/* The GType for a LspSymbol */ +#define LSP_TYPE_SYMBOL (lsp_symbol_get_type()) + +GType lsp_symbol_get_type(void) G_GNUC_CONST; +void lsp_symbol_unref(LspSymbol *sym); +LspSymbol *lsp_symbol_ref(LspSymbol *sym); + +LspSymbol *lsp_symbol_new(const gchar *name, const gchar *detail, const gchar *scope, const gchar *file, + GeanyFiletypeID ft_id, glong kind, gulong line, gulong pos, guint icon); + +gulong lsp_symbol_get_line(const LspSymbol *sym); +gulong lsp_symbol_get_pos(const LspSymbol *sym); +glong lsp_symbol_get_kind(const LspSymbol *sym); +TMIcon lsp_symbol_get_icon(const LspSymbol *sym); +const gchar *lsp_symbol_get_scope(const LspSymbol *sym); +const gchar *lsp_symbol_get_name(const LspSymbol *sym); +const gchar *lsp_symbol_get_file(const LspSymbol *sym); +const gchar *lsp_symbol_get_detail(const LspSymbol *sym); + +gchar *lsp_symbol_get_name_with_scope(const LspSymbol *sym); + +gchar *lsp_symbol_get_symtree_name(const LspSymbol *sym, gboolean include_scope); +gchar *lsp_symbol_get_symtree_tooltip(const LspSymbol *sym, const gchar *encoding); + +gboolean lsp_symbol_equal(const LspSymbol *a, const LspSymbol *b); + +G_END_DECLS + +#endif /* LSP_SYMBOL_H */ diff --git a/lsp/src/lsp-symbols.c b/lsp/src/lsp-symbols.c new file mode 100644 index 000000000..98cd6a115 --- /dev/null +++ b/lsp/src/lsp-symbols.c @@ -0,0 +1,309 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-symbols.h" +#include "lsp-rpc.h" +#include "lsp-utils.h" +#include "lsp-sync.h" +#include "lsp-symbol-kinds.h" +#include "lsp-symbol.h" + +#include + +#define CACHED_SYMBOLS_KEY "lsp_symbols_cached" + +typedef struct { + GeanyDocument *doc; + LspCallback callback; + gpointer user_data; +} LspSymbolUserData; + +typedef struct { + gint ft_id; + LspWorkspaceSymbolRequestCallback callback; + gpointer user_data; +} LspWorkspaceSymbolUserData; + + +extern GeanyPlugin *geany_plugin; +extern GeanyData *geany_data; + + +static void arr_free(GPtrArray *arr) +{ + if (arr) + g_ptr_array_free(arr, TRUE); +} + + +void lsp_symbols_destroy(void) +{ + guint i; + + foreach_document(i) + { + GeanyDocument *doc = documents[i]; + + plugin_set_document_data_full(geany_plugin, doc, CACHED_SYMBOLS_KEY, + NULL, (GDestroyNotify)arr_free); + } +} + + +static void parse_symbols(GPtrArray *symbols, GVariant *symbol_variant, const gchar *scope, + const gchar *scope_sep, gboolean workspace) +{ + GeanyDocument *doc = document_get_current(); + GVariant *member = NULL; + GVariantIter iter; + + g_variant_iter_init(&iter, symbol_variant); + + while (g_variant_iter_loop(&iter, "v", &member)) + { + LspSymbol *sym; + const gchar *name = NULL; + const gchar *detail = NULL; + const gchar *uri = NULL; + const gchar *container_name = NULL; + GVariant *loc_variant = NULL; + GVariant *children = NULL; + gchar *uri_str = NULL; + gint64 kind = 0; + gint line_num = 0; + gint line_pos = 0; + gchar *file_name = NULL; + const gchar *sym_scope = NULL; + + if (!JSONRPC_MESSAGE_PARSE(member, "name", JSONRPC_MESSAGE_GET_STRING(&name))) + continue; + if (!JSONRPC_MESSAGE_PARSE(member, "kind", JSONRPC_MESSAGE_GET_INT64(&kind))) + continue; + + if (JSONRPC_MESSAGE_PARSE(member, "selectionRange", JSONRPC_MESSAGE_GET_VARIANT(&loc_variant))) + { + LspRange range = lsp_utils_parse_range(loc_variant); + line_num = range.start.line; + line_pos = range.start.character; + } + else if (JSONRPC_MESSAGE_PARSE(member, "range", JSONRPC_MESSAGE_GET_VARIANT(&loc_variant))) + { + LspRange range = lsp_utils_parse_range(loc_variant); + line_num = range.start.line; + line_pos = range.start.character; + } + else if (JSONRPC_MESSAGE_PARSE(member, "location", JSONRPC_MESSAGE_GET_VARIANT(&loc_variant))) + { + LspLocation *loc = lsp_utils_parse_location(loc_variant); + if (loc) + { + line_num = loc->range.start.line; + line_pos = loc->range.start.character; + if (loc->uri) + uri_str = g_strdup(loc->uri); + lsp_utils_free_lsp_location(loc); + } + } + else if (!workspace) + { + if (loc_variant) + g_variant_unref(loc_variant); + continue; + } + + if (workspace) + { + JSONRPC_MESSAGE_PARSE(member, "containerName", JSONRPC_MESSAGE_GET_STRING(&container_name)); + + if (!uri_str) + { + if (JSONRPC_MESSAGE_PARSE(member, "location", "{", + "uri", JSONRPC_MESSAGE_GET_STRING(&uri), + "}")) + { + if (uri) + uri_str = g_strdup(uri); + } + + if (!uri_str) + continue; + } + } + + JSONRPC_MESSAGE_PARSE(member, "detail", JSONRPC_MESSAGE_GET_STRING(&detail)); + + if (scope) + sym_scope = scope; + else if (container_name) + sym_scope = container_name; + + if (uri_str) + file_name = lsp_utils_get_real_path_from_uri_utf8(uri_str); + else if (doc && doc->real_path) + file_name = utils_get_utf8_from_locale(doc->real_path); + + sym = lsp_symbol_new(name, detail, sym_scope, file_name, doc->file_type->id, kind, + line_num + 1, line_pos, lsp_symbol_kinds_get_symbol_icon(kind)); + + g_ptr_array_add(symbols, sym); + + if (JSONRPC_MESSAGE_PARSE(member, "children", JSONRPC_MESSAGE_GET_VARIANT(&children))) + { + gchar *new_scope; + + if (scope) + new_scope = g_strconcat(scope, scope_sep, lsp_symbol_get_name(sym), NULL); + else + new_scope = g_strdup(lsp_symbol_get_name(sym)); + parse_symbols(symbols, children, new_scope, scope_sep, FALSE); + g_free(new_scope); + } + + if (loc_variant) + g_variant_unref(loc_variant); + if (children) + g_variant_unref(children); + g_free(uri_str); + g_free(file_name); + } +} + + +static void symbols_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + LspSymbolUserData *data = user_data; + + if (!error && g_variant_is_of_type(return_value, G_VARIANT_TYPE_ARRAY)) + { + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + + if (data->doc == document_get_current()) + { + GPtrArray *cached_symbols = g_ptr_array_new_full(0, (GDestroyNotify)lsp_symbol_unref); + + parse_symbols(cached_symbols, return_value, NULL, LSP_SCOPE_SEPARATOR, FALSE); + + plugin_set_document_data_full(geany_plugin, data->doc, CACHED_SYMBOLS_KEY, + cached_symbols, (GDestroyNotify)arr_free); + } + } + + data->callback(data->user_data); + + g_free(user_data); +} + + +GPtrArray *lsp_symbols_doc_get_cached(GeanyDocument *doc) +{ + if (!doc) + return NULL; + + return plugin_get_document_data(geany_plugin, doc, CACHED_SYMBOLS_KEY); +} + + +void lsp_symbols_doc_request(GeanyDocument *doc, LspCallback callback, + gpointer user_data) +{ + LspServer *server = lsp_server_get(doc); + LspSymbolUserData *data; + GVariant *node; + gchar *doc_uri; + + if (!doc || !doc->real_path || !server) + return; + + data = g_new0(LspSymbolUserData, 1); + data->user_data = user_data; + data->doc = doc; + data->callback = callback; + + doc_uri = lsp_utils_get_doc_uri(doc); + + /* Geany requests symbols before firing "document-activate" signal so we may + * need to request document opening here */ + if (!lsp_sync_is_document_open(doc)) + lsp_sync_text_document_did_open(server, doc); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}" + ); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + lsp_rpc_call(server, "textDocument/documentSymbol", node, + symbols_cb, data); + + g_free(doc_uri); + g_variant_unref(node); +} + + +static void workspace_symbols_cb(GVariant *return_value, GError *error, gpointer user_data) +{ + LspWorkspaceSymbolUserData *data = user_data; + GPtrArray *ret = g_ptr_array_new_full(0, (GDestroyNotify)lsp_symbol_unref); + + if (!error && g_variant_is_of_type(return_value, G_VARIANT_TYPE_ARRAY)) + { + //printf("%s\n\n\n", lsp_utils_json_pretty_print(return_value)); + + //scope separator doesn't matter here + parse_symbols(ret, return_value, NULL, "", TRUE); + } + + data->callback(ret, data->user_data); + + g_ptr_array_free(ret, TRUE); + g_free(user_data); +} + + +void lsp_symbols_workspace_request(GeanyDocument *doc, const gchar *query, + LspWorkspaceSymbolRequestCallback callback, gpointer user_data) +{ + LspServer *server = lsp_server_get(doc); + LspWorkspaceSymbolUserData *data; + GVariant *node; + + if (!server) + return; + + data = g_new0(LspWorkspaceSymbolUserData, 1); + data->user_data = user_data; + data->callback = callback; + data->ft_id = doc->file_type->id; + + node = JSONRPC_MESSAGE_NEW ( + "query", JSONRPC_MESSAGE_PUT_STRING(query) + ); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + lsp_rpc_call(server, "workspace/symbol", node, + workspace_symbols_cb, data); + + g_variant_unref(node); +} diff --git a/lsp/src/lsp-symbols.h b/lsp/src/lsp-symbols.h new file mode 100644 index 000000000..5ee09f91e --- /dev/null +++ b/lsp/src/lsp-symbols.h @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_SYMBOLS_H +#define LSP_SYMBOLS_H 1 + +#include "lsp-server.h" + +#include + +void lsp_symbols_doc_request(GeanyDocument *doc, LspCallback callback, + gpointer user_data); + +GPtrArray *lsp_symbols_doc_get_cached(GeanyDocument *doc); + + +typedef void (*LspWorkspaceSymbolRequestCallback) (GPtrArray *arr, gpointer user_data); + +void lsp_symbols_workspace_request(GeanyDocument *doc, const gchar *query, LspWorkspaceSymbolRequestCallback callback, + gpointer user_data); + +void lsp_symbols_destroy(void); + +#endif /* LSP_SYMBOLS_H */ diff --git a/lsp/src/lsp-sync.c b/lsp/src/lsp-sync.c new file mode 100644 index 000000000..24b6082a4 --- /dev/null +++ b/lsp/src/lsp-sync.c @@ -0,0 +1,227 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-sync.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" +#include "lsp-diagnostics.h" +#include "lsp-highlight.h" +#include "lsp-semtokens.h" +#include "lsp-workspace-folders.h" + +#include + +#define VERSION_NUM_KEY "lsp_sync_version_num" + +extern GeanyPlugin *geany_plugin; + +static GHashTable *open_docs = NULL; + + +void lsp_sync_init() +{ + if (!open_docs) + open_docs = g_hash_table_new(NULL, NULL); + g_hash_table_remove_all(open_docs); +} + + +static guint get_next_doc_version_num(GeanyDocument *doc) +{ + guint num = GPOINTER_TO_UINT(plugin_get_document_data(geany_plugin, doc, VERSION_NUM_KEY)); + + num++; + plugin_set_document_data(geany_plugin, doc, VERSION_NUM_KEY, GUINT_TO_POINTER(num)); + return num; +} + + +gboolean lsp_sync_is_document_open(GeanyDocument *doc) +{ + return g_hash_table_lookup(open_docs, doc) != NULL; +} + + +void lsp_sync_text_document_did_open(LspServer *server, GeanyDocument *doc) +{ + GVariant *node; + gchar *doc_uri; + gchar *lang_id = NULL; + gchar *doc_text; + guint doc_version; + + if (lsp_sync_is_document_open(doc)) + return; + + lsp_workspace_folders_doc_open(doc); + + g_hash_table_add(open_docs, doc); + + lsp_server_get_ft(doc, &lang_id); + doc_uri = lsp_utils_get_doc_uri(doc); + doc_text = sci_get_contents(doc->editor->sci, -1); + doc_version = get_next_doc_version_num(doc); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "languageId", JSONRPC_MESSAGE_PUT_STRING(lang_id), + "version", JSONRPC_MESSAGE_PUT_INT32(doc_version), + "text", JSONRPC_MESSAGE_PUT_STRING(doc_text), + "}" + ); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + lsp_rpc_notify(server, "textDocument/didOpen", node, NULL, NULL); + + g_free(doc_uri); + g_free(lang_id); + g_free(doc_text); + + g_variant_unref(node); +} + + +void lsp_sync_text_document_did_close(LspServer *server, GeanyDocument *doc) +{ + GVariant *node; + gchar *doc_uri; + + if (!lsp_sync_is_document_open(doc)) + return; + + doc_uri = lsp_utils_get_doc_uri(doc); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}" + ); + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + g_hash_table_remove(open_docs, doc); + + lsp_rpc_notify(server, "textDocument/didClose", node, NULL, NULL); + + lsp_workspace_folders_doc_closed(doc); + + g_free(doc_uri); + g_variant_unref(node); +} + + +void lsp_sync_text_document_did_save(LspServer *server, GeanyDocument *doc) +{ + gchar *doc_uri; + GVariant *node; + + if (!server->send_did_save) + return; + + doc_uri = lsp_utils_get_doc_uri(doc); + + if (server->include_text_on_save) + { + gchar *doc_text = sci_get_contents(doc->editor->sci, -1); + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}", + "text", JSONRPC_MESSAGE_PUT_STRING(doc_text) + ); + g_free(doc_text); + } + else + { + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "}" + ); + } + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + lsp_rpc_notify(server, "textDocument/didSave", node, NULL, NULL); + + g_free(doc_uri); + g_variant_unref(node); +} + + +void lsp_sync_text_document_did_change(LspServer *server, GeanyDocument *doc, + LspPosition pos_start, LspPosition pos_end, gchar *text) +{ + GVariant *node; + gchar *doc_uri = lsp_utils_get_doc_uri(doc); + guint doc_version = get_next_doc_version_num(doc); + + if (server->use_incremental_sync) + { + gint range = lsp_utils_lsp_pos_to_scintilla(doc->editor->sci, pos_end) - + lsp_utils_lsp_pos_to_scintilla(doc->editor->sci, pos_start); + + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "version", JSONRPC_MESSAGE_PUT_INT32(doc_version), + "}", + "contentChanges", "[", "{", + "range", "{", + "start", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(pos_start.line), + "character", JSONRPC_MESSAGE_PUT_INT32(pos_start.character), + "}", + "end", "{", + "line", JSONRPC_MESSAGE_PUT_INT32(pos_end.line), + "character", JSONRPC_MESSAGE_PUT_INT32(pos_end.character), + "}", + "}", + // not required but the lemminx server crashes without it + "rangeLength", JSONRPC_MESSAGE_PUT_INT32(range), + "text", JSONRPC_MESSAGE_PUT_STRING(text), + "}", "]" + ); + } + else + { + node = JSONRPC_MESSAGE_NEW ( + "textDocument", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(doc_uri), + "version", JSONRPC_MESSAGE_PUT_INT32(doc_version), + "}", + "contentChanges", "[", "{", + "text", JSONRPC_MESSAGE_PUT_STRING(text), + "}", "]" + ); + } + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + lsp_rpc_notify(server, "textDocument/didChange", node, NULL, NULL); + + g_free(doc_uri); + + g_variant_unref(node); +} diff --git a/lsp/src/lsp-sync.h b/lsp/src/lsp-sync.h new file mode 100644 index 000000000..7dc261ce4 --- /dev/null +++ b/lsp/src/lsp-sync.h @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_SYNC_H +#define LSP_SYNC_H 1 + + +#include "lsp-server.h" +#include "lsp-utils.h" + +void lsp_sync_init(); + +void lsp_sync_text_document_did_open(LspServer *server, GeanyDocument *doc); +void lsp_sync_text_document_did_close(LspServer *server, GeanyDocument *doc); +void lsp_sync_text_document_did_save(LspServer *server, GeanyDocument *doc); +void lsp_sync_text_document_did_change(LspServer *server, GeanyDocument *doc, + LspPosition pos_start, LspPosition pos_end, gchar *text); + +gboolean lsp_sync_is_document_open(GeanyDocument *doc); + +#endif /* LSP_SYNC_H */ diff --git a/lsp/src/lsp-utils.c b/lsp/src/lsp-utils.c new file mode 100644 index 000000000..8cbdf02f8 --- /dev/null +++ b/lsp/src/lsp-utils.c @@ -0,0 +1,1146 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "lsp-utils.h" +#include "lsp-server.h" + +#include +#include +#include + + +extern GeanyData *geany_data; + +extern LspProjectConfiguration project_configuration; +extern LspProjectConfigurationType project_configuration_type; +extern gchar *project_configuration_file; + + +LspPosition lsp_utils_scintilla_pos_to_lsp(ScintillaObject *sci, gint sci_pos) +{ + LspPosition lsp_pos; + gint line_start_pos; + + lsp_pos.line = sci_get_line_from_position(sci, sci_pos); + line_start_pos = sci_get_position_from_line(sci, lsp_pos.line); + lsp_pos.character = SSM(sci, SCI_COUNTCODEUNITS, line_start_pos, sci_pos); + return lsp_pos; +} + + +gint lsp_utils_lsp_pos_to_scintilla(ScintillaObject *sci, LspPosition lsp_pos) +{ + gint line_start_pos = sci_get_position_from_line(sci, lsp_pos.line); + return SSM(sci, SCI_POSITIONRELATIVECODEUNITS, line_start_pos, lsp_pos.character); +} + + +gchar *lsp_utils_get_lsp_lang_id(GeanyDocument *doc) +{ + GString *s; + const gchar *new_name = NULL; + + if (!doc || !doc->file_type) + return NULL; + + s = g_string_new(doc->file_type->name); + g_string_ascii_down(s); + + if (g_strcmp0(s->str, "none") == 0) + new_name = "plaintext"; + else if (g_strcmp0(s->str, "batch") == 0) + new_name = "bat"; + else if (g_strcmp0(s->str, "c++") == 0) + new_name = "cpp"; + else if (g_strcmp0(s->str, "c#") == 0) + new_name = "csharp"; + else if (g_strcmp0(s->str, "conf") == 0) + new_name = "ini"; + else if (g_strcmp0(s->str, "cython") == 0) + new_name = "python"; + else if (g_strcmp0(s->str, "f77") == 0) + new_name = "fortran"; + else if (g_strcmp0(s->str, "freebasic") == 0) + new_name = "vb"; // visual basic + else if (g_strcmp0(s->str, "make") == 0) + new_name = "makefile"; + else if (g_strcmp0(s->str, "matlab/octave") == 0) + new_name = "matlab"; + else if (g_strcmp0(s->str, "sh") == 0) + new_name = "shellscript"; + + if (new_name) + { + g_string_free(s, TRUE); + return g_strdup(new_name); + } + + return g_string_free(s, FALSE); +} + + +#if 0 +gchar *lsp_utils_get_locale() +{ + gchar *locale = setlocale(LC_CTYPE, NULL); + if (locale && strlen(locale) >= 2) + { + locale = g_strdup(locale); + if (locale[2] == '_') + locale[2] = '\0'; + else if (locale[3] == '_') + locale[3] = '\0'; + } + else { + locale = g_strdup("en"); + } + return locale; +} +#endif + + +gchar *lsp_utils_json_pretty_print(GVariant *variant) +{ + JsonNode *node; + gchar *ret; + + if (!variant) + return g_strdup(""); + + node = json_gvariant_serialize(variant); + if (node) + { + ret = json_to_string(node, TRUE); + json_node_unref(node); + } + else + ret = g_strdup(""); + return ret; +} + + +gchar *lsp_utils_get_doc_uri(GeanyDocument *doc) +{ + gchar *fname; + + g_return_val_if_fail(doc->real_path, NULL); + + fname = g_filename_to_uri(doc->real_path, NULL, NULL); + + g_return_val_if_fail(fname, NULL); + + return fname; +} + + +gchar *lsp_utils_get_real_path_from_uri_locale(const gchar *uri) +{ + gchar *fname; + + g_return_val_if_fail(uri, NULL); + + fname = g_filename_from_uri(uri, NULL, NULL); + + g_return_val_if_fail(fname, NULL); + + SETPTR(fname, utils_get_real_path(fname)); + + return fname; +} + + +gchar *lsp_utils_get_real_path_from_uri_utf8(const gchar *uri) +{ + gchar *locale_fname = lsp_utils_get_real_path_from_uri_locale(uri); + + if (!locale_fname) + return NULL; + + SETPTR(locale_fname, utils_get_utf8_from_locale(locale_fname)); + return locale_fname; +} + + +static gchar *utf8_real_path_from_utf8(const gchar *utf8_path) +{ + gchar *ret = utils_get_locale_from_utf8(utf8_path); + SETPTR(ret, utils_get_real_path(ret)); + SETPTR(ret, utils_get_utf8_from_locale(ret)); + return ret; +} + + +/* utf8 */ +gchar *lsp_utils_get_project_base_path(void) +{ + gchar *ret = NULL; + + GeanyProject *project = geany_data->app->project; + if (project && !EMPTY(project->base_path)) + { + if (g_path_is_absolute(project->base_path)) + return utf8_real_path_from_utf8(project->base_path); + else + { /* build base_path out of project file name's dir and base_path */ + gchar *path; + gchar *dir = g_path_get_dirname(project->file_name); + + if (utils_str_equal(project->base_path, "./")) + return dir; + + path = g_build_filename(dir, project->base_path, NULL); + SETPTR(path, utf8_real_path_from_utf8(path)); + g_free(dir); + return path; + } + } + return ret; +} + + +static gchar *get_data_dir_path(const gchar *filename) +{ + gchar *prefix = NULL; + gchar *path; + +# if defined(G_OS_WIN32) + prefix = g_win32_get_package_installation_directory_of_module(NULL); +# elif defined(__APPLE__) + if (g_getenv("GEANY_PLUGINS_SHARE_PATH")) + return g_build_filename(g_getenv("GEANY_PLUGINS_SHARE_PATH"), + PLUGIN, filename, NULL); +# endif + path = g_build_filename(prefix ? prefix : "", PLUGINDATADIR, filename, NULL); + g_free(prefix); + return path; +} + + +/* locale */ +const gchar *lsp_utils_get_global_config_filename(void) +{ + static gchar *filename = NULL; + + if (filename) + return filename; + + filename = get_data_dir_path(PLUGIN".conf"); + return filename; +} + + +/* locale */ +const gchar *lsp_utils_get_user_config_filename(void) +{ + static gchar *filename = NULL; + gchar *dirname; + + if (filename) + return filename; + + dirname = g_build_filename(geany_data->app->configdir, "plugins", PLUGIN, NULL); + filename = g_build_filename(dirname, PLUGIN".conf", NULL); + + if (!g_file_test(filename, G_FILE_TEST_EXISTS)) + { + const gchar *global_config = lsp_utils_get_global_config_filename(); + gchar *cont = NULL; + + utils_mkdir(dirname, TRUE); + msgwin_status_add(_("User LSP config filename %s does not exist, creating"), filename); + if (!g_file_get_contents(global_config, &cont, NULL, NULL)) + msgwin_status_add(_("Cannot read global LSP config filename %s"), global_config); + if (!g_file_set_contents(filename, cont ? cont : "", -1, NULL)) + msgwin_status_add(_("Cannot write user LSP config filename %s"), filename); + g_free(cont); + } + + g_free(dirname); + return filename; +} + + +/* locale */ +const gchar *lsp_utils_get_project_config_filename(void) +{ + if (project_configuration_type == ProjectConfigurationType && project_configuration_file) + { + if (g_file_test(project_configuration_file, G_FILE_TEST_EXISTS)) + return project_configuration_file; + } + + return NULL; +} + + +/* locale */ +const gchar *lsp_utils_get_config_filename(void) +{ + const gchar *project_fname = lsp_utils_get_project_config_filename(); + + return project_fname ? project_fname : lsp_utils_get_user_config_filename(); +} + + +gboolean lsp_utils_is_lsp_disabled_for_project(void) +{ + LspServerConfig *all_cfg = lsp_server_get_all_section_config(); + + return geany->app->project && + (project_configuration == DisabledConfiguration || + (project_configuration == UnconfiguredConfiguration && !all_cfg->enable_by_default)); +} + + +LspPosition lsp_utils_parse_pos(GVariant *variant) +{ + LspPosition lsp_pos; + + JSONRPC_MESSAGE_PARSE(variant, + "character", JSONRPC_MESSAGE_GET_INT64(&lsp_pos.character), + "line", JSONRPC_MESSAGE_GET_INT64(&lsp_pos.line)); + + return lsp_pos; +} + + +LspRange lsp_utils_parse_range(GVariant *variant) +{ + LspRange range; + + JSONRPC_MESSAGE_PARSE(variant, + "start", "{", + "character", JSONRPC_MESSAGE_GET_INT64(&range.start.character), + "line", JSONRPC_MESSAGE_GET_INT64(&range.start.line), + "}", + "end", "{", + "character", JSONRPC_MESSAGE_GET_INT64(&range.end.character), + "line", JSONRPC_MESSAGE_GET_INT64(&range.end.line), + "}"); + + return range; +} + + +void lsp_utils_free_lsp_text_edit(LspTextEdit *e) +{ + if (!e) + return; + g_free(e->new_text); + g_free(e); +} + + +LspTextEdit *lsp_utils_parse_text_edit(GVariant *variant) +{ + GVariant *range = NULL; + const char *text = NULL; + LspTextEdit *ret = NULL; + + gboolean success = JSONRPC_MESSAGE_PARSE(variant, + "newText", JSONRPC_MESSAGE_GET_STRING(&text), + "range", JSONRPC_MESSAGE_GET_VARIANT(&range)); + + if (success) + { + ret = g_new0(LspTextEdit, 1); + ret->new_text = g_strdup(text); + ret->range = lsp_utils_parse_range(range); + } + + if (range) + g_variant_unref(range); + + return ret; +} + + +GPtrArray *lsp_utils_parse_text_edits(GVariantIter *iter) +{ + GPtrArray *ret = NULL; + GVariant *val = NULL; + + if (!iter) + return NULL; + + ret = g_ptr_array_new_full(1, (GDestroyNotify)lsp_utils_free_lsp_text_edit); + while (g_variant_iter_loop(iter, "v", &val)) + { + LspTextEdit *edit = lsp_utils_parse_text_edit(val); + if (edit) + g_ptr_array_add(ret, edit); + } + + return ret; +} + + +void lsp_utils_apply_text_edit(ScintillaObject *sci, LspTextEdit *e, gboolean process_snippets) +{ + GSList *cursor_positions = NULL; + gboolean first_sel = TRUE; + gint start_pos; + gint end_pos; + gchar *processed; + GSList *node; + + if (!e) + return; + + start_pos = lsp_utils_lsp_pos_to_scintilla(sci, e->range.start); + end_pos = lsp_utils_lsp_pos_to_scintilla(sci, e->range.end); + + SSM(sci, SCI_DELETERANGE, start_pos, end_pos - start_pos); + + if (process_snippets) + processed = lsp_utils_process_snippet(e->new_text, &cursor_positions); + else + { + processed = g_strdup(e->new_text); + cursor_positions = g_slist_prepend(cursor_positions, GINT_TO_POINTER(strlen(e->new_text))); + } + + sci_insert_text(sci, start_pos, processed); + + foreach_slist(node, cursor_positions) // sorted in reverse order + { + gint pos = start_pos + GPOINTER_TO_INT(node->data); + SSM(sci, first_sel ? SCI_SETSELECTION : SCI_ADDSELECTION, pos, pos); + first_sel = FALSE; + } + + g_free(processed); + g_slist_free(cursor_positions); +} + + +static gint sort_edits(gconstpointer a, gconstpointer b) +{ + LspTextEdit *e1 = *((LspTextEdit **)a); + LspTextEdit *e2 = *((LspTextEdit **)b); + gint line_delta = e2->range.start.line - e1->range.start.line; + gint char_delta = e2->range.start.character - e1->range.start.character; + + if (line_delta != 0) + return line_delta; + + return char_delta; +} + + +void lsp_utils_apply_text_edits(ScintillaObject *sci, LspTextEdit *edit, GPtrArray *edits, + gboolean process_snippets) +{ + GPtrArray *arr; + gint i; + + if (!edit && !edits) + return; + + arr = g_ptr_array_new_full(edits ? edits->len : 1, NULL); + if (edit) + g_ptr_array_add(arr, edit); + if (edits) + { + for (i = 0; i < edits->len; i++) + g_ptr_array_add(arr, edits->pdata[i]); + } + + // We get the text edit ranges against the original document. We need to sort + // the edits by position in reverse order so we don't affect positions by applying + // earlier edits (edits are guaranteed to be non-overlapping by the LSP specs) + g_ptr_array_sort(arr, sort_edits); + + for (i = 0; i < arr->len; i++) + { + LspTextEdit *e = arr->pdata[i]; + lsp_utils_apply_text_edit(sci, e, process_snippets); + } + + g_ptr_array_free(arr, TRUE); +} + + +static void apply_edits_in_file(const gchar *uri, GPtrArray *edits) +{ + gchar *fname = lsp_utils_get_real_path_from_uri_utf8(uri); + gchar *fname_locale = lsp_utils_get_real_path_from_uri_locale(uri); + + if (fname && fname_locale) + { + GeanyDocument *doc = document_find_by_filename(fname); + ScintillaObject *sci = NULL; + + if (doc) + sci = doc->editor->sci; + else + sci = lsp_utils_new_sci_from_file(fname); + + sci_start_undo_action(sci); + lsp_utils_apply_text_edits(sci, NULL, edits, FALSE); + sci_end_undo_action(sci); + + if (!doc) + { + gchar *contents = sci_get_contents(sci, -1); + + g_file_set_contents(fname, contents, -1, NULL); + +#if 0 + GVariant *node; + node = JSONRPC_MESSAGE_NEW ( + "changes", "[", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(uri), + "type", JSONRPC_MESSAGE_PUT_INT32(2), //changed + "}", "]" + ); + + lsp_client_notify(srv, "workspace/didChangeWatchedFiles", node, NULL, NULL); + g_variant_unref(node); +#endif + + g_free(contents); + g_object_ref_sink(sci); + g_object_unref(sci); + } + } + g_free(fname); + g_free(fname_locale); +} + + +gboolean lsp_utils_apply_workspace_edit(GVariant *workspace_edit) +{ + GVariant *changes = NULL; + gboolean ret = FALSE; + + JSONRPC_MESSAGE_PARSE(workspace_edit, + "changes", JSONRPC_MESSAGE_GET_VARIANT(&changes) + ); + + if (changes && g_variant_is_of_type(changes, G_VARIANT_TYPE_DICTIONARY)) + { + GVariantIter iter; + GVariant *text_edits; + gchar *uri; + + g_variant_iter_init(&iter, changes); + while (g_variant_iter_loop(&iter, "{sv}", &uri, &text_edits)) + { + GVariantIter iter2; + GPtrArray *edits; + + g_variant_iter_init(&iter2, text_edits); + + edits = lsp_utils_parse_text_edits(&iter2); + apply_edits_in_file(uri, edits); + + g_ptr_array_free(edits, TRUE); + } + + ret = TRUE; + } + + if (changes) + g_variant_unref(changes); + + if (!ret) + { + GVariantIter *iter = NULL; + GVariant *document_chages = NULL; + + JSONRPC_MESSAGE_PARSE(workspace_edit, + "documentChanges", JSONRPC_MESSAGE_GET_ITER(&iter) + ); + + while (iter && g_variant_iter_loop(iter, "v", &document_chages)) + { + const gchar *uri = NULL; + GVariantIter *iter2 = NULL; + + //TODO: support CreateFile, RenameFile, DeleteFile + JSONRPC_MESSAGE_PARSE(document_chages, + "textDocument", "{", + "uri", JSONRPC_MESSAGE_GET_STRING(&uri), + "}", + "edits", JSONRPC_MESSAGE_GET_ITER(&iter2) + ); + + if (uri && iter2) + { + GPtrArray *edits = lsp_utils_parse_text_edits(iter2); + + apply_edits_in_file(uri, edits); + ret = TRUE; + + g_ptr_array_free(edits, TRUE); + g_variant_iter_free(iter2); + } + } + + if (iter) + g_variant_iter_free(iter); + } + + return ret; +} + + +void lsp_utils_free_lsp_location(LspLocation *e) +{ + if (!e) + return; + g_free(e->uri); + g_free(e); +} + + +LspLocation *lsp_utils_parse_location(GVariant *variant) +{ + LspLocation *ret = NULL; + GVariant *range = NULL; + const gchar *uri; + + gboolean success = JSONRPC_MESSAGE_PARSE(variant, + "uri", JSONRPC_MESSAGE_GET_STRING(&uri), + "range", JSONRPC_MESSAGE_GET_VARIANT(&range)); + + if (success) + { + ret = g_new0(LspLocation, 1); + ret->uri = g_strdup(uri); + ret->range = lsp_utils_parse_range(range); + } + + if (range) + g_variant_unref(range); + + return ret; +} + + +GPtrArray *lsp_utils_parse_locations(GVariantIter *iter) +{ + GPtrArray *ret = NULL; + GVariant *val = NULL; + + if (!iter) + return NULL; + + ret = g_ptr_array_new_full(1, (GDestroyNotify)lsp_utils_free_lsp_location); + while (g_variant_iter_loop(iter, "v", &val)) + { + LspLocation *loc = lsp_utils_parse_location(val); + if (loc) + g_ptr_array_add(ret, loc); + } + + return ret; +} + + +/* Stolen from Geany */ +/* Wraps a string in place, replacing a space with a newline character. + * wrapstart is the minimum position to start wrapping or -1 for default */ +gboolean lsp_utils_wrap_string(gchar *string, gint wrapstart) +{ + gchar *pos, *linestart; + gboolean ret = FALSE; + + if (wrapstart < 0) + wrapstart = 80; + + for (pos = linestart = string; *pos != '\0'; pos++) + { + if (pos - linestart >= wrapstart && *pos == ' ') + { + *pos = '\n'; + linestart = pos; + ret = TRUE; + } + } + return ret; +} + + +static gchar *utf8_strdown(const gchar *str) +{ + gchar *down; + + if (g_utf8_validate(str, -1, NULL)) + down = g_utf8_strdown(str, -1); + else + { + down = g_locale_to_utf8(str, -1, NULL, NULL, NULL); + if (down) + SETPTR(down, g_utf8_strdown(down, -1)); + } + + return down; +} + + +gpointer lsp_utils_lowercase_cmp(LspUtilsCmpFn cmp, const gchar *s1, const gchar *s2) +{ + gchar *tmp1, *tmp2; + gpointer result; + + g_return_val_if_fail(s1 != NULL, GINT_TO_POINTER(1)); + g_return_val_if_fail(s2 != NULL, GINT_TO_POINTER(-1)); + + /* ensure strings are UTF-8 and lowercase */ + tmp1 = utf8_strdown(s1); + if (!tmp1) + return GINT_TO_POINTER(1); + tmp2 = utf8_strdown(s2); + if (!tmp2) + { + g_free(tmp1); + return GINT_TO_POINTER(-1); + } + + /* compare */ + result = cmp(tmp1, tmp2); + + g_free(tmp1); + g_free(tmp2); + return result; +} + + +JsonNode *lsp_utils_parse_json_file(const gchar *utf8_fname, const gchar *fallback_json) +{ + JsonNode *json_node = NULL; + gchar *file_contents; + gchar *fname; + gboolean success; + GError *error = NULL; + + if (!EMPTY(fallback_json)) + { + json_node = json_from_string(fallback_json, &error); + if (error) + { + msgwin_status_add(_("JSON parsing error: initialization_options: %s"), error->message); + g_error_free(error); + error = NULL; + } + } + + if (!json_node) + json_node = json_from_string("{}", NULL); + + if (EMPTY(utf8_fname)) + return json_node; + + fname = utils_get_locale_from_utf8(utf8_fname); + if (!fname) + return json_node; + + success = g_file_get_contents(fname, &file_contents, NULL, NULL); + g_free(fname); + + if (!success) + return json_node; + + json_node_free(json_node); + + json_node = json_from_string(file_contents, &error); + if (error) + { + msgwin_status_add(_("JSON parsing error: initialization_options_file: %s"), error->message); + g_error_free(error); + } + + g_free(file_contents); + return json_node; +} + + +GVariant *lsp_utils_parse_json_file_as_variant(const gchar *utf8_fname, const gchar *fallback_json) +{ + JsonNode *json_node = lsp_utils_parse_json_file(utf8_fname, fallback_json); + GVariant *variant = json_gvariant_deserialize(json_node, NULL, NULL); + + json_node_free(json_node); + return variant; +} + + +ScintillaObject *lsp_utils_new_sci_from_file(const gchar *utf8_fname) +{ + ScintillaObject *sci; + gchar *file_contents; + gchar *fname; + gboolean success; + + if (!utf8_fname) + return NULL; + + fname = utils_get_locale_from_utf8(utf8_fname); + if (!fname) + return NULL; + + success = g_file_get_contents(fname, &file_contents, NULL, NULL); + g_free(fname); + + if (!success) + return NULL; + + sci = SCINTILLA(scintilla_object_new()); + + gtk_widget_set_direction(GTK_WIDGET(sci), GTK_TEXT_DIR_LTR); + + SSM(sci, SCI_SETCODEPAGE, (uptr_t) SC_CP_UTF8, 0); + SSM(sci, SCI_SETWRAPMODE, SC_WRAP_NONE, 0); + + sci_set_text(sci, file_contents); + + g_free(file_contents); + return sci; +} + + +gchar *lsp_utils_get_current_iden(GeanyDocument *doc, gint current_pos, const gchar *wordchars) +{ + ScintillaObject *sci = doc->editor->sci; + gint start_pos, end_pos, pos; + + pos = current_pos; + while (TRUE) + { + gint new_pos = SSM(sci, SCI_POSITIONBEFORE, pos, 0); + if (new_pos == pos) + break; + if (pos - new_pos == 1) + { + gchar c = sci_get_char_at(sci, new_pos); + if (!strchr(wordchars, c)) + break; + } + pos = new_pos; + } + start_pos = pos; + + pos = current_pos; + while (TRUE) + { + gint new_pos = SSM(sci, SCI_POSITIONAFTER, pos, 0); + if (new_pos == pos) + break; + if (new_pos - pos == 1) + { + gchar c = sci_get_char_at(sci, pos); + if (!strchr(wordchars, c)) + break; + } + pos = new_pos; + } + end_pos = pos; + + if (start_pos == end_pos) + return NULL; + + return sci_get_contents_range(sci, start_pos, end_pos); +} + + +gint lsp_utils_set_indicator_style(ScintillaObject *sci, const gchar *style_str) +{ + gchar **comps = g_strsplit(style_str, ";", -1); + GdkRGBA color; + gint indc = 0; + gint indicator = 0; + gint alpha_fill = 255; + gint alpha_outline = 255; + gint style = 0; + gint i = 0; + + gdk_rgba_parse(&color, "red"); + + for (i = 0; comps && comps[i]; i++) + { + switch (i) + { + case 0: + indc = CLAMP(atoi(comps[i]), 8, 31); + break; + case 1: + { + if (!gdk_rgba_parse(&color, comps[i])) + gdk_rgba_parse(&color, "red"); + break; + } + case 2: + alpha_fill = CLAMP(atoi(comps[i]), 0, 255); + break; + case 3: + alpha_outline = CLAMP(atoi(comps[i]), 0, 255); + break; + case 4: + style = CLAMP(atoi(comps[i]), 0, 22); + indicator = indc; + break; + } + } + + if (indicator > 0) + { + SSM(sci, SCI_INDICSETSTYLE, indicator, style); + SSM(sci, SCI_INDICSETFORE, indicator, + ((unsigned char)(color.red * 255)) | + ((unsigned char)(color.green * 255) << 8) | + ((unsigned char)(color.blue * 255) << 16)); + SSM(sci, SCI_INDICSETALPHA, indicator, alpha_fill); + SSM(sci, SCI_INDICSETOUTLINEALPHA, indicator, alpha_outline); + } + + g_strfreev(comps); + + return indicator; +} + + +/* utf8 */ +gchar *lsp_utils_get_relative_path(const gchar *utf8_parent, const gchar *utf8_descendant) +{ + GFile *gf_parent, *gf_descendant; + gchar *locale_parent, *locale_descendant; + gchar *locale_ret, *utf8_ret; + + locale_parent = utils_get_locale_from_utf8(utf8_parent); + locale_descendant = utils_get_locale_from_utf8(utf8_descendant); + gf_parent = g_file_new_for_path(locale_parent); + gf_descendant = g_file_new_for_path(locale_descendant); + + locale_ret = g_file_get_relative_path(gf_parent, gf_descendant); + utf8_ret = utils_get_utf8_from_locale(locale_ret); + + g_object_unref(gf_parent); + g_object_unref(gf_descendant); + g_free(locale_parent); + g_free(locale_descendant); + g_free(locale_ret); + + return utf8_ret; +} + + +void lsp_utils_save_all_docs(void) +{ + guint i; + + foreach_document(i) + { + GeanyDocument *doc = documents[i]; + + if (doc->changed) + document_save_file(doc, FALSE); + } +} + + +static gboolean content_matches_pattern(const gchar *dirname, gchar **patterns) +{ + gboolean success = FALSE; + const gchar *filename; + GDir *dir; + + dir = g_dir_open(dirname, 0, NULL); + if (!dir) + return FALSE; + + while ((filename = g_dir_read_name(dir)) && !success) + { + gchar **pattern; + + foreach_strv(pattern, patterns) + { + if (g_pattern_match_simple(*pattern, filename)) + { + success = TRUE; + break; + } + } + } + + g_dir_close(dir); + + return success; +} + + +gchar *lsp_utils_find_project_root(GeanyDocument *doc, LspServerConfig *cfg) +{ + gchar *dirname; + + if (!cfg || !cfg->project_root_marker_patterns || !doc->real_path) + return NULL; + + dirname = g_path_get_dirname(doc->real_path); + + while (dirname) + { + gchar *new_dirname; + + if (content_matches_pattern(dirname, cfg->project_root_marker_patterns)) + break; + + new_dirname = g_path_get_dirname(dirname); + if (strlen(new_dirname) >= strlen(dirname)) + { + g_free(new_dirname); + new_dirname = NULL; + } + g_free(dirname); + dirname = new_dirname; + } + + if (dirname && !g_str_has_suffix(dirname, G_DIR_SEPARATOR_S)) + SETPTR(dirname, g_strconcat(dirname, G_DIR_SEPARATOR_S, NULL)); + + return dirname; +} + + +enum { + SnippetOuter, + SnippetAfterDollar, + SnippetAfterDollarDigit, + SnippetAfterDollarAlpha, + SnippetAfterBrace +}; + + +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax +// just removes $foo, ${foo} and detects $0, $1, ${0...}, ${1...} +// saves multiple $1 locations if found to support simultaneous edits like e.g. +// in latex \begin{$1}\end{$1} +gchar *lsp_utils_process_snippet(const gchar *snippet, GSList **positions) +{ + gint len = strlen(snippet); + GString *res = g_string_new(""); + GSList *dollar0_positions = NULL; + GSList *dollar1_positions = NULL; + gint state = SnippetOuter; + gint i; + + for (i = 0; i < len; i++) + { + gchar c = snippet[i]; + + switch (state) + { + case SnippetOuter: + if (c == '$') + state = SnippetAfterDollar; + else + g_string_append_c(res, c); + break; + + case SnippetAfterDollar: + if (isdigit(c)) + { + if (c == '0' && !isdigit(snippet[i+1])) + dollar0_positions = g_slist_prepend(dollar0_positions, GINT_TO_POINTER(res->len)); + else if (c == '1' && !isdigit(snippet[i+1])) + dollar1_positions = g_slist_prepend(dollar1_positions, GINT_TO_POINTER(res->len)); + state = SnippetAfterDollarDigit; + } + else if (isalpha(c) || c == '_') + state = SnippetAfterDollarAlpha; + else if (c == '{') + state = SnippetAfterBrace; + else // something invalid + { + state = SnippetOuter; + g_string_append_c(res, '$'); + g_string_append_c(res, c); + } + break; + + case SnippetAfterDollarDigit: // tabstop + if (!isdigit(c)) + { + state = SnippetOuter; + g_string_append_c(res, c); + } + break; + + case SnippetAfterDollarAlpha: // variable + if (!(isalpha(c) || isdigit(c) || c == '_')) + { + state = SnippetOuter; + g_string_append_c(res, c); + } + break; + + case SnippetAfterBrace: // after ${ + { + gint braces_num = 1; + + if (isdigit(c)) + { + if (c == '0' && !isdigit(snippet[i+1])) + dollar0_positions = g_slist_prepend(dollar0_positions, GINT_TO_POINTER(res->len)); + else if (c == '1' && !isdigit(snippet[i+1])) + dollar1_positions = g_slist_prepend(dollar1_positions, GINT_TO_POINTER(res->len)); + } + + // skip everything until }, including nested {} + while (i < len) + { + if (c == '{') + braces_num++; + else if (c == '}') + braces_num--; + + if (braces_num == 0) + break; + + c = snippet[++i]; + } + + state = SnippetOuter; + break; + } + } + } + + if (positions) + { + if (dollar1_positions) + *positions = dollar1_positions; + else if (dollar0_positions) + *positions = dollar0_positions; + else + { + dollar1_positions = g_slist_prepend(dollar1_positions, GINT_TO_POINTER(res->len)); + *positions = dollar1_positions; + } + + if (dollar0_positions && dollar0_positions != *positions) + g_slist_free(dollar0_positions); + } + + return g_string_free(res, FALSE); +} diff --git a/lsp/src/lsp-utils.h b/lsp/src/lsp-utils.h new file mode 100644 index 000000000..86bfe54ea --- /dev/null +++ b/lsp/src/lsp-utils.h @@ -0,0 +1,133 @@ +/* + * Copyright 2023 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_UTILS_H +#define LSP_UTILS_H 1 + +#include +#include + +#define SSM(s, m, w, l) scintilla_send_message(s, m, w, l) + +struct LspServerConfig; +typedef struct LspServerConfig LspServerConfig; + +typedef enum +{ + UnconfiguredConfiguration = -1, + DisabledConfiguration, + EnabledConfiguration +} LspProjectConfiguration; + + +typedef enum +{ + UserConfigurationType, + ProjectConfigurationType, +} LspProjectConfigurationType; + + +typedef struct +{ + gint64 line; // zero-based + gint64 character; // pos. on line - number of code units in UTF-16, zero-based +} LspPosition; + + +typedef struct +{ + LspPosition start; + LspPosition end; +} LspRange; + + +typedef struct +{ + gchar *new_text; + LspRange range; +} LspTextEdit; + + +typedef struct +{ + gchar *uri; + LspRange range; +} LspLocation; + + +typedef gpointer (* LspUtilsCmpFn)(const gchar *s1, const gchar *s2); + + +void lsp_utils_free_lsp_text_edit(LspTextEdit *e); +void lsp_utils_free_lsp_location(LspLocation *e); + +LspPosition lsp_utils_scintilla_pos_to_lsp(ScintillaObject *sci, gint sci_pos); +gint lsp_utils_lsp_pos_to_scintilla(ScintillaObject *sci, LspPosition lsp_pos); + +gchar *lsp_utils_get_doc_uri(GeanyDocument *doc); +gchar *lsp_utils_get_lsp_lang_id(GeanyDocument *doc); +gchar *lsp_utils_get_real_path_from_uri_locale(const gchar *uri); +gchar *lsp_utils_get_real_path_from_uri_utf8(const gchar *uri); + +gchar *lsp_utils_json_pretty_print(GVariant *variant); + +gchar *lsp_utils_get_project_base_path(void); + +const gchar *lsp_utils_get_global_config_filename(void); +const gchar *lsp_utils_get_user_config_filename(void); +const gchar *lsp_utils_get_project_config_filename(void); +const gchar *lsp_utils_get_config_filename(void); + +gboolean lsp_utils_is_lsp_disabled_for_project(void); + +LspPosition lsp_utils_parse_pos(GVariant *variant); +LspRange lsp_utils_parse_range(GVariant *variant); + +LspTextEdit *lsp_utils_parse_text_edit(GVariant *variant); +GPtrArray *lsp_utils_parse_text_edits(GVariantIter *iter); + +LspLocation *lsp_utils_parse_location(GVariant *variant); +GPtrArray *lsp_utils_parse_locations(GVariantIter *iter); + +void lsp_utils_apply_text_edit(ScintillaObject *sci, LspTextEdit *e, gboolean process_snippets); +void lsp_utils_apply_text_edits(ScintillaObject *sci, LspTextEdit *edit, GPtrArray *edits, + gboolean process_snippets); +gboolean lsp_utils_apply_workspace_edit(GVariant *workspace_edit); + +gboolean lsp_utils_wrap_string(gchar *string, gint wrapstart); + +gpointer lsp_utils_lowercase_cmp(LspUtilsCmpFn cmp, const gchar *s1, const gchar *s2); + +GVariant *lsp_utils_parse_json_file_as_variant(const gchar *utf8_fname, const gchar *fallback_json); +JsonNode *lsp_utils_parse_json_file(const gchar *utf8_fname, const gchar *fallback_json); + +ScintillaObject *lsp_utils_new_sci_from_file(const gchar *utf8_fname); + +gchar *lsp_utils_get_current_iden(GeanyDocument *doc, gint current_pos, const gchar *wordchars); + +gint lsp_utils_set_indicator_style(ScintillaObject *sci, const gchar *style_str); + +gchar *lsp_utils_get_relative_path(const gchar *utf8_parent, const gchar *utf8_descendant); + +void lsp_utils_save_all_docs(void); + +gchar *lsp_utils_find_project_root(GeanyDocument *doc, LspServerConfig *cfg); + +gchar *lsp_utils_process_snippet(const gchar *snippet, GSList **positions); + +#endif /* LSP_UTILS_H */ diff --git a/lsp/src/lsp-workspace-folders.c b/lsp/src/lsp-workspace-folders.c new file mode 100644 index 000000000..8f9044d6b --- /dev/null +++ b/lsp/src/lsp-workspace-folders.c @@ -0,0 +1,203 @@ +/* + * Copyright 2024 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include + +#include "lsp-workspace-folders.h" +#include "lsp-utils.h" +#include "lsp-rpc.h" + + +extern GeanyData *geany_data; + +static GHashTable *folder_table = NULL; + + +void lsp_workspace_folders_init(void) +{ + if (!folder_table) + folder_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + g_hash_table_remove_all(folder_table); +} + + +void lsp_workspace_folders_destroy(void) +{ + if (folder_table) + g_hash_table_destroy(folder_table); + folder_table = NULL; +} + + +static void noitfy_root_change(LspServer *srv, const gchar *root, gboolean added) +{ + gchar *root_uri = g_filename_to_uri(root, NULL, NULL); + GVariant *node; + + if (added) + { + node = JSONRPC_MESSAGE_NEW ( + "event", "{", + "added", "[", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(root_uri), + "name", JSONRPC_MESSAGE_PUT_STRING(root), + "}", "]", + "removed", "[", + "]", + "}" + ); + } + else + { + node = JSONRPC_MESSAGE_NEW ( + "event", "{", + "added", "[", + "]", + "removed", "[", "{", + "uri", JSONRPC_MESSAGE_PUT_STRING(root_uri), + "name", JSONRPC_MESSAGE_PUT_STRING(root), + "}", "]", + "}" + ); + } + + //printf("%s\n\n\n", lsp_utils_json_pretty_print(node)); + + lsp_rpc_notify(srv, "workspace/didChangeWorkspaceFolders", node, NULL, NULL); + + g_free(root_uri); + g_variant_unref(node); +} + + +void lsp_workspace_folders_add_project_root(LspServer *srv) +{ + gchar *base_path; + + if (!srv || !srv->use_workspace_folders) + return; + + base_path = lsp_utils_get_project_base_path(); + if (base_path) + noitfy_root_change(srv, base_path, TRUE); + g_free(base_path); +} + + +void lsp_workspace_folders_doc_open(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get_if_running(doc); + gchar *project_root; + gchar *project_base; + + if (!doc->real_path || !srv || !srv->use_workspace_folders) + return; + + project_base = lsp_utils_get_project_base_path(); + if (project_base) + { + SETPTR(project_base, g_strconcat(project_base, G_DIR_SEPARATOR_S, NULL)); + if (g_str_has_prefix(doc->real_path, project_base)) // already added during initialize + { + g_free(project_base); + return; + } + g_free(project_base); + } + + project_root = lsp_utils_find_project_root(doc, &srv->config); + if (!project_root) + return; + + if (!g_hash_table_contains(folder_table, project_root)) + { + g_hash_table_insert(folder_table, project_root, GINT_TO_POINTER(0)); + + noitfy_root_change(srv, project_root, TRUE); + } + else + g_free(project_root); +} + + +void lsp_workspace_folders_doc_closed(GeanyDocument *doc) +{ + LspServer *srv = lsp_server_get_if_running(doc); + GList *roots, *root; + + if (!srv || !srv->use_workspace_folders) + return; + + roots = g_hash_table_get_keys(folder_table); + + foreach_list(root, roots) + { + gboolean root_used = FALSE; + guint i; + + foreach_document(i) + { + GeanyDocument *document = documents[i]; + + if (doc == document) + continue; + + if (g_str_has_prefix(document->real_path, root->data)) + { + root_used = TRUE; + break; + } + } + + if (!root_used) + { + noitfy_root_change(srv, root->data, FALSE); + + g_hash_table_remove(folder_table, root->data); + } + } + + g_list_free(roots); +} + + +GPtrArray *lsp_workspace_folders_get(void) +{ + GPtrArray *arr = g_ptr_array_new_full(1, g_free); + gchar *project_base; + GList *node, *lst; + + if (!folder_table) + lsp_workspace_folders_init(); + + project_base = lsp_utils_get_project_base_path(); + if (project_base) + g_ptr_array_add(arr, project_base); + g_free(project_base); + + lst = g_hash_table_get_keys(folder_table); + foreach_list(node, lst) + g_ptr_array_add(arr, g_strdup(node->data)); + g_list_free(lst); + + return arr; +} diff --git a/lsp/src/lsp-workspace-folders.h b/lsp/src/lsp-workspace-folders.h new file mode 100644 index 000000000..3f59b8b80 --- /dev/null +++ b/lsp/src/lsp-workspace-folders.h @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Jiri Techet + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LSP_WORKSPACE_FOLDERS_H +#define LSP_WORKSPACE_FOLDERS_H 1 + +#include "lsp-server.h" + +void lsp_workspace_folders_init(void); +void lsp_workspace_folders_destroy(void); + +void lsp_workspace_folders_add_project_root(LspServer *srv); + +void lsp_workspace_folders_doc_open(GeanyDocument *doc); +void lsp_workspace_folders_doc_closed(GeanyDocument *doc); + +GPtrArray *lsp_workspace_folders_get(void); + +#endif /* LSP_WORKSPACE_FOLDERS */ diff --git a/po/POTFILES.in b/po/POTFILES.in index 8c566d8a7..ec0eba7f6 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -178,6 +178,17 @@ lineoperations/src/lo_prefs.c # lipsum lipsum/src/lipsum.c +# LSP +lsp/src/lsp-code-lens.c +lsp/src/lsp-diagnostics.c +lsp/src/lsp-goto-anywhere.c +lsp/src/lsp-log.c +lsp/src/lsp-main.c +lsp/src/lsp-rename.c +lsp/src/lsp-server.c +lsp/src/lsp-symbol-tree.c +lsp/src/lsp-utils.c + # Markdown markdown/src/conf.c markdown/src/plugin.c diff --git a/po/POTFILES.skip b/po/POTFILES.skip index 54f53c26d..f93d796fe 100644 --- a/po/POTFILES.skip +++ b/po/POTFILES.skip @@ -2,5 +2,12 @@ # geanyvc geanyvc/src/commit.glade +# LSP +lsp/deps/json-glib/json-gobject.c +lsp/deps/json-glib/json-gvariant.c +lsp/deps/json-glib/json-parser.c +lsp/deps/json-glib/json-path.c +lsp/deps/json-glib/json-reader.c + # WebHelper webhelper/src/gwh-enum-types.c