From 432ad24e382681f1c13b07e8486e91063dd96e2e Mon Sep 17 00:00:00 2001 From: Robin Haberkorn Date: Sun, 30 May 2021 02:38:43 +0200 Subject: THE GREAT CEEIFICATION EVENT This is a total conversion of SciTECO to plain C (GNU C11). The chance was taken to improve a lot of internal datastructures, fix fundamental bugs and lay the foundations of future features. The GTK user interface is now in an useable state! All changes have been squashed together. The language itself has almost not changed at all, except for: * Detection of string terminators (usually Escape) now takes the string building characters into account. A string is only terminated outside of string building characters. In other words, you can now for instance write I^EQ[Hello$world]$ This removes one of the last bits of shellisms which is out of place in SciTECO where no tokenization/lexing is performed. Consequently, the current termination character can also be escaped using ^Q/^R. This is used by auto completions to make sure that strings are inserted verbatim and without unwanted sideeffects. * All strings can now safely contain null-characters (see also: 8-bit cleanliness). The null-character itself (^@) is not (yet) a valid SciTECO command, though. An incomplete list of changes: * We got rid of the BSD headers for RB trees and lists/queues. The problem with them was that they used a form of metaprogramming only to gain a bit of type safety. It also resulted in less readble code. This was a C++ desease. The new code avoids metaprogramming only to gain type safety. The BSD tree.h has been replaced by rb3ptr by Jens Stimpfle (https://github.com/jstimpfle/rb3ptr). This implementation is also more memory efficient than BSD's. The BSD list.h and queue.h has been replaced with a custom src/list.h. * Fixed crashes, performance issues and compatibility issues with the Gtk 3 User Interface. It is now more or less ready for general use. The GDK lock is no longer used to avoid using deprecated functions. On the downside, the new implementation (driving the Gtk event loop stepwise) is even slower than the old one. A few glitches remain (see TODO), but it is hoped that they will be resolved by the Scintilla update which will be performed soon. * A lot of program units have been split up, so they are shorter and easier to maintain: core-commands.c, qreg-commands.c, goto-commands.c, file-utils.h. * Parser states are simply structs of callbacks now. They still use a kind of polymorphy using a preprocessor trick. TECO_DEFINE_STATE() takes an initializer list that will be merged with the default list of field initializers. To "subclass" states, you can simply define new macros that add initializers to existing macros. * Parsers no longer have a "transitions" table but the input_cb() may use switch-case statements. There are also teco_machine_main_transition_t now which can be used to implement simple transitions. Additionally, you can specify functions to execute during transitions. This largely avoids long switch-case-statements. * Parsers are embeddable/reusable now, at least in parse-only mode. This does not currently bring any advantages but may later be used to write a Scintilla lexer for TECO syntax highlighting. Once parsers are fully embeddable, it will also be possible to run TECO macros in a kind of coroutine which would allow them to process string arguments in real time. * undo.[ch] still uses metaprogramming extensively but via the C preprocessor of course. On the downside, most undo token generators must be initiated explicitly (theoretically we could have used embedded functions / trampolines to instantiate automatically but this has turned out to be dangereous). There is a TECO_DEFINE_UNDO_CALL() to generate closures for arbitrary functions now (ie. to call an arbitrary function at undo-time). This simplified a lot of code and is much shorter than manually pushing undo tokens in many cases. * Instead of the ridiculous C++ Curiously Recurring Template Pattern to achieve static polymorphy for user interface implementations, we now simply declare all functions to implement in interface.h and link in the implementations. This is possible since we no longer hace to define interface subclasses (all state is static variables in the interface's *.c files). * Headers are now significantly shorter than in C++ since we can often hide more of our "class" implementations. * Memory counting is based on dlmalloc for most platforms now. Unfortunately, there is no malloc implementation that provides an efficient constant-time memory counter that is guaranteed to decrease when freeing memory. But since we use a defined malloc implementation now, malloc_usable_size() can be used safely for tracking memory use. malloc() replacement is very tricky on Windows, so we use a poll thread on Windows. This can also be enabled on other supported platforms using --disable-malloc-replacement. All in all, I'm still not pleased with the state of memory limiting. It is a mess. * Error handling uses GError now. This has the advantage that the GError codes can be reused once we support error catching in the SciTECO language. * Added a few more test suite cases. * Haiku is no longer supported as builds are instable and I did not manage to debug them - quite possibly Haiku bugs were responsible. * Glib v2.44 or later are now required. The GTK UI requires Gtk+ v3.12 or later now. The GtkFlowBox fallback and sciteco-wrapper workaround are no longer required. * We now extensively use the GCC/Clang-specific g_auto feature (automatic deallocations when leaving the current code block). * Updated copyright to 2021. SciTECO has been in continuous development, even though there have been no commits since 2018. * Since these changes are so significant, the target release has been set to v2.0. It is planned that beginning with v3.0, the language will be kept stable. --- .gitignore | 9 +- INSTALL | 21 +- Makefile.am | 6 +- README | 12 +- TODO | 217 +- bootstrap.am | 6 +- compat/bsd/sys/cdefs.h | 89 - compat/bsd/sys/queue.h | 620 --- compat/bsd/sys/tree.h | 763 --- configure.ac | 136 +- contrib/dlmalloc/Makefile.am | 23 + contrib/dlmalloc/malloc.c | 6280 +++++++++++++++++++++++++ contrib/dlmalloc/malloc.h | 620 +++ contrib/rb3ptr/Makefile.am | 6 + contrib/rb3ptr/rb3ptr.c | 505 ++ contrib/rb3ptr/rb3ptr.h | 474 ++ debian/copyright | 44 +- doc/Doxyfile.in | 25 +- doc/Makefile.am | 17 +- doc/sciteco.1.in | 23 +- doc/sciteco.7.template | 21 +- m4/m4_ax_check_enable_debug.m4 | 124 + src/Makefile.am | 100 +- src/cmdline.c | 1058 +++++ src/cmdline.cpp | 1043 ---- src/cmdline.h | 145 +- src/core-commands.c | 2510 ++++++++++ src/core-commands.h | 71 + src/doc.c | 209 + src/doc.h | 115 + src/document.cpp | 122 - src/document.h | 141 - src/eol.c | 461 ++ src/eol.cpp | 359 -- src/eol.h | 180 +- src/error.c | 173 + src/error.cpp | 171 - src/error.h | 316 +- src/expressions.c | 384 ++ src/expressions.cpp | 277 -- src/expressions.h | 399 +- src/file-utils.c | 343 ++ src/file-utils.h | 106 + src/glob.c | 573 +++ src/glob.cpp | 554 --- src/glob.h | 60 +- src/goto-commands.c | 171 + src/goto-commands.h | 25 + src/goto.c | 182 + src/goto.cpp | 193 - src/goto.h | 144 +- src/help.c | 389 ++ src/help.cpp | 322 -- src/help.h | 78 +- src/interface-curses/Makefile.am | 11 +- src/interface-curses/curses-info-popup.c | 211 + src/interface-curses/curses-info-popup.cpp | 219 - src/interface-curses/curses-info-popup.h | 96 +- src/interface-curses/curses-utils.c | 144 + src/interface-curses/curses-utils.cpp | 152 - src/interface-curses/curses-utils.h | 22 +- src/interface-curses/interface-curses.cpp | 1613 ------- src/interface-curses/interface-curses.h | 199 - src/interface-curses/interface.c | 1723 +++++++ src/interface-gtk/Makefile.am | 26 +- src/interface-gtk/fallback.css | 51 +- src/interface-gtk/gtk-canonicalized-label.gob | 226 - src/interface-gtk/gtk-info-popup.gob | 331 -- src/interface-gtk/gtkflowbox.c | 4795 ------------------- src/interface-gtk/gtkflowbox.h | 180 - src/interface-gtk/interface-gtk.cpp | 1132 ----- src/interface-gtk/interface-gtk.h | 179 - src/interface-gtk/interface.c | 1203 +++++ src/interface-gtk/teco-gtk-info-popup.gob | 446 ++ src/interface-gtk/teco-gtk-label.gob | 253 + src/interface.c | 120 + src/interface.cpp | 180 - src/interface.h | 446 +- src/ioview.cpp | 512 -- src/ioview.h | 130 - src/list.h | 94 + src/main.c | 459 ++ src/main.cpp | 498 -- src/memory.c | 672 +++ src/memory.cpp | 350 -- src/memory.h | 76 +- src/parser.c | 902 ++++ src/parser.cpp | 2883 ------------ src/parser.h | 845 ++-- src/qreg-commands.c | 760 +++ src/qreg-commands.h | 90 + src/qreg.c | 1542 ++++++ src/qreg.h | 238 + src/qregisters.cpp | 1680 ------- src/qregisters.h | 666 --- src/rb3str.c | 150 + src/rb3str.h | 69 + src/rbtree.cpp | 90 - src/rbtree.h | 205 - src/ring.c | 580 +++ src/ring.cpp | 461 -- src/ring.h | 266 +- src/scintilla.c | 349 ++ src/scintilla.h | 64 + src/sciteco.h | 159 +- src/search.c | 1130 +++++ src/search.cpp | 946 ---- src/search.h | 130 +- src/spawn.c | 667 +++ src/spawn.cpp | 662 --- src/spawn.h | 67 +- src/string-utils.c | 185 + src/string-utils.cpp | 87 - src/string-utils.h | 178 +- src/symbols-extract.tes | 22 +- src/symbols-minimal.cpp | 32 - src/symbols.cpp | 75 - src/symbols.h | 69 - src/undo.c | 163 + src/undo.cpp | 104 - src/undo.h | 399 +- src/view.c | 439 ++ src/view.h | 75 + tests/atlocal.in | 4 + tests/testsuite.at | 59 + 125 files changed, 29575 insertions(+), 26081 deletions(-) delete mode 100644 compat/bsd/sys/cdefs.h delete mode 100644 compat/bsd/sys/queue.h delete mode 100644 compat/bsd/sys/tree.h create mode 100644 contrib/dlmalloc/Makefile.am create mode 100644 contrib/dlmalloc/malloc.c create mode 100644 contrib/dlmalloc/malloc.h create mode 100644 contrib/rb3ptr/Makefile.am create mode 100644 contrib/rb3ptr/rb3ptr.c create mode 100644 contrib/rb3ptr/rb3ptr.h create mode 100644 m4/m4_ax_check_enable_debug.m4 create mode 100644 src/cmdline.c delete mode 100644 src/cmdline.cpp create mode 100644 src/core-commands.c create mode 100644 src/core-commands.h create mode 100644 src/doc.c create mode 100644 src/doc.h delete mode 100644 src/document.cpp delete mode 100644 src/document.h create mode 100644 src/eol.c delete mode 100644 src/eol.cpp create mode 100644 src/error.c delete mode 100644 src/error.cpp create mode 100644 src/expressions.c delete mode 100644 src/expressions.cpp create mode 100644 src/file-utils.c create mode 100644 src/file-utils.h create mode 100644 src/glob.c delete mode 100644 src/glob.cpp create mode 100644 src/goto-commands.c create mode 100644 src/goto-commands.h create mode 100644 src/goto.c delete mode 100644 src/goto.cpp create mode 100644 src/help.c delete mode 100644 src/help.cpp create mode 100644 src/interface-curses/curses-info-popup.c delete mode 100644 src/interface-curses/curses-info-popup.cpp create mode 100644 src/interface-curses/curses-utils.c delete mode 100644 src/interface-curses/curses-utils.cpp delete mode 100644 src/interface-curses/interface-curses.cpp delete mode 100644 src/interface-curses/interface-curses.h create mode 100644 src/interface-curses/interface.c delete mode 100644 src/interface-gtk/gtk-canonicalized-label.gob delete mode 100644 src/interface-gtk/gtk-info-popup.gob delete mode 100644 src/interface-gtk/gtkflowbox.c delete mode 100644 src/interface-gtk/gtkflowbox.h delete mode 100644 src/interface-gtk/interface-gtk.cpp delete mode 100644 src/interface-gtk/interface-gtk.h create mode 100644 src/interface-gtk/interface.c create mode 100644 src/interface-gtk/teco-gtk-info-popup.gob create mode 100644 src/interface-gtk/teco-gtk-label.gob create mode 100644 src/interface.c delete mode 100644 src/interface.cpp delete mode 100644 src/ioview.cpp delete mode 100644 src/ioview.h create mode 100644 src/list.h create mode 100644 src/main.c delete mode 100644 src/main.cpp create mode 100644 src/memory.c delete mode 100644 src/memory.cpp create mode 100644 src/parser.c delete mode 100644 src/parser.cpp create mode 100644 src/qreg-commands.c create mode 100644 src/qreg-commands.h create mode 100644 src/qreg.c create mode 100644 src/qreg.h delete mode 100644 src/qregisters.cpp delete mode 100644 src/qregisters.h create mode 100644 src/rb3str.c create mode 100644 src/rb3str.h delete mode 100644 src/rbtree.cpp delete mode 100644 src/rbtree.h create mode 100644 src/ring.c delete mode 100644 src/ring.cpp create mode 100644 src/scintilla.c create mode 100644 src/scintilla.h create mode 100644 src/search.c delete mode 100644 src/search.cpp create mode 100644 src/spawn.c delete mode 100644 src/spawn.cpp create mode 100644 src/string-utils.c delete mode 100644 src/string-utils.cpp delete mode 100644 src/symbols-minimal.cpp delete mode 100644 src/symbols.cpp delete mode 100644 src/symbols.h create mode 100644 src/undo.c delete mode 100644 src/undo.cpp create mode 100644 src/view.c create mode 100644 src/view.h diff --git a/.gitignore b/.gitignore index f089685..47c9379 100644 --- a/.gitignore +++ b/.gitignore @@ -32,13 +32,12 @@ testsuite.dir # Binaries /src/sciteco /src/sciteco-minimal -/src/sciteco-wrapper # Generated source files -/src/interface-gtk/gtk-info-popup.[ch] -/src/interface-gtk/gtk-canonicalized-label.[ch] -/src/symbols-scintilla.cpp -/src/symbols-scilexer.cpp +/src/interface-gtk/teco-gtk-info-popup.[ch] +/src/interface-gtk/teco-gtk-label.[ch] +/src/symbols-scintilla.c +/src/symbols-scilexer.c # Generated documentation /doc/sciteco.[17] diff --git a/INSTALL b/INSTALL index 4467ec8..eb5ff8b 100644 --- a/INSTALL +++ b/INSTALL @@ -3,7 +3,7 @@ Installation Instructions Copyright (C) 1994, 1995, 1996, 1999, 2000, 2001, 2002, 2004, 2005, 2006, 2007, 2008, 2009 Free Software Foundation, Inc. -Copyright (C) 2013-2017 Robin Haberkorn +Copyright (C) 2013-2021 Robin Haberkorn Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright @@ -15,15 +15,17 @@ SciTECO Build and Runtime Dependencies * Autotools and an UNIX-like environment * GNU Make - * A C++11 compiler, e.g. GNU C/C++ (v4.4 or later) or - LLVM/gcc or LLVM/Clang - * Glib 2 as a cross-platform runtime library - (v2.28 or later on Unix, v2.34 or later for MinGW): + * A GCC-compatible C11 and C++11 compiler, e.g. GNU C/C++ + (v4.4 or later) or LLVM/gcc or LLVM/Clang. + SciTECO itself does not require C++, but Scintilla does. + * Glib 2 as a cross-platform runtime library (v2.44 or later): https://developer.gnome.org/glib/ * When choosing the Curses interface, you need one of: * NCurses (http://www.gnu.org/software/ncurses/). If you plan to use the ncurses MinGW port, I recommend ncurses 6.0 or later. + * NetBSD Curses (https://github.com/sabotage-linux/netbsd-curses). + This is the default on NetBSD. * PDCurses/XCurses (http://pdcurses.sourceforge.net/). Note that XCurses v3.4 appears to be broken, you should build from PDCurses Git instead. @@ -31,7 +33,7 @@ SciTECO Build and Runtime Dependencies * PDCurses/EMCurses (https://github.com/rhaberkorn/emcurses). * other curses implementations might work as well but are untested * When choosing the GTK interface: - * GTK+ v3.10 or later: http://www.gtk.org/ + * GTK+ v3.12 or later: http://www.gtk.org/ * GObject Builder v2.0.20 or later: http://www.jirka.org/gob.html * GNU roff (groff): https://www.gnu.org/software/groff/ Required at build-time, but it is already shipped on most @@ -48,6 +50,13 @@ installed by the user manually: * Scinterm (v1.7 or later): http://foicica.com/scinterm/ +On Ubuntu, you can install all dependencies you could possibly need +as follows: + + $ sudo apt-get install git build-essential autoconf automake libtool \ + libglib2.0-dev libncurses-dev libgtk-3-dev gob2 \ + groff doxygen + Building from Source Tar Ball or Repository =========================================== diff --git a/Makefile.am b/Makefile.am index cd36bca..8ab86f6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2,14 +2,10 @@ # silence libtoolize: ACLOCAL_AMFLAGS = -I m4 -SUBDIRS = lib src doc tests +SUBDIRS = lib contrib/dlmalloc contrib/rb3ptr src doc tests dist_pkgdata_DATA = sample.teco_ini -noinst_HEADERS = compat/bsd/sys/cdefs.h \ - compat/bsd/sys/queue.h \ - compat/bsd/sys/tree.h - EXTRA_DIST = README TODO # Only the lower resolution PNG icons are installed as they are diff --git a/README b/README index c791c47..3ea348a 100644 --- a/README +++ b/README @@ -2,7 +2,7 @@ Overview ======== SciTECO -SciTECO is an interactive TECO dialect, similar to [Video TECO](http://www.copters.com/teco.html). +SciTECO is an interactive TECO dialect, similar to Video TECO. It also adds features from classic TECO-11, as well as unique new ideas. The basic approach to text editing is both radically different and surprisingly similar to popular @@ -24,8 +24,8 @@ The Curses frontend is verified to work with [ncurses](https://www.gnu.org/softw [EMCurses](https://github.com/rhaberkorn/emcurses). Others might work as well. -Linux, FreeBSD, Windows (MinGW 32/64) and -[Haiku](https://www.haiku-os.org/) (gcc4) are tested and supported. +Linux, FreeBSD, Windows (MinGW 32/64) ~~and +[Haiku](https://www.haiku-os.org/) (gcc4)~~ are tested and supported. SciTECO compiles with both GCC and Clang. SciTECO should compile just fine on other UNIX-compatible platforms, like Mac OS X - however I cannot test it regularily and there is currently no @@ -36,12 +36,12 @@ some implementation of Curses, it should be easy to port to even more exotic pla __Warning: The SciTECO language is work in progress and has not yet fully stabilized. It may change drastically and in backwards-incompatible ways in the repository's -master branch and between releases until version 1.0 is released.__ +master branch and between releases until version 3.0 is released.__ Features ======== -* Supports most of the Video TECO commands +* Supports most of the [Video TECO](http://www.copters.com/teco.html) commands * Improved parser compared to classic TECOs, making SciTECO more similar to other imperative languages. * Operator precedence in arithmetic expressions and an argument stack that may be modified @@ -72,6 +72,8 @@ Features * Munging: Macros may be munged, that is executed in batch mode. In other words, SciTECO can be used for scripting. By default, a profile is munged. +* 8-bit clean: SciTECO can be used to edit binary files if automatic EOL conversion + is turned off (`16,0ED`). * Self-documenting: An integrated indexed help system allows browsing formatted documentation about commands, macros and concepts within SciTECO (`?` command). Macro packages can be documented with the `tedoc` tool, generating man pages. diff --git a/TODO b/TODO index e88723c..b08a0a1 100644 --- a/TODO +++ b/TODO @@ -1,23 +1,32 @@ Tasks: - * submit patch for libglib (initialization when - linking statically with win32 threads - see glib/glib-init.c). - Also gspawn helpers should probably link with -all-static when compiling - a static glib. Why would be build a static glib but have the programs - depend on other libraries? * Wiki page about creating and maintaining lexer configurations. Also mention how to use the "lexer.test..." macros in the "edit" hook. - * OS X port (macports and/or homebrew) - * Scinterm: implement wattrget() for netbsd-curses + * Use Travis CI for Continuous Integration and perhaps even for providing + nightly builds on Gitlab. This would solve the problem of releases + lagging behind and esp. the satisfy Windows users who continuously + ask for prebuilt binaries. + * OS X port (macports and/or homebrew). + Maybe Travis CI can help as well. + * Scinterm: implement wattrget() for netbsd-curses. + May already be fixed in newer versions. Known Bugs: - * ECxclip -selection clipboard -in$ hangs. The stdout-watcher is - never activated. - * ECcat /dev/zero$ can easily exceed the memory limit. - Perhaps we should add a check to stdout_watcher_cb. + * Characters are not correctly drawn in the GTK backend (especially underscores). + This may be a regression due to ever changing GTK APIs and + upgrading Scintilla may already help. + * Rubbing out via ^W will rub out more than expected. + * After commands like ECcat /dev/zero$ result in OOM, + we do not correctly recover, even though malloc_trim() is called. + This could be because of Scintilla's undo token. + Perhaps it would be best to disable any undo handling by Scintilla + via SCI_SETUNDOCOLLECTION. This would also save memory. + * S^ES^N<$ does not find the first line that does not begin with "<" + ^ES is apparently not greedy. * fnkeys.tes: Cursor movements will swallow all preceding braced expressions - there should be more checks. * rubout of EB does not always restore the view to an edited Q-Register. + (Is this still relevant after the Great Ceification event?) * Colors are still wrong in Linux console even if TERM=linux-16color when using Solarized. Affects e.g. the message line which uses the reverse of STYLE_DEFAULT. @@ -33,8 +42,8 @@ Known Bugs: old radix. Perhaps it's better to make the radix a property of the current macro invocation frame and guarantee ^R == 10 at the beginning of macros. - * Null-byte in strings not always handled transparently - (SciTECO is not 8-bit clean.) + Since :M should probably inherit the radix, having a ^R register would + still be useful. * Saving another user's file will only preserve the user when run as root. Generally, it is hard to ensure that a) save point files can be created and b) the file mode and ownership of re-created files can be preserved. @@ -44,13 +53,15 @@ Known Bugs: Happens because the Glib regex engine is based on a recursive Perl regex library. This is apparently impossible to fix as long as we do not - have control over the regex engine build. We should either use C++11 - regex support, UNIX regex (unportable) or some other library. - Perhaps allowing us to interpret the SciTECO matching language - directly. + have control over the regex engine build. + We should therefore switch the underlying Regex engine. + Oniguruma looks promising and is also packed for Ubuntu (libonig2). + It would also directly allow globbing by tweaking the syntax. + TRE also looks promising and is smaller than Oniguruma. + GRegEx (PCRE) could still be supported as a fallback. * It is still possible to crash SciTECO using recursive functions, - since they map to the C++ program's call stack. - It is perhaps best to use another ValueStack as a stack of + since they map to the C program's call stack. + It is perhaps best to use another stack of macro strings and implement our own function calling. * SciTECO crashes can leave orphaned savepoint files lying around. Unfortunately, both the Windows and Linux ways of deleting files @@ -73,26 +84,11 @@ Known Bugs: There is also MoveFileEx(file, NULL, MOVEFILE_DELAY_UNTIL_REBOOT). * Windows has file system forks, but they can be orphaned just like ordinary files but are harder to locate and clean up manually. - * Clipboard registers are prone to race conditions if the - contents change between get_size() and get_string() calls. - Also it's a common idiom to query a string and its size, - so the internal API must be changed. * Setting window title is broken on ncurses/XTerm. Perhaps do some XTerm magic here. We can also restore window titles on exit using XTerm. * Glib (error) messages are not integrated with SciTECO's logging system. - * Auto-completions are prone to unexpected results when - the insertion contains string-building characters, braces - (for Q-Register auto-completions) or escape characters - (just consider autocompleting after @FG/...). - Insertions should thus be escaped. - A new string building command should consequently be - introduced to insert an ASCII character by digits, - e.g. ^E0XXX. - Also, auto-completions within string-building constructs - (except Q-Reg specs) should generally be disabled since - the result will be unpredictable. Features: * Auto-indention could be implemented via context-sensitive @@ -109,6 +105,25 @@ Features: Macros could be special QRegs that are not backed by a Scintilla document but a normal string. This would immensely speed up macro calls. + Perhaps more generically, we should add a number of alternative + balanced string terminators (<> [] () {}) and assign one + to parse SciTECO code. "' is not really an option here, since + we want to be able to write @I"..." etc. + Since ] and } can occur as stand-alone commands, we would have to + use <> and/or () for SciTECO code parsing. + The advantage would be that we save introducing a special + assignment command and can use the same escape with @I<> for + editing macro files while still getting immediate interactive + syntax feedback. + Plus: Once we have a parser-based terminator, there would + be no more real need for command variants with disabled + string building (as string building will naturally always + be disabled in parser-terminator-mode). + Instead, a special string building character for disabling + string building character processing can be introduced, + and all the command variants like EI and EU can be repurposed. + Q-Reg specs should support alternative balanced escapes as well + for symmetry. * Numbers could be separate states instead of stack operating commands. The current behaviour has few benefits. If a number is a regular command that stops parsing at the @@ -130,10 +145,17 @@ Features: integer and floating point types internally. The operator decides how to interpret the arguments and the return type. + * Having a separate number parser state will simplify + number syntax highlighting. * Function key masking flag for the beginning of the command line. May be useful e.g. for solarized's F5 key (i.e. function key macros that need to terminate the command line as they cannot be rubbed out properly). + * fnkeys.tec could preserve the column more reliably when + moving up and down by encoding a character offset into the + command line. E.g. (100-3C) would tell us that we have to add + 3 to the real column when moving up/down because the current + line is too short. * Function key macros should behave more like regular macros: If inserting a character results in an error, the entire macro should be rubbed out. This means it would be OK to @@ -148,6 +170,8 @@ Features: state machine, perhaps only in insertion commands, this could be used to make the cursor movement keys work in insertion commands by automatically terminating the command. + Even more simple, the function key flag could be effective + only when the termination character is $. * Function key handling should always be enabled. This was configurable because of the way escape was handled in ncurses. Now that escape is always immediate, there is little benefit @@ -164,7 +188,7 @@ Features: This will make it easy to write command line filters, We will need flags like --8-bit-clean and --quiet with single-letter forms to make it possible to write hash-bang - lines like #!...sciteco-wrapper -q8iom + lines like #!...sciteco -q8iom Command line arguments should then also be handled differently, passing them in an array or single string register, so they no longer affect the unnamed buffer. @@ -181,6 +205,12 @@ Features: between effective and rubbed out command line - without resetting it. This would add another alternative to { and } for fixing up a command line. + * Instead of discarding a rubbed out command line once the user + presses a non-matching key, a redo-tree could be built instead. + When you rub out to a character where the tree branches, + the next character typed always determines whether and which + existing redo branch will be activated (ie become the new + rubbed out command line). * some missing useful VideoTECO/TECO-11 commands and unnecessary incompatibilities: * EF with buffer id @@ -192,7 +222,8 @@ Features: e.g. for execute macro with string argument or as a special version of EI that considers $SCITECOPATH. Current use of EI (insert without string building) will have - to move, e.g. to FI. + to move, but it might vanish anyway once we can disable string building + with a special character. * ::S for string "comparisons" (anchored search). This is supposed to be an alias for .,.:FB which would be .,.:S in SciTECO. Apparanetly, the bounded search is still @@ -205,13 +236,18 @@ Features: * ^A, T and stdio in general * Search for beginning of string; i.e. a version of S that leaves dot before the search string, similar to FK - (request of N.M.). Could be called _ (a global-search variant - in classic TECO). + (request of N.M.). + Could be called <_> (a global-search variant in classic TECO). * Shortcut for cutting into Q-Register. Typing 10Xq10K is very annoying to type. We could use the @ modifier 10@Xq or define a new command, like ^X (search-mode flag in classic TECO). On the other hand, a search mode setting would be useful in SciTECO as well! + FX would be available as well, but is perhaps best reserved + for some mmenonics. + An elegant alternative might be to introduce single-character + stack operating commands for duplicating the last AND the last two + arguments. However, this will not help for cutting a number of lines. * For symmetry, there should be a command for -W, eg. P. Macros and modifiers are obviously not a solution here since they're too long. @@ -236,25 +272,22 @@ Features: * Command to free Q-Register (remove from table). e.g. FQ (free Q). :FQ could free by QRegister prefix name for the common use case of Q-Register subtables and lists. - * TECO syntax highlighting + * TECO syntax highlighting. + This should now be relatively easy to implement by reusing + the parser. * multiline commandline * perhaps use Scintilla view as mini buffer. This means patching Scintilla, so it does not break lines on new line characters. * A Scintilla view will allow syntax highlighting - * improve GTK interface - * proper command-line widget (best would be a Scintilla view, s.a.) - * speed improvements + * improve speed of GTK interface * command line could highlight dead branches (e.g. gray them out) - * backup files, or even better Journal files: - could write a Macro file for each modified file containing - only basic commands (no loops etc.). it is removed when the file is - saved. in case of an abnormal program termination the - journal file can be replayed. This could be done automatically - in the profile. * Add special Q-Register for dot: Would simplify inserting dot with string building and saving/restoring - dot on the QReg stack + dot on the QReg stack. + Since [. is currently not valid and [[.] is cumbersome, there should be + a special syntactic workaround to allow [.. or perhaps we'll simply call + it :, so you can write [: * EL command could also be used to convert all EOLs in the current buffer. * nEL should perhaps dirtify the buffer, at least when automatic @@ -267,17 +300,25 @@ Features: any changes another process could have done on the file. Therefore open buffers should be locked using the flock(), fcntl() or lockf() interfaces. On Windows we can even enforce mandatory locks. + A generic fallback could use lock files -- this would guard against + concurrent SciTECO instances at least. + * Multi-window support is probably never going to realize. + Perhaps we could add a Gtk-frontend option like -X for opening + all filenames with the same options in separate processes. + For the Curses version, you will need a shell wrapper, or we could + add an environment variable like $SCITECO_LAUNCH that can be set + to a command for launching a new terminal. + For multi-window SciTECO to work properly, file locking is probably + a must as it is otherwise too easy to confuse SciTECO if multiple + instances open the same file. * Touch restored save point files - should perhaps be configurable. This is important when working with Makefiles, as make looks at the modification times of files. - * Instead of implementing split screens, it is better to leave - tiling to programs dedicated to it (tmux, window manager). - SciTECO could create pseudo-terminals (see pty(7)), set up - one curses screen as the master of that PTY and spawn - a process accessing it as a slave (e.g. urxvt -pty-fd). - Each Scintilla view could then be associated with at most - one curses screen. - GTK+ would simply manage a list of windows. + * There should really be a backup mechanism. It would be relatively + easy to implement portably, by using timeout() on Curses. + The Gtk version can simply use a glib timer. + Backup files should NOT be hidden and the timeout should be + configurable (EJ?). * Error handling in SciTECO macros: Allow throwing errors with e.g. [n]EE$ where n is an error code, defaulting to 0 and description is the error string - there could be code-specific @@ -285,17 +326,36 @@ Features: Errors could be catched using a structured try-catch-like construct or by defining an error handling label. Macros may retrieve the code and string of the last error. + * Once we have our own function call stack, + it will be possible, although not trivial, to add support for + user-definable macros that accept string arguments, eg. + EMq$ + This will have to switch back and forth between the macro and + the invoking frame supplying the macro (similar to a coroutine). + In the most simple case, a special command returns the next character + produced by the callers string building machine including rubout and + the user will have to implement rubout-support manually. + For a lot of common cases, we could also allow a string building + construct that symbolizes the string parameter supplied by the + caller. This could activate interactive processing in the macro's + current command, allowing you to easily "wrap" interactive commands in + macros. The same construct would also be useful with + non-interactive commands as a way to store the supplied parameter + using EU for instance. * Adding a secret command line option to process immediate editing commands in batch mode with undo would allow us to add some test cases that otherwise only occur in interactive mode. + * Emscripten nodejs port. + This may be a viable way to run SciTECO "cross"-platform, at least + for evaluation... on UNIX-like systems in absence of prebuilt binaries. + I already got netbsd-curses to build against + Emscripten/nodejs using some UNIX shell wrapper calls, so practically + all of SciTECO can be run against nodejs as a runtime. + I'm not aware of any (working) alternatives, like cross-compiling + for the JVM. Optimizations: - * The Windows-specific memory limiting using GetProcessMemoryInfo() - is very slow. Perhaps malloc() hooking can be implemented there, - using _msize() to measure the memory required by individual chunks. - This must be benchmarked. - * Add G_UNLIKELY to all error throws. - * String::append() could be optimized by ORing a padding + * teco_string_append() could be optimized by ORing a padding into the realloc() size (e.g. 0xFF). However, this has not proven effective on Linux/glibc probably because it will already allocate in blocks of @@ -309,34 +369,6 @@ Optimizations: * commonly used (special) Q-Registers could be cached, saving the q-reg table lookup * refactor search commands (create proper base class) - * refactor match-char state machine using MicroStateMachine class - * The current C-like programming style of SciTECO causes - problems with C++'s RAII. Exceptions have to be caught - always in every stack frame that owns a heap object - (e.g. glib string). This is often hard to predict. - There are two solutions: Wrap - every such C pointer in a class that implements RAII, - e.g. using C++11 unique_ptr or by a custom class template. - The downside is meta-programming madness and lots of overloading - to make this convenient. - Alternatively, we could avoid C++ exceptions largely and - use a custom error reporting system similar to GError. - This makes error handling and forwarding explicit as in - plain C code. RTTI can be used to discern different - exception types. By adding an error code to the error object - (which we will need anyway for supporting error handling in SciTECO - macros), we may even avoid RTTI. - Should also allow us to completely disable exceptions via -fno-exceptions. - * RTTI could be disabled (-fno-rtti). It's only still required - because of StdError() for handling arbitrary C++ exceptions. - This is probably not required. - * RBTrees define an entry field for storing node color. - This can be avoided on most - platforms where G_MEM_ALIGN > 1 by encoding the color in the - lowest bit of one of the pointers. - The parent pointer is not required for RBTrees in general, - but we do use the PREV/NEXT ops to iterate prefixes which requires - the parent pointer to be maintained. * Add a configure-switch for LTO (--enable-lto). Documentation: @@ -349,4 +381,5 @@ Documentation: * Write a cheat sheet. Either on www.cheatography.com, or using Groff and include with SciTECO. * Write some tutorials for the Wiki, e.g. about paragraph - reflowing. + reflowing... + Object-oriented SciTECO ideoms etc. ;-) diff --git a/bootstrap.am b/bootstrap.am index 1ca730e..474a672 100644 --- a/bootstrap.am +++ b/bootstrap.am @@ -18,13 +18,11 @@ SCITECO_MINIMAL = @SCITECO@ SCITECO_FULL = @SCITECO@ endif -# Path of installed `sciteco` and `sciteco-wrapper` binaries, +# Path of installed `sciteco` binary, # taking --program-prefix into account. -# These variables MUST NOT be put in single-quotes. +# This variables MUST NOT be put in single-quotes. SCITECO_INSTALLED = \ $(bindir)/`echo sciteco | @SED@ '$(transform)'`$(EXEEXT) -SCITECO_WRAPPER_INSTALLED = \ - $(libexecdir)/`echo sciteco-wrapper | @SED@ '$(transform)'`$(EXEEXT) SUBST_MACRO = eb$<\e \ j \ diff --git a/compat/bsd/sys/cdefs.h b/compat/bsd/sys/cdefs.h deleted file mode 100644 index 4d9aa49..0000000 --- a/compat/bsd/sys/cdefs.h +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright © 2004, 2005, 2006, 2009 Guillem Jover - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL - * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef LIBBSD_CDEFS_H -#define LIBBSD_CDEFS_H - -#include - -#ifndef __dead2 -# define __dead2 -#endif - -#ifndef __pure2 -# define __pure2 -#endif - -/* Linux headers define a struct with a member names __unused. - * Debian bugs: #522773 (linux), #522774 (libc). - * Disable for now. */ -#if 0 -#ifndef __unused -# ifdef __GNUC__ -# define __unused __attribute__((unused)) -# else -# define __unused -# endif -#endif -#endif - -#ifndef __printflike -# ifdef __GNUC__ -# define __printflike(x, y) __attribute((format(printf, (x), (y)))) -# else -# define __printflike(x, y) -# endif -#endif - -#ifndef __bounded__ -# define __bounded__(x, y, z) -#endif - -#ifndef __RCSID -# define __RCSID(x) -#endif - -#ifndef __FBSDID -# define __FBSDID(x) -#endif - -#ifndef __RCSID -# define __RCSID(x) -#endif - -#ifndef __RCSID_SOURCE -# define __RCSID_SOURCE(x) -#endif - -#ifndef __SCCSID -# define __SCCSID(x) -#endif - -#ifndef __COPYRIGHT -# define __COPYRIGHT(x) -#endif - -#endif diff --git a/compat/bsd/sys/queue.h b/compat/bsd/sys/queue.h deleted file mode 100644 index 9101cf0..0000000 --- a/compat/bsd/sys/queue.h +++ /dev/null @@ -1,620 +0,0 @@ -/*- - * Copyright (c) 1991, 1993 - * The Regents of the University of California. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 4. Neither the name of the University nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY - * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - * - * @(#)queue.h 8.5 (Berkeley) 8/20/94 - * $FreeBSD$ - */ - -#ifndef _SYS_QUEUE_H_ -#define _SYS_QUEUE_H_ - -/* - * This file defines four types of data structures: singly-linked lists, - * singly-linked tail queues, lists and tail queues. - * - * A singly-linked list is headed by a single forward pointer. The elements - * are singly linked for minimum space and pointer manipulation overhead at - * the expense of O(n) removal for arbitrary elements. New elements can be - * added to the list after an existing element or at the head of the list. - * Elements being removed from the head of the list should use the explicit - * macro for this purpose for optimum efficiency. A singly-linked list may - * only be traversed in the forward direction. Singly-linked lists are ideal - * for applications with large datasets and few or no removals or for - * implementing a LIFO queue. - * - * A singly-linked tail queue is headed by a pair of pointers, one to the - * head of the list and the other to the tail of the list. The elements are - * singly linked for minimum space and pointer manipulation overhead at the - * expense of O(n) removal for arbitrary elements. New elements can be added - * to the list after an existing element, at the head of the list, or at the - * end of the list. Elements being removed from the head of the tail queue - * should use the explicit macro for this purpose for optimum efficiency. - * A singly-linked tail queue may only be traversed in the forward direction. - * Singly-linked tail queues are ideal for applications with large datasets - * and few or no removals or for implementing a FIFO queue. - * - * A list is headed by a single forward pointer (or an array of forward - * pointers for a hash table header). The elements are doubly linked - * so that an arbitrary element can be removed without a need to - * traverse the list. New elements can be added to the list before - * or after an existing element or at the head of the list. A list - * may only be traversed in the forward direction. - * - * A tail queue is headed by a pair of pointers, one to the head of the - * list and the other to the tail of the list. The elements are doubly - * linked so that an arbitrary element can be removed without a need to - * traverse the list. New elements can be added to the list before or - * after an existing element, at the head of the list, or at the end of - * the list. A tail queue may be traversed in either direction. - * - * For details on the use of these macros, see the queue(3) manual page. - * - * - * SLIST LIST STAILQ TAILQ - * _HEAD + + + + - * _HEAD_INITIALIZER + + + + - * _ENTRY + + + + - * _INIT + + + + - * _EMPTY + + + + - * _FIRST + + + + - * _NEXT + + + + - * _PREV - - - + - * _LAST - - + + - * _FOREACH + + + + - * _FOREACH_SAFE + + + + - * _FOREACH_REVERSE - - - + - * _FOREACH_REVERSE_SAFE - - - + - * _INSERT_HEAD + + + + - * _INSERT_BEFORE - + - + - * _INSERT_AFTER + + + + - * _INSERT_TAIL - - + + - * _CONCAT - - + + - * _REMOVE_AFTER + - + - - * _REMOVE_HEAD + - + - - * _REMOVE + + + + - * - */ -#ifdef QUEUE_MACRO_DEBUG -/* Store the last 2 places the queue element or head was altered */ -struct qm_trace { - char * lastfile; - int lastline; - char * prevfile; - int prevline; -}; - -#define TRACEBUF struct qm_trace trace; -#define TRASHIT(x) do {(x) = (void *)-1;} while (0) - -#define QMD_TRACE_HEAD(head) do { \ - (head)->trace.prevline = (head)->trace.lastline; \ - (head)->trace.prevfile = (head)->trace.lastfile; \ - (head)->trace.lastline = __LINE__; \ - (head)->trace.lastfile = __FILE__; \ -} while (0) - -#define QMD_TRACE_ELEM(elem) do { \ - (elem)->trace.prevline = (elem)->trace.lastline; \ - (elem)->trace.prevfile = (elem)->trace.lastfile; \ - (elem)->trace.lastline = __LINE__; \ - (elem)->trace.lastfile = __FILE__; \ -} while (0) - -#else -#define QMD_TRACE_ELEM(elem) -#define QMD_TRACE_HEAD(head) -#define TRACEBUF -#define TRASHIT(x) -#endif /* QUEUE_MACRO_DEBUG */ - -/* - * Singly-linked List declarations. - */ -#define SLIST_HEAD(name, type) \ -struct name { \ - struct type *slh_first; /* first element */ \ -} - -#define SLIST_HEAD_INITIALIZER(head) \ - { NULL } - -#define SLIST_ENTRY(type) \ -struct { \ - struct type *sle_next; /* next element */ \ -} - -/* - * Singly-linked List functions. - */ -#define SLIST_EMPTY(head) ((head)->slh_first == NULL) - -#define SLIST_FIRST(head) ((head)->slh_first) - -#define SLIST_FOREACH(var, head, field) \ - for ((var) = SLIST_FIRST((head)); \ - (var); \ - (var) = SLIST_NEXT((var), field)) - -#define SLIST_FOREACH_SAFE(var, head, field, tvar) \ - for ((var) = SLIST_FIRST((head)); \ - (var) && ((tvar) = SLIST_NEXT((var), field), 1); \ - (var) = (tvar)) - -#define SLIST_FOREACH_PREVPTR(var, varp, head, field) \ - for ((varp) = &SLIST_FIRST((head)); \ - ((var) = *(varp)) != NULL; \ - (varp) = &SLIST_NEXT((var), field)) - -#define SLIST_INIT(head) do { \ - SLIST_FIRST((head)) = NULL; \ -} while (0) - -#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \ - SLIST_NEXT((elm), field) = SLIST_NEXT((slistelm), field); \ - SLIST_NEXT((slistelm), field) = (elm); \ -} while (0) - -#define SLIST_INSERT_HEAD(head, elm, field) do { \ - SLIST_NEXT((elm), field) = SLIST_FIRST((head)); \ - SLIST_FIRST((head)) = (elm); \ -} while (0) - -#define SLIST_NEXT(elm, field) ((elm)->field.sle_next) - -#define SLIST_REMOVE(head, elm, type, field) do { \ - if (SLIST_FIRST((head)) == (elm)) { \ - SLIST_REMOVE_HEAD((head), field); \ - } \ - else { \ - struct type *curelm = SLIST_FIRST((head)); \ - while (SLIST_NEXT(curelm, field) != (elm)) \ - curelm = SLIST_NEXT(curelm, field); \ - SLIST_REMOVE_AFTER(curelm, field); \ - } \ - TRASHIT((elm)->field.sle_next); \ -} while (0) - -#define SLIST_REMOVE_AFTER(elm, field) do { \ - SLIST_NEXT(elm, field) = \ - SLIST_NEXT(SLIST_NEXT(elm, field), field); \ -} while (0) - -#define SLIST_REMOVE_HEAD(head, field) do { \ - SLIST_FIRST((head)) = SLIST_NEXT(SLIST_FIRST((head)), field); \ -} while (0) - -/* - * Singly-linked Tail queue declarations. - */ -#define STAILQ_HEAD(name, type) \ -struct name { \ - struct type *stqh_first;/* first element */ \ - struct type **stqh_last;/* addr of last next element */ \ -} - -#define STAILQ_HEAD_INITIALIZER(head) \ - { NULL, &(head).stqh_first } - -#define STAILQ_ENTRY(type) \ -struct { \ - struct type *stqe_next; /* next element */ \ -} - -/* - * Singly-linked Tail queue functions. - */ -#define STAILQ_CONCAT(head1, head2) do { \ - if (!STAILQ_EMPTY((head2))) { \ - *(head1)->stqh_last = (head2)->stqh_first; \ - (head1)->stqh_last = (head2)->stqh_last; \ - STAILQ_INIT((head2)); \ - } \ -} while (0) - -#define STAILQ_EMPTY(head) ((head)->stqh_first == NULL) - -#define STAILQ_FIRST(head) ((head)->stqh_first) - -#define STAILQ_FOREACH(var, head, field) \ - for((var) = STAILQ_FIRST((head)); \ - (var); \ - (var) = STAILQ_NEXT((var), field)) - - -#define STAILQ_FOREACH_SAFE(var, head, field, tvar) \ - for ((var) = STAILQ_FIRST((head)); \ - (var) && ((tvar) = STAILQ_NEXT((var), field), 1); \ - (var) = (tvar)) - -#define STAILQ_INIT(head) do { \ - STAILQ_FIRST((head)) = NULL; \ - (head)->stqh_last = &STAILQ_FIRST((head)); \ -} while (0) - -#define STAILQ_INSERT_AFTER(head, tqelm, elm, field) do { \ - if ((STAILQ_NEXT((elm), field) = STAILQ_NEXT((tqelm), field)) == NULL)\ - (head)->stqh_last = &STAILQ_NEXT((elm), field); \ - STAILQ_NEXT((tqelm), field) = (elm); \ -} while (0) - -#define STAILQ_INSERT_HEAD(head, elm, field) do { \ - if ((STAILQ_NEXT((elm), field) = STAILQ_FIRST((head))) == NULL) \ - (head)->stqh_last = &STAILQ_NEXT((elm), field); \ - STAILQ_FIRST((head)) = (elm); \ -} while (0) - -#define STAILQ_INSERT_TAIL(head, elm, field) do { \ - STAILQ_NEXT((elm), field) = NULL; \ - *(head)->stqh_last = (elm); \ - (head)->stqh_last = &STAILQ_NEXT((elm), field); \ -} while (0) - -#define STAILQ_LAST(head, type, field) \ - (STAILQ_EMPTY((head)) ? \ - NULL : \ - ((struct type *)(void *) \ - ((char *)((head)->stqh_last) - __offsetof(struct type, field)))) - -#define STAILQ_NEXT(elm, field) ((elm)->field.stqe_next) - -#define STAILQ_REMOVE(head, elm, type, field) do { \ - if (STAILQ_FIRST((head)) == (elm)) { \ - STAILQ_REMOVE_HEAD((head), field); \ - } \ - else { \ - struct type *curelm = STAILQ_FIRST((head)); \ - while (STAILQ_NEXT(curelm, field) != (elm)) \ - curelm = STAILQ_NEXT(curelm, field); \ - STAILQ_REMOVE_AFTER(head, curelm, field); \ - } \ - TRASHIT((elm)->field.stqe_next); \ -} while (0) - -#define STAILQ_REMOVE_HEAD(head, field) do { \ - if ((STAILQ_FIRST((head)) = \ - STAILQ_NEXT(STAILQ_FIRST((head)), field)) == NULL) \ - (head)->stqh_last = &STAILQ_FIRST((head)); \ -} while (0) - -#define STAILQ_REMOVE_AFTER(head, elm, field) do { \ - if ((STAILQ_NEXT(elm, field) = \ - STAILQ_NEXT(STAILQ_NEXT(elm, field), field)) == NULL) \ - (head)->stqh_last = &STAILQ_NEXT((elm), field); \ -} while (0) - -#define STAILQ_SWAP(head1, head2, type) do { \ - struct type *swap_first = STAILQ_FIRST(head1); \ - struct type **swap_last = (head1)->stqh_last; \ - STAILQ_FIRST(head1) = STAILQ_FIRST(head2); \ - (head1)->stqh_last = (head2)->stqh_last; \ - STAILQ_FIRST(head2) = swap_first; \ - (head2)->stqh_last = swap_last; \ - if (STAILQ_EMPTY(head1)) \ - (head1)->stqh_last = &STAILQ_FIRST(head1); \ - if (STAILQ_EMPTY(head2)) \ - (head2)->stqh_last = &STAILQ_FIRST(head2); \ -} while (0) - - -/* - * List declarations. - */ -#define LIST_HEAD(name, type) \ -struct name { \ - struct type *lh_first; /* first element */ \ -} - -#define LIST_HEAD_INITIALIZER(head) \ - { NULL } - -#define LIST_ENTRY(type) \ -struct { \ - struct type *le_next; /* next element */ \ - struct type **le_prev; /* address of previous next element */ \ -} - -/* - * List functions. - */ - -#if (defined(_KERNEL) && defined(INVARIANTS)) -#define QMD_LIST_CHECK_HEAD(head, field) do { \ - if (LIST_FIRST((head)) != NULL && \ - LIST_FIRST((head))->field.le_prev != \ - &LIST_FIRST((head))) \ - panic("Bad list head %p first->prev != head", (head)); \ -} while (0) - -#define QMD_LIST_CHECK_NEXT(elm, field) do { \ - if (LIST_NEXT((elm), field) != NULL && \ - LIST_NEXT((elm), field)->field.le_prev != \ - &((elm)->field.le_next)) \ - panic("Bad link elm %p next->prev != elm", (elm)); \ -} while (0) - -#define QMD_LIST_CHECK_PREV(elm, field) do { \ - if (*(elm)->field.le_prev != (elm)) \ - panic("Bad link elm %p prev->next != elm", (elm)); \ -} while (0) -#else -#define QMD_LIST_CHECK_HEAD(head, field) -#define QMD_LIST_CHECK_NEXT(elm, field) -#define QMD_LIST_CHECK_PREV(elm, field) -#endif /* (_KERNEL && INVARIANTS) */ - -#define LIST_EMPTY(head) ((head)->lh_first == NULL) - -#define LIST_FIRST(head) ((head)->lh_first) - -#define LIST_FOREACH(var, head, field) \ - for ((var) = LIST_FIRST((head)); \ - (var); \ - (var) = LIST_NEXT((var), field)) - -#define LIST_FOREACH_SAFE(var, head, field, tvar) \ - for ((var) = LIST_FIRST((head)); \ - (var) && ((tvar) = LIST_NEXT((var), field), 1); \ - (var) = (tvar)) - -#define LIST_INIT(head) do { \ - LIST_FIRST((head)) = NULL; \ -} while (0) - -#define LIST_INSERT_AFTER(listelm, elm, field) do { \ - QMD_LIST_CHECK_NEXT(listelm, field); \ - if ((LIST_NEXT((elm), field) = LIST_NEXT((listelm), field)) != NULL)\ - LIST_NEXT((listelm), field)->field.le_prev = \ - &LIST_NEXT((elm), field); \ - LIST_NEXT((listelm), field) = (elm); \ - (elm)->field.le_prev = &LIST_NEXT((listelm), field); \ -} while (0) - -#define LIST_INSERT_BEFORE(listelm, elm, field) do { \ - QMD_LIST_CHECK_PREV(listelm, field); \ - (elm)->field.le_prev = (listelm)->field.le_prev; \ - LIST_NEXT((elm), field) = (listelm); \ - *(listelm)->field.le_prev = (elm); \ - (listelm)->field.le_prev = &LIST_NEXT((elm), field); \ -} while (0) - -#define LIST_INSERT_HEAD(head, elm, field) do { \ - QMD_LIST_CHECK_HEAD((head), field); \ - if ((LIST_NEXT((elm), field) = LIST_FIRST((head))) != NULL) \ - LIST_FIRST((head))->field.le_prev = &LIST_NEXT((elm), field);\ - LIST_FIRST((head)) = (elm); \ - (elm)->field.le_prev = &LIST_FIRST((head)); \ -} while (0) - -#define LIST_NEXT(elm, field) ((elm)->field.le_next) - -#define LIST_REMOVE(elm, field) do { \ - QMD_LIST_CHECK_NEXT(elm, field); \ - QMD_LIST_CHECK_PREV(elm, field); \ - if (LIST_NEXT((elm), field) != NULL) \ - LIST_NEXT((elm), field)->field.le_prev = \ - (elm)->field.le_prev; \ - *(elm)->field.le_prev = LIST_NEXT((elm), field); \ - TRASHIT((elm)->field.le_next); \ - TRASHIT((elm)->field.le_prev); \ -} while (0) - -#define LIST_SWAP(head1, head2, type, field) do { \ - struct type *swap_tmp = LIST_FIRST((head1)); \ - LIST_FIRST((head1)) = LIST_FIRST((head2)); \ - LIST_FIRST((head2)) = swap_tmp; \ - if ((swap_tmp = LIST_FIRST((head1))) != NULL) \ - swap_tmp->field.le_prev = &LIST_FIRST((head1)); \ - if ((swap_tmp = LIST_FIRST((head2))) != NULL) \ - swap_tmp->field.le_prev = &LIST_FIRST((head2)); \ -} while (0) - -/* - * Tail queue declarations. - */ -#define TAILQ_HEAD(name, type) \ -struct name { \ - struct type *tqh_first; /* first element */ \ - struct type **tqh_last; /* addr of last next element */ \ - TRACEBUF \ -} - -#define TAILQ_HEAD_INITIALIZER(head) \ - { NULL, &(head).tqh_first } - -#define TAILQ_ENTRY(type) \ -struct { \ - struct type *tqe_next; /* next element */ \ - struct type **tqe_prev; /* address of previous next element */ \ - TRACEBUF \ -} - -/* - * Tail queue functions. - */ -#if (defined(_KERNEL) && defined(INVARIANTS)) -#define QMD_TAILQ_CHECK_HEAD(head, field) do { \ - if (!TAILQ_EMPTY(head) && \ - TAILQ_FIRST((head))->field.tqe_prev != \ - &TAILQ_FIRST((head))) \ - panic("Bad tailq head %p first->prev != head", (head)); \ -} while (0) - -#define QMD_TAILQ_CHECK_TAIL(head, field) do { \ - if (*(head)->tqh_last != NULL) \ - panic("Bad tailq NEXT(%p->tqh_last) != NULL", (head)); \ -} while (0) - -#define QMD_TAILQ_CHECK_NEXT(elm, field) do { \ - if (TAILQ_NEXT((elm), field) != NULL && \ - TAILQ_NEXT((elm), field)->field.tqe_prev != \ - &((elm)->field.tqe_next)) \ - panic("Bad link elm %p next->prev != elm", (elm)); \ -} while (0) - -#define QMD_TAILQ_CHECK_PREV(elm, field) do { \ - if (*(elm)->field.tqe_prev != (elm)) \ - panic("Bad link elm %p prev->next != elm", (elm)); \ -} while (0) -#else -#define QMD_TAILQ_CHECK_HEAD(head, field) -#define QMD_TAILQ_CHECK_TAIL(head, headname) -#define QMD_TAILQ_CHECK_NEXT(elm, field) -#define QMD_TAILQ_CHECK_PREV(elm, field) -#endif /* (_KERNEL && INVARIANTS) */ - -#define TAILQ_CONCAT(head1, head2, field) do { \ - if (!TAILQ_EMPTY(head2)) { \ - *(head1)->tqh_last = (head2)->tqh_first; \ - (head2)->tqh_first->field.tqe_prev = (head1)->tqh_last; \ - (head1)->tqh_last = (head2)->tqh_last; \ - TAILQ_INIT((head2)); \ - QMD_TRACE_HEAD(head1); \ - QMD_TRACE_HEAD(head2); \ - } \ -} while (0) - -#define TAILQ_EMPTY(head) ((head)->tqh_first == NULL) - -#define TAILQ_FIRST(head) ((head)->tqh_first) - -#define TAILQ_FOREACH(var, head, field) \ - for ((var) = TAILQ_FIRST((head)); \ - (var); \ - (var) = TAILQ_NEXT((var), field)) - -#define TAILQ_FOREACH_SAFE(var, head, field, tvar) \ - for ((var) = TAILQ_FIRST((head)); \ - (var) && ((tvar) = TAILQ_NEXT((var), field), 1); \ - (var) = (tvar)) - -#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \ - for ((var) = TAILQ_LAST((head), headname); \ - (var); \ - (var) = TAILQ_PREV((var), headname, field)) - -#define TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar) \ - for ((var) = TAILQ_LAST((head), headname); \ - (var) && ((tvar) = TAILQ_PREV((var), headname, field), 1); \ - (var) = (tvar)) - -#define TAILQ_INIT(head) do { \ - TAILQ_FIRST((head)) = NULL; \ - (head)->tqh_last = &TAILQ_FIRST((head)); \ - QMD_TRACE_HEAD(head); \ -} while (0) - -#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do { \ - QMD_TAILQ_CHECK_NEXT(listelm, field); \ - if ((TAILQ_NEXT((elm), field) = TAILQ_NEXT((listelm), field)) != NULL)\ - TAILQ_NEXT((elm), field)->field.tqe_prev = \ - &TAILQ_NEXT((elm), field); \ - else { \ - (head)->tqh_last = &TAILQ_NEXT((elm), field); \ - QMD_TRACE_HEAD(head); \ - } \ - TAILQ_NEXT((listelm), field) = (elm); \ - (elm)->field.tqe_prev = &TAILQ_NEXT((listelm), field); \ - QMD_TRACE_ELEM(&(elm)->field); \ - QMD_TRACE_ELEM(&listelm->field); \ -} while (0) - -#define TAILQ_INSERT_BEFORE(listelm, elm, field) do { \ - QMD_TAILQ_CHECK_PREV(listelm, field); \ - (elm)->field.tqe_prev = (listelm)->field.tqe_prev; \ - TAILQ_NEXT((elm), field) = (listelm); \ - *(listelm)->field.tqe_prev = (elm); \ - (listelm)->field.tqe_prev = &TAILQ_NEXT((elm), field); \ - QMD_TRACE_ELEM(&(elm)->field); \ - QMD_TRACE_ELEM(&listelm->field); \ -} while (0) - -#define TAILQ_INSERT_HEAD(head, elm, field) do { \ - QMD_TAILQ_CHECK_HEAD(head, field); \ - if ((TAILQ_NEXT((elm), field) = TAILQ_FIRST((head))) != NULL) \ - TAILQ_FIRST((head))->field.tqe_prev = \ - &TAILQ_NEXT((elm), field); \ - else \ - (head)->tqh_last = &TAILQ_NEXT((elm), field); \ - TAILQ_FIRST((head)) = (elm); \ - (elm)->field.tqe_prev = &TAILQ_FIRST((head)); \ - QMD_TRACE_HEAD(head); \ - QMD_TRACE_ELEM(&(elm)->field); \ -} while (0) - -#define TAILQ_INSERT_TAIL(head, elm, field) do { \ - QMD_TAILQ_CHECK_TAIL(head, field); \ - TAILQ_NEXT((elm), field) = NULL; \ - (elm)->field.tqe_prev = (head)->tqh_last; \ - *(head)->tqh_last = (elm); \ - (head)->tqh_last = &TAILQ_NEXT((elm), field); \ - QMD_TRACE_HEAD(head); \ - QMD_TRACE_ELEM(&(elm)->field); \ -} while (0) - -#define TAILQ_LAST(head, headname) \ - (*(((struct headname *)((head)->tqh_last))->tqh_last)) - -#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next) - -#define TAILQ_PREV(elm, headname, field) \ - (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last)) - -#define TAILQ_REMOVE(head, elm, field) do { \ - QMD_TAILQ_CHECK_NEXT(elm, field); \ - QMD_TAILQ_CHECK_PREV(elm, field); \ - if ((TAILQ_NEXT((elm), field)) != NULL) \ - TAILQ_NEXT((elm), field)->field.tqe_prev = \ - (elm)->field.tqe_prev; \ - else { \ - (head)->tqh_last = (elm)->field.tqe_prev; \ - QMD_TRACE_HEAD(head); \ - } \ - *(elm)->field.tqe_prev = TAILQ_NEXT((elm), field); \ - TRASHIT((elm)->field.tqe_next); \ - TRASHIT((elm)->field.tqe_prev); \ - QMD_TRACE_ELEM(&(elm)->field); \ -} while (0) - -#define TAILQ_SWAP(head1, head2, type, field) do { \ - struct type *swap_first = (head1)->tqh_first; \ - struct type **swap_last = (head1)->tqh_last; \ - (head1)->tqh_first = (head2)->tqh_first; \ - (head1)->tqh_last = (head2)->tqh_last; \ - (head2)->tqh_first = swap_first; \ - (head2)->tqh_last = swap_last; \ - if ((swap_first = (head1)->tqh_first) != NULL) \ - swap_first->field.tqe_prev = &(head1)->tqh_first; \ - else \ - (head1)->tqh_last = &(head1)->tqh_first; \ - if ((swap_first = (head2)->tqh_first) != NULL) \ - swap_first->field.tqe_prev = &(head2)->tqh_first; \ - else \ - (head2)->tqh_last = &(head2)->tqh_first; \ -} while (0) - -#endif /* !_SYS_QUEUE_H_ */ diff --git a/compat/bsd/sys/tree.h b/compat/bsd/sys/tree.h deleted file mode 100644 index a7626ea..0000000 --- a/compat/bsd/sys/tree.h +++ /dev/null @@ -1,763 +0,0 @@ -/* $NetBSD: tree.h,v 1.8 2004/03/28 19:38:30 provos Exp $ */ -/* $OpenBSD: tree.h,v 1.7 2002/10/17 21:51:54 art Exp $ */ -/* $FreeBSD$ */ - -/*- - * Copyright 2002 Niels Provos - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef _SYS_TREE_H_ -#define _SYS_TREE_H_ - -/* - * This file defines data structures for different types of trees: - * splay trees and red-black trees. - * - * A splay tree is a self-organizing data structure. Every operation - * on the tree causes a splay to happen. The splay moves the requested - * node to the root of the tree and partly rebalances it. - * - * This has the benefit that request locality causes faster lookups as - * the requested nodes move to the top of the tree. On the other hand, - * every lookup causes memory writes. - * - * The Balance Theorem bounds the total access time for m operations - * and n inserts on an initially empty tree as O((m + n)lg n). The - * amortized cost for a sequence of m accesses to a splay tree is O(lg n); - * - * A red-black tree is a binary search tree with the node color as an - * extra attribute. It fulfills a set of conditions: - * - every search path from the root to a leaf consists of the - * same number of black nodes, - * - each red node (except for the root) has a black parent, - * - each leaf node is black. - * - * Every operation on a red-black tree is bounded as O(lg n). - * The maximum height of a red-black tree is 2lg (n+1). - */ - -#define SPLAY_HEAD(name, type) \ -struct name { \ - struct type *sph_root; /* root of the tree */ \ -} - -#define SPLAY_INITIALIZER(root) \ - { NULL } - -#define SPLAY_INIT(root) do { \ - (root)->sph_root = NULL; \ -} while (/*CONSTCOND*/ 0) - -#define SPLAY_ENTRY(type) \ -struct { \ - struct type *spe_left; /* left element */ \ - struct type *spe_right; /* right element */ \ -} - -#define SPLAY_LEFT(elm, field) (elm)->field.spe_left -#define SPLAY_RIGHT(elm, field) (elm)->field.spe_right -#define SPLAY_ROOT(head) (head)->sph_root -#define SPLAY_EMPTY(head) (SPLAY_ROOT(head) == NULL) - -/* SPLAY_ROTATE_{LEFT,RIGHT} expect that tmp hold SPLAY_{RIGHT,LEFT} */ -#define SPLAY_ROTATE_RIGHT(head, tmp, field) do { \ - SPLAY_LEFT((head)->sph_root, field) = SPLAY_RIGHT(tmp, field); \ - SPLAY_RIGHT(tmp, field) = (head)->sph_root; \ - (head)->sph_root = tmp; \ -} while (/*CONSTCOND*/ 0) - -#define SPLAY_ROTATE_LEFT(head, tmp, field) do { \ - SPLAY_RIGHT((head)->sph_root, field) = SPLAY_LEFT(tmp, field); \ - SPLAY_LEFT(tmp, field) = (head)->sph_root; \ - (head)->sph_root = tmp; \ -} while (/*CONSTCOND*/ 0) - -#define SPLAY_LINKLEFT(head, tmp, field) do { \ - SPLAY_LEFT(tmp, field) = (head)->sph_root; \ - tmp = (head)->sph_root; \ - (head)->sph_root = SPLAY_LEFT((head)->sph_root, field); \ -} while (/*CONSTCOND*/ 0) - -#define SPLAY_LINKRIGHT(head, tmp, field) do { \ - SPLAY_RIGHT(tmp, field) = (head)->sph_root; \ - tmp = (head)->sph_root; \ - (head)->sph_root = SPLAY_RIGHT((head)->sph_root, field); \ -} while (/*CONSTCOND*/ 0) - -#define SPLAY_ASSEMBLE(head, node, left, right, field) do { \ - SPLAY_RIGHT(left, field) = SPLAY_LEFT((head)->sph_root, field); \ - SPLAY_LEFT(right, field) = SPLAY_RIGHT((head)->sph_root, field);\ - SPLAY_LEFT((head)->sph_root, field) = SPLAY_RIGHT(node, field); \ - SPLAY_RIGHT((head)->sph_root, field) = SPLAY_LEFT(node, field); \ -} while (/*CONSTCOND*/ 0) - -/* Generates prototypes and inline functions */ - -#define SPLAY_PROTOTYPE(name, type, field, cmp) \ -void name##_SPLAY(struct name *, struct type *); \ -void name##_SPLAY_MINMAX(struct name *, int); \ -struct type *name##_SPLAY_INSERT(struct name *, struct type *); \ -struct type *name##_SPLAY_REMOVE(struct name *, struct type *); \ - \ -/* Finds the node with the same key as elm */ \ -static __inline struct type * \ -name##_SPLAY_FIND(struct name *head, struct type *elm) \ -{ \ - if (SPLAY_EMPTY(head)) \ - return(NULL); \ - name##_SPLAY(head, elm); \ - if ((cmp)(elm, (head)->sph_root) == 0) \ - return (head->sph_root); \ - return (NULL); \ -} \ - \ -static __inline struct type * \ -name##_SPLAY_NEXT(struct name *head, struct type *elm) \ -{ \ - name##_SPLAY(head, elm); \ - if (SPLAY_RIGHT(elm, field) != NULL) { \ - elm = SPLAY_RIGHT(elm, field); \ - while (SPLAY_LEFT(elm, field) != NULL) { \ - elm = SPLAY_LEFT(elm, field); \ - } \ - } else \ - elm = NULL; \ - return (elm); \ -} \ - \ -static __inline struct type * \ -name##_SPLAY_MIN_MAX(struct name *head, int val) \ -{ \ - name##_SPLAY_MINMAX(head, val); \ - return (SPLAY_ROOT(head)); \ -} - -/* Main splay operation. - * Moves node close to the key of elm to top - */ -#define SPLAY_GENERATE(name, type, field, cmp) \ -struct type * \ -name##_SPLAY_INSERT(struct name *head, struct type *elm) \ -{ \ - if (SPLAY_EMPTY(head)) { \ - SPLAY_LEFT(elm, field) = SPLAY_RIGHT(elm, field) = NULL; \ - } else { \ - int __comp; \ - name##_SPLAY(head, elm); \ - __comp = (cmp)(elm, (head)->sph_root); \ - if(__comp < 0) { \ - SPLAY_LEFT(elm, field) = SPLAY_LEFT((head)->sph_root, field);\ - SPLAY_RIGHT(elm, field) = (head)->sph_root; \ - SPLAY_LEFT((head)->sph_root, field) = NULL; \ - } else if (__comp > 0) { \ - SPLAY_RIGHT(elm, field) = SPLAY_RIGHT((head)->sph_root, field);\ - SPLAY_LEFT(elm, field) = (head)->sph_root; \ - SPLAY_RIGHT((head)->sph_root, field) = NULL; \ - } else \ - return ((head)->sph_root); \ - } \ - (head)->sph_root = (elm); \ - return (NULL); \ -} \ - \ -struct type * \ -name##_SPLAY_REMOVE(struct name *head, struct type *elm) \ -{ \ - struct type *__tmp; \ - if (SPLAY_EMPTY(head)) \ - return (NULL); \ - name##_SPLAY(head, elm); \ - if ((cmp)(elm, (head)->sph_root) == 0) { \ - if (SPLAY_LEFT((head)->sph_root, field) == NULL) { \ - (head)->sph_root = SPLAY_RIGHT((head)->sph_root, field);\ - } else { \ - __tmp = SPLAY_RIGHT((head)->sph_root, field); \ - (head)->sph_root = SPLAY_LEFT((head)->sph_root, field);\ - name##_SPLAY(head, elm); \ - SPLAY_RIGHT((head)->sph_root, field) = __tmp; \ - } \ - return (elm); \ - } \ - return (NULL); \ -} \ - \ -void \ -name##_SPLAY(struct name *head, struct type *elm) \ -{ \ - struct type __node, *__left, *__right, *__tmp; \ - int __comp; \ -\ - SPLAY_LEFT(&__node, field) = SPLAY_RIGHT(&__node, field) = NULL;\ - __left = __right = &__node; \ -\ - while ((__comp = (cmp)(elm, (head)->sph_root)) != 0) { \ - if (__comp < 0) { \ - __tmp = SPLAY_LEFT((head)->sph_root, field); \ - if (__tmp == NULL) \ - break; \ - if ((cmp)(elm, __tmp) < 0){ \ - SPLAY_ROTATE_RIGHT(head, __tmp, field); \ - if (SPLAY_LEFT((head)->sph_root, field) == NULL)\ - break; \ - } \ - SPLAY_LINKLEFT(head, __right, field); \ - } else if (__comp > 0) { \ - __tmp = SPLAY_RIGHT((head)->sph_root, field); \ - if (__tmp == NULL) \ - break; \ - if ((cmp)(elm, __tmp) > 0){ \ - SPLAY_ROTATE_LEFT(head, __tmp, field); \ - if (SPLAY_RIGHT((head)->sph_root, field) == NULL)\ - break; \ - } \ - SPLAY_LINKRIGHT(head, __left, field); \ - } \ - } \ - SPLAY_ASSEMBLE(head, &__node, __left, __right, field); \ -} \ - \ -/* Splay with either the minimum or the maximum element \ - * Used to find minimum or maximum element in tree. \ - */ \ -void name##_SPLAY_MINMAX(struct name *head, int __comp) \ -{ \ - struct type __node, *__left, *__right, *__tmp; \ -\ - SPLAY_LEFT(&__node, field) = SPLAY_RIGHT(&__node, field) = NULL;\ - __left = __right = &__node; \ -\ - while (1) { \ - if (__comp < 0) { \ - __tmp = SPLAY_LEFT((head)->sph_root, field); \ - if (__tmp == NULL) \ - break; \ - if (__comp < 0){ \ - SPLAY_ROTATE_RIGHT(head, __tmp, field); \ - if (SPLAY_LEFT((head)->sph_root, field) == NULL)\ - break; \ - } \ - SPLAY_LINKLEFT(head, __right, field); \ - } else if (__comp > 0) { \ - __tmp = SPLAY_RIGHT((head)->sph_root, field); \ - if (__tmp == NULL) \ - break; \ - if (__comp > 0) { \ - SPLAY_ROTATE_LEFT(head, __tmp, field); \ - if (SPLAY_RIGHT((head)->sph_root, field) == NULL)\ - break; \ - } \ - SPLAY_LINKRIGHT(head, __left, field); \ - } \ - } \ - SPLAY_ASSEMBLE(head, &__node, __left, __right, field); \ -} - -#define SPLAY_NEGINF -1 -#define SPLAY_INF 1 - -#define SPLAY_INSERT(name, x, y) name##_SPLAY_INSERT(x, y) -#define SPLAY_REMOVE(name, x, y) name##_SPLAY_REMOVE(x, y) -#define SPLAY_FIND(name, x, y) name##_SPLAY_FIND(x, y) -#define SPLAY_NEXT(name, x, y) name##_SPLAY_NEXT(x, y) -#define SPLAY_MIN(name, x) (SPLAY_EMPTY(x) ? NULL \ - : name##_SPLAY_MIN_MAX(x, SPLAY_NEGINF)) -#define SPLAY_MAX(name, x) (SPLAY_EMPTY(x) ? NULL \ - : name##_SPLAY_MIN_MAX(x, SPLAY_INF)) - -#define SPLAY_FOREACH(x, name, head) \ - for ((x) = SPLAY_MIN(name, head); \ - (x) != NULL; \ - (x) = SPLAY_NEXT(name, head, x)) - -/* Macros that define a red-black tree */ -#define RB_HEAD(name, type) \ -struct name { \ - struct type *rbh_root; /* root of the tree */ \ -} - -#define RB_INITIALIZER(root) \ - { NULL } - -#define RB_INIT(root) do { \ - (root)->rbh_root = NULL; \ -} while (/*CONSTCOND*/ 0) - -#define RB_BLACK 0 -#define RB_RED 1 -#define RB_ENTRY(type) \ -struct { \ - struct type *rbe_left; /* left element */ \ - struct type *rbe_right; /* right element */ \ - struct type *rbe_parent; /* parent element */ \ - int rbe_color; /* node color */ \ -} - -#define RB_LEFT(elm, field) (elm)->field.rbe_left -#define RB_RIGHT(elm, field) (elm)->field.rbe_right -#define RB_PARENT(elm, field) (elm)->field.rbe_parent -#define RB_COLOR(elm, field) (elm)->field.rbe_color -#define RB_ROOT(head) (head)->rbh_root -#define RB_EMPTY(head) (RB_ROOT(head) == NULL) - -#define RB_SET(elm, parent, field) do { \ - RB_PARENT(elm, field) = parent; \ - RB_LEFT(elm, field) = RB_RIGHT(elm, field) = NULL; \ - RB_COLOR(elm, field) = RB_RED; \ -} while (/*CONSTCOND*/ 0) - -#define RB_SET_BLACKRED(black, red, field) do { \ - RB_COLOR(black, field) = RB_BLACK; \ - RB_COLOR(red, field) = RB_RED; \ -} while (/*CONSTCOND*/ 0) - -#ifndef RB_AUGMENT -#define RB_AUGMENT(x) do {} while (0) -#endif - -#define RB_ROTATE_LEFT(head, elm, tmp, field) do { \ - (tmp) = RB_RIGHT(elm, field); \ - if ((RB_RIGHT(elm, field) = RB_LEFT(tmp, field)) != NULL) { \ - RB_PARENT(RB_LEFT(tmp, field), field) = (elm); \ - } \ - RB_AUGMENT(elm); \ - if ((RB_PARENT(tmp, field) = RB_PARENT(elm, field)) != NULL) { \ - if ((elm) == RB_LEFT(RB_PARENT(elm, field), field)) \ - RB_LEFT(RB_PARENT(elm, field), field) = (tmp); \ - else \ - RB_RIGHT(RB_PARENT(elm, field), field) = (tmp); \ - } else \ - (head)->rbh_root = (tmp); \ - RB_LEFT(tmp, field) = (elm); \ - RB_PARENT(elm, field) = (tmp); \ - RB_AUGMENT(tmp); \ - if ((RB_PARENT(tmp, field))) \ - RB_AUGMENT(RB_PARENT(tmp, field)); \ -} while (/*CONSTCOND*/ 0) - -#define RB_ROTATE_RIGHT(head, elm, tmp, field) do { \ - (tmp) = RB_LEFT(elm, field); \ - if ((RB_LEFT(elm, field) = RB_RIGHT(tmp, field)) != NULL) { \ - RB_PARENT(RB_RIGHT(tmp, field), field) = (elm); \ - } \ - RB_AUGMENT(elm); \ - if ((RB_PARENT(tmp, field) = RB_PARENT(elm, field)) != NULL) { \ - if ((elm) == RB_LEFT(RB_PARENT(elm, field), field)) \ - RB_LEFT(RB_PARENT(elm, field), field) = (tmp); \ - else \ - RB_RIGHT(RB_PARENT(elm, field), field) = (tmp); \ - } else \ - (head)->rbh_root = (tmp); \ - RB_RIGHT(tmp, field) = (elm); \ - RB_PARENT(elm, field) = (tmp); \ - RB_AUGMENT(tmp); \ - if ((RB_PARENT(tmp, field))) \ - RB_AUGMENT(RB_PARENT(tmp, field)); \ -} while (/*CONSTCOND*/ 0) - -/* Generates prototypes and inline functions */ -#define RB_PROTOTYPE(name, type, field, cmp) \ - RB_PROTOTYPE_INTERNAL(name, type, field, cmp,) -#define RB_PROTOTYPE_STATIC(name, type, field, cmp) \ - RB_PROTOTYPE_INTERNAL(name, type, field, cmp, __unused static) -#define RB_PROTOTYPE_INTERNAL(name, type, field, cmp, attr) \ -attr void name##_RB_INSERT_COLOR(struct name *, struct type *); \ -attr void name##_RB_REMOVE_COLOR(struct name *, struct type *, struct type *);\ -attr struct type *name##_RB_REMOVE(struct name *, struct type *); \ -attr struct type *name##_RB_INSERT(struct name *, struct type *); \ -attr struct type *name##_RB_FIND(struct name *, struct type *); \ -attr struct type *name##_RB_NFIND(struct name *, struct type *); \ -attr struct type *name##_RB_NEXT(struct type *); \ -attr struct type *name##_RB_PREV(struct type *); \ -attr struct type *name##_RB_MINMAX(struct name *, int); \ - \ - -/* Main rb operation. - * Moves node close to the key of elm to top - */ -#define RB_GENERATE(name, type, field, cmp) \ - RB_GENERATE_INTERNAL(name, type, field, cmp,) -#define RB_GENERATE_STATIC(name, type, field, cmp) \ - RB_GENERATE_INTERNAL(name, type, field, cmp, __unused static) -#define RB_GENERATE_INTERNAL(name, type, field, cmp, attr) \ -attr void \ -name##_RB_INSERT_COLOR(struct name *head, struct type *elm) \ -{ \ - struct type *parent, *gparent, *tmp; \ - while ((parent = RB_PARENT(elm, field)) != NULL && \ - RB_COLOR(parent, field) == RB_RED) { \ - gparent = RB_PARENT(parent, field); \ - if (parent == RB_LEFT(gparent, field)) { \ - tmp = RB_RIGHT(gparent, field); \ - if (tmp && RB_COLOR(tmp, field) == RB_RED) { \ - RB_COLOR(tmp, field) = RB_BLACK; \ - RB_SET_BLACKRED(parent, gparent, field);\ - elm = gparent; \ - continue; \ - } \ - if (RB_RIGHT(parent, field) == elm) { \ - RB_ROTATE_LEFT(head, parent, tmp, field);\ - tmp = parent; \ - parent = elm; \ - elm = tmp; \ - } \ - RB_SET_BLACKRED(parent, gparent, field); \ - RB_ROTATE_RIGHT(head, gparent, tmp, field); \ - } else { \ - tmp = RB_LEFT(gparent, field); \ - if (tmp && RB_COLOR(tmp, field) == RB_RED) { \ - RB_COLOR(tmp, field) = RB_BLACK; \ - RB_SET_BLACKRED(parent, gparent, field);\ - elm = gparent; \ - continue; \ - } \ - if (RB_LEFT(parent, field) == elm) { \ - RB_ROTATE_RIGHT(head, parent, tmp, field);\ - tmp = parent; \ - parent = elm; \ - elm = tmp; \ - } \ - RB_SET_BLACKRED(parent, gparent, field); \ - RB_ROTATE_LEFT(head, gparent, tmp, field); \ - } \ - } \ - RB_COLOR(head->rbh_root, field) = RB_BLACK; \ -} \ - \ -attr void \ -name##_RB_REMOVE_COLOR(struct name *head, struct type *parent, struct type *elm) \ -{ \ - struct type *tmp; \ - while ((elm == NULL || RB_COLOR(elm, field) == RB_BLACK) && \ - elm != RB_ROOT(head)) { \ - if (RB_LEFT(parent, field) == elm) { \ - tmp = RB_RIGHT(parent, field); \ - if (RB_COLOR(tmp, field) == RB_RED) { \ - RB_SET_BLACKRED(tmp, parent, field); \ - RB_ROTATE_LEFT(head, parent, tmp, field);\ - tmp = RB_RIGHT(parent, field); \ - } \ - if ((RB_LEFT(tmp, field) == NULL || \ - RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) &&\ - (RB_RIGHT(tmp, field) == NULL || \ - RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK)) {\ - RB_COLOR(tmp, field) = RB_RED; \ - elm = parent; \ - parent = RB_PARENT(elm, field); \ - } else { \ - if (RB_RIGHT(tmp, field) == NULL || \ - RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK) {\ - struct type *oleft; \ - if ((oleft = RB_LEFT(tmp, field)) \ - != NULL) \ - RB_COLOR(oleft, field) = RB_BLACK;\ - RB_COLOR(tmp, field) = RB_RED; \ - RB_ROTATE_RIGHT(head, tmp, oleft, field);\ - tmp = RB_RIGHT(parent, field); \ - } \ - RB_COLOR(tmp, field) = RB_COLOR(parent, field);\ - RB_COLOR(parent, field) = RB_BLACK; \ - if (RB_RIGHT(tmp, field)) \ - RB_COLOR(RB_RIGHT(tmp, field), field) = RB_BLACK;\ - RB_ROTATE_LEFT(head, parent, tmp, field);\ - elm = RB_ROOT(head); \ - break; \ - } \ - } else { \ - tmp = RB_LEFT(parent, field); \ - if (RB_COLOR(tmp, field) == RB_RED) { \ - RB_SET_BLACKRED(tmp, parent, field); \ - RB_ROTATE_RIGHT(head, parent, tmp, field);\ - tmp = RB_LEFT(parent, field); \ - } \ - if ((RB_LEFT(tmp, field) == NULL || \ - RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) &&\ - (RB_RIGHT(tmp, field) == NULL || \ - RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK)) {\ - RB_COLOR(tmp, field) = RB_RED; \ - elm = parent; \ - parent = RB_PARENT(elm, field); \ - } else { \ - if (RB_LEFT(tmp, field) == NULL || \ - RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) {\ - struct type *oright; \ - if ((oright = RB_RIGHT(tmp, field)) \ - != NULL) \ - RB_COLOR(oright, field) = RB_BLACK;\ - RB_COLOR(tmp, field) = RB_RED; \ - RB_ROTATE_LEFT(head, tmp, oright, field);\ - tmp = RB_LEFT(parent, field); \ - } \ - RB_COLOR(tmp, field) = RB_COLOR(parent, field);\ - RB_COLOR(parent, field) = RB_BLACK; \ - if (RB_LEFT(tmp, field)) \ - RB_COLOR(RB_LEFT(tmp, field), field) = RB_BLACK;\ - RB_ROTATE_RIGHT(head, parent, tmp, field);\ - elm = RB_ROOT(head); \ - break; \ - } \ - } \ - } \ - if (elm) \ - RB_COLOR(elm, field) = RB_BLACK; \ -} \ - \ -attr struct type * \ -name##_RB_REMOVE(struct name *head, struct type *elm) \ -{ \ - struct type *child, *parent, *old = elm; \ - int color; \ - if (RB_LEFT(elm, field) == NULL) \ - child = RB_RIGHT(elm, field); \ - else if (RB_RIGHT(elm, field) == NULL) \ - child = RB_LEFT(elm, field); \ - else { \ - struct type *left; \ - elm = RB_RIGHT(elm, field); \ - while ((left = RB_LEFT(elm, field)) != NULL) \ - elm = left; \ - child = RB_RIGHT(elm, field); \ - parent = RB_PARENT(elm, field); \ - color = RB_COLOR(elm, field); \ - if (child) \ - RB_PARENT(child, field) = parent; \ - if (parent) { \ - if (RB_LEFT(parent, field) == elm) \ - RB_LEFT(parent, field) = child; \ - else \ - RB_RIGHT(parent, field) = child; \ - RB_AUGMENT(parent); \ - } else \ - RB_ROOT(head) = child; \ - if (RB_PARENT(elm, field) == old) \ - parent = elm; \ - (elm)->field = (old)->field; \ - if (RB_PARENT(old, field)) { \ - if (RB_LEFT(RB_PARENT(old, field), field) == old)\ - RB_LEFT(RB_PARENT(old, field), field) = elm;\ - else \ - RB_RIGHT(RB_PARENT(old, field), field) = elm;\ - RB_AUGMENT(RB_PARENT(old, field)); \ - } else \ - RB_ROOT(head) = elm; \ - RB_PARENT(RB_LEFT(old, field), field) = elm; \ - if (RB_RIGHT(old, field)) \ - RB_PARENT(RB_RIGHT(old, field), field) = elm; \ - if (parent) { \ - left = parent; \ - do { \ - RB_AUGMENT(left); \ - } while ((left = RB_PARENT(left, field)) != NULL); \ - } \ - goto color; \ - } \ - parent = RB_PARENT(elm, field); \ - color = RB_COLOR(elm, field); \ - if (child) \ - RB_PARENT(child, field) = parent; \ - if (parent) { \ - if (RB_LEFT(parent, field) == elm) \ - RB_LEFT(parent, field) = child; \ - else \ - RB_RIGHT(parent, field) = child; \ - RB_AUGMENT(parent); \ - } else \ - RB_ROOT(head) = child; \ -color: \ - if (color == RB_BLACK) \ - name##_RB_REMOVE_COLOR(head, parent, child); \ - return (old); \ -} \ - \ -/* Inserts a node into the RB tree */ \ -attr struct type * \ -name##_RB_INSERT(struct name *head, struct type *elm) \ -{ \ - struct type *tmp; \ - struct type *parent = NULL; \ - int comp = 0; \ - tmp = RB_ROOT(head); \ - while (tmp) { \ - parent = tmp; \ - comp = (cmp)(elm, parent); \ - if (comp < 0) \ - tmp = RB_LEFT(tmp, field); \ - else if (comp > 0) \ - tmp = RB_RIGHT(tmp, field); \ - else \ - return (tmp); \ - } \ - RB_SET(elm, parent, field); \ - if (parent != NULL) { \ - if (comp < 0) \ - RB_LEFT(parent, field) = elm; \ - else \ - RB_RIGHT(parent, field) = elm; \ - RB_AUGMENT(parent); \ - } else \ - RB_ROOT(head) = elm; \ - name##_RB_INSERT_COLOR(head, elm); \ - return (NULL); \ -} \ - \ -/* Finds the node with the same key as elm */ \ -attr struct type * \ -name##_RB_FIND(struct name *head, struct type *elm) \ -{ \ - struct type *tmp = RB_ROOT(head); \ - int comp; \ - while (tmp) { \ - comp = cmp(elm, tmp); \ - if (comp < 0) \ - tmp = RB_LEFT(tmp, field); \ - else if (comp > 0) \ - tmp = RB_RIGHT(tmp, field); \ - else \ - return (tmp); \ - } \ - return (NULL); \ -} \ - \ -/* Finds the first node greater than or equal to the search key */ \ -attr struct type * \ -name##_RB_NFIND(struct name *head, struct type *elm) \ -{ \ - struct type *tmp = RB_ROOT(head); \ - struct type *res = NULL; \ - int comp; \ - while (tmp) { \ - comp = cmp(elm, tmp); \ - if (comp < 0) { \ - res = tmp; \ - tmp = RB_LEFT(tmp, field); \ - } \ - else if (comp > 0) \ - tmp = RB_RIGHT(tmp, field); \ - else \ - return (tmp); \ - } \ - return (res); \ -} \ - \ -/* ARGSUSED */ \ -attr struct type * \ -name##_RB_NEXT(struct type *elm) \ -{ \ - if (RB_RIGHT(elm, field)) { \ - elm = RB_RIGHT(elm, field); \ - while (RB_LEFT(elm, field)) \ - elm = RB_LEFT(elm, field); \ - } else { \ - if (RB_PARENT(elm, field) && \ - (elm == RB_LEFT(RB_PARENT(elm, field), field))) \ - elm = RB_PARENT(elm, field); \ - else { \ - while (RB_PARENT(elm, field) && \ - (elm == RB_RIGHT(RB_PARENT(elm, field), field)))\ - elm = RB_PARENT(elm, field); \ - elm = RB_PARENT(elm, field); \ - } \ - } \ - return (elm); \ -} \ - \ -/* ARGSUSED */ \ -attr struct type * \ -name##_RB_PREV(struct type *elm) \ -{ \ - if (RB_LEFT(elm, field)) { \ - elm = RB_LEFT(elm, field); \ - while (RB_RIGHT(elm, field)) \ - elm = RB_RIGHT(elm, field); \ - } else { \ - if (RB_PARENT(elm, field) && \ - (elm == RB_RIGHT(RB_PARENT(elm, field), field))) \ - elm = RB_PARENT(elm, field); \ - else { \ - while (RB_PARENT(elm, field) && \ - (elm == RB_LEFT(RB_PARENT(elm, field), field)))\ - elm = RB_PARENT(elm, field); \ - elm = RB_PARENT(elm, field); \ - } \ - } \ - return (elm); \ -} \ - \ -attr struct type * \ -name##_RB_MINMAX(struct name *head, int val) \ -{ \ - struct type *tmp = RB_ROOT(head); \ - struct type *parent = NULL; \ - while (tmp) { \ - parent = tmp; \ - if (val < 0) \ - tmp = RB_LEFT(tmp, field); \ - else \ - tmp = RB_RIGHT(tmp, field); \ - } \ - return (parent); \ -} - -#define RB_NEGINF -1 -#define RB_INF 1 - -#define RB_INSERT(name, x, y) name##_RB_INSERT(x, y) -#define RB_REMOVE(name, x, y) name##_RB_REMOVE(x, y) -#define RB_FIND(name, x, y) name##_RB_FIND(x, y) -#define RB_NFIND(name, x, y) name##_RB_NFIND(x, y) -#define RB_NEXT(name, x, y) name##_RB_NEXT(y) -#define RB_PREV(name, x, y) name##_RB_PREV(y) -#define RB_MIN(name, x) name##_RB_MINMAX(x, RB_NEGINF) -#define RB_MAX(name, x) name##_RB_MINMAX(x, RB_INF) - -#define RB_FOREACH(x, name, head) \ - for ((x) = RB_MIN(name, head); \ - (x) != NULL; \ - (x) = name##_RB_NEXT(x)) - -#define RB_FOREACH_FROM(x, name, y) \ - for ((x) = (y); \ - ((x) != NULL) && ((y) = name##_RB_NEXT(x), (x) != NULL); \ - (x) = (y)) - -#define RB_FOREACH_SAFE(x, name, head, y) \ - for ((x) = RB_MIN(name, head); \ - ((x) != NULL) && ((y) = name##_RB_NEXT(x), (x) != NULL); \ - (x) = (y)) - -#define RB_FOREACH_REVERSE(x, name, head) \ - for ((x) = RB_MAX(name, head); \ - (x) != NULL; \ - (x) = name##_RB_PREV(x)) - -#define RB_FOREACH_REVERSE_FROM(x, name, y) \ - for ((x) = (y); \ - ((x) != NULL) && ((y) = name##_RB_PREV(x), (x) != NULL); \ - (x) = (y)) - -#define RB_FOREACH_REVERSE_SAFE(x, name, head, y) \ - for ((x) = RB_MAX(name, head); \ - ((x) != NULL) && ((y) = name##_RB_PREV(x), (x) != NULL); \ - (x) = (y)) - -#endif /* _SYS_TREE_H_ */ diff --git a/configure.ac b/configure.ac index dd4854d..38d33bf 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.65]) -AC_INIT([SciTECO], [0.6.4], +AC_INIT([SciTECO], [2.0.0], [robin.haberkorn@googlemail.com], [sciteco], [http://sciteco.sf.net/]) @@ -15,7 +15,7 @@ AC_CONFIG_AUX_DIR(config) AM_INIT_AUTOMAKE AC_CONFIG_TESTDIR([tests]) -AC_CONFIG_SRCDIR([src/main.cpp]) +AC_CONFIG_SRCDIR([src/main.c]) AC_CONFIG_HEADERS([config.h]) AC_CANONICAL_BUILD @@ -52,9 +52,8 @@ canonicalize() { # Checks for programs. LT_INIT -AC_PROG_CXX -AX_CXX_COMPILE_STDCXX(11, noext, mandatory) AC_PROG_CC +# FIXME: We want -std=gnu11 AC_PROG_CC_C99 AC_PROG_SED AC_PROG_INSTALL @@ -72,8 +71,9 @@ if [[ x$ax_cv_gnu_make_command = x ]]; then fi # Additionally required by the Scintilla build process: +AC_PROG_CXX +AX_CXX_COMPILE_STDCXX(11, noext, mandatory) AC_CHECK_TOOL(AR, ar) -AC_PROG_RANLIB # Check for Windows resource compiler and define # WIN32 conditional @@ -87,13 +87,6 @@ case $host in ;; esac -# Detect Clang (including Emscripten). -# A particular warning does not cooperate well with our use -# of the BSD data structures. -AC_CHECK_DECL(__clang__, [ - CXXFLAGS="$CXXFLAGS -Wno-mismatched-tags" -]) - # Changing the EXEEXT on emscripten ensures that we don't # need a special Makefile rule to generate Javascript files. AC_CHECK_DECL(EMSCRIPTEN, [ @@ -120,7 +113,8 @@ if [[ x$GROFF = x ]]; then AC_MSG_ERROR([GNU roff required!]) fi -# not necessarily required (depends on --enable-developer-doc) +# Doxygen is not necessarily required as long as +# you do not run `make devdoc`. AC_CHECK_PROG(DOXYGEN, doxygen, doxygen) AC_CHECK_PROG(DOT, dot, dot) DOXYGEN_HAVE_DOT=YES @@ -131,7 +125,7 @@ AC_SUBST(DOXYGEN_HAVE_DOT) AC_CHECK_PROG(SCITECO, sciteco, sciteco) # Checks for libraries. -PKG_CHECK_MODULES(LIBGLIB, [glib-2.0 >= 2.28], [ +PKG_CHECK_MODULES(LIBGLIB, [glib-2.0 >= 2.44], [ CFLAGS="$CFLAGS $LIBGLIB_CFLAGS" CXXFLAGS="$CXXFLAGS $LIBGLIB_CFLAGS" LIBS="$LIBS $LIBGLIB_LIBS" @@ -140,21 +134,11 @@ PKG_CHECK_MODULES(LIBGLIB, [glib-2.0 >= 2.28], [ # Checks for header files. AC_HEADER_STDC -AC_CHECK_HEADERS([bsd/sys/queue.h], [], [ - AM_CPPFLAGS="$AM_CPPFLAGS -I\$(top_srcdir)/compat" -]) - case $host in *-mingw*) - AC_CHECK_HEADERS([windows.h psapi.h], , [ + AC_CHECK_HEADERS([windows.h], , [ AC_MSG_ERROR([Missing Windows headers!]) - ], [ - #include ]) - - # Make sure we get GetProcessMemoryInfo(): - AM_CPPFLAGS="$AM_CPPFLAGS -DPSAPI_VERSION=1" - LIBS="$LIBS -lpsapi" ;; esac @@ -165,35 +149,30 @@ AC_TYPE_SIZE_T # Checks for library functions. # They must exist on every target system. -AC_CHECK_FUNCS([memset setlocale strchr strrchr fstat], , [ +AC_CHECK_FUNCS([memset setlocale strchr strrchr fstat sscanf], , [ AC_MSG_ERROR([Missing libc function]) ]) # Library functions that we assume exist on UNIX/Linux # and UNIXoid systems, so that G_OS_UNIX is sufficient # to test for them. +# +# NOTE: mmap() is currently only used by dlmalloc(). +# # FIXME: Perhaps it would be more elegant to check whether -# glib defines G_OS_UNIX || G_OS_HAIKU instead... +# glib defines G_OS_UNIX instead... case $host in *-*-linux* | *-*-*bsd* | *-*-darwin* | *-*-cygwin* | *-*-haiku*) - AC_CHECK_FUNCS([realpath fchown dup dup2], , [ + AC_CHECK_FUNCS([realpath fchown dup dup2 getpid open read mmap], , [ AC_MSG_ERROR([Missing libc function]) ]) ;; esac -# Check for optional libc features. -# Some of this will only be found on glibc, -# others on FreeBSD/jemalloc. -AC_CHECK_HEADERS([malloc.h malloc_np.h]) -AC_CHECK_FUNCS([malloc_trim malloc_usable_size], [ - # Sometimes the declaration is missing. - AC_CHECK_DECLS([malloc_trim]) -]) - # # Config options # + AC_ARG_WITH(scintilla, AS_HELP_STRING([--with-scintilla=PATH], [Specify Scintilla's path [default=./scintilla]]), @@ -310,20 +289,13 @@ case $INTERFACE in ;; gtk) - # NOTE: Ubuntu 14.04 only has Gtk+ 3.10, so we have to support it. - # This version lacks GtkFlowBox. # gmodule is required by Scintilla. - PKG_CHECK_MODULES(LIBGTK, [gtk+-3.0 >= 3.10 gmodule-2.0], [ + PKG_CHECK_MODULES(LIBGTK, [gtk+-3.0 >= 3.12 gmodule-2.0], [ CFLAGS="$CFLAGS $LIBGTK_CFLAGS" CXXFLAGS="$CXXFLAGS $LIBGTK_CFLAGS" LIBS="$LIBS $LIBGTK_LIBS" ]) - # Should be available since v3.12 - AC_CHECK_FUNCS(gtk_flow_box_new, [], [ - GTK_FLOW_BOX_FALLBACK=true - ]) - GOB2_CHECK(2.0.20) AC_DEFINE(INTERFACE_GTK, 1, [Build with GTK+ 3.0 support]) @@ -353,17 +325,6 @@ AC_ARG_ENABLE(html-manual, [html_man=$enableval], [html_man=no]) AM_CONDITIONAL(BUILD_HTMLMAN, [test $html_man = yes]) -AC_ARG_ENABLE(developer-doc, - AS_HELP_STRING([--enable-developer-doc], - [Generate developer documentation using Doxygen [default=no]]), - [dev_doc=$enableval], [dev_doc=no]) -if [[ $dev_doc = yes -a x$DOXYGEN = x ]]; then - AC_MSG_ERROR([Enabled generating developer documentation, - but Doxygen cannot be found! - Try --disable-developer-doc.]) -fi -AM_CONDITIONAL(BUILD_DEVDOC, [test $dev_doc = yes]) - AC_ARG_ENABLE(bootstrap, AS_HELP_STRING([--disable-bootstrap], [Bootstrap using sciteco-minimal, @@ -375,6 +336,67 @@ if [[ $bootstrap = no -a x$SCITECO = x ]]; then fi AM_CONDITIONAL(BOOTSTRAP, [test x$bootstrap = xyes]) +AC_ARG_ENABLE(malloc-replacement, + AS_HELP_STRING([--enable-malloc-replacement], + [Replace the libc malloc() [default=check]]), + [malloc_replacement=$enableval], [malloc_replacement=check]) +if [[ $malloc_replacement = check ]]; then + # We currently do not support dlmalloc on Windows. + case $host in + *-mingw*) malloc_replacement=no;; + *) malloc_replacement=yes;; + esac +fi +AM_CONDITIONAL(REPLACE_MALLOC, [test $malloc_replacement = yes]) +if [[ $malloc_replacement = yes ]]; then + AM_CPPFLAGS="$AM_CPPFLAGS -I\$(top_srcdir)/contrib/dlmalloc" + AC_DEFINE(REPLACE_MALLOC, 1, [Define to 1 if the system malloc() is replaced.]) + AC_DEFINE(HAVE_MALLOC_H, 1, [Define to 1 if you have the header file.]) + AC_DEFINE(HAVE_MALLOC_TRIM, 1, [Define to 1 if you have the `malloc_trim' function.]) +else + # NOTE: We don't check for malloc_footprint() since even if existing, + # it cannot be guaranteed to work (eg. as in dlmalloc with HAVE_MORECORE=1). + AC_CHECK_HEADERS([malloc.h malloc_np.h]) + AC_CHECK_FUNCS([malloc_trim], [ + AC_CHECK_DECLS([malloc_trim], , , [ + #include + ]) + ]) + + case $host in + *-mingw*) + AC_CHECK_HEADERS([psapi.h], , [ + AC_MSG_ERROR([Missing Windows headers!]) + ], [ + #include + ]) + + # Make sure we get GetProcessMemoryInfo(): + AM_CPPFLAGS="$AM_CPPFLAGS -DPSAPI_VERSION=1" + LIBS="$LIBS -lpsapi" + ;; + *-*-linux*) + AC_CHECK_HEADERS([sys/time.h sys/resource.h]) + AC_CHECK_FUNCS([sysconf]) + + # FIXME: procfs might be available on other UNIXoid platforms. + # Perhaps add a --with-procfs? + # However, we currently also depend on sysconf(). + # Also, it should generally not be necessary to + # --disable-malloc-replacement on UNIX. + AC_DEFINE(HAVE_PROCFS, 1, [Whether procfs (/proc) is supported]) + ;; + *-*-*bsd*) + AC_CHECK_HEADERS([sys/types.h sys/sysctl.h]) + AC_CHECK_FUNCS([sysctl]) + ;; + *-*-darwin*) + AC_CHECK_HEADERS([mach/mach.h mach/message.h mach/kern_return.h mach/task_info.h]) + AC_CHECK_FUNCS([task_info mach_task_self]) + ;; + esac +fi + # This cannot be done with --enable-static as it only controls # which kind of libraries libtool builds. # Also, it cannot be controlled reliably by setting LDFLAGS for @@ -392,6 +414,8 @@ AM_CONDITIONAL(STATIC_EXECUTABLES, [test x$static_executables = xyes]) AC_CONFIG_FILES([GNUmakefile:Makefile.in src/GNUmakefile:src/Makefile.in] [src/interface-gtk/GNUmakefile:src/interface-gtk/Makefile.in] [src/interface-curses/GNUmakefile:src/interface-curses/Makefile.in] + [contrib/dlmalloc/GNUmakefile:contrib/dlmalloc/Makefile.in] + [contrib/rb3ptr/GNUmakefile:contrib/rb3ptr/Makefile.in] [lib/GNUmakefile:lib/Makefile.in] [doc/GNUmakefile:doc/Makefile.in doc/Doxyfile] [tests/GNUmakefile:tests/Makefile.in tests/atlocal]) diff --git a/contrib/dlmalloc/Makefile.am b/contrib/dlmalloc/Makefile.am new file mode 100644 index 0000000..e575683 --- /dev/null +++ b/contrib/dlmalloc/Makefile.am @@ -0,0 +1,23 @@ +# Source: http://gee.cs.oswego.edu/dl/html/malloc.html +# +# FIXME: On FreeBSD, we might implement a compatible mremap() based on the BSD mremap() and pass +# in -DHAVE_MREMAP=1 -DMREMAP=mremap_bsd. We'll have to add a declaration to malloc.c or +# use -include mremap_bsd.h in CPPFLAGS. +# +# FIXME: It may be advisable to add malloc.c directly to the programs' SOURCES variable +# for increased portability. There is also AC_LIBOBJ, but it's usually for defining sources of +# replacement libraries. + +AM_CPPFLAGS = -DINSECURE='defined(NDEBUG)' -DNO_MALLINFO=1 -DNO_MALLOC_STATS=1 \ + -DUSE_LOCKS=1 -DUSE_DL_PREFIX + +# FIXME: This optimization is still broken as of GCC v9.3.0. +# This is a known GCC bug, triggered by memset() in calloc(). +# See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=93213 +# FIXME: This must not be passed on Clang +AM_CFLAGS = -fno-optimize-strlen + +noinst_LTLIBRARIES = libdlmalloc.la +if REPLACE_MALLOC +libdlmalloc_la_SOURCES = malloc.c malloc.h +endif diff --git a/contrib/dlmalloc/malloc.c b/contrib/dlmalloc/malloc.c new file mode 100644 index 0000000..649cfbc --- /dev/null +++ b/contrib/dlmalloc/malloc.c @@ -0,0 +1,6280 @@ +/* + This is a version (aka dlmalloc) of malloc/free/realloc written by + Doug Lea and released to the public domain, as explained at + http://creativecommons.org/publicdomain/zero/1.0/ Send questions, + comments, complaints, performance data, etc to dl@cs.oswego.edu + +* Version 2.8.6 Wed Aug 29 06:57:58 2012 Doug Lea + Note: There may be an updated version of this malloc obtainable at + ftp://gee.cs.oswego.edu/pub/misc/malloc.c + Check before installing! + +* Quickstart + + This library is all in one file to simplify the most common usage: + ftp it, compile it (-O3), and link it into another program. All of + the compile-time options default to reasonable values for use on + most platforms. You might later want to step through various + compile-time and dynamic tuning options. + + For convenience, an include file for code using this malloc is at: + ftp://gee.cs.oswego.edu/pub/misc/malloc-2.8.6.h + You don't really need this .h file unless you call functions not + defined in your system include files. The .h file contains only the + excerpts from this file needed for using this malloc on ANSI C/C++ + systems, so long as you haven't changed compile-time options about + naming and tuning parameters. If you do, then you can create your + own malloc.h that does include all settings by cutting at the point + indicated below. Note that you may already by default be using a C + library containing a malloc that is based on some version of this + malloc (for example in linux). You might still want to use the one + in this file to customize settings or to avoid overheads associated + with library versions. + +* Vital statistics: + + Supported pointer/size_t representation: 4 or 8 bytes + size_t MUST be an unsigned type of the same width as + pointers. (If you are using an ancient system that declares + size_t as a signed type, or need it to be a different width + than pointers, you can use a previous release of this malloc + (e.g. 2.7.2) supporting these.) + + Alignment: 8 bytes (minimum) + This suffices for nearly all current machines and C compilers. + However, you can define MALLOC_ALIGNMENT to be wider than this + if necessary (up to 128bytes), at the expense of using more space. + + Minimum overhead per allocated chunk: 4 or 8 bytes (if 4byte sizes) + 8 or 16 bytes (if 8byte sizes) + Each malloced chunk has a hidden word of overhead holding size + and status information, and additional cross-check word + if FOOTERS is defined. + + Minimum allocated size: 4-byte ptrs: 16 bytes (including overhead) + 8-byte ptrs: 32 bytes (including overhead) + + Even a request for zero bytes (i.e., malloc(0)) returns a + pointer to something of the minimum allocatable size. + The maximum overhead wastage (i.e., number of extra bytes + allocated than were requested in malloc) is less than or equal + to the minimum size, except for requests >= mmap_threshold that + are serviced via mmap(), where the worst case wastage is about + 32 bytes plus the remainder from a system page (the minimal + mmap unit); typically 4096 or 8192 bytes. + + Security: static-safe; optionally more or less + The "security" of malloc refers to the ability of malicious + code to accentuate the effects of errors (for example, freeing + space that is not currently malloc'ed or overwriting past the + ends of chunks) in code that calls malloc. This malloc + guarantees not to modify any memory locations below the base of + heap, i.e., static variables, even in the presence of usage + errors. The routines additionally detect most improper frees + and reallocs. All this holds as long as the static bookkeeping + for malloc itself is not corrupted by some other means. This + is only one aspect of security -- these checks do not, and + cannot, detect all possible programming errors. + + If FOOTERS is defined nonzero, then each allocated chunk + carries an additional check word to verify that it was malloced + from its space. These check words are the same within each + execution of a program using malloc, but differ across + executions, so externally crafted fake chunks cannot be + freed. This improves security by rejecting frees/reallocs that + could corrupt heap memory, in addition to the checks preventing + writes to statics that are always on. This may further improve + security at the expense of time and space overhead. (Note that + FOOTERS may also be worth using with MSPACES.) + + By default detected errors cause the program to abort (calling + "abort()"). You can override this to instead proceed past + errors by defining PROCEED_ON_ERROR. In this case, a bad free + has no effect, and a malloc that encounters a bad address + caused by user overwrites will ignore the bad address by + dropping pointers and indices to all known memory. This may + be appropriate for programs that should continue if at all + possible in the face of programming errors, although they may + run out of memory because dropped memory is never reclaimed. + + If you don't like either of these options, you can define + CORRUPTION_ERROR_ACTION and USAGE_ERROR_ACTION to do anything + else. And if if you are sure that your program using malloc has + no errors or vulnerabilities, you can define INSECURE to 1, + which might (or might not) provide a small performance improvement. + + It is also possible to limit the maximum total allocatable + space, using malloc_set_footprint_limit. This is not + designed as a security feature in itself (calls to set limits + are not screened or privileged), but may be useful as one + aspect of a secure implementation. + + Thread-safety: NOT thread-safe unless USE_LOCKS defined non-zero + When USE_LOCKS is defined, each public call to malloc, free, + etc is surrounded with a lock. By default, this uses a plain + pthread mutex, win32 critical section, or a spin-lock if if + available for the platform and not disabled by setting + USE_SPIN_LOCKS=0. However, if USE_RECURSIVE_LOCKS is defined, + recursive versions are used instead (which are not required for + base functionality but may be needed in layered extensions). + Using a global lock is not especially fast, and can be a major + bottleneck. It is designed only to provide minimal protection + in concurrent environments, and to provide a basis for + extensions. If you are using malloc in a concurrent program, + consider instead using nedmalloc + (http://www.nedprod.com/programs/portable/nedmalloc/) or + ptmalloc (See http://www.malloc.de), which are derived from + versions of this malloc. + + System requirements: Any combination of MORECORE and/or MMAP/MUNMAP + This malloc can use unix sbrk or any emulation (invoked using + the CALL_MORECORE macro) and/or mmap/munmap or any emulation + (invoked using CALL_MMAP/CALL_MUNMAP) to get and release system + memory. On most unix systems, it tends to work best if both + MORECORE and MMAP are enabled. On Win32, it uses emulations + based on VirtualAlloc. It also uses common C library functions + like memset. + + Compliance: I believe it is compliant with the Single Unix Specification + (See http://www.unix.org). Also SVID/XPG, ANSI C, and probably + others as well. + +* Overview of algorithms + + This is not the fastest, most space-conserving, most portable, or + most tunable malloc ever written. However it is among the fastest + while also being among the most space-conserving, portable and + tunable. Consistent balance across these factors results in a good + general-purpose allocator for malloc-intensive programs. + + In most ways, this malloc is a best-fit allocator. Generally, it + chooses the best-fitting existing chunk for a request, with ties + broken in approximately least-recently-used order. (This strategy + normally maintains low fragmentation.) However, for requests less + than 256bytes, it deviates from best-fit when there is not an + exactly fitting available chunk by preferring to use space adjacent + to that used for the previous small request, as well as by breaking + ties in approximately most-recently-used order. (These enhance + locality of series of small allocations.) And for very large requests + (>= 256Kb by default), it relies on system memory mapping + facilities, if supported. (This helps avoid carrying around and + possibly fragmenting memory used only for large chunks.) + + All operations (except malloc_stats and mallinfo) have execution + times that are bounded by a constant factor of the number of bits in + a size_t, not counting any clearing in calloc or copying in realloc, + or actions surrounding MORECORE and MMAP that have times + proportional to the number of non-contiguous regions returned by + system allocation routines, which is often just 1. In real-time + applications, you can optionally suppress segment traversals using + NO_SEGMENT_TRAVERSAL, which assures bounded execution even when + system allocators return non-contiguous spaces, at the typical + expense of carrying around more memory and increased fragmentation. + + The implementation is not very modular and seriously overuses + macros. Perhaps someday all C compilers will do as good a job + inlining modular code as can now be done by brute-force expansion, + but now, enough of them seem not to. + + Some compilers issue a lot of warnings about code that is + dead/unreachable only on some platforms, and also about intentional + uses of negation on unsigned types. All known cases of each can be + ignored. + + For a longer but out of date high-level description, see + http://gee.cs.oswego.edu/dl/html/malloc.html + +* MSPACES + If MSPACES is defined, then in addition to malloc, free, etc., + this file also defines mspace_malloc, mspace_free, etc. These + are versions of malloc routines that take an "mspace" argument + obtained using create_mspace, to control all internal bookkeeping. + If ONLY_MSPACES is defined, only these versions are compiled. + So if you would like to use this allocator for only some allocations, + and your system malloc for others, you can compile with + ONLY_MSPACES and then do something like... + static mspace mymspace = create_mspace(0,0); // for example + #define mymalloc(bytes) mspace_malloc(mymspace, bytes) + + (Note: If you only need one instance of an mspace, you can instead + use "USE_DL_PREFIX" to relabel the global malloc.) + + You can similarly create thread-local allocators by storing + mspaces as thread-locals. For example: + static __thread mspace tlms = 0; + void* tlmalloc(size_t bytes) { + if (tlms == 0) tlms = create_mspace(0, 0); + return mspace_malloc(tlms, bytes); + } + void tlfree(void* mem) { mspace_free(tlms, mem); } + + Unless FOOTERS is defined, each mspace is completely independent. + You cannot allocate from one and free to another (although + conformance is only weakly checked, so usage errors are not always + caught). If FOOTERS is defined, then each chunk carries around a tag + indicating its originating mspace, and frees are directed to their + originating spaces. Normally, this requires use of locks. + + ------------------------- Compile-time options --------------------------- + +Be careful in setting #define values for numerical constants of type +size_t. On some systems, literal values are not automatically extended +to size_t precision unless they are explicitly casted. You can also +use the symbolic values MAX_SIZE_T, SIZE_T_ONE, etc below. + +WIN32 default: defined if _WIN32 defined + Defining WIN32 sets up defaults for MS environment and compilers. + Otherwise defaults are for unix. Beware that there seem to be some + cases where this malloc might not be a pure drop-in replacement for + Win32 malloc: Random-looking failures from Win32 GDI API's (eg; + SetDIBits()) may be due to bugs in some video driver implementations + when pixel buffers are malloc()ed, and the region spans more than + one VirtualAlloc()ed region. Because dlmalloc uses a small (64Kb) + default granularity, pixel buffers may straddle virtual allocation + regions more often than when using the Microsoft allocator. You can + avoid this by using VirtualAlloc() and VirtualFree() for all pixel + buffers rather than using malloc(). If this is not possible, + recompile this malloc with a larger DEFAULT_GRANULARITY. Note: + in cases where MSC and gcc (cygwin) are known to differ on WIN32, + conditions use _MSC_VER to distinguish them. + +DLMALLOC_EXPORT default: extern + Defines how public APIs are declared. If you want to export via a + Windows DLL, you might define this as + #define DLMALLOC_EXPORT extern __declspec(dllexport) + If you want a POSIX ELF shared object, you might use + #define DLMALLOC_EXPORT extern __attribute__((visibility("default"))) + +MALLOC_ALIGNMENT default: (size_t)(2 * sizeof(void *)) + Controls the minimum alignment for malloc'ed chunks. It must be a + power of two and at least 8, even on machines for which smaller + alignments would suffice. It may be defined as larger than this + though. Note however that code and data structures are optimized for + the case of 8-byte alignment. + +MSPACES default: 0 (false) + If true, compile in support for independent allocation spaces. + This is only supported if HAVE_MMAP is true. + +ONLY_MSPACES default: 0 (false) + If true, only compile in mspace versions, not regular versions. + +USE_LOCKS default: 0 (false) + Causes each call to each public routine to be surrounded with + pthread or WIN32 mutex lock/unlock. (If set true, this can be + overridden on a per-mspace basis for mspace versions.) If set to a + non-zero value other than 1, locks are used, but their + implementation is left out, so lock functions must be supplied manually, + as described below. + +USE_SPIN_LOCKS default: 1 iff USE_LOCKS and spin locks available + If true, uses custom spin locks for locking. This is currently + supported only gcc >= 4.1, older gccs on x86 platforms, and recent + MS compilers. Otherwise, posix locks or win32 critical sections are + used. + +USE_RECURSIVE_LOCKS default: not defined + If defined nonzero, uses recursive (aka reentrant) locks, otherwise + uses plain mutexes. This is not required for malloc proper, but may + be needed for layered allocators such as nedmalloc. + +LOCK_AT_FORK default: not defined + If defined nonzero, performs pthread_atfork upon initialization + to initialize child lock while holding parent lock. The implementation + assumes that pthread locks (not custom locks) are being used. In other + cases, you may need to customize the implementation. + +FOOTERS default: 0 + If true, provide extra checking and dispatching by placing + information in the footers of allocated chunks. This adds + space and time overhead. + +INSECURE default: 0 + If true, omit checks for usage errors and heap space overwrites. + +USE_DL_PREFIX default: NOT defined + Causes compiler to prefix all public routines with the string 'dl'. + This can be useful when you only want to use this malloc in one part + of a program, using your regular system malloc elsewhere. + +MALLOC_INSPECT_ALL default: NOT defined + If defined, compiles malloc_inspect_all and mspace_inspect_all, that + perform traversal of all heap space. Unless access to these + functions is otherwise restricted, you probably do not want to + include them in secure implementations. + +ABORT default: defined as abort() + Defines how to abort on failed checks. On most systems, a failed + check cannot die with an "assert" or even print an informative + message, because the underlying print routines in turn call malloc, + which will fail again. Generally, the best policy is to simply call + abort(). It's not very useful to do more than this because many + errors due to overwriting will show up as address faults (null, odd + addresses etc) rather than malloc-triggered checks, so will also + abort. Also, most compilers know that abort() does not return, so + can better optimize code conditionally calling it. + +PROCEED_ON_ERROR default: defined as 0 (false) + Controls whether detected bad addresses cause them to bypassed + rather than aborting. If set, detected bad arguments to free and + realloc are ignored. And all bookkeeping information is zeroed out + upon a detected overwrite of freed heap space, thus losing the + ability to ever return it from malloc again, but enabling the + application to proceed. If PROCEED_ON_ERROR is defined, the + static variable malloc_corruption_error_count is compiled in + and can be examined to see if errors have occurred. This option + generates slower code than the default abort policy. + +DEBUG default: NOT defined + The DEBUG setting is mainly intended for people trying to modify + this code or diagnose problems when porting to new platforms. + However, it may also be able to better isolate user errors than just + using runtime checks. The assertions in the check routines spell + out in more detail the assumptions and invariants underlying the + algorithms. The checking is fairly extensive, and will slow down + execution noticeably. Calling malloc_stats or mallinfo with DEBUG + set will attempt to check every non-mmapped allocated and free chunk + in the course of computing the summaries. + +ABORT_ON_ASSERT_FAILURE default: defined as 1 (true) + Debugging assertion failures can be nearly impossible if your + version of the assert macro causes malloc to be called, which will + lead to a cascade of further failures, blowing the runtime stack. + ABORT_ON_ASSERT_FAILURE cause assertions failures to call abort(), + which will usually make debugging easier. + +MALLOC_FAILURE_ACTION default: sets errno to ENOMEM, or no-op on win32 + The action to take before "return 0" when malloc fails to be able to + return memory because there is none available. + +HAVE_MORECORE default: 1 (true) unless win32 or ONLY_MSPACES + True if this system supports sbrk or an emulation of it. + +MORECORE default: sbrk + The name of the sbrk-style system routine to call to obtain more + memory. See below for guidance on writing custom MORECORE + functions. The type of the argument to sbrk/MORECORE varies across + systems. It cannot be size_t, because it supports negative + arguments, so it is normally the signed type of the same width as + size_t (sometimes declared as "intptr_t"). It doesn't much matter + though. Internally, we only call it with arguments less than half + the max value of a size_t, which should work across all reasonable + possibilities, although sometimes generating compiler warnings. + +MORECORE_CONTIGUOUS default: 1 (true) if HAVE_MORECORE + If true, take advantage of fact that consecutive calls to MORECORE + with positive arguments always return contiguous increasing + addresses. This is true of unix sbrk. It does not hurt too much to + set it true anyway, since malloc copes with non-contiguities. + Setting it false when definitely non-contiguous saves time + and possibly wasted space it would take to discover this though. + +MORECORE_CANNOT_TRIM default: NOT defined + True if MORECORE cannot release space back to the system when given + negative arguments. This is generally necessary only if you are + using a hand-crafted MORECORE function that cannot handle negative + arguments. + +NO_SEGMENT_TRAVERSAL default: 0 + If non-zero, suppresses traversals of memory segments + returned by either MORECORE or CALL_MMAP. This disables + merging of segments that are contiguous, and selectively + releasing them to the OS if unused, but bounds execution times. + +HAVE_MMAP default: 1 (true) + True if this system supports mmap or an emulation of it. If so, and + HAVE_MORECORE is not true, MMAP is used for all system + allocation. If set and HAVE_MORECORE is true as well, MMAP is + primarily used to directly allocate very large blocks. It is also + used as a backup strategy in cases where MORECORE fails to provide + space from system. Note: A single call to MUNMAP is assumed to be + able to unmap memory that may have be allocated using multiple calls + to MMAP, so long as they are adjacent. + +HAVE_MREMAP default: 1 on linux, else 0 + If true realloc() uses mremap() to re-allocate large blocks and + extend or shrink allocation spaces. + +MMAP_CLEARS default: 1 except on WINCE. + True if mmap clears memory so calloc doesn't need to. This is true + for standard unix mmap using /dev/zero and on WIN32 except for WINCE. + +USE_BUILTIN_FFS default: 0 (i.e., not used) + Causes malloc to use the builtin ffs() function to compute indices. + Some compilers may recognize and intrinsify ffs to be faster than the + supplied C version. Also, the case of x86 using gcc is special-cased + to an asm instruction, so is already as fast as it can be, and so + this setting has no effect. Similarly for Win32 under recent MS compilers. + (On most x86s, the asm version is only slightly faster than the C version.) + +malloc_getpagesize default: derive from system includes, or 4096. + The system page size. To the extent possible, this malloc manages + memory from the system in page-size units. This may be (and + usually is) a function rather than a constant. This is ignored + if WIN32, where page size is determined using getSystemInfo during + initialization. + +USE_DEV_RANDOM default: 0 (i.e., not used) + Causes malloc to use /dev/random to initialize secure magic seed for + stamping footers. Otherwise, the current time is used. + +NO_MALLINFO default: 0 + If defined, don't compile "mallinfo". This can be a simple way + of dealing with mismatches between system declarations and + those in this file. + +MALLINFO_FIELD_TYPE default: size_t + The type of the fields in the mallinfo struct. This was originally + defined as "int" in SVID etc, but is more usefully defined as + size_t. The value is used only if HAVE_USR_INCLUDE_MALLOC_H is not set + +NO_MALLOC_STATS default: 0 + If defined, don't compile "malloc_stats". This avoids calls to + fprintf and bringing in stdio dependencies you might not want. + +REALLOC_ZERO_BYTES_FREES default: not defined + This should be set if a call to realloc with zero bytes should + be the same as a call to free. Some people think it should. Otherwise, + since this malloc returns a unique pointer for malloc(0), so does + realloc(p, 0). + +LACKS_UNISTD_H, LACKS_FCNTL_H, LACKS_SYS_PARAM_H, LACKS_SYS_MMAN_H +LACKS_STRINGS_H, LACKS_STRING_H, LACKS_SYS_TYPES_H, LACKS_ERRNO_H +LACKS_STDLIB_H LACKS_SCHED_H LACKS_TIME_H default: NOT defined unless on WIN32 + Define these if your system does not have these header files. + You might need to manually insert some of the declarations they provide. + +DEFAULT_GRANULARITY default: page size if MORECORE_CONTIGUOUS, + system_info.dwAllocationGranularity in WIN32, + otherwise 64K. + Also settable using mallopt(M_GRANULARITY, x) + The unit for allocating and deallocating memory from the system. On + most systems with contiguous MORECORE, there is no reason to + make this more than a page. However, systems with MMAP tend to + either require or encourage larger granularities. You can increase + this value to prevent system allocation functions to be called so + often, especially if they are slow. The value must be at least one + page and must be a power of two. Setting to 0 causes initialization + to either page size or win32 region size. (Note: In previous + versions of malloc, the equivalent of this option was called + "TOP_PAD") + +DEFAULT_TRIM_THRESHOLD default: 2MB + Also settable using mallopt(M_TRIM_THRESHOLD, x) + The maximum amount of unused top-most memory to keep before + releasing via malloc_trim in free(). Automatic trimming is mainly + useful in long-lived programs using contiguous MORECORE. Because + trimming via sbrk can be slow on some systems, and can sometimes be + wasteful (in cases where programs immediately afterward allocate + more large chunks) the value should be high enough so that your + overall system performance would improve by releasing this much + memory. As a rough guide, you might set to a value close to the + average size of a process (program) running on your system. + Releasing this much memory would allow such a process to run in + memory. Generally, it is worth tuning trim thresholds when a + program undergoes phases where several large chunks are allocated + and released in ways that can reuse each other's storage, perhaps + mixed with phases where there are no such chunks at all. The trim + value must be greater than page size to have any useful effect. To + disable trimming completely, you can set to MAX_SIZE_T. Note that the trick + some people use of mallocing a huge space and then freeing it at + program startup, in an attempt to reserve system memory, doesn't + have the intended effect under automatic trimming, since that memory + will immediately be returned to the system. + +DEFAULT_MMAP_THRESHOLD default: 256K + Also settable using mallopt(M_MMAP_THRESHOLD, x) + The request size threshold for using MMAP to directly service a + request. Requests of at least this size that cannot be allocated + using already-existing space will be serviced via mmap. (If enough + normal freed space already exists it is used instead.) Using mmap + segregates relatively large chunks of memory so that they can be + individually obtained and released from the host system. A request + serviced through mmap is never reused by any other request (at least + not directly; the system may just so happen to remap successive + requests to the same locations). Segregating space in this way has + the benefits that: Mmapped space can always be individually released + back to the system, which helps keep the system level memory demands + of a long-lived program low. Also, mapped memory doesn't become + `locked' between other chunks, as can happen with normally allocated + chunks, which means that even trimming via malloc_trim would not + release them. However, it has the disadvantage that the space + cannot be reclaimed, consolidated, and then used to service later + requests, as happens with normal chunks. The advantages of mmap + nearly always outweigh disadvantages for "large" chunks, but the + value of "large" may vary across systems. The default is an + empirically derived value that works well in most systems. You can + disable mmap by setting to MAX_SIZE_T. + +MAX_RELEASE_CHECK_RATE default: 4095 unless not HAVE_MMAP + The number of consolidated frees between checks to release + unused segments when freeing. When using non-contiguous segments, + especially with multiple mspaces, checking only for topmost space + doesn't always suffice to trigger trimming. To compensate for this, + free() will, with a period of MAX_RELEASE_CHECK_RATE (or the + current number of segments, if greater) try to release unused + segments to the OS when freeing chunks that result in + consolidation. The best value for this parameter is a compromise + between slowing down frees with relatively costly checks that + rarely trigger versus holding on to unused memory. To effectively + disable, set to MAX_SIZE_T. This may lead to a very slight speed + improvement at the expense of carrying around more memory. +*/ + +/* Version identifier to allow people to support multiple versions */ +#ifndef DLMALLOC_VERSION +#define DLMALLOC_VERSION 20806 +#endif /* DLMALLOC_VERSION */ + +#ifndef DLMALLOC_EXPORT +#define DLMALLOC_EXPORT extern +#endif + +#ifndef WIN32 +#ifdef _WIN32 +#define WIN32 1 +#endif /* _WIN32 */ +#ifdef _WIN32_WCE +#define LACKS_FCNTL_H +#define WIN32 1 +#endif /* _WIN32_WCE */ +#endif /* WIN32 */ +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#define HAVE_MMAP 1 +#define HAVE_MORECORE 0 +#define LACKS_UNISTD_H +#define LACKS_SYS_PARAM_H +#define LACKS_SYS_MMAN_H +#define LACKS_STRING_H +#define LACKS_STRINGS_H +#define LACKS_SYS_TYPES_H +#define LACKS_ERRNO_H +#define LACKS_SCHED_H +#ifndef MALLOC_FAILURE_ACTION +#define MALLOC_FAILURE_ACTION +#endif /* MALLOC_FAILURE_ACTION */ +#ifndef MMAP_CLEARS +#ifdef _WIN32_WCE /* WINCE reportedly does not clear */ +#define MMAP_CLEARS 0 +#else +#define MMAP_CLEARS 1 +#endif /* _WIN32_WCE */ +#endif /*MMAP_CLEARS */ +#endif /* WIN32 */ + +#if defined(DARWIN) || defined(_DARWIN) +/* Mac OSX docs advise not to use sbrk; it seems better to use mmap */ +#ifndef HAVE_MORECORE +#define HAVE_MORECORE 0 +#define HAVE_MMAP 1 +/* OSX allocators provide 16 byte alignment */ +#ifndef MALLOC_ALIGNMENT +#define MALLOC_ALIGNMENT ((size_t)16U) +#endif +#endif /* HAVE_MORECORE */ +#endif /* DARWIN */ + +#ifndef LACKS_SYS_TYPES_H +#include /* For size_t */ +#endif /* LACKS_SYS_TYPES_H */ + +/* The maximum possible size_t value has all bits set */ +#define MAX_SIZE_T (~(size_t)0) + +#ifndef USE_LOCKS /* ensure true if spin or recursive locks set */ +#define USE_LOCKS ((defined(USE_SPIN_LOCKS) && USE_SPIN_LOCKS != 0) || \ + (defined(USE_RECURSIVE_LOCKS) && USE_RECURSIVE_LOCKS != 0)) +#endif /* USE_LOCKS */ + +#if USE_LOCKS /* Spin locks for gcc >= 4.1, older gcc on x86, MSC >= 1310 */ +#if ((defined(__GNUC__) && \ + ((__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 1)) || \ + defined(__i386__) || defined(__x86_64__))) || \ + (defined(_MSC_VER) && _MSC_VER>=1310)) +#ifndef USE_SPIN_LOCKS +#define USE_SPIN_LOCKS 1 +#endif /* USE_SPIN_LOCKS */ +#elif USE_SPIN_LOCKS +#error "USE_SPIN_LOCKS defined without implementation" +#endif /* ... locks available... */ +#elif !defined(USE_SPIN_LOCKS) +#define USE_SPIN_LOCKS 0 +#endif /* USE_LOCKS */ + +#ifndef ONLY_MSPACES +#define ONLY_MSPACES 0 +#endif /* ONLY_MSPACES */ +#ifndef MSPACES +#if ONLY_MSPACES +#define MSPACES 1 +#else /* ONLY_MSPACES */ +#define MSPACES 0 +#endif /* ONLY_MSPACES */ +#endif /* MSPACES */ +#ifndef MALLOC_ALIGNMENT +#define MALLOC_ALIGNMENT ((size_t)(2 * sizeof(void *))) +#endif /* MALLOC_ALIGNMENT */ +#ifndef FOOTERS +#define FOOTERS 0 +#endif /* FOOTERS */ +#ifndef ABORT +#define ABORT abort() +#endif /* ABORT */ +#ifndef ABORT_ON_ASSERT_FAILURE +#define ABORT_ON_ASSERT_FAILURE 1 +#endif /* ABORT_ON_ASSERT_FAILURE */ +#ifndef PROCEED_ON_ERROR +#define PROCEED_ON_ERROR 0 +#endif /* PROCEED_ON_ERROR */ + +#ifndef INSECURE +#define INSECURE 0 +#endif /* INSECURE */ +#ifndef MALLOC_INSPECT_ALL +#define MALLOC_INSPECT_ALL 0 +#endif /* MALLOC_INSPECT_ALL */ +#ifndef HAVE_MMAP +#define HAVE_MMAP 1 +#endif /* HAVE_MMAP */ +#ifndef MMAP_CLEARS +#define MMAP_CLEARS 1 +#endif /* MMAP_CLEARS */ +#ifndef HAVE_MREMAP +#ifdef linux +#define HAVE_MREMAP 1 +#define _GNU_SOURCE /* Turns on mremap() definition */ +#else /* linux */ +#define HAVE_MREMAP 0 +#endif /* linux */ +#endif /* HAVE_MREMAP */ +#ifndef MALLOC_FAILURE_ACTION +#define MALLOC_FAILURE_ACTION errno = ENOMEM; +#endif /* MALLOC_FAILURE_ACTION */ +#ifndef HAVE_MORECORE +#if ONLY_MSPACES +#define HAVE_MORECORE 0 +#else /* ONLY_MSPACES */ +#define HAVE_MORECORE 1 +#endif /* ONLY_MSPACES */ +#endif /* HAVE_MORECORE */ +#if !HAVE_MORECORE +#define MORECORE_CONTIGUOUS 0 +#else /* !HAVE_MORECORE */ +#define MORECORE_DEFAULT sbrk +#ifndef MORECORE_CONTIGUOUS +#define MORECORE_CONTIGUOUS 1 +#endif /* MORECORE_CONTIGUOUS */ +#endif /* HAVE_MORECORE */ +#ifndef DEFAULT_GRANULARITY +#if (MORECORE_CONTIGUOUS || defined(WIN32)) +#define DEFAULT_GRANULARITY (0) /* 0 means to compute in init_mparams */ +#else /* MORECORE_CONTIGUOUS */ +#define DEFAULT_GRANULARITY ((size_t)64U * (size_t)1024U) +#endif /* MORECORE_CONTIGUOUS */ +#endif /* DEFAULT_GRANULARITY */ +#ifndef DEFAULT_TRIM_THRESHOLD +#ifndef MORECORE_CANNOT_TRIM +#define DEFAULT_TRIM_THRESHOLD ((size_t)2U * (size_t)1024U * (size_t)1024U) +#else /* MORECORE_CANNOT_TRIM */ +#define DEFAULT_TRIM_THRESHOLD MAX_SIZE_T +#endif /* MORECORE_CANNOT_TRIM */ +#endif /* DEFAULT_TRIM_THRESHOLD */ +#ifndef DEFAULT_MMAP_THRESHOLD +#if HAVE_MMAP +#define DEFAULT_MMAP_THRESHOLD ((size_t)256U * (size_t)1024U) +#else /* HAVE_MMAP */ +#define DEFAULT_MMAP_THRESHOLD MAX_SIZE_T +#endif /* HAVE_MMAP */ +#endif /* DEFAULT_MMAP_THRESHOLD */ +#ifndef MAX_RELEASE_CHECK_RATE +#if HAVE_MMAP +#define MAX_RELEASE_CHECK_RATE 4095 +#else +#define MAX_RELEASE_CHECK_RATE MAX_SIZE_T +#endif /* HAVE_MMAP */ +#endif /* MAX_RELEASE_CHECK_RATE */ +#ifndef USE_BUILTIN_FFS +#define USE_BUILTIN_FFS 0 +#endif /* USE_BUILTIN_FFS */ +#ifndef USE_DEV_RANDOM +#define USE_DEV_RANDOM 0 +#endif /* USE_DEV_RANDOM */ +#ifndef NO_MALLINFO +#define NO_MALLINFO 0 +#endif /* NO_MALLINFO */ +#ifndef MALLINFO_FIELD_TYPE +#define MALLINFO_FIELD_TYPE size_t +#endif /* MALLINFO_FIELD_TYPE */ +#ifndef NO_MALLOC_STATS +#define NO_MALLOC_STATS 0 +#endif /* NO_MALLOC_STATS */ +#ifndef NO_SEGMENT_TRAVERSAL +#define NO_SEGMENT_TRAVERSAL 0 +#endif /* NO_SEGMENT_TRAVERSAL */ + +/* + mallopt tuning options. SVID/XPG defines four standard parameter + numbers for mallopt, normally defined in malloc.h. None of these + are used in this malloc, so setting them has no effect. But this + malloc does support the following options. +*/ + +#define M_TRIM_THRESHOLD (-1) +#define M_GRANULARITY (-2) +#define M_MMAP_THRESHOLD (-3) + +/* ------------------------ Mallinfo declarations ------------------------ */ + +#if !NO_MALLINFO +/* + This version of malloc supports the standard SVID/XPG mallinfo + routine that returns a struct containing usage properties and + statistics. It should work on any system that has a + /usr/include/malloc.h defining struct mallinfo. The main + declaration needed is the mallinfo struct that is returned (by-copy) + by mallinfo(). The malloinfo struct contains a bunch of fields that + are not even meaningful in this version of malloc. These fields are + are instead filled by mallinfo() with other numbers that might be of + interest. + + HAVE_USR_INCLUDE_MALLOC_H should be set if you have a + /usr/include/malloc.h file that includes a declaration of struct + mallinfo. If so, it is included; else a compliant version is + declared below. These must be precisely the same for mallinfo() to + work. The original SVID version of this struct, defined on most + systems with mallinfo, declares all fields as ints. But some others + define as unsigned long. If your system defines the fields using a + type of different width than listed here, you MUST #include your + system version and #define HAVE_USR_INCLUDE_MALLOC_H. +*/ + +/* #define HAVE_USR_INCLUDE_MALLOC_H */ + +#ifdef HAVE_USR_INCLUDE_MALLOC_H +#include "/usr/include/malloc.h" +#else /* HAVE_USR_INCLUDE_MALLOC_H */ +#ifndef STRUCT_MALLINFO_DECLARED +/* HP-UX (and others?) redefines mallinfo unless _STRUCT_MALLINFO is defined */ +#define _STRUCT_MALLINFO +#define STRUCT_MALLINFO_DECLARED 1 +struct mallinfo { + MALLINFO_FIELD_TYPE arena; /* non-mmapped space allocated from system */ + MALLINFO_FIELD_TYPE ordblks; /* number of free chunks */ + MALLINFO_FIELD_TYPE smblks; /* always 0 */ + MALLINFO_FIELD_TYPE hblks; /* always 0 */ + MALLINFO_FIELD_TYPE hblkhd; /* space in mmapped regions */ + MALLINFO_FIELD_TYPE usmblks; /* maximum total allocated space */ + MALLINFO_FIELD_TYPE fsmblks; /* always 0 */ + MALLINFO_FIELD_TYPE uordblks; /* total allocated space */ + MALLINFO_FIELD_TYPE fordblks; /* total free space */ + MALLINFO_FIELD_TYPE keepcost; /* releasable (via malloc_trim) space */ +}; +#endif /* STRUCT_MALLINFO_DECLARED */ +#endif /* HAVE_USR_INCLUDE_MALLOC_H */ +#endif /* NO_MALLINFO */ + +/* + Try to persuade compilers to inline. The most critical functions for + inlining are defined as macros, so these aren't used for them. +*/ + +#ifndef FORCEINLINE + #if defined(__GNUC__) +#define FORCEINLINE __inline __attribute__ ((always_inline)) + #elif defined(_MSC_VER) + #define FORCEINLINE __forceinline + #endif +#endif +#ifndef NOINLINE + #if defined(__GNUC__) + #define NOINLINE __attribute__ ((noinline)) + #elif defined(_MSC_VER) + #define NOINLINE __declspec(noinline) + #else + #define NOINLINE + #endif +#endif + +#ifdef __cplusplus +extern "C" { +#ifndef FORCEINLINE + #define FORCEINLINE inline +#endif +#endif /* __cplusplus */ +#ifndef FORCEINLINE + #define FORCEINLINE +#endif + +#if !ONLY_MSPACES + +/* ------------------- Declarations of public routines ------------------- */ + +#ifndef USE_DL_PREFIX +#define dlcalloc calloc +#define dlfree free +#define dlmalloc malloc +#define dlmemalign memalign +#define dlposix_memalign posix_memalign +#define dlrealloc realloc +#define dlrealloc_in_place realloc_in_place +#define dlvalloc valloc +#define dlpvalloc pvalloc +#define dlmallinfo mallinfo +#define dlmallopt mallopt +#define dlmalloc_trim malloc_trim +#define dlmalloc_stats malloc_stats +#define dlmalloc_usable_size malloc_usable_size +#define dlmalloc_footprint malloc_footprint +#define dlmalloc_max_footprint malloc_max_footprint +#define dlmalloc_footprint_limit malloc_footprint_limit +#define dlmalloc_set_footprint_limit malloc_set_footprint_limit +#define dlmalloc_inspect_all malloc_inspect_all +#define dlindependent_calloc independent_calloc +#define dlindependent_comalloc independent_comalloc +#define dlbulk_free bulk_free +#endif /* USE_DL_PREFIX */ + +/* + malloc(size_t n) + Returns a pointer to a newly allocated chunk of at least n bytes, or + null if no space is available, in which case errno is set to ENOMEM + on ANSI C systems. + + If n is zero, malloc returns a minimum-sized chunk. (The minimum + size is 16 bytes on most 32bit systems, and 32 bytes on 64bit + systems.) Note that size_t is an unsigned type, so calls with + arguments that would be negative if signed are interpreted as + requests for huge amounts of space, which will often fail. The + maximum supported value of n differs across systems, but is in all + cases less than the maximum representable value of a size_t. +*/ +DLMALLOC_EXPORT void* dlmalloc(size_t); + +/* + free(void* p) + Releases the chunk of memory pointed to by p, that had been previously + allocated using malloc or a related routine such as realloc. + It has no effect if p is null. If p was not malloced or already + freed, free(p) will by default cause the current program to abort. +*/ +DLMALLOC_EXPORT void dlfree(void*); + +/* + calloc(size_t n_elements, size_t element_size); + Returns a pointer to n_elements * element_size bytes, with all locations + set to zero. +*/ +DLMALLOC_EXPORT void* dlcalloc(size_t, size_t); + +/* + realloc(void* p, size_t n) + Returns a pointer to a chunk of size n that contains the same data + as does chunk p up to the minimum of (n, p's size) bytes, or null + if no space is available. + + The returned pointer may or may not be the same as p. The algorithm + prefers extending p in most cases when possible, otherwise it + employs the equivalent of a malloc-copy-free sequence. + + If p is null, realloc is equivalent to malloc. + + If space is not available, realloc returns null, errno is set (if on + ANSI) and p is NOT freed. + + if n is for fewer bytes than already held by p, the newly unused + space is lopped off and freed if possible. realloc with a size + argument of zero (re)allocates a minimum-sized chunk. + + The old unix realloc convention of allowing the last-free'd chunk + to be used as an argument to realloc is not supported. +*/ +DLMALLOC_EXPORT void* dlrealloc(void*, size_t); + +/* + realloc_in_place(void* p, size_t n) + Resizes the space allocated for p to size n, only if this can be + done without moving p (i.e., only if there is adjacent space + available if n is greater than p's current allocated size, or n is + less than or equal to p's size). This may be used instead of plain + realloc if an alternative allocation strategy is needed upon failure + to expand space; for example, reallocation of a buffer that must be + memory-aligned or cleared. You can use realloc_in_place to trigger + these alternatives only when needed. + + Returns p if successful; otherwise null. +*/ +DLMALLOC_EXPORT void* dlrealloc_in_place(void*, size_t); + +/* + memalign(size_t alignment, size_t n); + Returns a pointer to a newly allocated chunk of n bytes, aligned + in accord with the alignment argument. + + The alignment argument should be a power of two. If the argument is + not a power of two, the nearest greater power is used. + 8-byte alignment is guaranteed by normal malloc calls, so don't + bother calling memalign with an argument of 8 or less. + + Overreliance on memalign is a sure way to fragment space. +*/ +DLMALLOC_EXPORT void* dlmemalign(size_t, size_t); + +/* + int posix_memalign(void** pp, size_t alignment, size_t n); + Allocates a chunk of n bytes, aligned in accord with the alignment + argument. Differs from memalign only in that it (1) assigns the + allocated memory to *pp rather than returning it, (2) fails and + returns EINVAL if the alignment is not a power of two (3) fails and + returns ENOMEM if memory cannot be allocated. +*/ +DLMALLOC_EXPORT int dlposix_memalign(void**, size_t, size_t); + +/* + valloc(size_t n); + Equivalent to memalign(pagesize, n), where pagesize is the page + size of the system. If the pagesize is unknown, 4096 is used. +*/ +DLMALLOC_EXPORT void* dlvalloc(size_t); + +/* + mallopt(int parameter_number, int parameter_value) + Sets tunable parameters The format is to provide a + (parameter-number, parameter-value) pair. mallopt then sets the + corresponding parameter to the argument value if it can (i.e., so + long as the value is meaningful), and returns 1 if successful else + 0. To workaround the fact that mallopt is specified to use int, + not size_t parameters, the value -1 is specially treated as the + maximum unsigned size_t value. + + SVID/XPG/ANSI defines four standard param numbers for mallopt, + normally defined in malloc.h. None of these are use in this malloc, + so setting them has no effect. But this malloc also supports other + options in mallopt. See below for details. Briefly, supported + parameters are as follows (listed defaults are for "typical" + configurations). + + Symbol param # default allowed param values + M_TRIM_THRESHOLD -1 2*1024*1024 any (-1 disables) + M_GRANULARITY -2 page size any power of 2 >= page size + M_MMAP_THRESHOLD -3 256*1024 any (or 0 if no MMAP support) +*/ +DLMALLOC_EXPORT int dlmallopt(int, int); + +/* + malloc_footprint(); + Returns the number of bytes obtained from the system. The total + number of bytes allocated by malloc, realloc etc., is less than this + value. Unlike mallinfo, this function returns only a precomputed + result, so can be called frequently to monitor memory consumption. + Even if locks are otherwise defined, this function does not use them, + so results might not be up to date. +*/ +DLMALLOC_EXPORT size_t dlmalloc_footprint(void); + +/* + malloc_max_footprint(); + Returns the maximum number of bytes obtained from the system. This + value will be greater than current footprint if deallocated space + has been reclaimed by the system. The peak number of bytes allocated + by malloc, realloc etc., is less than this value. Unlike mallinfo, + this function returns only a precomputed result, so can be called + frequently to monitor memory consumption. Even if locks are + otherwise defined, this function does not use them, so results might + not be up to date. +*/ +DLMALLOC_EXPORT size_t dlmalloc_max_footprint(void); + +/* + malloc_footprint_limit(); + Returns the number of bytes that the heap is allowed to obtain from + the system, returning the last value returned by + malloc_set_footprint_limit, or the maximum size_t value if + never set. The returned value reflects a permission. There is no + guarantee that this number of bytes can actually be obtained from + the system. +*/ +DLMALLOC_EXPORT size_t dlmalloc_footprint_limit(); + +/* + malloc_set_footprint_limit(); + Sets the maximum number of bytes to obtain from the system, causing + failure returns from malloc and related functions upon attempts to + exceed this value. The argument value may be subject to page + rounding to an enforceable limit; this actual value is returned. + Using an argument of the maximum possible size_t effectively + disables checks. If the argument is less than or equal to the + current malloc_footprint, then all future allocations that require + additional system memory will fail. However, invocation cannot + retroactively deallocate existing used memory. +*/ +DLMALLOC_EXPORT size_t dlmalloc_set_footprint_limit(size_t bytes); + +#if MALLOC_INSPECT_ALL +/* + malloc_inspect_all(void(*handler)(void *start, + void *end, + size_t used_bytes, + void* callback_arg), + void* arg); + Traverses the heap and calls the given handler for each managed + region, skipping all bytes that are (or may be) used for bookkeeping + purposes. Traversal does not include include chunks that have been + directly memory mapped. Each reported region begins at the start + address, and continues up to but not including the end address. The + first used_bytes of the region contain allocated data. If + used_bytes is zero, the region is unallocated. The handler is + invoked with the given callback argument. If locks are defined, they + are held during the entire traversal. It is a bad idea to invoke + other malloc functions from within the handler. + + For example, to count the number of in-use chunks with size greater + than 1000, you could write: + static int count = 0; + void count_chunks(void* start, void* end, size_t used, void* arg) { + if (used >= 1000) ++count; + } + then: + malloc_inspect_all(count_chunks, NULL); + + malloc_inspect_all is compiled only if MALLOC_INSPECT_ALL is defined. +*/ +DLMALLOC_EXPORT void dlmalloc_inspect_all(void(*handler)(void*, void *, size_t, void*), + void* arg); + +#endif /* MALLOC_INSPECT_ALL */ + +#if !NO_MALLINFO +/* + mallinfo() + Returns (by copy) a struct containing various summary statistics: + + arena: current total non-mmapped bytes allocated from system + ordblks: the number of free chunks + smblks: always zero. + hblks: current number of mmapped regions + hblkhd: total bytes held in mmapped regions + usmblks: the maximum total allocated space. This will be greater + than current total if trimming has occurred. + fsmblks: always zero + uordblks: current total allocated space (normal or mmapped) + fordblks: total free space + keepcost: the maximum number of bytes that could ideally be released + back to system via malloc_trim. ("ideally" means that + it ignores page restrictions etc.) + + Because these fields are ints, but internal bookkeeping may + be kept as longs, the reported values may wrap around zero and + thus be inaccurate. +*/ +DLMALLOC_EXPORT struct mallinfo dlmallinfo(void); +#endif /* NO_MALLINFO */ + +/* + independent_calloc(size_t n_elements, size_t element_size, void* chunks[]); + + independent_calloc is similar to calloc, but instead of returning a + single cleared space, it returns an array of pointers to n_elements + independent elements that can hold contents of size elem_size, each + of which starts out cleared, and can be independently freed, + realloc'ed etc. The elements are guaranteed to be adjacently + allocated (this is not guaranteed to occur with multiple callocs or + mallocs), which may also improve cache locality in some + applications. + + The "chunks" argument is optional (i.e., may be null, which is + probably the most typical usage). If it is null, the returned array + is itself dynamically allocated and should also be freed when it is + no longer needed. Otherwise, the chunks array must be of at least + n_elements in length. It is filled in with the pointers to the + chunks. + + In either case, independent_calloc returns this pointer array, or + null if the allocation failed. If n_elements is zero and "chunks" + is null, it returns a chunk representing an array with zero elements + (which should be freed if not wanted). + + Each element must be freed when it is no longer needed. This can be + done all at once using bulk_free. + + independent_calloc simplifies and speeds up implementations of many + kinds of pools. It may also be useful when constructing large data + structures that initially have a fixed number of fixed-sized nodes, + but the number is not known at compile time, and some of the nodes + may later need to be freed. For example: + + struct Node { int item; struct Node* next; }; + + struct Node* build_list() { + struct Node** pool; + int n = read_number_of_nodes_needed(); + if (n <= 0) return 0; + pool = (struct Node**)(independent_calloc(n, sizeof(struct Node), 0); + if (pool == 0) die(); + // organize into a linked list... + struct Node* first = pool[0]; + for (i = 0; i < n-1; ++i) + pool[i]->next = pool[i+1]; + free(pool); // Can now free the array (or not, if it is needed later) + return first; + } +*/ +DLMALLOC_EXPORT void** dlindependent_calloc(size_t, size_t, void**); + +/* + independent_comalloc(size_t n_elements, size_t sizes[], void* chunks[]); + + independent_comalloc allocates, all at once, a set of n_elements + chunks with sizes indicated in the "sizes" array. It returns + an array of pointers to these elements, each of which can be + independently freed, realloc'ed etc. The elements are guaranteed to + be adjacently allocated (this is not guaranteed to occur with + multiple callocs or mallocs), which may also improve cache locality + in some applications. + + The "chunks" argument is optional (i.e., may be null). If it is null + the returned array is itself dynamically allocated and should also + be freed when it is no longer needed. Otherwise, the chunks array + must be of at least n_elements in length. It is filled in with the + pointers to the chunks. + + In either case, independent_comalloc returns this pointer array, or + null if the allocation failed. If n_elements is zero and chunks is + null, it returns a chunk representing an array with zero elements + (which should be freed if not wanted). + + Each element must be freed when it is no longer needed. This can be + done all at once using bulk_free. + + independent_comallac differs from independent_calloc in that each + element may have a different size, and also that it does not + automatically clear elements. + + independent_comalloc can be used to speed up allocation in cases + where several structs or objects must always be allocated at the + same time. For example: + + struct Head { ... } + struct Foot { ... } + + void send_message(char* msg) { + int msglen = strlen(msg); + size_t sizes[3] = { sizeof(struct Head), msglen, sizeof(struct Foot) }; + void* chunks[3]; + if (independent_comalloc(3, sizes, chunks) == 0) + die(); + struct Head* head = (struct Head*)(chunks[0]); + char* body = (char*)(chunks[1]); + struct Foot* foot = (struct Foot*)(chunks[2]); + // ... + } + + In general though, independent_comalloc is worth using only for + larger values of n_elements. For small values, you probably won't + detect enough difference from series of malloc calls to bother. + + Overuse of independent_comalloc can increase overall memory usage, + since it cannot reuse existing noncontiguous small chunks that + might be available for some of the elements. +*/ +DLMALLOC_EXPORT void** dlindependent_comalloc(size_t, size_t*, void**); + +/* + bulk_free(void* array[], size_t n_elements) + Frees and clears (sets to null) each non-null pointer in the given + array. This is likely to be faster than freeing them one-by-one. + If footers are used, pointers that have been allocated in different + mspaces are not freed or cleared, and the count of all such pointers + is returned. For large arrays of pointers with poor locality, it + may be worthwhile to sort this array before calling bulk_free. +*/ +DLMALLOC_EXPORT size_t dlbulk_free(void**, size_t n_elements); + +/* + pvalloc(size_t n); + Equivalent to valloc(minimum-page-that-holds(n)), that is, + round up n to nearest pagesize. + */ +DLMALLOC_EXPORT void* dlpvalloc(size_t); + +/* + malloc_trim(size_t pad); + + If possible, gives memory back to the system (via negative arguments + to sbrk) if there is unused memory at the `high' end of the malloc + pool or in unused MMAP segments. You can call this after freeing + large blocks of memory to potentially reduce the system-level memory + requirements of a program. However, it cannot guarantee to reduce + memory. Under some allocation patterns, some large free blocks of + memory will be locked between two used chunks, so they cannot be + given back to the system. + + The `pad' argument to malloc_trim represents the amount of free + trailing space to leave untrimmed. If this argument is zero, only + the minimum amount of memory to maintain internal data structures + will be left. Non-zero arguments can be supplied to maintain enough + trailing space to service future expected allocations without having + to re-obtain memory from the system. + + Malloc_trim returns 1 if it actually released any memory, else 0. +*/ +DLMALLOC_EXPORT int dlmalloc_trim(size_t); + +/* + malloc_stats(); + Prints on stderr the amount of space obtained from the system (both + via sbrk and mmap), the maximum amount (which may be more than + current if malloc_trim and/or munmap got called), and the current + number of bytes allocated via malloc (or realloc, etc) but not yet + freed. Note that this is the number of bytes allocated, not the + number requested. It will be larger than the number requested + because of alignment and bookkeeping overhead. Because it includes + alignment wastage as being in use, this figure may be greater than + zero even when no user-level chunks are allocated. + + The reported current and maximum system memory can be inaccurate if + a program makes other calls to system memory allocation functions + (normally sbrk) outside of malloc. + + malloc_stats prints only the most commonly interesting statistics. + More information can be obtained by calling mallinfo. +*/ +DLMALLOC_EXPORT void dlmalloc_stats(void); + +/* + malloc_usable_size(void* p); + + Returns the number of bytes you can actually use in + an allocated chunk, which may be more than you requested (although + often not) due to alignment and minimum size constraints. + You can use this many bytes without worrying about + overwriting other allocated objects. This is not a particularly great + programming practice. malloc_usable_size can be more useful in + debugging and assertions, for example: + + p = malloc(n); + assert(malloc_usable_size(p) >= 256); +*/ +size_t dlmalloc_usable_size(void*); + +#endif /* ONLY_MSPACES */ + +#if MSPACES + +/* + mspace is an opaque type representing an independent + region of space that supports mspace_malloc, etc. +*/ +typedef void* mspace; + +/* + create_mspace creates and returns a new independent space with the + given initial capacity, or, if 0, the default granularity size. It + returns null if there is no system memory available to create the + space. If argument locked is non-zero, the space uses a separate + lock to control access. The capacity of the space will grow + dynamically as needed to service mspace_malloc requests. You can + control the sizes of incremental increases of this space by + compiling with a different DEFAULT_GRANULARITY or dynamically + setting with mallopt(M_GRANULARITY, value). +*/ +DLMALLOC_EXPORT mspace create_mspace(size_t capacity, int locked); + +/* + destroy_mspace destroys the given space, and attempts to return all + of its memory back to the system, returning the total number of + bytes freed. After destruction, the results of access to all memory + used by the space become undefined. +*/ +DLMALLOC_EXPORT size_t destroy_mspace(mspace msp); + +/* + create_mspace_with_base uses the memory supplied as the initial base + of a new mspace. Part (less than 128*sizeof(size_t) bytes) of this + space is used for bookkeeping, so the capacity must be at least this + large. (Otherwise 0 is returned.) When this initial space is + exhausted, additional memory will be obtained from the system. + Destroying this space will deallocate all additionally allocated + space (if possible) but not the initial base. +*/ +DLMALLOC_EXPORT mspace create_mspace_with_base(void* base, size_t capacity, int locked); + +/* + mspace_track_large_chunks controls whether requests for large chunks + are allocated in their own untracked mmapped regions, separate from + others in this mspace. By default large chunks are not tracked, + which reduces fragmentation. However, such chunks are not + necessarily released to the system upon destroy_mspace. Enabling + tracking by setting to true may increase fragmentation, but avoids + leakage when relying on destroy_mspace to release all memory + allocated using this space. The function returns the previous + setting. +*/ +DLMALLOC_EXPORT int mspace_track_large_chunks(mspace msp, int enable); + + +/* + mspace_malloc behaves as malloc, but operates within + the given space. +*/ +DLMALLOC_EXPORT void* mspace_malloc(mspace msp, size_t bytes); + +/* + mspace_free behaves as free, but operates within + the given space. + + If compiled with FOOTERS==1, mspace_free is not actually needed. + free may be called instead of mspace_free because freed chunks from + any space are handled by their originating spaces. +*/ +DLMALLOC_EXPORT void mspace_free(mspace msp, void* mem); + +/* + mspace_realloc behaves as realloc, but operates within + the given space. + + If compiled with FOOTERS==1, mspace_realloc is not actually + needed. realloc may be called instead of mspace_realloc because + realloced chunks from any space are handled by their originating + spaces. +*/ +DLMALLOC_EXPORT void* mspace_realloc(mspace msp, void* mem, size_t newsize); + +/* + mspace_calloc behaves as calloc, but operates within + the given space. +*/ +DLMALLOC_EXPORT void* mspace_calloc(mspace msp, size_t n_elements, size_t elem_size); + +/* + mspace_memalign behaves as memalign, but operates within + the given space. +*/ +DLMALLOC_EXPORT void* mspace_memalign(mspace msp, size_t alignment, size_t bytes); + +/* + mspace_independent_calloc behaves as independent_calloc, but + operates within the given space. +*/ +DLMALLOC_EXPORT void** mspace_independent_calloc(mspace msp, size_t n_elements, + size_t elem_size, void* chunks[]); + +/* + mspace_independent_comalloc behaves as independent_comalloc, but + operates within the given space. +*/ +DLMALLOC_EXPORT void** mspace_independent_comalloc(mspace msp, size_t n_elements, + size_t sizes[], void* chunks[]); + +/* + mspace_footprint() returns the number of bytes obtained from the + system for this space. +*/ +DLMALLOC_EXPORT size_t mspace_footprint(mspace msp); + +/* + mspace_max_footprint() returns the peak number of bytes obtained from the + system for this space. +*/ +DLMALLOC_EXPORT size_t mspace_max_footprint(mspace msp); + + +#if !NO_MALLINFO +/* + mspace_mallinfo behaves as mallinfo, but reports properties of + the given space. +*/ +DLMALLOC_EXPORT struct mallinfo mspace_mallinfo(mspace msp); +#endif /* NO_MALLINFO */ + +/* + malloc_usable_size(void* p) behaves the same as malloc_usable_size; +*/ +DLMALLOC_EXPORT size_t mspace_usable_size(const void* mem); + +/* + mspace_malloc_stats behaves as malloc_stats, but reports + properties of the given space. +*/ +DLMALLOC_EXPORT void mspace_malloc_stats(mspace msp); + +/* + mspace_trim behaves as malloc_trim, but + operates within the given space. +*/ +DLMALLOC_EXPORT int mspace_trim(mspace msp, size_t pad); + +/* + An alias for mallopt. +*/ +DLMALLOC_EXPORT int mspace_mallopt(int, int); + +#endif /* MSPACES */ + +#ifdef __cplusplus +} /* end of extern "C" */ +#endif /* __cplusplus */ + +/* + ======================================================================== + To make a fully customizable malloc.h header file, cut everything + above this line, put into file malloc.h, edit to suit, and #include it + on the next line, as well as in programs that use this malloc. + ======================================================================== +*/ + +/* #include "malloc.h" */ + +/*------------------------------ internal #includes ---------------------- */ + +#ifdef _MSC_VER +#pragma warning( disable : 4146 ) /* no "unsigned" warnings */ +#endif /* _MSC_VER */ +#if !NO_MALLOC_STATS +#include /* for printing in malloc_stats */ +#endif /* NO_MALLOC_STATS */ +#ifndef LACKS_ERRNO_H +#include /* for MALLOC_FAILURE_ACTION */ +#endif /* LACKS_ERRNO_H */ +#ifdef DEBUG +#if ABORT_ON_ASSERT_FAILURE +#undef assert +#define assert(x) if(!(x)) ABORT +#else /* ABORT_ON_ASSERT_FAILURE */ +#include +#endif /* ABORT_ON_ASSERT_FAILURE */ +#else /* DEBUG */ +#ifndef assert +#define assert(x) +#endif +#define DEBUG 0 +#endif /* DEBUG */ +#if !defined(WIN32) && !defined(LACKS_TIME_H) +#include /* for magic initialization */ +#endif /* WIN32 */ +#ifndef LACKS_STDLIB_H +#include /* for abort() */ +#endif /* LACKS_STDLIB_H */ +#ifndef LACKS_STRING_H +#include /* for memset etc */ +#endif /* LACKS_STRING_H */ +#if USE_BUILTIN_FFS +#ifndef LACKS_STRINGS_H +#include /* for ffs */ +#endif /* LACKS_STRINGS_H */ +#endif /* USE_BUILTIN_FFS */ +#if HAVE_MMAP +#ifndef LACKS_SYS_MMAN_H +/* On some versions of linux, mremap decl in mman.h needs __USE_GNU set */ +#if (defined(linux) && !defined(__USE_GNU)) +#define __USE_GNU 1 +#include /* for mmap */ +#undef __USE_GNU +#else +#include /* for mmap */ +#endif /* linux */ +#endif /* LACKS_SYS_MMAN_H */ +#ifndef LACKS_FCNTL_H +#include +#endif /* LACKS_FCNTL_H */ +#endif /* HAVE_MMAP */ +#ifndef LACKS_UNISTD_H +#include /* for sbrk, sysconf */ +#else /* LACKS_UNISTD_H */ +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) && !defined(__NetBSD__) +extern void* sbrk(ptrdiff_t); +#endif /* FreeBSD etc */ +#endif /* LACKS_UNISTD_H */ + +/* Declarations for locking */ +#if USE_LOCKS +#ifndef WIN32 +#if defined (__SVR4) && defined (__sun) /* solaris */ +#include +#elif !defined(LACKS_SCHED_H) +#include +#endif /* solaris or LACKS_SCHED_H */ +#if (defined(USE_RECURSIVE_LOCKS) && USE_RECURSIVE_LOCKS != 0) || !USE_SPIN_LOCKS +#include +#endif /* USE_RECURSIVE_LOCKS ... */ +#elif defined(_MSC_VER) +#ifndef _M_AMD64 +/* These are already defined on AMD64 builds */ +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ +LONG __cdecl _InterlockedCompareExchange(LONG volatile *Dest, LONG Exchange, LONG Comp); +LONG __cdecl _InterlockedExchange(LONG volatile *Target, LONG Value); +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* _M_AMD64 */ +#pragma intrinsic (_InterlockedCompareExchange) +#pragma intrinsic (_InterlockedExchange) +#define interlockedcompareexchange _InterlockedCompareExchange +#define interlockedexchange _InterlockedExchange +#elif defined(WIN32) && defined(__GNUC__) +#define interlockedcompareexchange(a, b, c) __sync_val_compare_and_swap(a, c, b) +#define interlockedexchange __sync_lock_test_and_set +#endif /* Win32 */ +#else /* USE_LOCKS */ +#endif /* USE_LOCKS */ + +#ifndef LOCK_AT_FORK +#define LOCK_AT_FORK 0 +#endif + +/* Declarations for bit scanning on win32 */ +#if defined(_MSC_VER) && _MSC_VER>=1300 +#ifndef BitScanForward /* Try to avoid pulling in WinNT.h */ +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ +unsigned char _BitScanForward(unsigned long *index, unsigned long mask); +unsigned char _BitScanReverse(unsigned long *index, unsigned long mask); +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#define BitScanForward _BitScanForward +#define BitScanReverse _BitScanReverse +#pragma intrinsic(_BitScanForward) +#pragma intrinsic(_BitScanReverse) +#endif /* BitScanForward */ +#endif /* defined(_MSC_VER) && _MSC_VER>=1300 */ + +#ifndef WIN32 +#ifndef malloc_getpagesize +# ifdef _SC_PAGESIZE /* some SVR4 systems omit an underscore */ +# ifndef _SC_PAGE_SIZE +# define _SC_PAGE_SIZE _SC_PAGESIZE +# endif +# endif +# ifdef _SC_PAGE_SIZE +# define malloc_getpagesize sysconf(_SC_PAGE_SIZE) +# else +# if defined(BSD) || defined(DGUX) || defined(HAVE_GETPAGESIZE) + extern size_t getpagesize(); +# define malloc_getpagesize getpagesize() +# else +# ifdef WIN32 /* use supplied emulation of getpagesize */ +# define malloc_getpagesize getpagesize() +# else +# ifndef LACKS_SYS_PARAM_H +# include +# endif +# ifdef EXEC_PAGESIZE +# define malloc_getpagesize EXEC_PAGESIZE +# else +# ifdef NBPG +# ifndef CLSIZE +# define malloc_getpagesize NBPG +# else +# define malloc_getpagesize (NBPG * CLSIZE) +# endif +# else +# ifdef NBPC +# define malloc_getpagesize NBPC +# else +# ifdef PAGESIZE +# define malloc_getpagesize PAGESIZE +# else /* just guess */ +# define malloc_getpagesize ((size_t)4096U) +# endif +# endif +# endif +# endif +# endif +# endif +# endif +#endif +#endif + +/* ------------------- size_t and alignment properties -------------------- */ + +/* The byte and bit size of a size_t */ +#define SIZE_T_SIZE (sizeof(size_t)) +#define SIZE_T_BITSIZE (sizeof(size_t) << 3) + +/* Some constants coerced to size_t */ +/* Annoying but necessary to avoid errors on some platforms */ +#define SIZE_T_ZERO ((size_t)0) +#define SIZE_T_ONE ((size_t)1) +#define SIZE_T_TWO ((size_t)2) +#define SIZE_T_FOUR ((size_t)4) +#define TWO_SIZE_T_SIZES (SIZE_T_SIZE<<1) +#define FOUR_SIZE_T_SIZES (SIZE_T_SIZE<<2) +#define SIX_SIZE_T_SIZES (FOUR_SIZE_T_SIZES+TWO_SIZE_T_SIZES) +#define HALF_MAX_SIZE_T (MAX_SIZE_T / 2U) + +/* The bit mask value corresponding to MALLOC_ALIGNMENT */ +#define CHUNK_ALIGN_MASK (MALLOC_ALIGNMENT - SIZE_T_ONE) + +/* True if address a has acceptable alignment */ +#define is_aligned(A) (((size_t)((A)) & (CHUNK_ALIGN_MASK)) == 0) + +/* the number of bytes to offset an address to align it */ +#define align_offset(A)\ + ((((size_t)(A) & CHUNK_ALIGN_MASK) == 0)? 0 :\ + ((MALLOC_ALIGNMENT - ((size_t)(A) & CHUNK_ALIGN_MASK)) & CHUNK_ALIGN_MASK)) + +/* -------------------------- MMAP preliminaries ------------------------- */ + +/* + If HAVE_MORECORE or HAVE_MMAP are false, we just define calls and + checks to fail so compiler optimizer can delete code rather than + using so many "#if"s. +*/ + + +/* MORECORE and MMAP must return MFAIL on failure */ +#define MFAIL ((void*)(MAX_SIZE_T)) +#define CMFAIL ((char*)(MFAIL)) /* defined for convenience */ + +#if HAVE_MMAP + +#ifndef WIN32 +#define MUNMAP_DEFAULT(a, s) munmap((a), (s)) +#define MMAP_PROT (PROT_READ|PROT_WRITE) +#if !defined(MAP_ANONYMOUS) && defined(MAP_ANON) +#define MAP_ANONYMOUS MAP_ANON +#endif /* MAP_ANON */ +#ifdef MAP_ANONYMOUS +#define MMAP_FLAGS (MAP_PRIVATE|MAP_ANONYMOUS) +#define MMAP_DEFAULT(s) mmap(0, (s), MMAP_PROT, MMAP_FLAGS, -1, 0) +#else /* MAP_ANONYMOUS */ +/* + Nearly all versions of mmap support MAP_ANONYMOUS, so the following + is unlikely to be needed, but is supplied just in case. +*/ +#define MMAP_FLAGS (MAP_PRIVATE) +static int dev_zero_fd = -1; /* Cached file descriptor for /dev/zero. */ +#define MMAP_DEFAULT(s) ((dev_zero_fd < 0) ? \ + (dev_zero_fd = open("/dev/zero", O_RDWR), \ + mmap(0, (s), MMAP_PROT, MMAP_FLAGS, dev_zero_fd, 0)) : \ + mmap(0, (s), MMAP_PROT, MMAP_FLAGS, dev_zero_fd, 0)) +#endif /* MAP_ANONYMOUS */ + +#define DIRECT_MMAP_DEFAULT(s) MMAP_DEFAULT(s) + +#else /* WIN32 */ + +/* Win32 MMAP via VirtualAlloc */ +static FORCEINLINE void* win32mmap(size_t size) { + void* ptr = VirtualAlloc(0, size, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); + return (ptr != 0)? ptr: MFAIL; +} + +/* For direct MMAP, use MEM_TOP_DOWN to minimize interference */ +static FORCEINLINE void* win32direct_mmap(size_t size) { + void* ptr = VirtualAlloc(0, size, MEM_RESERVE|MEM_COMMIT|MEM_TOP_DOWN, + PAGE_READWRITE); + return (ptr != 0)? ptr: MFAIL; +} + +/* This function supports releasing coalesed segments */ +static FORCEINLINE int win32munmap(void* ptr, size_t size) { + MEMORY_BASIC_INFORMATION minfo; + char* cptr = (char*)ptr; + while (size) { + if (VirtualQuery(cptr, &minfo, sizeof(minfo)) == 0) + return -1; + if (minfo.BaseAddress != cptr || minfo.AllocationBase != cptr || + minfo.State != MEM_COMMIT || minfo.RegionSize > size) + return -1; + if (VirtualFree(cptr, 0, MEM_RELEASE) == 0) + return -1; + cptr += minfo.RegionSize; + size -= minfo.RegionSize; + } + return 0; +} + +#define MMAP_DEFAULT(s) win32mmap(s) +#define MUNMAP_DEFAULT(a, s) win32munmap((a), (s)) +#define DIRECT_MMAP_DEFAULT(s) win32direct_mmap(s) +#endif /* WIN32 */ +#endif /* HAVE_MMAP */ + +#if HAVE_MREMAP +#ifndef WIN32 +#define MREMAP_DEFAULT(addr, osz, nsz, mv) mremap((addr), (osz), (nsz), (mv)) +#endif /* WIN32 */ +#endif /* HAVE_MREMAP */ + +/** + * Define CALL_MORECORE + */ +#if HAVE_MORECORE + #ifdef MORECORE + #define CALL_MORECORE(S) MORECORE(S) + #else /* MORECORE */ + #define CALL_MORECORE(S) MORECORE_DEFAULT(S) + #endif /* MORECORE */ +#else /* HAVE_MORECORE */ + #define CALL_MORECORE(S) MFAIL +#endif /* HAVE_MORECORE */ + +/** + * Define CALL_MMAP/CALL_MUNMAP/CALL_DIRECT_MMAP + */ +#if HAVE_MMAP + #define USE_MMAP_BIT (SIZE_T_ONE) + + #ifdef MMAP + #define CALL_MMAP(s) MMAP(s) + #else /* MMAP */ + #define CALL_MMAP(s) MMAP_DEFAULT(s) + #endif /* MMAP */ + #ifdef MUNMAP + #define CALL_MUNMAP(a, s) MUNMAP((a), (s)) + #else /* MUNMAP */ + #define CALL_MUNMAP(a, s) MUNMAP_DEFAULT((a), (s)) + #endif /* MUNMAP */ + #ifdef DIRECT_MMAP + #define CALL_DIRECT_MMAP(s) DIRECT_MMAP(s) + #else /* DIRECT_MMAP */ + #define CALL_DIRECT_MMAP(s) DIRECT_MMAP_DEFAULT(s) + #endif /* DIRECT_MMAP */ +#else /* HAVE_MMAP */ + #define USE_MMAP_BIT (SIZE_T_ZERO) + + #define MMAP(s) MFAIL + #define MUNMAP(a, s) (-1) + #define DIRECT_MMAP(s) MFAIL + #define CALL_DIRECT_MMAP(s) DIRECT_MMAP(s) + #define CALL_MMAP(s) MMAP(s) + #define CALL_MUNMAP(a, s) MUNMAP((a), (s)) +#endif /* HAVE_MMAP */ + +/** + * Define CALL_MREMAP + */ +#if HAVE_MMAP && HAVE_MREMAP + #ifdef MREMAP + #define CALL_MREMAP(addr, osz, nsz, mv) MREMAP((addr), (osz), (nsz), (mv)) + #else /* MREMAP */ + #define CALL_MREMAP(addr, osz, nsz, mv) MREMAP_DEFAULT((addr), (osz), (nsz), (mv)) + #endif /* MREMAP */ +#else /* HAVE_MMAP && HAVE_MREMAP */ + #define CALL_MREMAP(addr, osz, nsz, mv) MFAIL +#endif /* HAVE_MMAP && HAVE_MREMAP */ + +/* mstate bit set if continguous morecore disabled or failed */ +#define USE_NONCONTIGUOUS_BIT (4U) + +/* segment bit set in create_mspace_with_base */ +#define EXTERN_BIT (8U) + + +/* --------------------------- Lock preliminaries ------------------------ */ + +/* + When locks are defined, there is one global lock, plus + one per-mspace lock. + + The global lock_ensures that mparams.magic and other unique + mparams values are initialized only once. It also protects + sequences of calls to MORECORE. In many cases sys_alloc requires + two calls, that should not be interleaved with calls by other + threads. This does not protect against direct calls to MORECORE + by other threads not using this lock, so there is still code to + cope the best we can on interference. + + Per-mspace locks surround calls to malloc, free, etc. + By default, locks are simple non-reentrant mutexes. + + Because lock-protected regions generally have bounded times, it is + OK to use the supplied simple spinlocks. Spinlocks are likely to + improve performance for lightly contended applications, but worsen + performance under heavy contention. + + If USE_LOCKS is > 1, the definitions of lock routines here are + bypassed, in which case you will need to define the type MLOCK_T, + and at least INITIAL_LOCK, DESTROY_LOCK, ACQUIRE_LOCK, RELEASE_LOCK + and TRY_LOCK. You must also declare a + static MLOCK_T malloc_global_mutex = { initialization values };. + +*/ + +#if !USE_LOCKS +#define USE_LOCK_BIT (0U) +#define INITIAL_LOCK(l) (0) +#define DESTROY_LOCK(l) (0) +#define ACQUIRE_MALLOC_GLOBAL_LOCK() +#define RELEASE_MALLOC_GLOBAL_LOCK() + +#else +#if USE_LOCKS > 1 +/* ----------------------- User-defined locks ------------------------ */ +/* Define your own lock implementation here */ +/* #define INITIAL_LOCK(lk) ... */ +/* #define DESTROY_LOCK(lk) ... */ +/* #define ACQUIRE_LOCK(lk) ... */ +/* #define RELEASE_LOCK(lk) ... */ +/* #define TRY_LOCK(lk) ... */ +/* static MLOCK_T malloc_global_mutex = ... */ + +#elif USE_SPIN_LOCKS + +/* First, define CAS_LOCK and CLEAR_LOCK on ints */ +/* Note CAS_LOCK defined to return 0 on success */ + +#if defined(__GNUC__)&& (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 1)) +#define CAS_LOCK(sl) __sync_lock_test_and_set(sl, 1) +#define CLEAR_LOCK(sl) __sync_lock_release(sl) + +#elif (defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__))) +/* Custom spin locks for older gcc on x86 */ +static FORCEINLINE int x86_cas_lock(int *sl) { + int ret; + int val = 1; + int cmp = 0; + __asm__ __volatile__ ("lock; cmpxchgl %1, %2" + : "=a" (ret) + : "r" (val), "m" (*(sl)), "0"(cmp) + : "memory", "cc"); + return ret; +} + +static FORCEINLINE void x86_clear_lock(int* sl) { + assert(*sl != 0); + int prev = 0; + int ret; + __asm__ __volatile__ ("lock; xchgl %0, %1" + : "=r" (ret) + : "m" (*(sl)), "0"(prev) + : "memory"); +} + +#define CAS_LOCK(sl) x86_cas_lock(sl) +#define CLEAR_LOCK(sl) x86_clear_lock(sl) + +#else /* Win32 MSC */ +#define CAS_LOCK(sl) interlockedexchange(sl, (LONG)1) +#define CLEAR_LOCK(sl) interlockedexchange (sl, (LONG)0) + +#endif /* ... gcc spins locks ... */ + +/* How to yield for a spin lock */ +#define SPINS_PER_YIELD 63 +#if defined(_MSC_VER) +#define SLEEP_EX_DURATION 50 /* delay for yield/sleep */ +#define SPIN_LOCK_YIELD SleepEx(SLEEP_EX_DURATION, FALSE) +#elif defined (__SVR4) && defined (__sun) /* solaris */ +#define SPIN_LOCK_YIELD thr_yield(); +#elif !defined(LACKS_SCHED_H) +#define SPIN_LOCK_YIELD sched_yield(); +#else +#define SPIN_LOCK_YIELD +#endif /* ... yield ... */ + +#if !defined(USE_RECURSIVE_LOCKS) || USE_RECURSIVE_LOCKS == 0 +/* Plain spin locks use single word (embedded in malloc_states) */ +static int spin_acquire_lock(int *sl) { + int spins = 0; + while (*(volatile int *)sl != 0 || CAS_LOCK(sl)) { + if ((++spins & SPINS_PER_YIELD) == 0) { + SPIN_LOCK_YIELD; + } + } + return 0; +} + +#define MLOCK_T int +#define TRY_LOCK(sl) !CAS_LOCK(sl) +#define RELEASE_LOCK(sl) CLEAR_LOCK(sl) +#define ACQUIRE_LOCK(sl) (CAS_LOCK(sl)? spin_acquire_lock(sl) : 0) +#define INITIAL_LOCK(sl) (*sl = 0) +#define DESTROY_LOCK(sl) (0) +static MLOCK_T malloc_global_mutex = 0; + +#else /* USE_RECURSIVE_LOCKS */ +/* types for lock owners */ +#ifdef WIN32 +#define THREAD_ID_T DWORD +#define CURRENT_THREAD GetCurrentThreadId() +#define EQ_OWNER(X,Y) ((X) == (Y)) +#else +/* + Note: the following assume that pthread_t is a type that can be + initialized to (casted) zero. If this is not the case, you will need to + somehow redefine these or not use spin locks. +*/ +#define THREAD_ID_T pthread_t +#define CURRENT_THREAD pthread_self() +#define EQ_OWNER(X,Y) pthread_equal(X, Y) +#endif + +struct malloc_recursive_lock { + int sl; + unsigned int c; + THREAD_ID_T threadid; +}; + +#define MLOCK_T struct malloc_recursive_lock +static MLOCK_T malloc_global_mutex = { 0, 0, (THREAD_ID_T)0}; + +static FORCEINLINE void recursive_release_lock(MLOCK_T *lk) { + assert(lk->sl != 0); + if (--lk->c == 0) { + CLEAR_LOCK(&lk->sl); + } +} + +static FORCEINLINE int recursive_acquire_lock(MLOCK_T *lk) { + THREAD_ID_T mythreadid = CURRENT_THREAD; + int spins = 0; + for (;;) { + if (*((volatile int *)(&lk->sl)) == 0) { + if (!CAS_LOCK(&lk->sl)) { + lk->threadid = mythreadid; + lk->c = 1; + return 0; + } + } + else if (EQ_OWNER(lk->threadid, mythreadid)) { + ++lk->c; + return 0; + } + if ((++spins & SPINS_PER_YIELD) == 0) { + SPIN_LOCK_YIELD; + } + } +} + +static FORCEINLINE int recursive_try_lock(MLOCK_T *lk) { + THREAD_ID_T mythreadid = CURRENT_THREAD; + if (*((volatile int *)(&lk->sl)) == 0) { + if (!CAS_LOCK(&lk->sl)) { + lk->threadid = mythreadid; + lk->c = 1; + return 1; + } + } + else if (EQ_OWNER(lk->threadid, mythreadid)) { + ++lk->c; + return 1; + } + return 0; +} + +#define RELEASE_LOCK(lk) recursive_release_lock(lk) +#define TRY_LOCK(lk) recursive_try_lock(lk) +#define ACQUIRE_LOCK(lk) recursive_acquire_lock(lk) +#define INITIAL_LOCK(lk) ((lk)->threadid = (THREAD_ID_T)0, (lk)->sl = 0, (lk)->c = 0) +#define DESTROY_LOCK(lk) (0) +#endif /* USE_RECURSIVE_LOCKS */ + +#elif defined(WIN32) /* Win32 critical sections */ +#define MLOCK_T CRITICAL_SECTION +#define ACQUIRE_LOCK(lk) (EnterCriticalSection(lk), 0) +#define RELEASE_LOCK(lk) LeaveCriticalSection(lk) +#define TRY_LOCK(lk) TryEnterCriticalSection(lk) +#define INITIAL_LOCK(lk) (!InitializeCriticalSectionAndSpinCount((lk), 0x80000000|4000)) +#define DESTROY_LOCK(lk) (DeleteCriticalSection(lk), 0) +#define NEED_GLOBAL_LOCK_INIT + +static MLOCK_T malloc_global_mutex; +static volatile LONG malloc_global_mutex_status; + +/* Use spin loop to initialize global lock */ +static void init_malloc_global_mutex() { + for (;;) { + long stat = malloc_global_mutex_status; + if (stat > 0) + return; + /* transition to < 0 while initializing, then to > 0) */ + if (stat == 0 && + interlockedcompareexchange(&malloc_global_mutex_status, (LONG)-1, (LONG)0) == 0) { + InitializeCriticalSection(&malloc_global_mutex); + interlockedexchange(&malloc_global_mutex_status, (LONG)1); + return; + } + SleepEx(0, FALSE); + } +} + +#else /* pthreads-based locks */ +#define MLOCK_T pthread_mutex_t +#define ACQUIRE_LOCK(lk) pthread_mutex_lock(lk) +#define RELEASE_LOCK(lk) pthread_mutex_unlock(lk) +#define TRY_LOCK(lk) (!pthread_mutex_trylock(lk)) +#define INITIAL_LOCK(lk) pthread_init_lock(lk) +#define DESTROY_LOCK(lk) pthread_mutex_destroy(lk) + +#if defined(USE_RECURSIVE_LOCKS) && USE_RECURSIVE_LOCKS != 0 && defined(linux) && !defined(PTHREAD_MUTEX_RECURSIVE) +/* Cope with old-style linux recursive lock initialization by adding */ +/* skipped internal declaration from pthread.h */ +extern int pthread_mutexattr_setkind_np __P ((pthread_mutexattr_t *__attr, + int __kind)); +#define PTHREAD_MUTEX_RECURSIVE PTHREAD_MUTEX_RECURSIVE_NP +#define pthread_mutexattr_settype(x,y) pthread_mutexattr_setkind_np(x,y) +#endif /* USE_RECURSIVE_LOCKS ... */ + +static MLOCK_T malloc_global_mutex = PTHREAD_MUTEX_INITIALIZER; + +static int pthread_init_lock (MLOCK_T *lk) { + pthread_mutexattr_t attr; + if (pthread_mutexattr_init(&attr)) return 1; +#if defined(USE_RECURSIVE_LOCKS) && USE_RECURSIVE_LOCKS != 0 + if (pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)) return 1; +#endif + if (pthread_mutex_init(lk, &attr)) return 1; + if (pthread_mutexattr_destroy(&attr)) return 1; + return 0; +} + +#endif /* ... lock types ... */ + +/* Common code for all lock types */ +#define USE_LOCK_BIT (2U) + +#ifndef ACQUIRE_MALLOC_GLOBAL_LOCK +#define ACQUIRE_MALLOC_GLOBAL_LOCK() ACQUIRE_LOCK(&malloc_global_mutex); +#endif + +#ifndef RELEASE_MALLOC_GLOBAL_LOCK +#define RELEASE_MALLOC_GLOBAL_LOCK() RELEASE_LOCK(&malloc_global_mutex); +#endif + +#endif /* USE_LOCKS */ + +/* ----------------------- Chunk representations ------------------------ */ + +/* + (The following includes lightly edited explanations by Colin Plumb.) + + The malloc_chunk declaration below is misleading (but accurate and + necessary). It declares a "view" into memory allowing access to + necessary fields at known offsets from a given base. + + Chunks of memory are maintained using a `boundary tag' method as + originally described by Knuth. (See the paper by Paul Wilson + ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps for a survey of such + techniques.) Sizes of free chunks are stored both in the front of + each chunk and at the end. This makes consolidating fragmented + chunks into bigger chunks fast. The head fields also hold bits + representing whether chunks are free or in use. + + Here are some pictures to make it clearer. They are "exploded" to + show that the state of a chunk can be thought of as extending from + the high 31 bits of the head field of its header through the + prev_foot and PINUSE_BIT bit of the following chunk header. + + A chunk that's in use looks like: + + chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Size of previous chunk (if P = 0) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |P| + | Size of this chunk 1| +-+ + mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + +- -+ + | | + +- -+ + | : + +- size - sizeof(size_t) available payload bytes -+ + : | + chunk-> +- -+ + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |1| + | Size of next chunk (may or may not be in use) | +-+ + mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + And if it's free, it looks like this: + + chunk-> +- -+ + | User payload (must be in use, or we would have merged!) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |P| + | Size of this chunk 0| +-+ + mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Next pointer | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Prev pointer | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | : + +- size - sizeof(struct chunk) unused bytes -+ + : | + chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Size of this chunk | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |0| + | Size of next chunk (must be in use, or we would have merged)| +-+ + mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | : + +- User payload -+ + : | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |0| + +-+ + Note that since we always merge adjacent free chunks, the chunks + adjacent to a free chunk must be in use. + + Given a pointer to a chunk (which can be derived trivially from the + payload pointer) we can, in O(1) time, find out whether the adjacent + chunks are free, and if so, unlink them from the lists that they + are on and merge them with the current chunk. + + Chunks always begin on even word boundaries, so the mem portion + (which is returned to the user) is also on an even word boundary, and + thus at least double-word aligned. + + The P (PINUSE_BIT) bit, stored in the unused low-order bit of the + chunk size (which is always a multiple of two words), is an in-use + bit for the *previous* chunk. If that bit is *clear*, then the + word before the current chunk size contains the previous chunk + size, and can be used to find the front of the previous chunk. + The very first chunk allocated always has this bit set, preventing + access to non-existent (or non-owned) memory. If pinuse is set for + any given chunk, then you CANNOT determine the size of the + previous chunk, and might even get a memory addressing fault when + trying to do so. + + The C (CINUSE_BIT) bit, stored in the unused second-lowest bit of + the chunk size redundantly records whether the current chunk is + inuse (unless the chunk is mmapped). This redundancy enables usage + checks within free and realloc, and reduces indirection when freeing + and consolidating chunks. + + Each freshly allocated chunk must have both cinuse and pinuse set. + That is, each allocated chunk borders either a previously allocated + and still in-use chunk, or the base of its memory arena. This is + ensured by making all allocations from the `lowest' part of any + found chunk. Further, no free chunk physically borders another one, + so each free chunk is known to be preceded and followed by either + inuse chunks or the ends of memory. + + Note that the `foot' of the current chunk is actually represented + as the prev_foot of the NEXT chunk. This makes it easier to + deal with alignments etc but can be very confusing when trying + to extend or adapt this code. + + The exceptions to all this are + + 1. The special chunk `top' is the top-most available chunk (i.e., + the one bordering the end of available memory). It is treated + specially. Top is never included in any bin, is used only if + no other chunk is available, and is released back to the + system if it is very large (see M_TRIM_THRESHOLD). In effect, + the top chunk is treated as larger (and thus less well + fitting) than any other available chunk. The top chunk + doesn't update its trailing size field since there is no next + contiguous chunk that would have to index off it. However, + space is still allocated for it (TOP_FOOT_SIZE) to enable + separation or merging when space is extended. + + 3. Chunks allocated via mmap, have both cinuse and pinuse bits + cleared in their head fields. Because they are allocated + one-by-one, each must carry its own prev_foot field, which is + also used to hold the offset this chunk has within its mmapped + region, which is needed to preserve alignment. Each mmapped + chunk is trailed by the first two fields of a fake next-chunk + for sake of usage checks. + +*/ + +struct malloc_chunk { + size_t prev_foot; /* Size of previous chunk (if free). */ + size_t head; /* Size and inuse bits. */ + struct malloc_chunk* fd; /* double links -- used only if free. */ + struct malloc_chunk* bk; +}; + +typedef struct malloc_chunk mchunk; +typedef struct malloc_chunk* mchunkptr; +typedef struct malloc_chunk* sbinptr; /* The type of bins of chunks */ +typedef unsigned int bindex_t; /* Described below */ +typedef unsigned int binmap_t; /* Described below */ +typedef unsigned int flag_t; /* The type of various bit flag sets */ + +/* ------------------- Chunks sizes and alignments ----------------------- */ + +#define MCHUNK_SIZE (sizeof(mchunk)) + +#if FOOTERS +#define CHUNK_OVERHEAD (TWO_SIZE_T_SIZES) +#else /* FOOTERS */ +#define CHUNK_OVERHEAD (SIZE_T_SIZE) +#endif /* FOOTERS */ + +/* MMapped chunks need a second word of overhead ... */ +#define MMAP_CHUNK_OVERHEAD (TWO_SIZE_T_SIZES) +/* ... and additional padding for fake next-chunk at foot */ +#define MMAP_FOOT_PAD (FOUR_SIZE_T_SIZES) + +/* The smallest size we can malloc is an aligned minimal chunk */ +#define MIN_CHUNK_SIZE\ + ((MCHUNK_SIZE + CHUNK_ALIGN_MASK) & ~CHUNK_ALIGN_MASK) + +/* conversion from malloc headers to user pointers, and back */ +#define chunk2mem(p) ((void*)((char*)(p) + TWO_SIZE_T_SIZES)) +#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - TWO_SIZE_T_SIZES)) +/* chunk associated with aligned address A */ +#define align_as_chunk(A) (mchunkptr)((A) + align_offset(chunk2mem(A))) + +/* Bounds on request (not chunk) sizes. */ +#define MAX_REQUEST ((-MIN_CHUNK_SIZE) << 2) +#define MIN_REQUEST (MIN_CHUNK_SIZE - CHUNK_OVERHEAD - SIZE_T_ONE) + +/* pad request bytes into a usable size */ +#define pad_request(req) \ + (((req) + CHUNK_OVERHEAD + CHUNK_ALIGN_MASK) & ~CHUNK_ALIGN_MASK) + +/* pad request, checking for minimum (but not maximum) */ +#define request2size(req) \ + (((req) < MIN_REQUEST)? MIN_CHUNK_SIZE : pad_request(req)) + + +/* ------------------ Operations on head and foot fields ----------------- */ + +/* + The head field of a chunk is or'ed with PINUSE_BIT when previous + adjacent chunk in use, and or'ed with CINUSE_BIT if this chunk is in + use, unless mmapped, in which case both bits are cleared. + + FLAG4_BIT is not used by this malloc, but might be useful in extensions. +*/ + +#define PINUSE_BIT (SIZE_T_ONE) +#define CINUSE_BIT (SIZE_T_TWO) +#define FLAG4_BIT (SIZE_T_FOUR) +#define INUSE_BITS (PINUSE_BIT|CINUSE_BIT) +#define FLAG_BITS (PINUSE_BIT|CINUSE_BIT|FLAG4_BIT) + +/* Head value for fenceposts */ +#define FENCEPOST_HEAD (INUSE_BITS|SIZE_T_SIZE) + +/* extraction of fields from head words */ +#define cinuse(p) ((p)->head & CINUSE_BIT) +#define pinuse(p) ((p)->head & PINUSE_BIT) +#define flag4inuse(p) ((p)->head & FLAG4_BIT) +#define is_inuse(p) (((p)->head & INUSE_BITS) != PINUSE_BIT) +#define is_mmapped(p) (((p)->head & INUSE_BITS) == 0) + +#define chunksize(p) ((p)->head & ~(FLAG_BITS)) + +#define clear_pinuse(p) ((p)->head &= ~PINUSE_BIT) +#define set_flag4(p) ((p)->head |= FLAG4_BIT) +#define clear_flag4(p) ((p)->head &= ~FLAG4_BIT) + +/* Treat space at ptr +/- offset as a chunk */ +#define chunk_plus_offset(p, s) ((mchunkptr)(((char*)(p)) + (s))) +#define chunk_minus_offset(p, s) ((mchunkptr)(((char*)(p)) - (s))) + +/* Ptr to next or previous physical malloc_chunk. */ +#define next_chunk(p) ((mchunkptr)( ((char*)(p)) + ((p)->head & ~FLAG_BITS))) +#define prev_chunk(p) ((mchunkptr)( ((char*)(p)) - ((p)->prev_foot) )) + +/* extract next chunk's pinuse bit */ +#define next_pinuse(p) ((next_chunk(p)->head) & PINUSE_BIT) + +/* Get/set size at footer */ +#define get_foot(p, s) (((mchunkptr)((char*)(p) + (s)))->prev_foot) +#define set_foot(p, s) (((mchunkptr)((char*)(p) + (s)))->prev_foot = (s)) + +/* Set size, pinuse bit, and foot */ +#define set_size_and_pinuse_of_free_chunk(p, s)\ + ((p)->head = (s|PINUSE_BIT), set_foot(p, s)) + +/* Set size, pinuse bit, foot, and clear next pinuse */ +#define set_free_with_pinuse(p, s, n)\ + (clear_pinuse(n), set_size_and_pinuse_of_free_chunk(p, s)) + +/* Get the internal overhead associated with chunk p */ +#define overhead_for(p)\ + (is_mmapped(p)? MMAP_CHUNK_OVERHEAD : CHUNK_OVERHEAD) + +/* Return true if malloced space is not necessarily cleared */ +#if MMAP_CLEARS +#define calloc_must_clear(p) (!is_mmapped(p)) +#else /* MMAP_CLEARS */ +#define calloc_must_clear(p) (1) +#endif /* MMAP_CLEARS */ + +/* ---------------------- Overlaid data structures ----------------------- */ + +/* + When chunks are not in use, they are treated as nodes of either + lists or trees. + + "Small" chunks are stored in circular doubly-linked lists, and look + like this: + + chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Size of previous chunk | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + `head:' | Size of chunk, in bytes |P| + mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Forward pointer to next chunk in list | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Back pointer to previous chunk in list | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Unused space (may be 0 bytes long) . + . . + . | +nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + `foot:' | Size of chunk, in bytes | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Larger chunks are kept in a form of bitwise digital trees (aka + tries) keyed on chunksizes. Because malloc_tree_chunks are only for + free chunks greater than 256 bytes, their size doesn't impose any + constraints on user chunk sizes. Each node looks like: + + chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Size of previous chunk | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + `head:' | Size of chunk, in bytes |P| + mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Forward pointer to next chunk of same size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Back pointer to previous chunk of same size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Pointer to left child (child[0]) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Pointer to right child (child[1]) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Pointer to parent | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | bin index of this chunk | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Unused space . + . | +nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + `foot:' | Size of chunk, in bytes | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Each tree holding treenodes is a tree of unique chunk sizes. Chunks + of the same size are arranged in a circularly-linked list, with only + the oldest chunk (the next to be used, in our FIFO ordering) + actually in the tree. (Tree members are distinguished by a non-null + parent pointer.) If a chunk with the same size an an existing node + is inserted, it is linked off the existing node using pointers that + work in the same way as fd/bk pointers of small chunks. + + Each tree contains a power of 2 sized range of chunk sizes (the + smallest is 0x100 <= x < 0x180), which is is divided in half at each + tree level, with the chunks in the smaller half of the range (0x100 + <= x < 0x140 for the top nose) in the left subtree and the larger + half (0x140 <= x < 0x180) in the right subtree. This is, of course, + done by inspecting individual bits. + + Using these rules, each node's left subtree contains all smaller + sizes than its right subtree. However, the node at the root of each + subtree has no particular ordering relationship to either. (The + dividing line between the subtree sizes is based on trie relation.) + If we remove the last chunk of a given size from the interior of the + tree, we need to replace it with a leaf node. The tree ordering + rules permit a node to be replaced by any leaf below it. + + The smallest chunk in a tree (a common operation in a best-fit + allocator) can be found by walking a path to the leftmost leaf in + the tree. Unlike a usual binary tree, where we follow left child + pointers until we reach a null, here we follow the right child + pointer any time the left one is null, until we reach a leaf with + both child pointers null. The smallest chunk in the tree will be + somewhere along that path. + + The worst case number of steps to add, find, or remove a node is + bounded by the number of bits differentiating chunks within + bins. Under current bin calculations, this ranges from 6 up to 21 + (for 32 bit sizes) or up to 53 (for 64 bit sizes). The typical case + is of course much better. +*/ + +struct malloc_tree_chunk { + /* The first four fields must be compatible with malloc_chunk */ + size_t prev_foot; + size_t head; + struct malloc_tree_chunk* fd; + struct malloc_tree_chunk* bk; + + struct malloc_tree_chunk* child[2]; + struct malloc_tree_chunk* parent; + bindex_t index; +}; + +typedef struct malloc_tree_chunk tchunk; +typedef struct malloc_tree_chunk* tchunkptr; +typedef struct malloc_tree_chunk* tbinptr; /* The type of bins of trees */ + +/* A little helper macro for trees */ +#define leftmost_child(t) ((t)->child[0] != 0? (t)->child[0] : (t)->child[1]) + +/* ----------------------------- Segments -------------------------------- */ + +/* + Each malloc space may include non-contiguous segments, held in a + list headed by an embedded malloc_segment record representing the + top-most space. Segments also include flags holding properties of + the space. Large chunks that are directly allocated by mmap are not + included in this list. They are instead independently created and + destroyed without otherwise keeping track of them. + + Segment management mainly comes into play for spaces allocated by + MMAP. Any call to MMAP might or might not return memory that is + adjacent to an existing segment. MORECORE normally contiguously + extends the current space, so this space is almost always adjacent, + which is simpler and faster to deal with. (This is why MORECORE is + used preferentially to MMAP when both are available -- see + sys_alloc.) When allocating using MMAP, we don't use any of the + hinting mechanisms (inconsistently) supported in various + implementations of unix mmap, or distinguish reserving from + committing memory. Instead, we just ask for space, and exploit + contiguity when we get it. It is probably possible to do + better than this on some systems, but no general scheme seems + to be significantly better. + + Management entails a simpler variant of the consolidation scheme + used for chunks to reduce fragmentation -- new adjacent memory is + normally prepended or appended to an existing segment. However, + there are limitations compared to chunk consolidation that mostly + reflect the fact that segment processing is relatively infrequent + (occurring only when getting memory from system) and that we + don't expect to have huge numbers of segments: + + * Segments are not indexed, so traversal requires linear scans. (It + would be possible to index these, but is not worth the extra + overhead and complexity for most programs on most platforms.) + * New segments are only appended to old ones when holding top-most + memory; if they cannot be prepended to others, they are held in + different segments. + + Except for the top-most segment of an mstate, each segment record + is kept at the tail of its segment. Segments are added by pushing + segment records onto the list headed by &mstate.seg for the + containing mstate. + + Segment flags control allocation/merge/deallocation policies: + * If EXTERN_BIT set, then we did not allocate this segment, + and so should not try to deallocate or merge with others. + (This currently holds only for the initial segment passed + into create_mspace_with_base.) + * If USE_MMAP_BIT set, the segment may be merged with + other surrounding mmapped segments and trimmed/de-allocated + using munmap. + * If neither bit is set, then the segment was obtained using + MORECORE so can be merged with surrounding MORECORE'd segments + and deallocated/trimmed using MORECORE with negative arguments. +*/ + +struct malloc_segment { + char* base; /* base address */ + size_t size; /* allocated size */ + struct malloc_segment* next; /* ptr to next segment */ + flag_t sflags; /* mmap and extern flag */ +}; + +#define is_mmapped_segment(S) ((S)->sflags & USE_MMAP_BIT) +#define is_extern_segment(S) ((S)->sflags & EXTERN_BIT) + +typedef struct malloc_segment msegment; +typedef struct malloc_segment* msegmentptr; + +/* ---------------------------- malloc_state ----------------------------- */ + +/* + A malloc_state holds all of the bookkeeping for a space. + The main fields are: + + Top + The topmost chunk of the currently active segment. Its size is + cached in topsize. The actual size of topmost space is + topsize+TOP_FOOT_SIZE, which includes space reserved for adding + fenceposts and segment records if necessary when getting more + space from the system. The size at which to autotrim top is + cached from mparams in trim_check, except that it is disabled if + an autotrim fails. + + Designated victim (dv) + This is the preferred chunk for servicing small requests that + don't have exact fits. It is normally the chunk split off most + recently to service another small request. Its size is cached in + dvsize. The link fields of this chunk are not maintained since it + is not kept in a bin. + + SmallBins + An array of bin headers for free chunks. These bins hold chunks + with sizes less than MIN_LARGE_SIZE bytes. Each bin contains + chunks of all the same size, spaced 8 bytes apart. To simplify + use in double-linked lists, each bin header acts as a malloc_chunk + pointing to the real first node, if it exists (else pointing to + itself). This avoids special-casing for headers. But to avoid + waste, we allocate only the fd/bk pointers of bins, and then use + repositioning tricks to treat these as the fields of a chunk. + + TreeBins + Treebins are pointers to the roots of trees holding a range of + sizes. There are 2 equally spaced treebins for each power of two + from TREE_SHIFT to TREE_SHIFT+16. The last bin holds anything + larger. + + Bin maps + There is one bit map for small bins ("smallmap") and one for + treebins ("treemap). Each bin sets its bit when non-empty, and + clears the bit when empty. Bit operations are then used to avoid + bin-by-bin searching -- nearly all "search" is done without ever + looking at bins that won't be selected. The bit maps + conservatively use 32 bits per map word, even if on 64bit system. + For a good description of some of the bit-based techniques used + here, see Henry S. Warren Jr's book "Hacker's Delight" (and + supplement at http://hackersdelight.org/). Many of these are + intended to reduce the branchiness of paths through malloc etc, as + well as to reduce the number of memory locations read or written. + + Segments + A list of segments headed by an embedded malloc_segment record + representing the initial space. + + Address check support + The least_addr field is the least address ever obtained from + MORECORE or MMAP. Attempted frees and reallocs of any address less + than this are trapped (unless INSECURE is defined). + + Magic tag + A cross-check field that should always hold same value as mparams.magic. + + Max allowed footprint + The maximum allowed bytes to allocate from system (zero means no limit) + + Flags + Bits recording whether to use MMAP, locks, or contiguous MORECORE + + Statistics + Each space keeps track of current and maximum system memory + obtained via MORECORE or MMAP. + + Trim support + Fields holding the amount of unused topmost memory that should trigger + trimming, and a counter to force periodic scanning to release unused + non-topmost segments. + + Locking + If USE_LOCKS is defined, the "mutex" lock is acquired and released + around every public call using this mspace. + + Extension support + A void* pointer and a size_t field that can be used to help implement + extensions to this malloc. +*/ + +/* Bin types, widths and sizes */ +#define NSMALLBINS (32U) +#define NTREEBINS (32U) +#define SMALLBIN_SHIFT (3U) +#define SMALLBIN_WIDTH (SIZE_T_ONE << SMALLBIN_SHIFT) +#define TREEBIN_SHIFT (8U) +#define MIN_LARGE_SIZE (SIZE_T_ONE << TREEBIN_SHIFT) +#define MAX_SMALL_SIZE (MIN_LARGE_SIZE - SIZE_T_ONE) +#define MAX_SMALL_REQUEST (MAX_SMALL_SIZE - CHUNK_ALIGN_MASK - CHUNK_OVERHEAD) + +struct malloc_state { + binmap_t smallmap; + binmap_t treemap; + size_t dvsize; + size_t topsize; + char* least_addr; + mchunkptr dv; + mchunkptr top; + size_t trim_check; + size_t release_checks; + size_t magic; + mchunkptr smallbins[(NSMALLBINS+1)*2]; + tbinptr treebins[NTREEBINS]; + size_t footprint; + size_t max_footprint; + size_t footprint_limit; /* zero means no limit */ + flag_t mflags; +#if USE_LOCKS + MLOCK_T mutex; /* locate lock among fields that rarely change */ +#endif /* USE_LOCKS */ + msegment seg; + void* extp; /* Unused but available for extensions */ + size_t exts; +}; + +typedef struct malloc_state* mstate; + +/* ------------- Global malloc_state and malloc_params ------------------- */ + +/* + malloc_params holds global properties, including those that can be + dynamically set using mallopt. There is a single instance, mparams, + initialized in init_mparams. Note that the non-zeroness of "magic" + also serves as an initialization flag. +*/ + +struct malloc_params { + size_t magic; + size_t page_size; + size_t granularity; + size_t mmap_threshold; + size_t trim_threshold; + flag_t default_mflags; +}; + +static struct malloc_params mparams; + +/* Ensure mparams initialized */ +#define ensure_initialization() (void)(mparams.magic != 0 || init_mparams()) + +#if !ONLY_MSPACES + +/* The global malloc_state used for all non-"mspace" calls */ +static struct malloc_state _gm_; +#define gm (&_gm_) +#define is_global(M) ((M) == &_gm_) + +#endif /* !ONLY_MSPACES */ + +#define is_initialized(M) ((M)->top != 0) + +/* -------------------------- system alloc setup ------------------------- */ + +/* Operations on mflags */ + +#define use_lock(M) ((M)->mflags & USE_LOCK_BIT) +#define enable_lock(M) ((M)->mflags |= USE_LOCK_BIT) +#if USE_LOCKS +#define disable_lock(M) ((M)->mflags &= ~USE_LOCK_BIT) +#else +#define disable_lock(M) +#endif + +#define use_mmap(M) ((M)->mflags & USE_MMAP_BIT) +#define enable_mmap(M) ((M)->mflags |= USE_MMAP_BIT) +#if HAVE_MMAP +#define disable_mmap(M) ((M)->mflags &= ~USE_MMAP_BIT) +#else +#define disable_mmap(M) +#endif + +#define use_noncontiguous(M) ((M)->mflags & USE_NONCONTIGUOUS_BIT) +#define disable_contiguous(M) ((M)->mflags |= USE_NONCONTIGUOUS_BIT) + +#define set_lock(M,L)\ + ((M)->mflags = (L)?\ + ((M)->mflags | USE_LOCK_BIT) :\ + ((M)->mflags & ~USE_LOCK_BIT)) + +/* page-align a size */ +#define page_align(S)\ + (((S) + (mparams.page_size - SIZE_T_ONE)) & ~(mparams.page_size - SIZE_T_ONE)) + +/* granularity-align a size */ +#define granularity_align(S)\ + (((S) + (mparams.granularity - SIZE_T_ONE))\ + & ~(mparams.granularity - SIZE_T_ONE)) + + +/* For mmap, use granularity alignment on windows, else page-align */ +#ifdef WIN32 +#define mmap_align(S) granularity_align(S) +#else +#define mmap_align(S) page_align(S) +#endif + +/* For sys_alloc, enough padding to ensure can malloc request on success */ +#define SYS_ALLOC_PADDING (TOP_FOOT_SIZE + MALLOC_ALIGNMENT) + +#define is_page_aligned(S)\ + (((size_t)(S) & (mparams.page_size - SIZE_T_ONE)) == 0) +#define is_granularity_aligned(S)\ + (((size_t)(S) & (mparams.granularity - SIZE_T_ONE)) == 0) + +/* True if segment S holds address A */ +#define segment_holds(S, A)\ + ((char*)(A) >= S->base && (char*)(A) < S->base + S->size) + +/* Return segment holding given address */ +static msegmentptr segment_holding(mstate m, char* addr) { + msegmentptr sp = &m->seg; + for (;;) { + if (addr >= sp->base && addr < sp->base + sp->size) + return sp; + if ((sp = sp->next) == 0) + return 0; + } +} + +/* Return true if segment contains a segment link */ +static int has_segment_link(mstate m, msegmentptr ss) { + msegmentptr sp = &m->seg; + for (;;) { + if ((char*)sp >= ss->base && (char*)sp < ss->base + ss->size) + return 1; + if ((sp = sp->next) == 0) + return 0; + } +} + +#ifndef MORECORE_CANNOT_TRIM +#define should_trim(M,s) ((s) > (M)->trim_check) +#else /* MORECORE_CANNOT_TRIM */ +#define should_trim(M,s) (0) +#endif /* MORECORE_CANNOT_TRIM */ + +/* + TOP_FOOT_SIZE is padding at the end of a segment, including space + that may be needed to place segment records and fenceposts when new + noncontiguous segments are added. +*/ +#define TOP_FOOT_SIZE\ + (align_offset(chunk2mem(0))+pad_request(sizeof(struct malloc_segment))+MIN_CHUNK_SIZE) + + +/* ------------------------------- Hooks -------------------------------- */ + +/* + PREACTION should be defined to return 0 on success, and nonzero on + failure. If you are not using locking, you can redefine these to do + anything you like. +*/ + +#if USE_LOCKS +#define PREACTION(M) ((use_lock(M))? ACQUIRE_LOCK(&(M)->mutex) : 0) +#define POSTACTION(M) { if (use_lock(M)) RELEASE_LOCK(&(M)->mutex); } +#else /* USE_LOCKS */ + +#ifndef PREACTION +#define PREACTION(M) (0) +#endif /* PREACTION */ + +#ifndef POSTACTION +#define POSTACTION(M) +#endif /* POSTACTION */ + +#endif /* USE_LOCKS */ + +/* + CORRUPTION_ERROR_ACTION is triggered upon detected bad addresses. + USAGE_ERROR_ACTION is triggered on detected bad frees and + reallocs. The argument p is an address that might have triggered the + fault. It is ignored by the two predefined actions, but might be + useful in custom actions that try to help diagnose errors. +*/ + +#if PROCEED_ON_ERROR + +/* A count of the number of corruption errors causing resets */ +int malloc_corruption_error_count; + +/* default corruption action */ +static void reset_on_error(mstate m); + +#define CORRUPTION_ERROR_ACTION(m) reset_on_error(m) +#define USAGE_ERROR_ACTION(m, p) + +#else /* PROCEED_ON_ERROR */ + +#ifndef CORRUPTION_ERROR_ACTION +#define CORRUPTION_ERROR_ACTION(m) ABORT +#endif /* CORRUPTION_ERROR_ACTION */ + +#ifndef USAGE_ERROR_ACTION +#define USAGE_ERROR_ACTION(m,p) ABORT +#endif /* USAGE_ERROR_ACTION */ + +#endif /* PROCEED_ON_ERROR */ + + +/* -------------------------- Debugging setup ---------------------------- */ + +#if ! DEBUG + +#define check_free_chunk(M,P) +#define check_inuse_chunk(M,P) +#define check_malloced_chunk(M,P,N) +#define check_mmapped_chunk(M,P) +#define check_malloc_state(M) +#define check_top_chunk(M,P) + +#else /* DEBUG */ +#define check_free_chunk(M,P) do_check_free_chunk(M,P) +#define check_inuse_chunk(M,P) do_check_inuse_chunk(M,P) +#define check_top_chunk(M,P) do_check_top_chunk(M,P) +#define check_malloced_chunk(M,P,N) do_check_malloced_chunk(M,P,N) +#define check_mmapped_chunk(M,P) do_check_mmapped_chunk(M,P) +#define check_malloc_state(M) do_check_malloc_state(M) + +static void do_check_any_chunk(mstate m, mchunkptr p); +static void do_check_top_chunk(mstate m, mchunkptr p); +static void do_check_mmapped_chunk(mstate m, mchunkptr p); +static void do_check_inuse_chunk(mstate m, mchunkptr p); +static void do_check_free_chunk(mstate m, mchunkptr p); +static void do_check_malloced_chunk(mstate m, void* mem, size_t s); +static void do_check_tree(mstate m, tchunkptr t); +static void do_check_treebin(mstate m, bindex_t i); +static void do_check_smallbin(mstate m, bindex_t i); +static void do_check_malloc_state(mstate m); +static int bin_find(mstate m, mchunkptr x); +static size_t traverse_and_check(mstate m); +#endif /* DEBUG */ + +/* ---------------------------- Indexing Bins ---------------------------- */ + +#define is_small(s) (((s) >> SMALLBIN_SHIFT) < NSMALLBINS) +#define small_index(s) (bindex_t)((s) >> SMALLBIN_SHIFT) +#define small_index2size(i) ((i) << SMALLBIN_SHIFT) +#define MIN_SMALL_INDEX (small_index(MIN_CHUNK_SIZE)) + +/* addressing by index. See above about smallbin repositioning */ +#define smallbin_at(M, i) ((sbinptr)((char*)&((M)->smallbins[(i)<<1]))) +#define treebin_at(M,i) (&((M)->treebins[i])) + +/* assign tree index for size S to variable I. Use x86 asm if possible */ +#if defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) +#define compute_tree_index(S, I)\ +{\ + unsigned int X = S >> TREEBIN_SHIFT;\ + if (X == 0)\ + I = 0;\ + else if (X > 0xFFFF)\ + I = NTREEBINS-1;\ + else {\ + unsigned int K = (unsigned) sizeof(X)*__CHAR_BIT__ - 1 - (unsigned) __builtin_clz(X); \ + I = (bindex_t)((K << 1) + ((S >> (K + (TREEBIN_SHIFT-1)) & 1)));\ + }\ +} + +#elif defined (__INTEL_COMPILER) +#define compute_tree_index(S, I)\ +{\ + size_t X = S >> TREEBIN_SHIFT;\ + if (X == 0)\ + I = 0;\ + else if (X > 0xFFFF)\ + I = NTREEBINS-1;\ + else {\ + unsigned int K = _bit_scan_reverse (X); \ + I = (bindex_t)((K << 1) + ((S >> (K + (TREEBIN_SHIFT-1)) & 1)));\ + }\ +} + +#elif defined(_MSC_VER) && _MSC_VER>=1300 +#define compute_tree_index(S, I)\ +{\ + size_t X = S >> TREEBIN_SHIFT;\ + if (X == 0)\ + I = 0;\ + else if (X > 0xFFFF)\ + I = NTREEBINS-1;\ + else {\ + unsigned int K;\ + _BitScanReverse((DWORD *) &K, (DWORD) X);\ + I = (bindex_t)((K << 1) + ((S >> (K + (TREEBIN_SHIFT-1)) & 1)));\ + }\ +} + +#else /* GNUC */ +#define compute_tree_index(S, I)\ +{\ + size_t X = S >> TREEBIN_SHIFT;\ + if (X == 0)\ + I = 0;\ + else if (X > 0xFFFF)\ + I = NTREEBINS-1;\ + else {\ + unsigned int Y = (unsigned int)X;\ + unsigned int N = ((Y - 0x100) >> 16) & 8;\ + unsigned int K = (((Y <<= N) - 0x1000) >> 16) & 4;\ + N += K;\ + N += K = (((Y <<= K) - 0x4000) >> 16) & 2;\ + K = 14 - N + ((Y <<= K) >> 15);\ + I = (K << 1) + ((S >> (K + (TREEBIN_SHIFT-1)) & 1));\ + }\ +} +#endif /* GNUC */ + +/* Bit representing maximum resolved size in a treebin at i */ +#define bit_for_tree_index(i) \ + (i == NTREEBINS-1)? (SIZE_T_BITSIZE-1) : (((i) >> 1) + TREEBIN_SHIFT - 2) + +/* Shift placing maximum resolved bit in a treebin at i as sign bit */ +#define leftshift_for_tree_index(i) \ + ((i == NTREEBINS-1)? 0 : \ + ((SIZE_T_BITSIZE-SIZE_T_ONE) - (((i) >> 1) + TREEBIN_SHIFT - 2))) + +/* The size of the smallest chunk held in bin with index i */ +#define minsize_for_tree_index(i) \ + ((SIZE_T_ONE << (((i) >> 1) + TREEBIN_SHIFT)) | \ + (((size_t)((i) & SIZE_T_ONE)) << (((i) >> 1) + TREEBIN_SHIFT - 1))) + + +/* ------------------------ Operations on bin maps ----------------------- */ + +/* bit corresponding to given index */ +#define idx2bit(i) ((binmap_t)(1) << (i)) + +/* Mark/Clear bits with given index */ +#define mark_smallmap(M,i) ((M)->smallmap |= idx2bit(i)) +#define clear_smallmap(M,i) ((M)->smallmap &= ~idx2bit(i)) +#define smallmap_is_marked(M,i) ((M)->smallmap & idx2bit(i)) + +#define mark_treemap(M,i) ((M)->treemap |= idx2bit(i)) +#define clear_treemap(M,i) ((M)->treemap &= ~idx2bit(i)) +#define treemap_is_marked(M,i) ((M)->treemap & idx2bit(i)) + +/* isolate the least set bit of a bitmap */ +#define least_bit(x) ((x) & -(x)) + +/* mask with all bits to left of least bit of x on */ +#define left_bits(x) ((x<<1) | -(x<<1)) + +/* mask with all bits to left of or equal to least bit of x on */ +#define same_or_left_bits(x) ((x) | -(x)) + +/* index corresponding to given bit. Use x86 asm if possible */ + +#if defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) +#define compute_bit2idx(X, I)\ +{\ + unsigned int J;\ + J = __builtin_ctz(X); \ + I = (bindex_t)J;\ +} + +#elif defined (__INTEL_COMPILER) +#define compute_bit2idx(X, I)\ +{\ + unsigned int J;\ + J = _bit_scan_forward (X); \ + I = (bindex_t)J;\ +} + +#elif defined(_MSC_VER) && _MSC_VER>=1300 +#define compute_bit2idx(X, I)\ +{\ + unsigned int J;\ + _BitScanForward((DWORD *) &J, X);\ + I = (bindex_t)J;\ +} + +#elif USE_BUILTIN_FFS +#define compute_bit2idx(X, I) I = ffs(X)-1 + +#else +#define compute_bit2idx(X, I)\ +{\ + unsigned int Y = X - 1;\ + unsigned int K = Y >> (16-4) & 16;\ + unsigned int N = K; Y >>= K;\ + N += K = Y >> (8-3) & 8; Y >>= K;\ + N += K = Y >> (4-2) & 4; Y >>= K;\ + N += K = Y >> (2-1) & 2; Y >>= K;\ + N += K = Y >> (1-0) & 1; Y >>= K;\ + I = (bindex_t)(N + Y);\ +} +#endif /* GNUC */ + + +/* ----------------------- Runtime Check Support ------------------------- */ + +/* + For security, the main invariant is that malloc/free/etc never + writes to a static address other than malloc_state, unless static + malloc_state itself has been corrupted, which cannot occur via + malloc (because of these checks). In essence this means that we + believe all pointers, sizes, maps etc held in malloc_state, but + check all of those linked or offsetted from other embedded data + structures. These checks are interspersed with main code in a way + that tends to minimize their run-time cost. + + When FOOTERS is defined, in addition to range checking, we also + verify footer fields of inuse chunks, which can be used guarantee + that the mstate controlling malloc/free is intact. This is a + streamlined version of the approach described by William Robertson + et al in "Run-time Detection of Heap-based Overflows" LISA'03 + http://www.usenix.org/events/lisa03/tech/robertson.html The footer + of an inuse chunk holds the xor of its mstate and a random seed, + that is checked upon calls to free() and realloc(). This is + (probabalistically) unguessable from outside the program, but can be + computed by any code successfully malloc'ing any chunk, so does not + itself provide protection against code that has already broken + security through some other means. Unlike Robertson et al, we + always dynamically check addresses of all offset chunks (previous, + next, etc). This turns out to be cheaper than relying on hashes. +*/ + +#if !INSECURE +/* Check if address a is at least as high as any from MORECORE or MMAP */ +#define ok_address(M, a) ((char*)(a) >= (M)->least_addr) +/* Check if address of next chunk n is higher than base chunk p */ +#define ok_next(p, n) ((char*)(p) < (char*)(n)) +/* Check if p has inuse status */ +#define ok_inuse(p) is_inuse(p) +/* Check if p has its pinuse bit on */ +#define ok_pinuse(p) pinuse(p) + +#else /* !INSECURE */ +#define ok_address(M, a) (1) +#define ok_next(b, n) (1) +#define ok_inuse(p) (1) +#define ok_pinuse(p) (1) +#endif /* !INSECURE */ + +#if (FOOTERS && !INSECURE) +/* Check if (alleged) mstate m has expected magic field */ +#define ok_magic(M) ((M)->magic == mparams.magic) +#else /* (FOOTERS && !INSECURE) */ +#define ok_magic(M) (1) +#endif /* (FOOTERS && !INSECURE) */ + +/* In gcc, use __builtin_expect to minimize impact of checks */ +#if !INSECURE +#if defined(__GNUC__) && __GNUC__ >= 3 +#define RTCHECK(e) __builtin_expect(e, 1) +#else /* GNUC */ +#define RTCHECK(e) (e) +#endif /* GNUC */ +#else /* !INSECURE */ +#define RTCHECK(e) (1) +#endif /* !INSECURE */ + +/* macros to set up inuse chunks with or without footers */ + +#if !FOOTERS + +#define mark_inuse_foot(M,p,s) + +/* Macros for setting head/foot of non-mmapped chunks */ + +/* Set cinuse bit and pinuse bit of next chunk */ +#define set_inuse(M,p,s)\ + ((p)->head = (((p)->head & PINUSE_BIT)|s|CINUSE_BIT),\ + ((mchunkptr)(((char*)(p)) + (s)))->head |= PINUSE_BIT) + +/* Set cinuse and pinuse of this chunk and pinuse of next chunk */ +#define set_inuse_and_pinuse(M,p,s)\ + ((p)->head = (s|PINUSE_BIT|CINUSE_BIT),\ + ((mchunkptr)(((char*)(p)) + (s)))->head |= PINUSE_BIT) + +/* Set size, cinuse and pinuse bit of this chunk */ +#define set_size_and_pinuse_of_inuse_chunk(M, p, s)\ + ((p)->head = (s|PINUSE_BIT|CINUSE_BIT)) + +#else /* FOOTERS */ + +/* Set foot of inuse chunk to be xor of mstate and seed */ +#define mark_inuse_foot(M,p,s)\ + (((mchunkptr)((char*)(p) + (s)))->prev_foot = ((size_t)(M) ^ mparams.magic)) + +#define get_mstate_for(p)\ + ((mstate)(((mchunkptr)((char*)(p) +\ + (chunksize(p))))->prev_foot ^ mparams.magic)) + +#define set_inuse(M,p,s)\ + ((p)->head = (((p)->head & PINUSE_BIT)|s|CINUSE_BIT),\ + (((mchunkptr)(((char*)(p)) + (s)))->head |= PINUSE_BIT), \ + mark_inuse_foot(M,p,s)) + +#define set_inuse_and_pinuse(M,p,s)\ + ((p)->head = (s|PINUSE_BIT|CINUSE_BIT),\ + (((mchunkptr)(((char*)(p)) + (s)))->head |= PINUSE_BIT),\ + mark_inuse_foot(M,p,s)) + +#define set_size_and_pinuse_of_inuse_chunk(M, p, s)\ + ((p)->head = (s|PINUSE_BIT|CINUSE_BIT),\ + mark_inuse_foot(M, p, s)) + +#endif /* !FOOTERS */ + +/* ---------------------------- setting mparams -------------------------- */ + +#if LOCK_AT_FORK +static void pre_fork(void) { ACQUIRE_LOCK(&(gm)->mutex); } +static void post_fork_parent(void) { RELEASE_LOCK(&(gm)->mutex); } +static void post_fork_child(void) { INITIAL_LOCK(&(gm)->mutex); } +#endif /* LOCK_AT_FORK */ + +/* Initialize mparams */ +static int init_mparams(void) { +#ifdef NEED_GLOBAL_LOCK_INIT + if (malloc_global_mutex_status <= 0) + init_malloc_global_mutex(); +#endif + + ACQUIRE_MALLOC_GLOBAL_LOCK(); + if (mparams.magic == 0) { + size_t magic; + size_t psize; + size_t gsize; + +#ifndef WIN32 + psize = malloc_getpagesize; + gsize = ((DEFAULT_GRANULARITY != 0)? DEFAULT_GRANULARITY : psize); +#else /* WIN32 */ + { + SYSTEM_INFO system_info; + GetSystemInfo(&system_info); + psize = system_info.dwPageSize; + gsize = ((DEFAULT_GRANULARITY != 0)? + DEFAULT_GRANULARITY : system_info.dwAllocationGranularity); + } +#endif /* WIN32 */ + + /* Sanity-check configuration: + size_t must be unsigned and as wide as pointer type. + ints must be at least 4 bytes. + alignment must be at least 8. + Alignment, min chunk size, and page size must all be powers of 2. + */ + if ((sizeof(size_t) != sizeof(char*)) || + (MAX_SIZE_T < MIN_CHUNK_SIZE) || + (sizeof(int) < 4) || + (MALLOC_ALIGNMENT < (size_t)8U) || + ((MALLOC_ALIGNMENT & (MALLOC_ALIGNMENT-SIZE_T_ONE)) != 0) || + ((MCHUNK_SIZE & (MCHUNK_SIZE-SIZE_T_ONE)) != 0) || + ((gsize & (gsize-SIZE_T_ONE)) != 0) || + ((psize & (psize-SIZE_T_ONE)) != 0)) + ABORT; + mparams.granularity = gsize; + mparams.page_size = psize; + mparams.mmap_threshold = DEFAULT_MMAP_THRESHOLD; + mparams.trim_threshold = DEFAULT_TRIM_THRESHOLD; +#if MORECORE_CONTIGUOUS + mparams.default_mflags = USE_LOCK_BIT|USE_MMAP_BIT; +#else /* MORECORE_CONTIGUOUS */ + mparams.default_mflags = USE_LOCK_BIT|USE_MMAP_BIT|USE_NONCONTIGUOUS_BIT; +#endif /* MORECORE_CONTIGUOUS */ + +#if !ONLY_MSPACES + /* Set up lock for main malloc area */ + gm->mflags = mparams.default_mflags; + (void)INITIAL_LOCK(&gm->mutex); +#endif +#if LOCK_AT_FORK + pthread_atfork(&pre_fork, &post_fork_parent, &post_fork_child); +#endif + + { +#if USE_DEV_RANDOM + int fd; + unsigned char buf[sizeof(size_t)]; + /* Try to use /dev/urandom, else fall back on using time */ + if ((fd = open("/dev/urandom", O_RDONLY)) >= 0 && + read(fd, buf, sizeof(buf)) == sizeof(buf)) { + magic = *((size_t *) buf); + close(fd); + } + else +#endif /* USE_DEV_RANDOM */ +#ifdef WIN32 + magic = (size_t)(GetTickCount() ^ (size_t)0x55555555U); +#elif defined(LACKS_TIME_H) + magic = (size_t)&magic ^ (size_t)0x55555555U; +#else + magic = (size_t)(time(0) ^ (size_t)0x55555555U); +#endif + magic |= (size_t)8U; /* ensure nonzero */ + magic &= ~(size_t)7U; /* improve chances of fault for bad values */ + /* Until memory modes commonly available, use volatile-write */ + (*(volatile size_t *)(&(mparams.magic))) = magic; + } + } + + RELEASE_MALLOC_GLOBAL_LOCK(); + return 1; +} + +/* support for mallopt */ +static int change_mparam(int param_number, int value) { + size_t val; + ensure_initialization(); + val = (value == -1)? MAX_SIZE_T : (size_t)value; + switch(param_number) { + case M_TRIM_THRESHOLD: + mparams.trim_threshold = val; + return 1; + case M_GRANULARITY: + if (val >= mparams.page_size && ((val & (val-1)) == 0)) { + mparams.granularity = val; + return 1; + } + else + return 0; + case M_MMAP_THRESHOLD: + mparams.mmap_threshold = val; + return 1; + default: + return 0; + } +} + +#if DEBUG +/* ------------------------- Debugging Support --------------------------- */ + +/* Check properties of any chunk, whether free, inuse, mmapped etc */ +static void do_check_any_chunk(mstate m, mchunkptr p) { + assert((is_aligned(chunk2mem(p))) || (p->head == FENCEPOST_HEAD)); + assert(ok_address(m, p)); +} + +/* Check properties of top chunk */ +static void do_check_top_chunk(mstate m, mchunkptr p) { + msegmentptr sp = segment_holding(m, (char*)p); + size_t sz = p->head & ~INUSE_BITS; /* third-lowest bit can be set! */ + assert(sp != 0); + assert((is_aligned(chunk2mem(p))) || (p->head == FENCEPOST_HEAD)); + assert(ok_address(m, p)); + assert(sz == m->topsize); + assert(sz > 0); + assert(sz == ((sp->base + sp->size) - (char*)p) - TOP_FOOT_SIZE); + assert(pinuse(p)); + assert(!pinuse(chunk_plus_offset(p, sz))); +} + +/* Check properties of (inuse) mmapped chunks */ +static void do_check_mmapped_chunk(mstate m, mchunkptr p) { + size_t sz = chunksize(p); + size_t len = (sz + (p->prev_foot) + MMAP_FOOT_PAD); + assert(is_mmapped(p)); + assert(use_mmap(m)); + assert((is_aligned(chunk2mem(p))) || (p->head == FENCEPOST_HEAD)); + assert(ok_address(m, p)); + assert(!is_small(sz)); + assert((len & (mparams.page_size-SIZE_T_ONE)) == 0); + assert(chunk_plus_offset(p, sz)->head == FENCEPOST_HEAD); + assert(chunk_plus_offset(p, sz+SIZE_T_SIZE)->head == 0); +} + +/* Check properties of inuse chunks */ +static void do_check_inuse_chunk(mstate m, mchunkptr p) { + do_check_any_chunk(m, p); + assert(is_inuse(p)); + assert(next_pinuse(p)); + /* If not pinuse and not mmapped, previous chunk has OK offset */ + assert(is_mmapped(p) || pinuse(p) || next_chunk(prev_chunk(p)) == p); + if (is_mmapped(p)) + do_check_mmapped_chunk(m, p); +} + +/* Check properties of free chunks */ +static void do_check_free_chunk(mstate m, mchunkptr p) { + size_t sz = chunksize(p); + mchunkptr next = chunk_plus_offset(p, sz); + do_check_any_chunk(m, p); + assert(!is_inuse(p)); + assert(!next_pinuse(p)); + assert (!is_mmapped(p)); + if (p != m->dv && p != m->top) { + if (sz >= MIN_CHUNK_SIZE) { + assert((sz & CHUNK_ALIGN_MASK) == 0); + assert(is_aligned(chunk2mem(p))); + assert(next->prev_foot == sz); + assert(pinuse(p)); + assert (next == m->top || is_inuse(next)); + assert(p->fd->bk == p); + assert(p->bk->fd == p); + } + else /* markers are always of size SIZE_T_SIZE */ + assert(sz == SIZE_T_SIZE); + } +} + +/* Check properties of malloced chunks at the point they are malloced */ +static void do_check_malloced_chunk(mstate m, void* mem, size_t s) { + if (mem != 0) { + mchunkptr p = mem2chunk(mem); + size_t sz = p->head & ~INUSE_BITS; + do_check_inuse_chunk(m, p); + assert((sz & CHUNK_ALIGN_MASK) == 0); + assert(sz >= MIN_CHUNK_SIZE); + assert(sz >= s); + /* unless mmapped, size is less than MIN_CHUNK_SIZE more than request */ + assert(is_mmapped(p) || sz < (s + MIN_CHUNK_SIZE)); + } +} + +/* Check a tree and its subtrees. */ +static void do_check_tree(mstate m, tchunkptr t) { + tchunkptr head = 0; + tchunkptr u = t; + bindex_t tindex = t->index; + size_t tsize = chunksize(t); + bindex_t idx; + compute_tree_index(tsize, idx); + assert(tindex == idx); + assert(tsize >= MIN_LARGE_SIZE); + assert(tsize >= minsize_for_tree_index(idx)); + assert((idx == NTREEBINS-1) || (tsize < minsize_for_tree_index((idx+1)))); + + do { /* traverse through chain of same-sized nodes */ + do_check_any_chunk(m, ((mchunkptr)u)); + assert(u->index == tindex); + assert(chunksize(u) == tsize); + assert(!is_inuse(u)); + assert(!next_pinuse(u)); + assert(u->fd->bk == u); + assert(u->bk->fd == u); + if (u->parent == 0) { + assert(u->child[0] == 0); + assert(u->child[1] == 0); + } + else { + assert(head == 0); /* only one node on chain has parent */ + head = u; + assert(u->parent != u); + assert (u->parent->child[0] == u || + u->parent->child[1] == u || + *((tbinptr*)(u->parent)) == u); + if (u->child[0] != 0) { + assert(u->child[0]->parent == u); + assert(u->child[0] != u); + do_check_tree(m, u->child[0]); + } + if (u->child[1] != 0) { + assert(u->child[1]->parent == u); + assert(u->child[1] != u); + do_check_tree(m, u->child[1]); + } + if (u->child[0] != 0 && u->child[1] != 0) { + assert(chunksize(u->child[0]) < chunksize(u->child[1])); + } + } + u = u->fd; + } while (u != t); + assert(head != 0); +} + +/* Check all the chunks in a treebin. */ +static void do_check_treebin(mstate m, bindex_t i) { + tbinptr* tb = treebin_at(m, i); + tchunkptr t = *tb; + int empty = (m->treemap & (1U << i)) == 0; + if (t == 0) + assert(empty); + if (!empty) + do_check_tree(m, t); +} + +/* Check all the chunks in a smallbin. */ +static void do_check_smallbin(mstate m, bindex_t i) { + sbinptr b = smallbin_at(m, i); + mchunkptr p = b->bk; + unsigned int empty = (m->smallmap & (1U << i)) == 0; + if (p == b) + assert(empty); + if (!empty) { + for (; p != b; p = p->bk) { + size_t size = chunksize(p); + mchunkptr q; + /* each chunk claims to be free */ + do_check_free_chunk(m, p); + /* chunk belongs in bin */ + assert(small_index(size) == i); + assert(p->bk == b || chunksize(p->bk) == chunksize(p)); + /* chunk is followed by an inuse chunk */ + q = next_chunk(p); + if (q->head != FENCEPOST_HEAD) + do_check_inuse_chunk(m, q); + } + } +} + +/* Find x in a bin. Used in other check functions. */ +static int bin_find(mstate m, mchunkptr x) { + size_t size = chunksize(x); + if (is_small(size)) { + bindex_t sidx = small_index(size); + sbinptr b = smallbin_at(m, sidx); + if (smallmap_is_marked(m, sidx)) { + mchunkptr p = b; + do { + if (p == x) + return 1; + } while ((p = p->fd) != b); + } + } + else { + bindex_t tidx; + compute_tree_index(size, tidx); + if (treemap_is_marked(m, tidx)) { + tchunkptr t = *treebin_at(m, tidx); + size_t sizebits = size << leftshift_for_tree_index(tidx); + while (t != 0 && chunksize(t) != size) { + t = t->child[(sizebits >> (SIZE_T_BITSIZE-SIZE_T_ONE)) & 1]; + sizebits <<= 1; + } + if (t != 0) { + tchunkptr u = t; + do { + if (u == (tchunkptr)x) + return 1; + } while ((u = u->fd) != t); + } + } + } + return 0; +} + +/* Traverse each chunk and check it; return total */ +static size_t traverse_and_check(mstate m) { + size_t sum = 0; + if (is_initialized(m)) { + msegmentptr s = &m->seg; + sum += m->topsize + TOP_FOOT_SIZE; + while (s != 0) { + mchunkptr q = align_as_chunk(s->base); + mchunkptr lastq = 0; + assert(pinuse(q)); + while (segment_holds(s, q) && + q != m->top && q->head != FENCEPOST_HEAD) { + sum += chunksize(q); + if (is_inuse(q)) { + assert(!bin_find(m, q)); + do_check_inuse_chunk(m, q); + } + else { + assert(q == m->dv || bin_find(m, q)); + assert(lastq == 0 || is_inuse(lastq)); /* Not 2 consecutive free */ + do_check_free_chunk(m, q); + } + lastq = q; + q = next_chunk(q); + } + s = s->next; + } + } + return sum; +} + + +/* Check all properties of malloc_state. */ +static void do_check_malloc_state(mstate m) { + bindex_t i; + size_t total; + /* check bins */ + for (i = 0; i < NSMALLBINS; ++i) + do_check_smallbin(m, i); + for (i = 0; i < NTREEBINS; ++i) + do_check_treebin(m, i); + + if (m->dvsize != 0) { /* check dv chunk */ + do_check_any_chunk(m, m->dv); + assert(m->dvsize == chunksize(m->dv)); + assert(m->dvsize >= MIN_CHUNK_SIZE); + assert(bin_find(m, m->dv) == 0); + } + + if (m->top != 0) { /* check top chunk */ + do_check_top_chunk(m, m->top); + /*assert(m->topsize == chunksize(m->top)); redundant */ + assert(m->topsize > 0); + assert(bin_find(m, m->top) == 0); + } + + total = traverse_and_check(m); + assert(total <= m->footprint); + assert(m->footprint <= m->max_footprint); +} +#endif /* DEBUG */ + +/* ----------------------------- statistics ------------------------------ */ + +#if !NO_MALLINFO +static struct mallinfo internal_mallinfo(mstate m) { + struct mallinfo nm = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + ensure_initialization(); + if (!PREACTION(m)) { + check_malloc_state(m); + if (is_initialized(m)) { + size_t nfree = SIZE_T_ONE; /* top always free */ + size_t mfree = m->topsize + TOP_FOOT_SIZE; + size_t sum = mfree; + msegmentptr s = &m->seg; + while (s != 0) { + mchunkptr q = align_as_chunk(s->base); + while (segment_holds(s, q) && + q != m->top && q->head != FENCEPOST_HEAD) { + size_t sz = chunksize(q); + sum += sz; + if (!is_inuse(q)) { + mfree += sz; + ++nfree; + } + q = next_chunk(q); + } + s = s->next; + } + + nm.arena = sum; + nm.ordblks = nfree; + nm.hblkhd = m->footprint - sum; + nm.usmblks = m->max_footprint; + nm.uordblks = m->footprint - mfree; + nm.fordblks = mfree; + nm.keepcost = m->topsize; + } + + POSTACTION(m); + } + return nm; +} +#endif /* !NO_MALLINFO */ + +#if !NO_MALLOC_STATS +static void internal_malloc_stats(mstate m) { + ensure_initialization(); + if (!PREACTION(m)) { + size_t maxfp = 0; + size_t fp = 0; + size_t used = 0; + check_malloc_state(m); + if (is_initialized(m)) { + msegmentptr s = &m->seg; + maxfp = m->max_footprint; + fp = m->footprint; + used = fp - (m->topsize + TOP_FOOT_SIZE); + + while (s != 0) { + mchunkptr q = align_as_chunk(s->base); + while (segment_holds(s, q) && + q != m->top && q->head != FENCEPOST_HEAD) { + if (!is_inuse(q)) + used -= chunksize(q); + q = next_chunk(q); + } + s = s->next; + } + } + POSTACTION(m); /* drop lock */ + fprintf(stderr, "max system bytes = %10lu\n", (unsigned long)(maxfp)); + fprintf(stderr, "system bytes = %10lu\n", (unsigned long)(fp)); + fprintf(stderr, "in use bytes = %10lu\n", (unsigned long)(used)); + } +} +#endif /* NO_MALLOC_STATS */ + +/* ----------------------- Operations on smallbins ----------------------- */ + +/* + Various forms of linking and unlinking are defined as macros. Even + the ones for trees, which are very long but have very short typical + paths. This is ugly but reduces reliance on inlining support of + compilers. +*/ + +/* Link a free chunk into a smallbin */ +#define insert_small_chunk(M, P, S) {\ + bindex_t I = small_index(S);\ + mchunkptr B = smallbin_at(M, I);\ + mchunkptr F = B;\ + assert(S >= MIN_CHUNK_SIZE);\ + if (!smallmap_is_marked(M, I))\ + mark_smallmap(M, I);\ + else if (RTCHECK(ok_address(M, B->fd)))\ + F = B->fd;\ + else {\ + CORRUPTION_ERROR_ACTION(M);\ + }\ + B->fd = P;\ + F->bk = P;\ + P->fd = F;\ + P->bk = B;\ +} + +/* Unlink a chunk from a smallbin */ +#define unlink_small_chunk(M, P, S) {\ + mchunkptr F = P->fd;\ + mchunkptr B = P->bk;\ + bindex_t I = small_index(S);\ + assert(P != B);\ + assert(P != F);\ + assert(chunksize(P) == small_index2size(I));\ + if (RTCHECK(F == smallbin_at(M,I) || (ok_address(M, F) && F->bk == P))) { \ + if (B == F) {\ + clear_smallmap(M, I);\ + }\ + else if (RTCHECK(B == smallbin_at(M,I) ||\ + (ok_address(M, B) && B->fd == P))) {\ + F->bk = B;\ + B->fd = F;\ + }\ + else {\ + CORRUPTION_ERROR_ACTION(M);\ + }\ + }\ + else {\ + CORRUPTION_ERROR_ACTION(M);\ + }\ +} + +/* Unlink the first chunk from a smallbin */ +#define unlink_first_small_chunk(M, B, P, I) {\ + mchunkptr F = P->fd;\ + assert(P != B);\ + assert(P != F);\ + assert(chunksize(P) == small_index2size(I));\ + if (B == F) {\ + clear_smallmap(M, I);\ + }\ + else if (RTCHECK(ok_address(M, F) && F->bk == P)) {\ + F->bk = B;\ + B->fd = F;\ + }\ + else {\ + CORRUPTION_ERROR_ACTION(M);\ + }\ +} + +/* Replace dv node, binning the old one */ +/* Used only when dvsize known to be small */ +#define replace_dv(M, P, S) {\ + size_t DVS = M->dvsize;\ + assert(is_small(DVS));\ + if (DVS != 0) {\ + mchunkptr DV = M->dv;\ + insert_small_chunk(M, DV, DVS);\ + }\ + M->dvsize = S;\ + M->dv = P;\ +} + +/* ------------------------- Operations on trees ------------------------- */ + +/* Insert chunk into tree */ +#define insert_large_chunk(M, X, S) {\ + tbinptr* H;\ + bindex_t I;\ + compute_tree_index(S, I);\ + H = treebin_at(M, I);\ + X->index = I;\ + X->child[0] = X->child[1] = 0;\ + if (!treemap_is_marked(M, I)) {\ + mark_treemap(M, I);\ + *H = X;\ + X->parent = (tchunkptr)H;\ + X->fd = X->bk = X;\ + }\ + else {\ + tchunkptr T = *H;\ + size_t K = S << leftshift_for_tree_index(I);\ + for (;;) {\ + if (chunksize(T) != S) {\ + tchunkptr* C = &(T->child[(K >> (SIZE_T_BITSIZE-SIZE_T_ONE)) & 1]);\ + K <<= 1;\ + if (*C != 0)\ + T = *C;\ + else if (RTCHECK(ok_address(M, C))) {\ + *C = X;\ + X->parent = T;\ + X->fd = X->bk = X;\ + break;\ + }\ + else {\ + CORRUPTION_ERROR_ACTION(M);\ + break;\ + }\ + }\ + else {\ + tchunkptr F = T->fd;\ + if (RTCHECK(ok_address(M, T) && ok_address(M, F))) {\ + T->fd = F->bk = X;\ + X->fd = F;\ + X->bk = T;\ + X->parent = 0;\ + break;\ + }\ + else {\ + CORRUPTION_ERROR_ACTION(M);\ + break;\ + }\ + }\ + }\ + }\ +} + +/* + Unlink steps: + + 1. If x is a chained node, unlink it from its same-sized fd/bk links + and choose its bk node as its replacement. + 2. If x was the last node of its size, but not a leaf node, it must + be replaced with a leaf node (not merely one with an open left or + right), to make sure that lefts and rights of descendents + correspond properly to bit masks. We use the rightmost descendent + of x. We could use any other leaf, but this is easy to locate and + tends to counteract removal of leftmosts elsewhere, and so keeps + paths shorter than minimally guaranteed. This doesn't loop much + because on average a node in a tree is near the bottom. + 3. If x is the base of a chain (i.e., has parent links) relink + x's parent and children to x's replacement (or null if none). +*/ + +#define unlink_large_chunk(M, X) {\ + tchunkptr XP = X->parent;\ + tchunkptr R;\ + if (X->bk != X) {\ + tchunkptr F = X->fd;\ + R = X->bk;\ + if (RTCHECK(ok_address(M, F) && F->bk == X && R->fd == X)) {\ + F->bk = R;\ + R->fd = F;\ + }\ + else {\ + CORRUPTION_ERROR_ACTION(M);\ + }\ + }\ + else {\ + tchunkptr* RP;\ + if (((R = *(RP = &(X->child[1]))) != 0) ||\ + ((R = *(RP = &(X->child[0]))) != 0)) {\ + tchunkptr* CP;\ + while ((*(CP = &(R->child[1])) != 0) ||\ + (*(CP = &(R->child[0])) != 0)) {\ + R = *(RP = CP);\ + }\ + if (RTCHECK(ok_address(M, RP)))\ + *RP = 0;\ + else {\ + CORRUPTION_ERROR_ACTION(M);\ + }\ + }\ + }\ + if (XP != 0) {\ + tbinptr* H = treebin_at(M, X->index);\ + if (X == *H) {\ + if ((*H = R) == 0) \ + clear_treemap(M, X->index);\ + }\ + else if (RTCHECK(ok_address(M, XP))) {\ + if (XP->child[0] == X) \ + XP->child[0] = R;\ + else \ + XP->child[1] = R;\ + }\ + else\ + CORRUPTION_ERROR_ACTION(M);\ + if (R != 0) {\ + if (RTCHECK(ok_address(M, R))) {\ + tchunkptr C0, C1;\ + R->parent = XP;\ + if ((C0 = X->child[0]) != 0) {\ + if (RTCHECK(ok_address(M, C0))) {\ + R->child[0] = C0;\ + C0->parent = R;\ + }\ + else\ + CORRUPTION_ERROR_ACTION(M);\ + }\ + if ((C1 = X->child[1]) != 0) {\ + if (RTCHECK(ok_address(M, C1))) {\ + R->child[1] = C1;\ + C1->parent = R;\ + }\ + else\ + CORRUPTION_ERROR_ACTION(M);\ + }\ + }\ + else\ + CORRUPTION_ERROR_ACTION(M);\ + }\ + }\ +} + +/* Relays to large vs small bin operations */ + +#define insert_chunk(M, P, S)\ + if (is_small(S)) insert_small_chunk(M, P, S)\ + else { tchunkptr TP = (tchunkptr)(P); insert_large_chunk(M, TP, S); } + +#define unlink_chunk(M, P, S)\ + if (is_small(S)) unlink_small_chunk(M, P, S)\ + else { tchunkptr TP = (tchunkptr)(P); unlink_large_chunk(M, TP); } + + +/* Relays to internal calls to malloc/free from realloc, memalign etc */ + +#if ONLY_MSPACES +#define internal_malloc(m, b) mspace_malloc(m, b) +#define internal_free(m, mem) mspace_free(m,mem); +#else /* ONLY_MSPACES */ +#if MSPACES +#define internal_malloc(m, b)\ + ((m == gm)? dlmalloc(b) : mspace_malloc(m, b)) +#define internal_free(m, mem)\ + if (m == gm) dlfree(mem); else mspace_free(m,mem); +#else /* MSPACES */ +#define internal_malloc(m, b) dlmalloc(b) +#define internal_free(m, mem) dlfree(mem) +#endif /* MSPACES */ +#endif /* ONLY_MSPACES */ + +/* ----------------------- Direct-mmapping chunks ----------------------- */ + +/* + Directly mmapped chunks are set up with an offset to the start of + the mmapped region stored in the prev_foot field of the chunk. This + allows reconstruction of the required argument to MUNMAP when freed, + and also allows adjustment of the returned chunk to meet alignment + requirements (especially in memalign). +*/ + +/* Malloc using mmap */ +static void* mmap_alloc(mstate m, size_t nb) { + size_t mmsize = mmap_align(nb + SIX_SIZE_T_SIZES + CHUNK_ALIGN_MASK); + if (m->footprint_limit != 0) { + size_t fp = m->footprint + mmsize; + if (fp <= m->footprint || fp > m->footprint_limit) + return 0; + } + if (mmsize > nb) { /* Check for wrap around 0 */ + char* mm = (char*)(CALL_DIRECT_MMAP(mmsize)); + if (mm != CMFAIL) { + size_t offset = align_offset(chunk2mem(mm)); + size_t psize = mmsize - offset - MMAP_FOOT_PAD; + mchunkptr p = (mchunkptr)(mm + offset); + p->prev_foot = offset; + p->head = psize; + mark_inuse_foot(m, p, psize); + chunk_plus_offset(p, psize)->head = FENCEPOST_HEAD; + chunk_plus_offset(p, psize+SIZE_T_SIZE)->head = 0; + + if (m->least_addr == 0 || mm < m->least_addr) + m->least_addr = mm; + if ((m->footprint += mmsize) > m->max_footprint) + m->max_footprint = m->footprint; + assert(is_aligned(chunk2mem(p))); + check_mmapped_chunk(m, p); + return chunk2mem(p); + } + } + return 0; +} + +/* Realloc using mmap */ +static mchunkptr mmap_resize(mstate m, mchunkptr oldp, size_t nb, int flags) { + size_t oldsize = chunksize(oldp); + (void)flags; /* placate people compiling -Wunused */ + if (is_small(nb)) /* Can't shrink mmap regions below small size */ + return 0; + /* Keep old chunk if big enough but not too big */ + if (oldsize >= nb + SIZE_T_SIZE && + (oldsize - nb) <= (mparams.granularity << 1)) + return oldp; + else { + size_t offset = oldp->prev_foot; + size_t oldmmsize = oldsize + offset + MMAP_FOOT_PAD; + size_t newmmsize = mmap_align(nb + SIX_SIZE_T_SIZES + CHUNK_ALIGN_MASK); + char* cp = (char*)CALL_MREMAP((char*)oldp - offset, + oldmmsize, newmmsize, flags); + if (cp != CMFAIL) { + mchunkptr newp = (mchunkptr)(cp + offset); + size_t psize = newmmsize - offset - MMAP_FOOT_PAD; + newp->head = psize; + mark_inuse_foot(m, newp, psize); + chunk_plus_offset(newp, psize)->head = FENCEPOST_HEAD; + chunk_plus_offset(newp, psize+SIZE_T_SIZE)->head = 0; + + if (cp < m->least_addr) + m->least_addr = cp; + if ((m->footprint += newmmsize - oldmmsize) > m->max_footprint) + m->max_footprint = m->footprint; + check_mmapped_chunk(m, newp); + return newp; + } + } + return 0; +} + + +/* -------------------------- mspace management -------------------------- */ + +/* Initialize top chunk and its size */ +static void init_top(mstate m, mchunkptr p, size_t psize) { + /* Ensure alignment */ + size_t offset = align_offset(chunk2mem(p)); + p = (mchunkptr)((char*)p + offset); + psize -= offset; + + m->top = p; + m->topsize = psize; + p->head = psize | PINUSE_BIT; + /* set size of fake trailing chunk holding overhead space only once */ + chunk_plus_offset(p, psize)->head = TOP_FOOT_SIZE; + m->trim_check = mparams.trim_threshold; /* reset on each update */ +} + +/* Initialize bins for a new mstate that is otherwise zeroed out */ +static void init_bins(mstate m) { + /* Establish circular links for smallbins */ + bindex_t i; + for (i = 0; i < NSMALLBINS; ++i) { + sbinptr bin = smallbin_at(m,i); + bin->fd = bin->bk = bin; + } +} + +#if PROCEED_ON_ERROR + +/* default corruption action */ +static void reset_on_error(mstate m) { + int i; + ++malloc_corruption_error_count; + /* Reinitialize fields to forget about all memory */ + m->smallmap = m->treemap = 0; + m->dvsize = m->topsize = 0; + m->seg.base = 0; + m->seg.size = 0; + m->seg.next = 0; + m->top = m->dv = 0; + for (i = 0; i < NTREEBINS; ++i) + *treebin_at(m, i) = 0; + init_bins(m); +} +#endif /* PROCEED_ON_ERROR */ + +/* Allocate chunk and prepend remainder with chunk in successor base. */ +static void* prepend_alloc(mstate m, char* newbase, char* oldbase, + size_t nb) { + mchunkptr p = align_as_chunk(newbase); + mchunkptr oldfirst = align_as_chunk(oldbase); + size_t psize = (char*)oldfirst - (char*)p; + mchunkptr q = chunk_plus_offset(p, nb); + size_t qsize = psize - nb; + set_size_and_pinuse_of_inuse_chunk(m, p, nb); + + assert((char*)oldfirst > (char*)q); + assert(pinuse(oldfirst)); + assert(qsize >= MIN_CHUNK_SIZE); + + /* consolidate remainder with first chunk of old base */ + if (oldfirst == m->top) { + size_t tsize = m->topsize += qsize; + m->top = q; + q->head = tsize | PINUSE_BIT; + check_top_chunk(m, q); + } + else if (oldfirst == m->dv) { + size_t dsize = m->dvsize += qsize; + m->dv = q; + set_size_and_pinuse_of_free_chunk(q, dsize); + } + else { + if (!is_inuse(oldfirst)) { + size_t nsize = chunksize(oldfirst); + unlink_chunk(m, oldfirst, nsize); + oldfirst = chunk_plus_offset(oldfirst, nsize); + qsize += nsize; + } + set_free_with_pinuse(q, qsize, oldfirst); + insert_chunk(m, q, qsize); + check_free_chunk(m, q); + } + + check_malloced_chunk(m, chunk2mem(p), nb); + return chunk2mem(p); +} + +/* Add a segment to hold a new noncontiguous region */ +static void add_segment(mstate m, char* tbase, size_t tsize, flag_t mmapped) { + /* Determine locations and sizes of segment, fenceposts, old top */ + char* old_top = (char*)m->top; + msegmentptr oldsp = segment_holding(m, old_top); + char* old_end = oldsp->base + oldsp->size; + size_t ssize = pad_request(sizeof(struct malloc_segment)); + char* rawsp = old_end - (ssize + FOUR_SIZE_T_SIZES + CHUNK_ALIGN_MASK); + size_t offset = align_offset(chunk2mem(rawsp)); + char* asp = rawsp + offset; + char* csp = (asp < (old_top + MIN_CHUNK_SIZE))? old_top : asp; + mchunkptr sp = (mchunkptr)csp; + msegmentptr ss = (msegmentptr)(chunk2mem(sp)); + mchunkptr tnext = chunk_plus_offset(sp, ssize); + mchunkptr p = tnext; + int nfences = 0; + + /* reset top to new space */ + init_top(m, (mchunkptr)tbase, tsize - TOP_FOOT_SIZE); + + /* Set up segment record */ + assert(is_aligned(ss)); + set_size_and_pinuse_of_inuse_chunk(m, sp, ssize); + *ss = m->seg; /* Push current record */ + m->seg.base = tbase; + m->seg.size = tsize; + m->seg.sflags = mmapped; + m->seg.next = ss; + + /* Insert trailing fenceposts */ + for (;;) { + mchunkptr nextp = chunk_plus_offset(p, SIZE_T_SIZE); + p->head = FENCEPOST_HEAD; + ++nfences; + if ((char*)(&(nextp->head)) < old_end) + p = nextp; + else + break; + } + assert(nfences >= 2); + + /* Insert the rest of old top into a bin as an ordinary free chunk */ + if (csp != old_top) { + mchunkptr q = (mchunkptr)old_top; + size_t psize = csp - old_top; + mchunkptr tn = chunk_plus_offset(q, psize); + set_free_with_pinuse(q, psize, tn); + insert_chunk(m, q, psize); + } + + check_top_chunk(m, m->top); +} + +/* -------------------------- System allocation -------------------------- */ + +/* Get memory from system using MORECORE or MMAP */ +static void* sys_alloc(mstate m, size_t nb) { + char* tbase = CMFAIL; + size_t tsize = 0; + flag_t mmap_flag = 0; + size_t asize; /* allocation size */ + + ensure_initialization(); + + /* Directly map large chunks, but only if already initialized */ + if (use_mmap(m) && nb >= mparams.mmap_threshold && m->topsize != 0) { + void* mem = mmap_alloc(m, nb); + if (mem != 0) + return mem; + } + + asize = granularity_align(nb + SYS_ALLOC_PADDING); + if (asize <= nb) + return 0; /* wraparound */ + if (m->footprint_limit != 0) { + size_t fp = m->footprint + asize; + if (fp <= m->footprint || fp > m->footprint_limit) + return 0; + } + + /* + Try getting memory in any of three ways (in most-preferred to + least-preferred order): + 1. A call to MORECORE that can normally contiguously extend memory. + (disabled if not MORECORE_CONTIGUOUS or not HAVE_MORECORE or + or main space is mmapped or a previous contiguous call failed) + 2. A call to MMAP new space (disabled if not HAVE_MMAP). + Note that under the default settings, if MORECORE is unable to + fulfill a request, and HAVE_MMAP is true, then mmap is + used as a noncontiguous system allocator. This is a useful backup + strategy for systems with holes in address spaces -- in this case + sbrk cannot contiguously expand the heap, but mmap may be able to + find space. + 3. A call to MORECORE that cannot usually contiguously extend memory. + (disabled if not HAVE_MORECORE) + + In all cases, we need to request enough bytes from system to ensure + we can malloc nb bytes upon success, so pad with enough space for + top_foot, plus alignment-pad to make sure we don't lose bytes if + not on boundary, and round this up to a granularity unit. + */ + + if (MORECORE_CONTIGUOUS && !use_noncontiguous(m)) { + char* br = CMFAIL; + size_t ssize = asize; /* sbrk call size */ + msegmentptr ss = (m->top == 0)? 0 : segment_holding(m, (char*)m->top); + ACQUIRE_MALLOC_GLOBAL_LOCK(); + + if (ss == 0) { /* First time through or recovery */ + char* base = (char*)CALL_MORECORE(0); + if (base != CMFAIL) { + size_t fp; + /* Adjust to end on a page boundary */ + if (!is_page_aligned(base)) + ssize += (page_align((size_t)base) - (size_t)base); + fp = m->footprint + ssize; /* recheck limits */ + if (ssize > nb && ssize < HALF_MAX_SIZE_T && + (m->footprint_limit == 0 || + (fp > m->footprint && fp <= m->footprint_limit)) && + (br = (char*)(CALL_MORECORE(ssize))) == base) { + tbase = base; + tsize = ssize; + } + } + } + else { + /* Subtract out existing available top space from MORECORE request. */ + ssize = granularity_align(nb - m->topsize + SYS_ALLOC_PADDING); + /* Use mem here only if it did continuously extend old space */ + if (ssize < HALF_MAX_SIZE_T && + (br = (char*)(CALL_MORECORE(ssize))) == ss->base+ss->size) { + tbase = br; + tsize = ssize; + } + } + + if (tbase == CMFAIL) { /* Cope with partial failure */ + if (br != CMFAIL) { /* Try to use/extend the space we did get */ + if (ssize < HALF_MAX_SIZE_T && + ssize < nb + SYS_ALLOC_PADDING) { + size_t esize = granularity_align(nb + SYS_ALLOC_PADDING - ssize); + if (esize < HALF_MAX_SIZE_T) { + char* end = (char*)CALL_MORECORE(esize); + if (end != CMFAIL) + ssize += esize; + else { /* Can't use; try to release */ + (void) CALL_MORECORE(-ssize); + br = CMFAIL; + } + } + } + } + if (br != CMFAIL) { /* Use the space we did get */ + tbase = br; + tsize = ssize; + } + else + disable_contiguous(m); /* Don't try contiguous path in the future */ + } + + RELEASE_MALLOC_GLOBAL_LOCK(); + } + + if (HAVE_MMAP && tbase == CMFAIL) { /* Try MMAP */ + char* mp = (char*)(CALL_MMAP(asize)); + if (mp != CMFAIL) { + tbase = mp; + tsize = asize; + mmap_flag = USE_MMAP_BIT; + } + } + + if (HAVE_MORECORE && tbase == CMFAIL) { /* Try noncontiguous MORECORE */ + if (asize < HALF_MAX_SIZE_T) { + char* br = CMFAIL; + char* end = CMFAIL; + ACQUIRE_MALLOC_GLOBAL_LOCK(); + br = (char*)(CALL_MORECORE(asize)); + end = (char*)(CALL_MORECORE(0)); + RELEASE_MALLOC_GLOBAL_LOCK(); + if (br != CMFAIL && end != CMFAIL && br < end) { + size_t ssize = end - br; + if (ssize > nb + TOP_FOOT_SIZE) { + tbase = br; + tsize = ssize; + } + } + } + } + + if (tbase != CMFAIL) { + + if ((m->footprint += tsize) > m->max_footprint) + m->max_footprint = m->footprint; + + if (!is_initialized(m)) { /* first-time initialization */ + if (m->least_addr == 0 || tbase < m->least_addr) + m->least_addr = tbase; + m->seg.base = tbase; + m->seg.size = tsize; + m->seg.sflags = mmap_flag; + m->magic = mparams.magic; + m->release_checks = MAX_RELEASE_CHECK_RATE; + init_bins(m); +#if !ONLY_MSPACES + if (is_global(m)) + init_top(m, (mchunkptr)tbase, tsize - TOP_FOOT_SIZE); + else +#endif + { + /* Offset top by embedded malloc_state */ + mchunkptr mn = next_chunk(mem2chunk(m)); + init_top(m, mn, (size_t)((tbase + tsize) - (char*)mn) -TOP_FOOT_SIZE); + } + } + + else { + /* Try to merge with an existing segment */ + msegmentptr sp = &m->seg; + /* Only consider most recent segment if traversal suppressed */ + while (sp != 0 && tbase != sp->base + sp->size) + sp = (NO_SEGMENT_TRAVERSAL) ? 0 : sp->next; + if (sp != 0 && + !is_extern_segment(sp) && + (sp->sflags & USE_MMAP_BIT) == mmap_flag && + segment_holds(sp, m->top)) { /* append */ + sp->size += tsize; + init_top(m, m->top, m->topsize + tsize); + } + else { + if (tbase < m->least_addr) + m->least_addr = tbase; + sp = &m->seg; + while (sp != 0 && sp->base != tbase + tsize) + sp = (NO_SEGMENT_TRAVERSAL) ? 0 : sp->next; + if (sp != 0 && + !is_extern_segment(sp) && + (sp->sflags & USE_MMAP_BIT) == mmap_flag) { + char* oldbase = sp->base; + sp->base = tbase; + sp->size += tsize; + return prepend_alloc(m, tbase, oldbase, nb); + } + else + add_segment(m, tbase, tsize, mmap_flag); + } + } + + if (nb < m->topsize) { /* Allocate from new or extended top space */ + size_t rsize = m->topsize -= nb; + mchunkptr p = m->top; + mchunkptr r = m->top = chunk_plus_offset(p, nb); + r->head = rsize | PINUSE_BIT; + set_size_and_pinuse_of_inuse_chunk(m, p, nb); + check_top_chunk(m, m->top); + check_malloced_chunk(m, chunk2mem(p), nb); + return chunk2mem(p); + } + } + + MALLOC_FAILURE_ACTION; + return 0; +} + +/* ----------------------- system deallocation -------------------------- */ + +/* Unmap and unlink any mmapped segments that don't contain used chunks */ +static size_t release_unused_segments(mstate m) { + size_t released = 0; + int nsegs = 0; + msegmentptr pred = &m->seg; + msegmentptr sp = pred->next; + while (sp != 0) { + char* base = sp->base; + size_t size = sp->size; + msegmentptr next = sp->next; + ++nsegs; + if (is_mmapped_segment(sp) && !is_extern_segment(sp)) { + mchunkptr p = align_as_chunk(base); + size_t psize = chunksize(p); + /* Can unmap if first chunk holds entire segment and not pinned */ + if (!is_inuse(p) && (char*)p + psize >= base + size - TOP_FOOT_SIZE) { + tchunkptr tp = (tchunkptr)p; + assert(segment_holds(sp, (char*)sp)); + if (p == m->dv) { + m->dv = 0; + m->dvsize = 0; + } + else { + unlink_large_chunk(m, tp); + } + if (CALL_MUNMAP(base, size) == 0) { + released += size; + m->footprint -= size; + /* unlink obsoleted record */ + sp = pred; + sp->next = next; + } + else { /* back out if cannot unmap */ + insert_large_chunk(m, tp, psize); + } + } + } + if (NO_SEGMENT_TRAVERSAL) /* scan only first segment */ + break; + pred = sp; + sp = next; + } + /* Reset check counter */ + m->release_checks = (((size_t) nsegs > (size_t) MAX_RELEASE_CHECK_RATE)? + (size_t) nsegs : (size_t) MAX_RELEASE_CHECK_RATE); + return released; +} + +static int sys_trim(mstate m, size_t pad) { + size_t released = 0; + ensure_initialization(); + if (pad < MAX_REQUEST && is_initialized(m)) { + pad += TOP_FOOT_SIZE; /* ensure enough room for segment overhead */ + + if (m->topsize > pad) { + /* Shrink top space in granularity-size units, keeping at least one */ + size_t unit = mparams.granularity; + size_t extra = ((m->topsize - pad + (unit - SIZE_T_ONE)) / unit - + SIZE_T_ONE) * unit; + msegmentptr sp = segment_holding(m, (char*)m->top); + + if (!is_extern_segment(sp)) { + if (is_mmapped_segment(sp)) { + if (HAVE_MMAP && + sp->size >= extra && + !has_segment_link(m, sp)) { /* can't shrink if pinned */ + size_t newsize = sp->size - extra; + (void)newsize; /* placate people compiling -Wunused-variable */ + /* Prefer mremap, fall back to munmap */ + if ((CALL_MREMAP(sp->base, sp->size, newsize, 0) != MFAIL) || + (CALL_MUNMAP(sp->base + newsize, extra) == 0)) { + released = extra; + } + } + } + else if (HAVE_MORECORE) { + if (extra >= HALF_MAX_SIZE_T) /* Avoid wrapping negative */ + extra = (HALF_MAX_SIZE_T) + SIZE_T_ONE - unit; + ACQUIRE_MALLOC_GLOBAL_LOCK(); + { + /* Make sure end of memory is where we last set it. */ + char* old_br = (char*)(CALL_MORECORE(0)); + if (old_br == sp->base + sp->size) { + char* rel_br = (char*)(CALL_MORECORE(-extra)); + char* new_br = (char*)(CALL_MORECORE(0)); + if (rel_br != CMFAIL && new_br < old_br) + released = old_br - new_br; + } + } + RELEASE_MALLOC_GLOBAL_LOCK(); + } + } + + if (released != 0) { + sp->size -= released; + m->footprint -= released; + init_top(m, m->top, m->topsize - released); + check_top_chunk(m, m->top); + } + } + + /* Unmap any unused mmapped segments */ + if (HAVE_MMAP) + released += release_unused_segments(m); + + /* On failure, disable autotrim to avoid repeated failed future calls */ + if (released == 0 && m->topsize > m->trim_check) + m->trim_check = MAX_SIZE_T; + } + + return (released != 0)? 1 : 0; +} + +/* Consolidate and bin a chunk. Differs from exported versions + of free mainly in that the chunk need not be marked as inuse. +*/ +static void dispose_chunk(mstate m, mchunkptr p, size_t psize) { + mchunkptr next = chunk_plus_offset(p, psize); + if (!pinuse(p)) { + mchunkptr prev; + size_t prevsize = p->prev_foot; + if (is_mmapped(p)) { + psize += prevsize + MMAP_FOOT_PAD; + if (CALL_MUNMAP((char*)p - prevsize, psize) == 0) + m->footprint -= psize; + return; + } + prev = chunk_minus_offset(p, prevsize); + psize += prevsize; + p = prev; + if (RTCHECK(ok_address(m, prev))) { /* consolidate backward */ + if (p != m->dv) { + unlink_chunk(m, p, prevsize); + } + else if ((next->head & INUSE_BITS) == INUSE_BITS) { + m->dvsize = psize; + set_free_with_pinuse(p, psize, next); + return; + } + } + else { + CORRUPTION_ERROR_ACTION(m); + return; + } + } + if (RTCHECK(ok_address(m, next))) { + if (!cinuse(next)) { /* consolidate forward */ + if (next == m->top) { + size_t tsize = m->topsize += psize; + m->top = p; + p->head = tsize | PINUSE_BIT; + if (p == m->dv) { + m->dv = 0; + m->dvsize = 0; + } + return; + } + else if (next == m->dv) { + size_t dsize = m->dvsize += psize; + m->dv = p; + set_size_and_pinuse_of_free_chunk(p, dsize); + return; + } + else { + size_t nsize = chunksize(next); + psize += nsize; + unlink_chunk(m, next, nsize); + set_size_and_pinuse_of_free_chunk(p, psize); + if (p == m->dv) { + m->dvsize = psize; + return; + } + } + } + else { + set_free_with_pinuse(p, psize, next); + } + insert_chunk(m, p, psize); + } + else { + CORRUPTION_ERROR_ACTION(m); + } +} + +/* ---------------------------- malloc --------------------------- */ + +/* allocate a large request from the best fitting chunk in a treebin */ +static void* tmalloc_large(mstate m, size_t nb) { + tchunkptr v = 0; + size_t rsize = -nb; /* Unsigned negation */ + tchunkptr t; + bindex_t idx; + compute_tree_index(nb, idx); + if ((t = *treebin_at(m, idx)) != 0) { + /* Traverse tree for this bin looking for node with size == nb */ + size_t sizebits = nb << leftshift_for_tree_index(idx); + tchunkptr rst = 0; /* The deepest untaken right subtree */ + for (;;) { + tchunkptr rt; + size_t trem = chunksize(t) - nb; + if (trem < rsize) { + v = t; + if ((rsize = trem) == 0) + break; + } + rt = t->child[1]; + t = t->child[(sizebits >> (SIZE_T_BITSIZE-SIZE_T_ONE)) & 1]; + if (rt != 0 && rt != t) + rst = rt; + if (t == 0) { + t = rst; /* set t to least subtree holding sizes > nb */ + break; + } + sizebits <<= 1; + } + } + if (t == 0 && v == 0) { /* set t to root of next non-empty treebin */ + binmap_t leftbits = left_bits(idx2bit(idx)) & m->treemap; + if (leftbits != 0) { + bindex_t i; + binmap_t leastbit = least_bit(leftbits); + compute_bit2idx(leastbit, i); + t = *treebin_at(m, i); + } + } + + while (t != 0) { /* find smallest of tree or subtree */ + size_t trem = chunksize(t) - nb; + if (trem < rsize) { + rsize = trem; + v = t; + } + t = leftmost_child(t); + } + + /* If dv is a better fit, return 0 so malloc will use it */ + if (v != 0 && rsize < (size_t)(m->dvsize - nb)) { + if (RTCHECK(ok_address(m, v))) { /* split */ + mchunkptr r = chunk_plus_offset(v, nb); + assert(chunksize(v) == rsize + nb); + if (RTCHECK(ok_next(v, r))) { + unlink_large_chunk(m, v); + if (rsize < MIN_CHUNK_SIZE) + set_inuse_and_pinuse(m, v, (rsize + nb)); + else { + set_size_and_pinuse_of_inuse_chunk(m, v, nb); + set_size_and_pinuse_of_free_chunk(r, rsize); + insert_chunk(m, r, rsize); + } + return chunk2mem(v); + } + } + CORRUPTION_ERROR_ACTION(m); + } + return 0; +} + +/* allocate a small request from the best fitting chunk in a treebin */ +static void* tmalloc_small(mstate m, size_t nb) { + tchunkptr t, v; + size_t rsize; + bindex_t i; + binmap_t leastbit = least_bit(m->treemap); + compute_bit2idx(leastbit, i); + v = t = *treebin_at(m, i); + rsize = chunksize(t) - nb; + + while ((t = leftmost_child(t)) != 0) { + size_t trem = chunksize(t) - nb; + if (trem < rsize) { + rsize = trem; + v = t; + } + } + + if (RTCHECK(ok_address(m, v))) { + mchunkptr r = chunk_plus_offset(v, nb); + assert(chunksize(v) == rsize + nb); + if (RTCHECK(ok_next(v, r))) { + unlink_large_chunk(m, v); + if (rsize < MIN_CHUNK_SIZE) + set_inuse_and_pinuse(m, v, (rsize + nb)); + else { + set_size_and_pinuse_of_inuse_chunk(m, v, nb); + set_size_and_pinuse_of_free_chunk(r, rsize); + replace_dv(m, r, rsize); + } + return chunk2mem(v); + } + } + + CORRUPTION_ERROR_ACTION(m); + return 0; +} + +#if !ONLY_MSPACES + +void* dlmalloc(size_t bytes) { + /* + Basic algorithm: + If a small request (< 256 bytes minus per-chunk overhead): + 1. If one exists, use a remainderless chunk in associated smallbin. + (Remainderless means that there are too few excess bytes to + represent as a chunk.) + 2. If it is big enough, use the dv chunk, which is normally the + chunk adjacent to the one used for the most recent small request. + 3. If one exists, split the smallest available chunk in a bin, + saving remainder in dv. + 4. If it is big enough, use the top chunk. + 5. If available, get memory from system and use it + Otherwise, for a large request: + 1. Find the smallest available binned chunk that fits, and use it + if it is better fitting than dv chunk, splitting if necessary. + 2. If better fitting than any binned chunk, use the dv chunk. + 3. If it is big enough, use the top chunk. + 4. If request size >= mmap threshold, try to directly mmap this chunk. + 5. If available, get memory from system and use it + + The ugly goto's here ensure that postaction occurs along all paths. + */ + +#if USE_LOCKS + ensure_initialization(); /* initialize in sys_alloc if not using locks */ +#endif + + if (!PREACTION(gm)) { + void* mem; + size_t nb; + if (bytes <= MAX_SMALL_REQUEST) { + bindex_t idx; + binmap_t smallbits; + nb = (bytes < MIN_REQUEST)? MIN_CHUNK_SIZE : pad_request(bytes); + idx = small_index(nb); + smallbits = gm->smallmap >> idx; + + if ((smallbits & 0x3U) != 0) { /* Remainderless fit to a smallbin. */ + mchunkptr b, p; + idx += ~smallbits & 1; /* Uses next bin if idx empty */ + b = smallbin_at(gm, idx); + p = b->fd; + assert(chunksize(p) == small_index2size(idx)); + unlink_first_small_chunk(gm, b, p, idx); + set_inuse_and_pinuse(gm, p, small_index2size(idx)); + mem = chunk2mem(p); + check_malloced_chunk(gm, mem, nb); + goto postaction; + } + + else if (nb > gm->dvsize) { + if (smallbits != 0) { /* Use chunk in next nonempty smallbin */ + mchunkptr b, p, r; + size_t rsize; + bindex_t i; + binmap_t leftbits = (smallbits << idx) & left_bits(idx2bit(idx)); + binmap_t leastbit = least_bit(leftbits); + compute_bit2idx(leastbit, i); + b = smallbin_at(gm, i); + p = b->fd; + assert(chunksize(p) == small_index2size(i)); + unlink_first_small_chunk(gm, b, p, i); + rsize = small_index2size(i) - nb; + /* Fit here cannot be remainderless if 4byte sizes */ + if (SIZE_T_SIZE != 4 && rsize < MIN_CHUNK_SIZE) + set_inuse_and_pinuse(gm, p, small_index2size(i)); + else { + set_size_and_pinuse_of_inuse_chunk(gm, p, nb); + r = chunk_plus_offset(p, nb); + set_size_and_pinuse_of_free_chunk(r, rsize); + replace_dv(gm, r, rsize); + } + mem = chunk2mem(p); + check_malloced_chunk(gm, mem, nb); + goto postaction; + } + + else if (gm->treemap != 0 && (mem = tmalloc_small(gm, nb)) != 0) { + check_malloced_chunk(gm, mem, nb); + goto postaction; + } + } + } + else if (bytes >= MAX_REQUEST) + nb = MAX_SIZE_T; /* Too big to allocate. Force failure (in sys alloc) */ + else { + nb = pad_request(bytes); + if (gm->treemap != 0 && (mem = tmalloc_large(gm, nb)) != 0) { + check_malloced_chunk(gm, mem, nb); + goto postaction; + } + } + + if (nb <= gm->dvsize) { + size_t rsize = gm->dvsize - nb; + mchunkptr p = gm->dv; + if (rsize >= MIN_CHUNK_SIZE) { /* split dv */ + mchunkptr r = gm->dv = chunk_plus_offset(p, nb); + gm->dvsize = rsize; + set_size_and_pinuse_of_free_chunk(r, rsize); + set_size_and_pinuse_of_inuse_chunk(gm, p, nb); + } + else { /* exhaust dv */ + size_t dvs = gm->dvsize; + gm->dvsize = 0; + gm->dv = 0; + set_inuse_and_pinuse(gm, p, dvs); + } + mem = chunk2mem(p); + check_malloced_chunk(gm, mem, nb); + goto postaction; + } + + else if (nb < gm->topsize) { /* Split top */ + size_t rsize = gm->topsize -= nb; + mchunkptr p = gm->top; + mchunkptr r = gm->top = chunk_plus_offset(p, nb); + r->head = rsize | PINUSE_BIT; + set_size_and_pinuse_of_inuse_chunk(gm, p, nb); + mem = chunk2mem(p); + check_top_chunk(gm, gm->top); + check_malloced_chunk(gm, mem, nb); + goto postaction; + } + + mem = sys_alloc(gm, nb); + + postaction: + POSTACTION(gm); + return mem; + } + + return 0; +} + +/* ---------------------------- free --------------------------- */ + +void dlfree(void* mem) { + /* + Consolidate freed chunks with preceeding or succeeding bordering + free chunks, if they exist, and then place in a bin. Intermixed + with special cases for top, dv, mmapped chunks, and usage errors. + */ + + if (mem != 0) { + mchunkptr p = mem2chunk(mem); +#if FOOTERS + mstate fm = get_mstate_for(p); + if (!ok_magic(fm)) { + USAGE_ERROR_ACTION(fm, p); + return; + } +#else /* FOOTERS */ +#define fm gm +#endif /* FOOTERS */ + if (!PREACTION(fm)) { + check_inuse_chunk(fm, p); + if (RTCHECK(ok_address(fm, p) && ok_inuse(p))) { + size_t psize = chunksize(p); + mchunkptr next = chunk_plus_offset(p, psize); + if (!pinuse(p)) { + size_t prevsize = p->prev_foot; + if (is_mmapped(p)) { + psize += prevsize + MMAP_FOOT_PAD; + if (CALL_MUNMAP((char*)p - prevsize, psize) == 0) + fm->footprint -= psize; + goto postaction; + } + else { + mchunkptr prev = chunk_minus_offset(p, prevsize); + psize += prevsize; + p = prev; + if (RTCHECK(ok_address(fm, prev))) { /* consolidate backward */ + if (p != fm->dv) { + unlink_chunk(fm, p, prevsize); + } + else if ((next->head & INUSE_BITS) == INUSE_BITS) { + fm->dvsize = psize; + set_free_with_pinuse(p, psize, next); + goto postaction; + } + } + else + goto erroraction; + } + } + + if (RTCHECK(ok_next(p, next) && ok_pinuse(next))) { + if (!cinuse(next)) { /* consolidate forward */ + if (next == fm->top) { + size_t tsize = fm->topsize += psize; + fm->top = p; + p->head = tsize | PINUSE_BIT; + if (p == fm->dv) { + fm->dv = 0; + fm->dvsize = 0; + } + if (should_trim(fm, tsize)) + sys_trim(fm, 0); + goto postaction; + } + else if (next == fm->dv) { + size_t dsize = fm->dvsize += psize; + fm->dv = p; + set_size_and_pinuse_of_free_chunk(p, dsize); + goto postaction; + } + else { + size_t nsize = chunksize(next); + psize += nsize; + unlink_chunk(fm, next, nsize); + set_size_and_pinuse_of_free_chunk(p, psize); + if (p == fm->dv) { + fm->dvsize = psize; + goto postaction; + } + } + } + else + set_free_with_pinuse(p, psize, next); + + if (is_small(psize)) { + insert_small_chunk(fm, p, psize); + check_free_chunk(fm, p); + } + else { + tchunkptr tp = (tchunkptr)p; + insert_large_chunk(fm, tp, psize); + check_free_chunk(fm, p); + if (--fm->release_checks == 0) + release_unused_segments(fm); + } + goto postaction; + } + } + erroraction: + USAGE_ERROR_ACTION(fm, p); + postaction: + POSTACTION(fm); + } + } +#if !FOOTERS +#undef fm +#endif /* FOOTERS */ +} + +void* dlcalloc(size_t n_elements, size_t elem_size) { + void* mem; + size_t req = 0; + if (n_elements != 0) { + req = n_elements * elem_size; + if (((n_elements | elem_size) & ~(size_t)0xffff) && + (req / n_elements != elem_size)) + req = MAX_SIZE_T; /* force downstream failure on overflow */ + } + mem = dlmalloc(req); + if (mem != 0 && calloc_must_clear(mem2chunk(mem))) + memset(mem, 0, req); + return mem; +} + +#endif /* !ONLY_MSPACES */ + +/* ------------ Internal support for realloc, memalign, etc -------------- */ + +/* Try to realloc; only in-place unless can_move true */ +static mchunkptr try_realloc_chunk(mstate m, mchunkptr p, size_t nb, + int can_move) { + mchunkptr newp = 0; + size_t oldsize = chunksize(p); + mchunkptr next = chunk_plus_offset(p, oldsize); + if (RTCHECK(ok_address(m, p) && ok_inuse(p) && + ok_next(p, next) && ok_pinuse(next))) { + if (is_mmapped(p)) { + newp = mmap_resize(m, p, nb, can_move); + } + else if (oldsize >= nb) { /* already big enough */ + size_t rsize = oldsize - nb; + if (rsize >= MIN_CHUNK_SIZE) { /* split off remainder */ + mchunkptr r = chunk_plus_offset(p, nb); + set_inuse(m, p, nb); + set_inuse(m, r, rsize); + dispose_chunk(m, r, rsize); + } + newp = p; + } + else if (next == m->top) { /* extend into top */ + if (oldsize + m->topsize > nb) { + size_t newsize = oldsize + m->topsize; + size_t newtopsize = newsize - nb; + mchunkptr newtop = chunk_plus_offset(p, nb); + set_inuse(m, p, nb); + newtop->head = newtopsize |PINUSE_BIT; + m->top = newtop; + m->topsize = newtopsize; + newp = p; + } + } + else if (next == m->dv) { /* extend into dv */ + size_t dvs = m->dvsize; + if (oldsize + dvs >= nb) { + size_t dsize = oldsize + dvs - nb; + if (dsize >= MIN_CHUNK_SIZE) { + mchunkptr r = chunk_plus_offset(p, nb); + mchunkptr n = chunk_plus_offset(r, dsize); + set_inuse(m, p, nb); + set_size_and_pinuse_of_free_chunk(r, dsize); + clear_pinuse(n); + m->dvsize = dsize; + m->dv = r; + } + else { /* exhaust dv */ + size_t newsize = oldsize + dvs; + set_inuse(m, p, newsize); + m->dvsize = 0; + m->dv = 0; + } + newp = p; + } + } + else if (!cinuse(next)) { /* extend into next free chunk */ + size_t nextsize = chunksize(next); + if (oldsize + nextsize >= nb) { + size_t rsize = oldsize + nextsize - nb; + unlink_chunk(m, next, nextsize); + if (rsize < MIN_CHUNK_SIZE) { + size_t newsize = oldsize + nextsize; + set_inuse(m, p, newsize); + } + else { + mchunkptr r = chunk_plus_offset(p, nb); + set_inuse(m, p, nb); + set_inuse(m, r, rsize); + dispose_chunk(m, r, rsize); + } + newp = p; + } + } + } + else { + USAGE_ERROR_ACTION(m, chunk2mem(p)); + } + return newp; +} + +static void* internal_memalign(mstate m, size_t alignment, size_t bytes) { + void* mem = 0; + if (alignment < MIN_CHUNK_SIZE) /* must be at least a minimum chunk size */ + alignment = MIN_CHUNK_SIZE; + if ((alignment & (alignment-SIZE_T_ONE)) != 0) {/* Ensure a power of 2 */ + size_t a = MALLOC_ALIGNMENT << 1; + while (a < alignment) a <<= 1; + alignment = a; + } + if (bytes >= MAX_REQUEST - alignment) { + if (m != 0) { /* Test isn't needed but avoids compiler warning */ + MALLOC_FAILURE_ACTION; + } + } + else { + size_t nb = request2size(bytes); + size_t req = nb + alignment + MIN_CHUNK_SIZE - CHUNK_OVERHEAD; + mem = internal_malloc(m, req); + if (mem != 0) { + mchunkptr p = mem2chunk(mem); + if (PREACTION(m)) + return 0; + if ((((size_t)(mem)) & (alignment - 1)) != 0) { /* misaligned */ + /* + Find an aligned spot inside chunk. Since we need to give + back leading space in a chunk of at least MIN_CHUNK_SIZE, if + the first calculation places us at a spot with less than + MIN_CHUNK_SIZE leader, we can move to the next aligned spot. + We've allocated enough total room so that this is always + possible. + */ + char* br = (char*)mem2chunk((size_t)(((size_t)((char*)mem + alignment - + SIZE_T_ONE)) & + -alignment)); + char* pos = ((size_t)(br - (char*)(p)) >= MIN_CHUNK_SIZE)? + br : br+alignment; + mchunkptr newp = (mchunkptr)pos; + size_t leadsize = pos - (char*)(p); + size_t newsize = chunksize(p) - leadsize; + + if (is_mmapped(p)) { /* For mmapped chunks, just adjust offset */ + newp->prev_foot = p->prev_foot + leadsize; + newp->head = newsize; + } + else { /* Otherwise, give back leader, use the rest */ + set_inuse(m, newp, newsize); + set_inuse(m, p, leadsize); + dispose_chunk(m, p, leadsize); + } + p = newp; + } + + /* Give back spare room at the end */ + if (!is_mmapped(p)) { + size_t size = chunksize(p); + if (size > nb + MIN_CHUNK_SIZE) { + size_t remainder_size = size - nb; + mchunkptr remainder = chunk_plus_offset(p, nb); + set_inuse(m, p, nb); + set_inuse(m, remainder, remainder_size); + dispose_chunk(m, remainder, remainder_size); + } + } + + mem = chunk2mem(p); + assert (chunksize(p) >= nb); + assert(((size_t)mem & (alignment - 1)) == 0); + check_inuse_chunk(m, p); + POSTACTION(m); + } + } + return mem; +} + +/* + Common support for independent_X routines, handling + all of the combinations that can result. + The opts arg has: + bit 0 set if all elements are same size (using sizes[0]) + bit 1 set if elements should be zeroed +*/ +static void** ialloc(mstate m, + size_t n_elements, + size_t* sizes, + int opts, + void* chunks[]) { + + size_t element_size; /* chunksize of each element, if all same */ + size_t contents_size; /* total size of elements */ + size_t array_size; /* request size of pointer array */ + void* mem; /* malloced aggregate space */ + mchunkptr p; /* corresponding chunk */ + size_t remainder_size; /* remaining bytes while splitting */ + void** marray; /* either "chunks" or malloced ptr array */ + mchunkptr array_chunk; /* chunk for malloced ptr array */ + flag_t was_enabled; /* to disable mmap */ + size_t size; + size_t i; + + ensure_initialization(); + /* compute array length, if needed */ + if (chunks != 0) { + if (n_elements == 0) + return chunks; /* nothing to do */ + marray = chunks; + array_size = 0; + } + else { + /* if empty req, must still return chunk representing empty array */ + if (n_elements == 0) + return (void**)internal_malloc(m, 0); + marray = 0; + array_size = request2size(n_elements * (sizeof(void*))); + } + + /* compute total element size */ + if (opts & 0x1) { /* all-same-size */ + element_size = request2size(*sizes); + contents_size = n_elements * element_size; + } + else { /* add up all the sizes */ + element_size = 0; + contents_size = 0; + for (i = 0; i != n_elements; ++i) + contents_size += request2size(sizes[i]); + } + + size = contents_size + array_size; + + /* + Allocate the aggregate chunk. First disable direct-mmapping so + malloc won't use it, since we would not be able to later + free/realloc space internal to a segregated mmap region. + */ + was_enabled = use_mmap(m); + disable_mmap(m); + mem = internal_malloc(m, size - CHUNK_OVERHEAD); + if (was_enabled) + enable_mmap(m); + if (mem == 0) + return 0; + + if (PREACTION(m)) return 0; + p = mem2chunk(mem); + remainder_size = chunksize(p); + + assert(!is_mmapped(p)); + + if (opts & 0x2) { /* optionally clear the elements */ + memset((size_t*)mem, 0, remainder_size - SIZE_T_SIZE - array_size); + } + + /* If not provided, allocate the pointer array as final part of chunk */ + if (marray == 0) { + size_t array_chunk_size; + array_chunk = chunk_plus_offset(p, contents_size); + array_chunk_size = remainder_size - contents_size; + marray = (void**) (chunk2mem(array_chunk)); + set_size_and_pinuse_of_inuse_chunk(m, array_chunk, array_chunk_size); + remainder_size = contents_size; + } + + /* split out elements */ + for (i = 0; ; ++i) { + marray[i] = chunk2mem(p); + if (i != n_elements-1) { + if (element_size != 0) + size = element_size; + else + size = request2size(sizes[i]); + remainder_size -= size; + set_size_and_pinuse_of_inuse_chunk(m, p, size); + p = chunk_plus_offset(p, size); + } + else { /* the final element absorbs any overallocation slop */ + set_size_and_pinuse_of_inuse_chunk(m, p, remainder_size); + break; + } + } + +#if DEBUG + if (marray != chunks) { + /* final element must have exactly exhausted chunk */ + if (element_size != 0) { + assert(remainder_size == element_size); + } + else { + assert(remainder_size == request2size(sizes[i])); + } + check_inuse_chunk(m, mem2chunk(marray)); + } + for (i = 0; i != n_elements; ++i) + check_inuse_chunk(m, mem2chunk(marray[i])); + +#endif /* DEBUG */ + + POSTACTION(m); + return marray; +} + +/* Try to free all pointers in the given array. + Note: this could be made faster, by delaying consolidation, + at the price of disabling some user integrity checks, We + still optimize some consolidations by combining adjacent + chunks before freeing, which will occur often if allocated + with ialloc or the array is sorted. +*/ +static size_t internal_bulk_free(mstate m, void* array[], size_t nelem) { + size_t unfreed = 0; + if (!PREACTION(m)) { + void** a; + void** fence = &(array[nelem]); + for (a = array; a != fence; ++a) { + void* mem = *a; + if (mem != 0) { + mchunkptr p = mem2chunk(mem); + size_t psize = chunksize(p); +#if FOOTERS + if (get_mstate_for(p) != m) { + ++unfreed; + continue; + } +#endif + check_inuse_chunk(m, p); + *a = 0; + if (RTCHECK(ok_address(m, p) && ok_inuse(p))) { + void ** b = a + 1; /* try to merge with next chunk */ + mchunkptr next = next_chunk(p); + if (b != fence && *b == chunk2mem(next)) { + size_t newsize = chunksize(next) + psize; + set_inuse(m, p, newsize); + *b = chunk2mem(p); + } + else + dispose_chunk(m, p, psize); + } + else { + CORRUPTION_ERROR_ACTION(m); + break; + } + } + } + if (should_trim(m, m->topsize)) + sys_trim(m, 0); + POSTACTION(m); + } + return unfreed; +} + +/* Traversal */ +#if MALLOC_INSPECT_ALL +static void internal_inspect_all(mstate m, + void(*handler)(void *start, + void *end, + size_t used_bytes, + void* callback_arg), + void* arg) { + if (is_initialized(m)) { + mchunkptr top = m->top; + msegmentptr s; + for (s = &m->seg; s != 0; s = s->next) { + mchunkptr q = align_as_chunk(s->base); + while (segment_holds(s, q) && q->head != FENCEPOST_HEAD) { + mchunkptr next = next_chunk(q); + size_t sz = chunksize(q); + size_t used; + void* start; + if (is_inuse(q)) { + used = sz - CHUNK_OVERHEAD; /* must not be mmapped */ + start = chunk2mem(q); + } + else { + used = 0; + if (is_small(sz)) { /* offset by possible bookkeeping */ + start = (void*)((char*)q + sizeof(struct malloc_chunk)); + } + else { + start = (void*)((char*)q + sizeof(struct malloc_tree_chunk)); + } + } + if (start < (void*)next) /* skip if all space is bookkeeping */ + handler(start, next, used, arg); + if (q == top) + break; + q = next; + } + } + } +} +#endif /* MALLOC_INSPECT_ALL */ + +/* ------------------ Exported realloc, memalign, etc -------------------- */ + +#if !ONLY_MSPACES + +void* dlrealloc(void* oldmem, size_t bytes) { + void* mem = 0; + if (oldmem == 0) { + mem = dlmalloc(bytes); + } + else if (bytes >= MAX_REQUEST) { + MALLOC_FAILURE_ACTION; + } +#ifdef REALLOC_ZERO_BYTES_FREES + else if (bytes == 0) { + dlfree(oldmem); + } +#endif /* REALLOC_ZERO_BYTES_FREES */ + else { + size_t nb = request2size(bytes); + mchunkptr oldp = mem2chunk(oldmem); +#if ! FOOTERS + mstate m = gm; +#else /* FOOTERS */ + mstate m = get_mstate_for(oldp); + if (!ok_magic(m)) { + USAGE_ERROR_ACTION(m, oldmem); + return 0; + } +#endif /* FOOTERS */ + if (!PREACTION(m)) { + mchunkptr newp = try_realloc_chunk(m, oldp, nb, 1); + POSTACTION(m); + if (newp != 0) { + check_inuse_chunk(m, newp); + mem = chunk2mem(newp); + } + else { + mem = internal_malloc(m, bytes); + if (mem != 0) { + size_t oc = chunksize(oldp) - overhead_for(oldp); + memcpy(mem, oldmem, (oc < bytes)? oc : bytes); + internal_free(m, oldmem); + } + } + } + } + return mem; +} + +void* dlrealloc_in_place(void* oldmem, size_t bytes) { + void* mem = 0; + if (oldmem != 0) { + if (bytes >= MAX_REQUEST) { + MALLOC_FAILURE_ACTION; + } + else { + size_t nb = request2size(bytes); + mchunkptr oldp = mem2chunk(oldmem); +#if ! FOOTERS + mstate m = gm; +#else /* FOOTERS */ + mstate m = get_mstate_for(oldp); + if (!ok_magic(m)) { + USAGE_ERROR_ACTION(m, oldmem); + return 0; + } +#endif /* FOOTERS */ + if (!PREACTION(m)) { + mchunkptr newp = try_realloc_chunk(m, oldp, nb, 0); + POSTACTION(m); + if (newp == oldp) { + check_inuse_chunk(m, newp); + mem = oldmem; + } + } + } + } + return mem; +} + +void* dlmemalign(size_t alignment, size_t bytes) { + if (alignment <= MALLOC_ALIGNMENT) { + return dlmalloc(bytes); + } + return internal_memalign(gm, alignment, bytes); +} + +int dlposix_memalign(void** pp, size_t alignment, size_t bytes) { + void* mem = 0; + if (alignment == MALLOC_ALIGNMENT) + mem = dlmalloc(bytes); + else { + size_t d = alignment / sizeof(void*); + size_t r = alignment % sizeof(void*); + if (r != 0 || d == 0 || (d & (d-SIZE_T_ONE)) != 0) + return EINVAL; + else if (bytes <= MAX_REQUEST - alignment) { + if (alignment < MIN_CHUNK_SIZE) + alignment = MIN_CHUNK_SIZE; + mem = internal_memalign(gm, alignment, bytes); + } + } + if (mem == 0) + return ENOMEM; + else { + *pp = mem; + return 0; + } +} + +void* dlvalloc(size_t bytes) { + size_t pagesz; + ensure_initialization(); + pagesz = mparams.page_size; + return dlmemalign(pagesz, bytes); +} + +void* dlpvalloc(size_t bytes) { + size_t pagesz; + ensure_initialization(); + pagesz = mparams.page_size; + return dlmemalign(pagesz, (bytes + pagesz - SIZE_T_ONE) & ~(pagesz - SIZE_T_ONE)); +} + +void** dlindependent_calloc(size_t n_elements, size_t elem_size, + void* chunks[]) { + size_t sz = elem_size; /* serves as 1-element array */ + return ialloc(gm, n_elements, &sz, 3, chunks); +} + +void** dlindependent_comalloc(size_t n_elements, size_t sizes[], + void* chunks[]) { + return ialloc(gm, n_elements, sizes, 0, chunks); +} + +size_t dlbulk_free(void* array[], size_t nelem) { + return internal_bulk_free(gm, array, nelem); +} + +#if MALLOC_INSPECT_ALL +void dlmalloc_inspect_all(void(*handler)(void *start, + void *end, + size_t used_bytes, + void* callback_arg), + void* arg) { + ensure_initialization(); + if (!PREACTION(gm)) { + internal_inspect_all(gm, handler, arg); + POSTACTION(gm); + } +} +#endif /* MALLOC_INSPECT_ALL */ + +int dlmalloc_trim(size_t pad) { + int result = 0; + ensure_initialization(); + if (!PREACTION(gm)) { + result = sys_trim(gm, pad); + POSTACTION(gm); + } + return result; +} + +size_t dlmalloc_footprint(void) { + return gm->footprint; +} + +size_t dlmalloc_max_footprint(void) { + return gm->max_footprint; +} + +size_t dlmalloc_footprint_limit(void) { + size_t maf = gm->footprint_limit; + return maf == 0 ? MAX_SIZE_T : maf; +} + +size_t dlmalloc_set_footprint_limit(size_t bytes) { + size_t result; /* invert sense of 0 */ + if (bytes == 0) + result = granularity_align(1); /* Use minimal size */ + if (bytes == MAX_SIZE_T) + result = 0; /* disable */ + else + result = granularity_align(bytes); + return gm->footprint_limit = result; +} + +#if !NO_MALLINFO +struct mallinfo dlmallinfo(void) { + return internal_mallinfo(gm); +} +#endif /* NO_MALLINFO */ + +#if !NO_MALLOC_STATS +void dlmalloc_stats() { + internal_malloc_stats(gm); +} +#endif /* NO_MALLOC_STATS */ + +int dlmallopt(int param_number, int value) { + return change_mparam(param_number, value); +} + +size_t dlmalloc_usable_size(void* mem) { + if (mem != 0) { + mchunkptr p = mem2chunk(mem); + if (is_inuse(p)) + return chunksize(p) - overhead_for(p); + } + return 0; +} + +#endif /* !ONLY_MSPACES */ + +/* ----------------------------- user mspaces ---------------------------- */ + +#if MSPACES + +static mstate init_user_mstate(char* tbase, size_t tsize) { + size_t msize = pad_request(sizeof(struct malloc_state)); + mchunkptr mn; + mchunkptr msp = align_as_chunk(tbase); + mstate m = (mstate)(chunk2mem(msp)); + memset(m, 0, msize); + (void)INITIAL_LOCK(&m->mutex); + msp->head = (msize|INUSE_BITS); + m->seg.base = m->least_addr = tbase; + m->seg.size = m->footprint = m->max_footprint = tsize; + m->magic = mparams.magic; + m->release_checks = MAX_RELEASE_CHECK_RATE; + m->mflags = mparams.default_mflags; + m->extp = 0; + m->exts = 0; + disable_contiguous(m); + init_bins(m); + mn = next_chunk(mem2chunk(m)); + init_top(m, mn, (size_t)((tbase + tsize) - (char*)mn) - TOP_FOOT_SIZE); + check_top_chunk(m, m->top); + return m; +} + +mspace create_mspace(size_t capacity, int locked) { + mstate m = 0; + size_t msize; + ensure_initialization(); + msize = pad_request(sizeof(struct malloc_state)); + if (capacity < (size_t) -(msize + TOP_FOOT_SIZE + mparams.page_size)) { + size_t rs = ((capacity == 0)? mparams.granularity : + (capacity + TOP_FOOT_SIZE + msize)); + size_t tsize = granularity_align(rs); + char* tbase = (char*)(CALL_MMAP(tsize)); + if (tbase != CMFAIL) { + m = init_user_mstate(tbase, tsize); + m->seg.sflags = USE_MMAP_BIT; + set_lock(m, locked); + } + } + return (mspace)m; +} + +mspace create_mspace_with_base(void* base, size_t capacity, int locked) { + mstate m = 0; + size_t msize; + ensure_initialization(); + msize = pad_request(sizeof(struct malloc_state)); + if (capacity > msize + TOP_FOOT_SIZE && + capacity < (size_t) -(msize + TOP_FOOT_SIZE + mparams.page_size)) { + m = init_user_mstate((char*)base, capacity); + m->seg.sflags = EXTERN_BIT; + set_lock(m, locked); + } + return (mspace)m; +} + +int mspace_track_large_chunks(mspace msp, int enable) { + int ret = 0; + mstate ms = (mstate)msp; + if (!PREACTION(ms)) { + if (!use_mmap(ms)) { + ret = 1; + } + if (!enable) { + enable_mmap(ms); + } else { + disable_mmap(ms); + } + POSTACTION(ms); + } + return ret; +} + +size_t destroy_mspace(mspace msp) { + size_t freed = 0; + mstate ms = (mstate)msp; + if (ok_magic(ms)) { + msegmentptr sp = &ms->seg; + (void)DESTROY_LOCK(&ms->mutex); /* destroy before unmapped */ + while (sp != 0) { + char* base = sp->base; + size_t size = sp->size; + flag_t flag = sp->sflags; + (void)base; /* placate people compiling -Wunused-variable */ + sp = sp->next; + if ((flag & USE_MMAP_BIT) && !(flag & EXTERN_BIT) && + CALL_MUNMAP(base, size) == 0) + freed += size; + } + } + else { + USAGE_ERROR_ACTION(ms,ms); + } + return freed; +} + +/* + mspace versions of routines are near-clones of the global + versions. This is not so nice but better than the alternatives. +*/ + +void* mspace_malloc(mspace msp, size_t bytes) { + mstate ms = (mstate)msp; + if (!ok_magic(ms)) { + USAGE_ERROR_ACTION(ms,ms); + return 0; + } + if (!PREACTION(ms)) { + void* mem; + size_t nb; + if (bytes <= MAX_SMALL_REQUEST) { + bindex_t idx; + binmap_t smallbits; + nb = (bytes < MIN_REQUEST)? MIN_CHUNK_SIZE : pad_request(bytes); + idx = small_index(nb); + smallbits = ms->smallmap >> idx; + + if ((smallbits & 0x3U) != 0) { /* Remainderless fit to a smallbin. */ + mchunkptr b, p; + idx += ~smallbits & 1; /* Uses next bin if idx empty */ + b = smallbin_at(ms, idx); + p = b->fd; + assert(chunksize(p) == small_index2size(idx)); + unlink_first_small_chunk(ms, b, p, idx); + set_inuse_and_pinuse(ms, p, small_index2size(idx)); + mem = chunk2mem(p); + check_malloced_chunk(ms, mem, nb); + goto postaction; + } + + else if (nb > ms->dvsize) { + if (smallbits != 0) { /* Use chunk in next nonempty smallbin */ + mchunkptr b, p, r; + size_t rsize; + bindex_t i; + binmap_t leftbits = (smallbits << idx) & left_bits(idx2bit(idx)); + binmap_t leastbit = least_bit(leftbits); + compute_bit2idx(leastbit, i); + b = smallbin_at(ms, i); + p = b->fd; + assert(chunksize(p) == small_index2size(i)); + unlink_first_small_chunk(ms, b, p, i); + rsize = small_index2size(i) - nb; + /* Fit here cannot be remainderless if 4byte sizes */ + if (SIZE_T_SIZE != 4 && rsize < MIN_CHUNK_SIZE) + set_inuse_and_pinuse(ms, p, small_index2size(i)); + else { + set_size_and_pinuse_of_inuse_chunk(ms, p, nb); + r = chunk_plus_offset(p, nb); + set_size_and_pinuse_of_free_chunk(r, rsize); + replace_dv(ms, r, rsize); + } + mem = chunk2mem(p); + check_malloced_chunk(ms, mem, nb); + goto postaction; + } + + else if (ms->treemap != 0 && (mem = tmalloc_small(ms, nb)) != 0) { + check_malloced_chunk(ms, mem, nb); + goto postaction; + } + } + } + else if (bytes >= MAX_REQUEST) + nb = MAX_SIZE_T; /* Too big to allocate. Force failure (in sys alloc) */ + else { + nb = pad_request(bytes); + if (ms->treemap != 0 && (mem = tmalloc_large(ms, nb)) != 0) { + check_malloced_chunk(ms, mem, nb); + goto postaction; + } + } + + if (nb <= ms->dvsize) { + size_t rsize = ms->dvsize - nb; + mchunkptr p = ms->dv; + if (rsize >= MIN_CHUNK_SIZE) { /* split dv */ + mchunkptr r = ms->dv = chunk_plus_offset(p, nb); + ms->dvsize = rsize; + set_size_and_pinuse_of_free_chunk(r, rsize); + set_size_and_pinuse_of_inuse_chunk(ms, p, nb); + } + else { /* exhaust dv */ + size_t dvs = ms->dvsize; + ms->dvsize = 0; + ms->dv = 0; + set_inuse_and_pinuse(ms, p, dvs); + } + mem = chunk2mem(p); + check_malloced_chunk(ms, mem, nb); + goto postaction; + } + + else if (nb < ms->topsize) { /* Split top */ + size_t rsize = ms->topsize -= nb; + mchunkptr p = ms->top; + mchunkptr r = ms->top = chunk_plus_offset(p, nb); + r->head = rsize | PINUSE_BIT; + set_size_and_pinuse_of_inuse_chunk(ms, p, nb); + mem = chunk2mem(p); + check_top_chunk(ms, ms->top); + check_malloced_chunk(ms, mem, nb); + goto postaction; + } + + mem = sys_alloc(ms, nb); + + postaction: + POSTACTION(ms); + return mem; + } + + return 0; +} + +void mspace_free(mspace msp, void* mem) { + if (mem != 0) { + mchunkptr p = mem2chunk(mem); +#if FOOTERS + mstate fm = get_mstate_for(p); + (void)msp; /* placate people compiling -Wunused */ +#else /* FOOTERS */ + mstate fm = (mstate)msp; +#endif /* FOOTERS */ + if (!ok_magic(fm)) { + USAGE_ERROR_ACTION(fm, p); + return; + } + if (!PREACTION(fm)) { + check_inuse_chunk(fm, p); + if (RTCHECK(ok_address(fm, p) && ok_inuse(p))) { + size_t psize = chunksize(p); + mchunkptr next = chunk_plus_offset(p, psize); + if (!pinuse(p)) { + size_t prevsize = p->prev_foot; + if (is_mmapped(p)) { + psize += prevsize + MMAP_FOOT_PAD; + if (CALL_MUNMAP((char*)p - prevsize, psize) == 0) + fm->footprint -= psize; + goto postaction; + } + else { + mchunkptr prev = chunk_minus_offset(p, prevsize); + psize += prevsize; + p = prev; + if (RTCHECK(ok_address(fm, prev))) { /* consolidate backward */ + if (p != fm->dv) { + unlink_chunk(fm, p, prevsize); + } + else if ((next->head & INUSE_BITS) == INUSE_BITS) { + fm->dvsize = psize; + set_free_with_pinuse(p, psize, next); + goto postaction; + } + } + else + goto erroraction; + } + } + + if (RTCHECK(ok_next(p, next) && ok_pinuse(next))) { + if (!cinuse(next)) { /* consolidate forward */ + if (next == fm->top) { + size_t tsize = fm->topsize += psize; + fm->top = p; + p->head = tsize | PINUSE_BIT; + if (p == fm->dv) { + fm->dv = 0; + fm->dvsize = 0; + } + if (should_trim(fm, tsize)) + sys_trim(fm, 0); + goto postaction; + } + else if (next == fm->dv) { + size_t dsize = fm->dvsize += psize; + fm->dv = p; + set_size_and_pinuse_of_free_chunk(p, dsize); + goto postaction; + } + else { + size_t nsize = chunksize(next); + psize += nsize; + unlink_chunk(fm, next, nsize); + set_size_and_pinuse_of_free_chunk(p, psize); + if (p == fm->dv) { + fm->dvsize = psize; + goto postaction; + } + } + } + else + set_free_with_pinuse(p, psize, next); + + if (is_small(psize)) { + insert_small_chunk(fm, p, psize); + check_free_chunk(fm, p); + } + else { + tchunkptr tp = (tchunkptr)p; + insert_large_chunk(fm, tp, psize); + check_free_chunk(fm, p); + if (--fm->release_checks == 0) + release_unused_segments(fm); + } + goto postaction; + } + } + erroraction: + USAGE_ERROR_ACTION(fm, p); + postaction: + POSTACTION(fm); + } + } +} + +void* mspace_calloc(mspace msp, size_t n_elements, size_t elem_size) { + void* mem; + size_t req = 0; + mstate ms = (mstate)msp; + if (!ok_magic(ms)) { + USAGE_ERROR_ACTION(ms,ms); + return 0; + } + if (n_elements != 0) { + req = n_elements * elem_size; + if (((n_elements | elem_size) & ~(size_t)0xffff) && + (req / n_elements != elem_size)) + req = MAX_SIZE_T; /* force downstream failure on overflow */ + } + mem = internal_malloc(ms, req); + if (mem != 0 && calloc_must_clear(mem2chunk(mem))) + memset(mem, 0, req); + return mem; +} + +void* mspace_realloc(mspace msp, void* oldmem, size_t bytes) { + void* mem = 0; + if (oldmem == 0) { + mem = mspace_malloc(msp, bytes); + } + else if (bytes >= MAX_REQUEST) { + MALLOC_FAILURE_ACTION; + } +#ifdef REALLOC_ZERO_BYTES_FREES + else if (bytes == 0) { + mspace_free(msp, oldmem); + } +#endif /* REALLOC_ZERO_BYTES_FREES */ + else { + size_t nb = request2size(bytes); + mchunkptr oldp = mem2chunk(oldmem); +#if ! FOOTERS + mstate m = (mstate)msp; +#else /* FOOTERS */ + mstate m = get_mstate_for(oldp); + if (!ok_magic(m)) { + USAGE_ERROR_ACTION(m, oldmem); + return 0; + } +#endif /* FOOTERS */ + if (!PREACTION(m)) { + mchunkptr newp = try_realloc_chunk(m, oldp, nb, 1); + POSTACTION(m); + if (newp != 0) { + check_inuse_chunk(m, newp); + mem = chunk2mem(newp); + } + else { + mem = mspace_malloc(m, bytes); + if (mem != 0) { + size_t oc = chunksize(oldp) - overhead_for(oldp); + memcpy(mem, oldmem, (oc < bytes)? oc : bytes); + mspace_free(m, oldmem); + } + } + } + } + return mem; +} + +void* mspace_realloc_in_place(mspace msp, void* oldmem, size_t bytes) { + void* mem = 0; + if (oldmem != 0) { + if (bytes >= MAX_REQUEST) { + MALLOC_FAILURE_ACTION; + } + else { + size_t nb = request2size(bytes); + mchunkptr oldp = mem2chunk(oldmem); +#if ! FOOTERS + mstate m = (mstate)msp; +#else /* FOOTERS */ + mstate m = get_mstate_for(oldp); + (void)msp; /* placate people compiling -Wunused */ + if (!ok_magic(m)) { + USAGE_ERROR_ACTION(m, oldmem); + return 0; + } +#endif /* FOOTERS */ + if (!PREACTION(m)) { + mchunkptr newp = try_realloc_chunk(m, oldp, nb, 0); + POSTACTION(m); + if (newp == oldp) { + check_inuse_chunk(m, newp); + mem = oldmem; + } + } + } + } + return mem; +} + +void* mspace_memalign(mspace msp, size_t alignment, size_t bytes) { + mstate ms = (mstate)msp; + if (!ok_magic(ms)) { + USAGE_ERROR_ACTION(ms,ms); + return 0; + } + if (alignment <= MALLOC_ALIGNMENT) + return mspace_malloc(msp, bytes); + return internal_memalign(ms, alignment, bytes); +} + +void** mspace_independent_calloc(mspace msp, size_t n_elements, + size_t elem_size, void* chunks[]) { + size_t sz = elem_size; /* serves as 1-element array */ + mstate ms = (mstate)msp; + if (!ok_magic(ms)) { + USAGE_ERROR_ACTION(ms,ms); + return 0; + } + return ialloc(ms, n_elements, &sz, 3, chunks); +} + +void** mspace_independent_comalloc(mspace msp, size_t n_elements, + size_t sizes[], void* chunks[]) { + mstate ms = (mstate)msp; + if (!ok_magic(ms)) { + USAGE_ERROR_ACTION(ms,ms); + return 0; + } + return ialloc(ms, n_elements, sizes, 0, chunks); +} + +size_t mspace_bulk_free(mspace msp, void* array[], size_t nelem) { + return internal_bulk_free((mstate)msp, array, nelem); +} + +#if MALLOC_INSPECT_ALL +void mspace_inspect_all(mspace msp, + void(*handler)(void *start, + void *end, + size_t used_bytes, + void* callback_arg), + void* arg) { + mstate ms = (mstate)msp; + if (ok_magic(ms)) { + if (!PREACTION(ms)) { + internal_inspect_all(ms, handler, arg); + POSTACTION(ms); + } + } + else { + USAGE_ERROR_ACTION(ms,ms); + } +} +#endif /* MALLOC_INSPECT_ALL */ + +int mspace_trim(mspace msp, size_t pad) { + int result = 0; + mstate ms = (mstate)msp; + if (ok_magic(ms)) { + if (!PREACTION(ms)) { + result = sys_trim(ms, pad); + POSTACTION(ms); + } + } + else { + USAGE_ERROR_ACTION(ms,ms); + } + return result; +} + +#if !NO_MALLOC_STATS +void mspace_malloc_stats(mspace msp) { + mstate ms = (mstate)msp; + if (ok_magic(ms)) { + internal_malloc_stats(ms); + } + else { + USAGE_ERROR_ACTION(ms,ms); + } +} +#endif /* NO_MALLOC_STATS */ + +size_t mspace_footprint(mspace msp) { + size_t result = 0; + mstate ms = (mstate)msp; + if (ok_magic(ms)) { + result = ms->footprint; + } + else { + USAGE_ERROR_ACTION(ms,ms); + } + return result; +} + +size_t mspace_max_footprint(mspace msp) { + size_t result = 0; + mstate ms = (mstate)msp; + if (ok_magic(ms)) { + result = ms->max_footprint; + } + else { + USAGE_ERROR_ACTION(ms,ms); + } + return result; +} + +size_t mspace_footprint_limit(mspace msp) { + size_t result = 0; + mstate ms = (mstate)msp; + if (ok_magic(ms)) { + size_t maf = ms->footprint_limit; + result = (maf == 0) ? MAX_SIZE_T : maf; + } + else { + USAGE_ERROR_ACTION(ms,ms); + } + return result; +} + +size_t mspace_set_footprint_limit(mspace msp, size_t bytes) { + size_t result = 0; + mstate ms = (mstate)msp; + if (ok_magic(ms)) { + if (bytes == 0) + result = granularity_align(1); /* Use minimal size */ + if (bytes == MAX_SIZE_T) + result = 0; /* disable */ + else + result = granularity_align(bytes); + ms->footprint_limit = result; + } + else { + USAGE_ERROR_ACTION(ms,ms); + } + return result; +} + +#if !NO_MALLINFO +struct mallinfo mspace_mallinfo(mspace msp) { + mstate ms = (mstate)msp; + if (!ok_magic(ms)) { + USAGE_ERROR_ACTION(ms,ms); + } + return internal_mallinfo(ms); +} +#endif /* NO_MALLINFO */ + +size_t mspace_usable_size(const void* mem) { + if (mem != 0) { + mchunkptr p = mem2chunk(mem); + if (is_inuse(p)) + return chunksize(p) - overhead_for(p); + } + return 0; +} + +int mspace_mallopt(int param_number, int value) { + return change_mparam(param_number, value); +} + +#endif /* MSPACES */ + + +/* -------------------- Alternative MORECORE functions ------------------- */ + +/* + Guidelines for creating a custom version of MORECORE: + + * For best performance, MORECORE should allocate in multiples of pagesize. + * MORECORE may allocate more memory than requested. (Or even less, + but this will usually result in a malloc failure.) + * MORECORE must not allocate memory when given argument zero, but + instead return one past the end address of memory from previous + nonzero call. + * For best performance, consecutive calls to MORECORE with positive + arguments should return increasing addresses, indicating that + space has been contiguously extended. + * Even though consecutive calls to MORECORE need not return contiguous + addresses, it must be OK for malloc'ed chunks to span multiple + regions in those cases where they do happen to be contiguous. + * MORECORE need not handle negative arguments -- it may instead + just return MFAIL when given negative arguments. + Negative arguments are always multiples of pagesize. MORECORE + must not misinterpret negative args as large positive unsigned + args. You can suppress all such calls from even occurring by defining + MORECORE_CANNOT_TRIM, + + As an example alternative MORECORE, here is a custom allocator + kindly contributed for pre-OSX macOS. It uses virtually but not + necessarily physically contiguous non-paged memory (locked in, + present and won't get swapped out). You can use it by uncommenting + this section, adding some #includes, and setting up the appropriate + defines above: + + #define MORECORE osMoreCore + + There is also a shutdown routine that should somehow be called for + cleanup upon program exit. + + #define MAX_POOL_ENTRIES 100 + #define MINIMUM_MORECORE_SIZE (64 * 1024U) + static int next_os_pool; + void *our_os_pools[MAX_POOL_ENTRIES]; + + void *osMoreCore(int size) + { + void *ptr = 0; + static void *sbrk_top = 0; + + if (size > 0) + { + if (size < MINIMUM_MORECORE_SIZE) + size = MINIMUM_MORECORE_SIZE; + if (CurrentExecutionLevel() == kTaskLevel) + ptr = PoolAllocateResident(size + RM_PAGE_SIZE, 0); + if (ptr == 0) + { + return (void *) MFAIL; + } + // save ptrs so they can be freed during cleanup + our_os_pools[next_os_pool] = ptr; + next_os_pool++; + ptr = (void *) ((((size_t) ptr) + RM_PAGE_MASK) & ~RM_PAGE_MASK); + sbrk_top = (char *) ptr + size; + return ptr; + } + else if (size < 0) + { + // we don't currently support shrink behavior + return (void *) MFAIL; + } + else + { + return sbrk_top; + } + } + + // cleanup any allocated memory pools + // called as last thing before shutting down driver + + void osCleanupMem(void) + { + void **ptr; + + for (ptr = our_os_pools; ptr < &our_os_pools[MAX_POOL_ENTRIES]; ptr++) + if (*ptr) + { + PoolDeallocate(*ptr); + *ptr = 0; + } + } + +*/ + + +/* ----------------------------------------------------------------------- +History: + v2.8.6 Wed Aug 29 06:57:58 2012 Doug Lea + * fix bad comparison in dlposix_memalign + * don't reuse adjusted asize in sys_alloc + * add LOCK_AT_FORK -- thanks to Kirill Artamonov for the suggestion + * reduce compiler warnings -- thanks to all who reported/suggested these + + v2.8.5 Sun May 22 10:26:02 2011 Doug Lea (dl at gee) + * Always perform unlink checks unless INSECURE + * Add posix_memalign. + * Improve realloc to expand in more cases; expose realloc_in_place. + Thanks to Peter Buhr for the suggestion. + * Add footprint_limit, inspect_all, bulk_free. Thanks + to Barry Hayes and others for the suggestions. + * Internal refactorings to avoid calls while holding locks + * Use non-reentrant locks by default. Thanks to Roland McGrath + for the suggestion. + * Small fixes to mspace_destroy, reset_on_error. + * Various configuration extensions/changes. Thanks + to all who contributed these. + + V2.8.4a Thu Apr 28 14:39:43 2011 (dl at gee.cs.oswego.edu) + * Update Creative Commons URL + + V2.8.4 Wed May 27 09:56:23 2009 Doug Lea (dl at gee) + * Use zeros instead of prev foot for is_mmapped + * Add mspace_track_large_chunks; thanks to Jean Brouwers + * Fix set_inuse in internal_realloc; thanks to Jean Brouwers + * Fix insufficient sys_alloc padding when using 16byte alignment + * Fix bad error check in mspace_footprint + * Adaptations for ptmalloc; thanks to Wolfram Gloger. + * Reentrant spin locks; thanks to Earl Chew and others + * Win32 improvements; thanks to Niall Douglas and Earl Chew + * Add NO_SEGMENT_TRAVERSAL and MAX_RELEASE_CHECK_RATE options + * Extension hook in malloc_state + * Various small adjustments to reduce warnings on some compilers + * Various configuration extensions/changes for more platforms. Thanks + to all who contributed these. + + V2.8.3 Thu Sep 22 11:16:32 2005 Doug Lea (dl at gee) + * Add max_footprint functions + * Ensure all appropriate literals are size_t + * Fix conditional compilation problem for some #define settings + * Avoid concatenating segments with the one provided + in create_mspace_with_base + * Rename some variables to avoid compiler shadowing warnings + * Use explicit lock initialization. + * Better handling of sbrk interference. + * Simplify and fix segment insertion, trimming and mspace_destroy + * Reinstate REALLOC_ZERO_BYTES_FREES option from 2.7.x + * Thanks especially to Dennis Flanagan for help on these. + + V2.8.2 Sun Jun 12 16:01:10 2005 Doug Lea (dl at gee) + * Fix memalign brace error. + + V2.8.1 Wed Jun 8 16:11:46 2005 Doug Lea (dl at gee) + * Fix improper #endif nesting in C++ + * Add explicit casts needed for C++ + + V2.8.0 Mon May 30 14:09:02 2005 Doug Lea (dl at gee) + * Use trees for large bins + * Support mspaces + * Use segments to unify sbrk-based and mmap-based system allocation, + removing need for emulation on most platforms without sbrk. + * Default safety checks + * Optional footer checks. Thanks to William Robertson for the idea. + * Internal code refactoring + * Incorporate suggestions and platform-specific changes. + Thanks to Dennis Flanagan, Colin Plumb, Niall Douglas, + Aaron Bachmann, Emery Berger, and others. + * Speed up non-fastbin processing enough to remove fastbins. + * Remove useless cfree() to avoid conflicts with other apps. + * Remove internal memcpy, memset. Compilers handle builtins better. + * Remove some options that no one ever used and rename others. + + V2.7.2 Sat Aug 17 09:07:30 2002 Doug Lea (dl at gee) + * Fix malloc_state bitmap array misdeclaration + + V2.7.1 Thu Jul 25 10:58:03 2002 Doug Lea (dl at gee) + * Allow tuning of FIRST_SORTED_BIN_SIZE + * Use PTR_UINT as type for all ptr->int casts. Thanks to John Belmonte. + * Better detection and support for non-contiguousness of MORECORE. + Thanks to Andreas Mueller, Conal Walsh, and Wolfram Gloger + * Bypass most of malloc if no frees. Thanks To Emery Berger. + * Fix freeing of old top non-contiguous chunk im sysmalloc. + * Raised default trim and map thresholds to 256K. + * Fix mmap-related #defines. Thanks to Lubos Lunak. + * Fix copy macros; added LACKS_FCNTL_H. Thanks to Neal Walfield. + * Branch-free bin calculation + * Default trim and mmap thresholds now 256K. + + V2.7.0 Sun Mar 11 14:14:06 2001 Doug Lea (dl at gee) + * Introduce independent_comalloc and independent_calloc. + Thanks to Michael Pachos for motivation and help. + * Make optional .h file available + * Allow > 2GB requests on 32bit systems. + * new WIN32 sbrk, mmap, munmap, lock code from . + Thanks also to Andreas Mueller , + and Anonymous. + * Allow override of MALLOC_ALIGNMENT (Thanks to Ruud Waij for + helping test this.) + * memalign: check alignment arg + * realloc: don't try to shift chunks backwards, since this + leads to more fragmentation in some programs and doesn't + seem to help in any others. + * Collect all cases in malloc requiring system memory into sysmalloc + * Use mmap as backup to sbrk + * Place all internal state in malloc_state + * Introduce fastbins (although similar to 2.5.1) + * Many minor tunings and cosmetic improvements + * Introduce USE_PUBLIC_MALLOC_WRAPPERS, USE_MALLOC_LOCK + * Introduce MALLOC_FAILURE_ACTION, MORECORE_CONTIGUOUS + Thanks to Tony E. Bennett and others. + * Include errno.h to support default failure action. + + V2.6.6 Sun Dec 5 07:42:19 1999 Doug Lea (dl at gee) + * return null for negative arguments + * Added Several WIN32 cleanups from Martin C. Fong + * Add 'LACKS_SYS_PARAM_H' for those systems without 'sys/param.h' + (e.g. WIN32 platforms) + * Cleanup header file inclusion for WIN32 platforms + * Cleanup code to avoid Microsoft Visual C++ compiler complaints + * Add 'USE_DL_PREFIX' to quickly allow co-existence with existing + memory allocation routines + * Set 'malloc_getpagesize' for WIN32 platforms (needs more work) + * Use 'assert' rather than 'ASSERT' in WIN32 code to conform to + usage of 'assert' in non-WIN32 code + * Improve WIN32 'sbrk()' emulation's 'findRegion()' routine to + avoid infinite loop + * Always call 'fREe()' rather than 'free()' + + V2.6.5 Wed Jun 17 15:57:31 1998 Doug Lea (dl at gee) + * Fixed ordering problem with boundary-stamping + + V2.6.3 Sun May 19 08:17:58 1996 Doug Lea (dl at gee) + * Added pvalloc, as recommended by H.J. Liu + * Added 64bit pointer support mainly from Wolfram Gloger + * Added anonymously donated WIN32 sbrk emulation + * Malloc, calloc, getpagesize: add optimizations from Raymond Nijssen + * malloc_extend_top: fix mask error that caused wastage after + foreign sbrks + * Add linux mremap support code from HJ Liu + + V2.6.2 Tue Dec 5 06:52:55 1995 Doug Lea (dl at gee) + * Integrated most documentation with the code. + * Add support for mmap, with help from + Wolfram Gloger (Gloger@lrz.uni-muenchen.de). + * Use last_remainder in more cases. + * Pack bins using idea from colin@nyx10.cs.du.edu + * Use ordered bins instead of best-fit threshhold + * Eliminate block-local decls to simplify tracing and debugging. + * Support another case of realloc via move into top + * Fix error occuring when initial sbrk_base not word-aligned. + * Rely on page size for units instead of SBRK_UNIT to + avoid surprises about sbrk alignment conventions. + * Add mallinfo, mallopt. Thanks to Raymond Nijssen + (raymond@es.ele.tue.nl) for the suggestion. + * Add `pad' argument to malloc_trim and top_pad mallopt parameter. + * More precautions for cases where other routines call sbrk, + courtesy of Wolfram Gloger (Gloger@lrz.uni-muenchen.de). + * Added macros etc., allowing use in linux libc from + H.J. Lu (hjl@gnu.ai.mit.edu) + * Inverted this history list + + V2.6.1 Sat Dec 2 14:10:57 1995 Doug Lea (dl at gee) + * Re-tuned and fixed to behave more nicely with V2.6.0 changes. + * Removed all preallocation code since under current scheme + the work required to undo bad preallocations exceeds + the work saved in good cases for most test programs. + * No longer use return list or unconsolidated bins since + no scheme using them consistently outperforms those that don't + given above changes. + * Use best fit for very large chunks to prevent some worst-cases. + * Added some support for debugging + + V2.6.0 Sat Nov 4 07:05:23 1995 Doug Lea (dl at gee) + * Removed footers when chunks are in use. Thanks to + Paul Wilson (wilson@cs.texas.edu) for the suggestion. + + V2.5.4 Wed Nov 1 07:54:51 1995 Doug Lea (dl at gee) + * Added malloc_trim, with help from Wolfram Gloger + (wmglo@Dent.MED.Uni-Muenchen.DE). + + V2.5.3 Tue Apr 26 10:16:01 1994 Doug Lea (dl at g) + + V2.5.2 Tue Apr 5 16:20:40 1994 Doug Lea (dl at g) + * realloc: try to expand in both directions + * malloc: swap order of clean-bin strategy; + * realloc: only conditionally expand backwards + * Try not to scavenge used bins + * Use bin counts as a guide to preallocation + * Occasionally bin return list chunks in first scan + * Add a few optimizations from colin@nyx10.cs.du.edu + + V2.5.1 Sat Aug 14 15:40:43 1993 Doug Lea (dl at g) + * faster bin computation & slightly different binning + * merged all consolidations to one part of malloc proper + (eliminating old malloc_find_space & malloc_clean_bin) + * Scan 2 returns chunks (not just 1) + * Propagate failure in realloc if malloc returns 0 + * Add stuff to allow compilation on non-ANSI compilers + from kpv@research.att.com + + V2.5 Sat Aug 7 07:41:59 1993 Doug Lea (dl at g.oswego.edu) + * removed potential for odd address access in prev_chunk + * removed dependency on getpagesize.h + * misc cosmetics and a bit more internal documentation + * anticosmetics: mangled names in macros to evade debugger strangeness + * tested on sparc, hp-700, dec-mips, rs6000 + with gcc & native cc (hp, dec only) allowing + Detlefs & Zorn comparison study (in SIGPLAN Notices.) + + Trial version Fri Aug 28 13:14:29 1992 Doug Lea (dl at g.oswego.edu) + * Based loosely on libg++-1.2X malloc. (It retains some of the overall + structure of old version, but most details differ.) + +*/ diff --git a/contrib/dlmalloc/malloc.h b/contrib/dlmalloc/malloc.h new file mode 100644 index 0000000..0e9f66d --- /dev/null +++ b/contrib/dlmalloc/malloc.h @@ -0,0 +1,620 @@ +/* + Default header file for malloc-2.8.x, written by Doug Lea + and released to the public domain, as explained at + http://creativecommons.org/publicdomain/zero/1.0/ + + This header is for ANSI C/C++ only. You can set any of + the following #defines before including: + + * If USE_DL_PREFIX is defined, it is assumed that malloc.c + was also compiled with this option, so all routines + have names starting with "dl". + + * If HAVE_USR_INCLUDE_MALLOC_H is defined, it is assumed that this + file will be #included AFTER . This is needed only if + your system defines a struct mallinfo that is incompatible with the + standard one declared here. Otherwise, you can include this file + INSTEAD of your system system . At least on ANSI, all + declarations should be compatible with system versions + + * If MSPACES is defined, declarations for mspace versions are included. +*/ + +#ifndef MALLOC_280_H +#define MALLOC_280_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include /* for size_t */ + +#ifndef ONLY_MSPACES +#define ONLY_MSPACES 0 /* define to a value */ +#elif ONLY_MSPACES != 0 +#define ONLY_MSPACES 1 +#endif /* ONLY_MSPACES */ +#ifndef NO_MALLINFO +#define NO_MALLINFO 0 +#endif /* NO_MALLINFO */ + +#ifndef MSPACES +#if ONLY_MSPACES +#define MSPACES 1 +#else /* ONLY_MSPACES */ +#define MSPACES 0 +#endif /* ONLY_MSPACES */ +#endif /* MSPACES */ + +#if !ONLY_MSPACES + +#ifndef USE_DL_PREFIX +#define dlcalloc calloc +#define dlfree free +#define dlmalloc malloc +#define dlmemalign memalign +#define dlposix_memalign posix_memalign +#define dlrealloc realloc +#define dlvalloc valloc +#define dlpvalloc pvalloc +#define dlmallinfo mallinfo +#define dlmallopt mallopt +#define dlmalloc_trim malloc_trim +#define dlmalloc_stats malloc_stats +#define dlmalloc_usable_size malloc_usable_size +#define dlmalloc_footprint malloc_footprint +#define dlmalloc_max_footprint malloc_max_footprint +#define dlmalloc_footprint_limit malloc_footprint_limit +#define dlmalloc_set_footprint_limit malloc_set_footprint_limit +#define dlmalloc_inspect_all malloc_inspect_all +#define dlindependent_calloc independent_calloc +#define dlindependent_comalloc independent_comalloc +#define dlbulk_free bulk_free +#endif /* USE_DL_PREFIX */ + +#if !NO_MALLINFO +#ifndef HAVE_USR_INCLUDE_MALLOC_H +#ifndef _MALLOC_H +#ifndef MALLINFO_FIELD_TYPE +#define MALLINFO_FIELD_TYPE size_t +#endif /* MALLINFO_FIELD_TYPE */ +#ifndef STRUCT_MALLINFO_DECLARED +#define STRUCT_MALLINFO_DECLARED 1 +struct mallinfo { + MALLINFO_FIELD_TYPE arena; /* non-mmapped space allocated from system */ + MALLINFO_FIELD_TYPE ordblks; /* number of free chunks */ + MALLINFO_FIELD_TYPE smblks; /* always 0 */ + MALLINFO_FIELD_TYPE hblks; /* always 0 */ + MALLINFO_FIELD_TYPE hblkhd; /* space in mmapped regions */ + MALLINFO_FIELD_TYPE usmblks; /* maximum total allocated space */ + MALLINFO_FIELD_TYPE fsmblks; /* always 0 */ + MALLINFO_FIELD_TYPE uordblks; /* total allocated space */ + MALLINFO_FIELD_TYPE fordblks; /* total free space */ + MALLINFO_FIELD_TYPE keepcost; /* releasable (via malloc_trim) space */ +}; +#endif /* STRUCT_MALLINFO_DECLARED */ +#endif /* _MALLOC_H */ +#endif /* HAVE_USR_INCLUDE_MALLOC_H */ +#endif /* !NO_MALLINFO */ + +/* + malloc(size_t n) + Returns a pointer to a newly allocated chunk of at least n bytes, or + null if no space is available, in which case errno is set to ENOMEM + on ANSI C systems. + + If n is zero, malloc returns a minimum-sized chunk. (The minimum + size is 16 bytes on most 32bit systems, and 32 bytes on 64bit + systems.) Note that size_t is an unsigned type, so calls with + arguments that would be negative if signed are interpreted as + requests for huge amounts of space, which will often fail. The + maximum supported value of n differs across systems, but is in all + cases less than the maximum representable value of a size_t. +*/ +void* dlmalloc(size_t); + +/* + free(void* p) + Releases the chunk of memory pointed to by p, that had been previously + allocated using malloc or a related routine such as realloc. + It has no effect if p is null. If p was not malloced or already + freed, free(p) will by default cuase the current program to abort. +*/ +void dlfree(void*); + +/* + calloc(size_t n_elements, size_t element_size); + Returns a pointer to n_elements * element_size bytes, with all locations + set to zero. +*/ +void* dlcalloc(size_t, size_t); + +/* + realloc(void* p, size_t n) + Returns a pointer to a chunk of size n that contains the same data + as does chunk p up to the minimum of (n, p's size) bytes, or null + if no space is available. + + The returned pointer may or may not be the same as p. The algorithm + prefers extending p in most cases when possible, otherwise it + employs the equivalent of a malloc-copy-free sequence. + + If p is null, realloc is equivalent to malloc. + + If space is not available, realloc returns null, errno is set (if on + ANSI) and p is NOT freed. + + if n is for fewer bytes than already held by p, the newly unused + space is lopped off and freed if possible. realloc with a size + argument of zero (re)allocates a minimum-sized chunk. + + The old unix realloc convention of allowing the last-free'd chunk + to be used as an argument to realloc is not supported. +*/ +void* dlrealloc(void*, size_t); + +/* + realloc_in_place(void* p, size_t n) + Resizes the space allocated for p to size n, only if this can be + done without moving p (i.e., only if there is adjacent space + available if n is greater than p's current allocated size, or n is + less than or equal to p's size). This may be used instead of plain + realloc if an alternative allocation strategy is needed upon failure + to expand space; for example, reallocation of a buffer that must be + memory-aligned or cleared. You can use realloc_in_place to trigger + these alternatives only when needed. + + Returns p if successful; otherwise null. +*/ +void* dlrealloc_in_place(void*, size_t); + +/* + memalign(size_t alignment, size_t n); + Returns a pointer to a newly allocated chunk of n bytes, aligned + in accord with the alignment argument. + + The alignment argument should be a power of two. If the argument is + not a power of two, the nearest greater power is used. + 8-byte alignment is guaranteed by normal malloc calls, so don't + bother calling memalign with an argument of 8 or less. + + Overreliance on memalign is a sure way to fragment space. +*/ +void* dlmemalign(size_t, size_t); + +/* + int posix_memalign(void** pp, size_t alignment, size_t n); + Allocates a chunk of n bytes, aligned in accord with the alignment + argument. Differs from memalign only in that it (1) assigns the + allocated memory to *pp rather than returning it, (2) fails and + returns EINVAL if the alignment is not a power of two (3) fails and + returns ENOMEM if memory cannot be allocated. +*/ +int dlposix_memalign(void**, size_t, size_t); + +/* + valloc(size_t n); + Equivalent to memalign(pagesize, n), where pagesize is the page + size of the system. If the pagesize is unknown, 4096 is used. +*/ +void* dlvalloc(size_t); + +/* + mallopt(int parameter_number, int parameter_value) + Sets tunable parameters The format is to provide a + (parameter-number, parameter-value) pair. mallopt then sets the + corresponding parameter to the argument value if it can (i.e., so + long as the value is meaningful), and returns 1 if successful else + 0. SVID/XPG/ANSI defines four standard param numbers for mallopt, + normally defined in malloc.h. None of these are use in this malloc, + so setting them has no effect. But this malloc also supports other + options in mallopt: + + Symbol param # default allowed param values + M_TRIM_THRESHOLD -1 2*1024*1024 any (-1U disables trimming) + M_GRANULARITY -2 page size any power of 2 >= page size + M_MMAP_THRESHOLD -3 256*1024 any (or 0 if no MMAP support) +*/ +int dlmallopt(int, int); + +#define M_TRIM_THRESHOLD (-1) +#define M_GRANULARITY (-2) +#define M_MMAP_THRESHOLD (-3) + + +/* + malloc_footprint(); + Returns the number of bytes obtained from the system. The total + number of bytes allocated by malloc, realloc etc., is less than this + value. Unlike mallinfo, this function returns only a precomputed + result, so can be called frequently to monitor memory consumption. + Even if locks are otherwise defined, this function does not use them, + so results might not be up to date. +*/ +size_t dlmalloc_footprint(void); + +/* + malloc_max_footprint(); + Returns the maximum number of bytes obtained from the system. This + value will be greater than current footprint if deallocated space + has been reclaimed by the system. The peak number of bytes allocated + by malloc, realloc etc., is less than this value. Unlike mallinfo, + this function returns only a precomputed result, so can be called + frequently to monitor memory consumption. Even if locks are + otherwise defined, this function does not use them, so results might + not be up to date. +*/ +size_t dlmalloc_max_footprint(void); + +/* + malloc_footprint_limit(); + Returns the number of bytes that the heap is allowed to obtain from + the system, returning the last value returned by + malloc_set_footprint_limit, or the maximum size_t value if + never set. The returned value reflects a permission. There is no + guarantee that this number of bytes can actually be obtained from + the system. +*/ +size_t dlmalloc_footprint_limit(void); + +/* + malloc_set_footprint_limit(); + Sets the maximum number of bytes to obtain from the system, causing + failure returns from malloc and related functions upon attempts to + exceed this value. The argument value may be subject to page + rounding to an enforceable limit; this actual value is returned. + Using an argument of the maximum possible size_t effectively + disables checks. If the argument is less than or equal to the + current malloc_footprint, then all future allocations that require + additional system memory will fail. However, invocation cannot + retroactively deallocate existing used memory. +*/ +size_t dlmalloc_set_footprint_limit(size_t bytes); + +/* + malloc_inspect_all(void(*handler)(void *start, + void *end, + size_t used_bytes, + void* callback_arg), + void* arg); + Traverses the heap and calls the given handler for each managed + region, skipping all bytes that are (or may be) used for bookkeeping + purposes. Traversal does not include include chunks that have been + directly memory mapped. Each reported region begins at the start + address, and continues up to but not including the end address. The + first used_bytes of the region contain allocated data. If + used_bytes is zero, the region is unallocated. The handler is + invoked with the given callback argument. If locks are defined, they + are held during the entire traversal. It is a bad idea to invoke + other malloc functions from within the handler. + + For example, to count the number of in-use chunks with size greater + than 1000, you could write: + static int count = 0; + void count_chunks(void* start, void* end, size_t used, void* arg) { + if (used >= 1000) ++count; + } + then: + malloc_inspect_all(count_chunks, NULL); + + malloc_inspect_all is compiled only if MALLOC_INSPECT_ALL is defined. +*/ +void dlmalloc_inspect_all(void(*handler)(void*, void *, size_t, void*), + void* arg); + +#if !NO_MALLINFO +/* + mallinfo() + Returns (by copy) a struct containing various summary statistics: + + arena: current total non-mmapped bytes allocated from system + ordblks: the number of free chunks + smblks: always zero. + hblks: current number of mmapped regions + hblkhd: total bytes held in mmapped regions + usmblks: the maximum total allocated space. This will be greater + than current total if trimming has occurred. + fsmblks: always zero + uordblks: current total allocated space (normal or mmapped) + fordblks: total free space + keepcost: the maximum number of bytes that could ideally be released + back to system via malloc_trim. ("ideally" means that + it ignores page restrictions etc.) + + Because these fields are ints, but internal bookkeeping may + be kept as longs, the reported values may wrap around zero and + thus be inaccurate. +*/ + +struct mallinfo dlmallinfo(void); +#endif /* NO_MALLINFO */ + +/* + independent_calloc(size_t n_elements, size_t element_size, void* chunks[]); + + independent_calloc is similar to calloc, but instead of returning a + single cleared space, it returns an array of pointers to n_elements + independent elements that can hold contents of size elem_size, each + of which starts out cleared, and can be independently freed, + realloc'ed etc. The elements are guaranteed to be adjacently + allocated (this is not guaranteed to occur with multiple callocs or + mallocs), which may also improve cache locality in some + applications. + + The "chunks" argument is optional (i.e., may be null, which is + probably the most typical usage). If it is null, the returned array + is itself dynamically allocated and should also be freed when it is + no longer needed. Otherwise, the chunks array must be of at least + n_elements in length. It is filled in with the pointers to the + chunks. + + In either case, independent_calloc returns this pointer array, or + null if the allocation failed. If n_elements is zero and "chunks" + is null, it returns a chunk representing an array with zero elements + (which should be freed if not wanted). + + Each element must be freed when it is no longer needed. This can be + done all at once using bulk_free. + + independent_calloc simplifies and speeds up implementations of many + kinds of pools. It may also be useful when constructing large data + structures that initially have a fixed number of fixed-sized nodes, + but the number is not known at compile time, and some of the nodes + may later need to be freed. For example: + + struct Node { int item; struct Node* next; }; + + struct Node* build_list() { + struct Node** pool; + int n = read_number_of_nodes_needed(); + if (n <= 0) return 0; + pool = (struct Node**)(independent_calloc(n, sizeof(struct Node), 0); + if (pool == 0) die(); + // organize into a linked list... + struct Node* first = pool[0]; + for (i = 0; i < n-1; ++i) + pool[i]->next = pool[i+1]; + free(pool); // Can now free the array (or not, if it is needed later) + return first; + } +*/ +void** dlindependent_calloc(size_t, size_t, void**); + +/* + independent_comalloc(size_t n_elements, size_t sizes[], void* chunks[]); + + independent_comalloc allocates, all at once, a set of n_elements + chunks with sizes indicated in the "sizes" array. It returns + an array of pointers to these elements, each of which can be + independently freed, realloc'ed etc. The elements are guaranteed to + be adjacently allocated (this is not guaranteed to occur with + multiple callocs or mallocs), which may also improve cache locality + in some applications. + + The "chunks" argument is optional (i.e., may be null). If it is null + the returned array is itself dynamically allocated and should also + be freed when it is no longer needed. Otherwise, the chunks array + must be of at least n_elements in length. It is filled in with the + pointers to the chunks. + + In either case, independent_comalloc returns this pointer array, or + null if the allocation failed. If n_elements is zero and chunks is + null, it returns a chunk representing an array with zero elements + (which should be freed if not wanted). + + Each element must be freed when it is no longer needed. This can be + done all at once using bulk_free. + + independent_comallac differs from independent_calloc in that each + element may have a different size, and also that it does not + automatically clear elements. + + independent_comalloc can be used to speed up allocation in cases + where several structs or objects must always be allocated at the + same time. For example: + + struct Head { ... } + struct Foot { ... } + + void send_message(char* msg) { + int msglen = strlen(msg); + size_t sizes[3] = { sizeof(struct Head), msglen, sizeof(struct Foot) }; + void* chunks[3]; + if (independent_comalloc(3, sizes, chunks) == 0) + die(); + struct Head* head = (struct Head*)(chunks[0]); + char* body = (char*)(chunks[1]); + struct Foot* foot = (struct Foot*)(chunks[2]); + // ... + } + + In general though, independent_comalloc is worth using only for + larger values of n_elements. For small values, you probably won't + detect enough difference from series of malloc calls to bother. + + Overuse of independent_comalloc can increase overall memory usage, + since it cannot reuse existing noncontiguous small chunks that + might be available for some of the elements. +*/ +void** dlindependent_comalloc(size_t, size_t*, void**); + +/* + bulk_free(void* array[], size_t n_elements) + Frees and clears (sets to null) each non-null pointer in the given + array. This is likely to be faster than freeing them one-by-one. + If footers are used, pointers that have been allocated in different + mspaces are not freed or cleared, and the count of all such pointers + is returned. For large arrays of pointers with poor locality, it + may be worthwhile to sort this array before calling bulk_free. +*/ +size_t dlbulk_free(void**, size_t n_elements); + +/* + pvalloc(size_t n); + Equivalent to valloc(minimum-page-that-holds(n)), that is, + round up n to nearest pagesize. + */ +void* dlpvalloc(size_t); + +/* + malloc_trim(size_t pad); + + If possible, gives memory back to the system (via negative arguments + to sbrk) if there is unused memory at the `high' end of the malloc + pool or in unused MMAP segments. You can call this after freeing + large blocks of memory to potentially reduce the system-level memory + requirements of a program. However, it cannot guarantee to reduce + memory. Under some allocation patterns, some large free blocks of + memory will be locked between two used chunks, so they cannot be + given back to the system. + + The `pad' argument to malloc_trim represents the amount of free + trailing space to leave untrimmed. If this argument is zero, only + the minimum amount of memory to maintain internal data structures + will be left. Non-zero arguments can be supplied to maintain enough + trailing space to service future expected allocations without having + to re-obtain memory from the system. + + Malloc_trim returns 1 if it actually released any memory, else 0. +*/ +int dlmalloc_trim(size_t); + +/* + malloc_stats(); + Prints on stderr the amount of space obtained from the system (both + via sbrk and mmap), the maximum amount (which may be more than + current if malloc_trim and/or munmap got called), and the current + number of bytes allocated via malloc (or realloc, etc) but not yet + freed. Note that this is the number of bytes allocated, not the + number requested. It will be larger than the number requested + because of alignment and bookkeeping overhead. Because it includes + alignment wastage as being in use, this figure may be greater than + zero even when no user-level chunks are allocated. + + The reported current and maximum system memory can be inaccurate if + a program makes other calls to system memory allocation functions + (normally sbrk) outside of malloc. + + malloc_stats prints only the most commonly interesting statistics. + More information can be obtained by calling mallinfo. + + malloc_stats is not compiled if NO_MALLOC_STATS is defined. +*/ +void dlmalloc_stats(void); + +#endif /* !ONLY_MSPACES */ + +/* + malloc_usable_size(void* p); + + Returns the number of bytes you can actually use in + an allocated chunk, which may be more than you requested (although + often not) due to alignment and minimum size constraints. + You can use this many bytes without worrying about + overwriting other allocated objects. This is not a particularly great + programming practice. malloc_usable_size can be more useful in + debugging and assertions, for example: + + p = malloc(n); + assert(malloc_usable_size(p) >= 256); +*/ +size_t dlmalloc_usable_size(void*); + +#if MSPACES + +/* + mspace is an opaque type representing an independent + region of space that supports mspace_malloc, etc. +*/ +typedef void* mspace; + +/* + create_mspace creates and returns a new independent space with the + given initial capacity, or, if 0, the default granularity size. It + returns null if there is no system memory available to create the + space. If argument locked is non-zero, the space uses a separate + lock to control access. The capacity of the space will grow + dynamically as needed to service mspace_malloc requests. You can + control the sizes of incremental increases of this space by + compiling with a different DEFAULT_GRANULARITY or dynamically + setting with mallopt(M_GRANULARITY, value). +*/ +mspace create_mspace(size_t capacity, int locked); + +/* + destroy_mspace destroys the given space, and attempts to return all + of its memory back to the system, returning the total number of + bytes freed. After destruction, the results of access to all memory + used by the space become undefined. +*/ +size_t destroy_mspace(mspace msp); + +/* + create_mspace_with_base uses the memory supplied as the initial base + of a new mspace. Part (less than 128*sizeof(size_t) bytes) of this + space is used for bookkeeping, so the capacity must be at least this + large. (Otherwise 0 is returned.) When this initial space is + exhausted, additional memory will be obtained from the system. + Destroying this space will deallocate all additionally allocated + space (if possible) but not the initial base. +*/ +mspace create_mspace_with_base(void* base, size_t capacity, int locked); + +/* + mspace_track_large_chunks controls whether requests for large chunks + are allocated in their own untracked mmapped regions, separate from + others in this mspace. By default large chunks are not tracked, + which reduces fragmentation. However, such chunks are not + necessarily released to the system upon destroy_mspace. Enabling + tracking by setting to true may increase fragmentation, but avoids + leakage when relying on destroy_mspace to release all memory + allocated using this space. The function returns the previous + setting. +*/ +int mspace_track_large_chunks(mspace msp, int enable); + +#if !NO_MALLINFO +/* + mspace_mallinfo behaves as mallinfo, but reports properties of + the given space. +*/ +struct mallinfo mspace_mallinfo(mspace msp); +#endif /* NO_MALLINFO */ + +/* + An alias for mallopt. +*/ +int mspace_mallopt(int, int); + +/* + The following operate identically to their malloc counterparts + but operate only for the given mspace argument +*/ +void* mspace_malloc(mspace msp, size_t bytes); +void mspace_free(mspace msp, void* mem); +void* mspace_calloc(mspace msp, size_t n_elements, size_t elem_size); +void* mspace_realloc(mspace msp, void* mem, size_t newsize); +void* mspace_realloc_in_place(mspace msp, void* mem, size_t newsize); +void* mspace_memalign(mspace msp, size_t alignment, size_t bytes); +void** mspace_independent_calloc(mspace msp, size_t n_elements, + size_t elem_size, void* chunks[]); +void** mspace_independent_comalloc(mspace msp, size_t n_elements, + size_t sizes[], void* chunks[]); +size_t mspace_bulk_free(mspace msp, void**, size_t n_elements); +size_t mspace_usable_size(const void* mem); +void mspace_malloc_stats(mspace msp); +int mspace_trim(mspace msp, size_t pad); +size_t mspace_footprint(mspace msp); +size_t mspace_max_footprint(mspace msp); +size_t mspace_footprint_limit(mspace msp); +size_t mspace_set_footprint_limit(mspace msp, size_t bytes); +void mspace_inspect_all(mspace msp, + void(*handler)(void *, void *, size_t, void*), + void* arg); +#endif /* MSPACES */ + +#ifdef __cplusplus +}; /* end of extern "C" */ +#endif + +#endif /* MALLOC_280_H */ diff --git a/contrib/rb3ptr/Makefile.am b/contrib/rb3ptr/Makefile.am new file mode 100644 index 0000000..fc660e7 --- /dev/null +++ b/contrib/rb3ptr/Makefile.am @@ -0,0 +1,6 @@ +# Source: http://jstimpfle.de/projects/rb3ptr/rb3ptr.html +# https://github.com/jstimpfle/rb3ptr +AM_CPPFLAGS = -I. + +noinst_LTLIBRARIES = librb3ptr.la +librb3ptr_la_SOURCES = rb3ptr.c rb3ptr.h diff --git a/contrib/rb3ptr/rb3ptr.c b/contrib/rb3ptr/rb3ptr.c new file mode 100644 index 0000000..aa72b1e --- /dev/null +++ b/contrib/rb3ptr/rb3ptr.c @@ -0,0 +1,505 @@ +/* rb3ptr -- Intrusively linked 3-pointer Red-black tree implementation */ + +/* Copyright (C) 2019, Jens Stimpfle */ + +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include +#include // offsetof() + +enum { + _RB3_DIR_BIT = 1 << 0, + _RB3_COLOR_BIT = 1 << 1, + _RB3_BLACK = 0, + _RB3_RED = _RB3_COLOR_BIT, +}; + +static inline rb3_ptr rb3_child_ptr(struct rb3_head *head, int color) +{ + return (rb3_ptr) head | color; +} + +static inline rb3_ptr rb3_parent_ptr(struct rb3_head *head, int dir) +{ + return (rb3_ptr) head | dir; +} + +static inline struct rb3_head *rb3_get_black_child(struct rb3_head *head, int dir) +{ + return (struct rb3_head *) head->child[dir]; +} + +static inline int rb3_get_color_bit(struct rb3_head *head, int dir) +{ + return head->child[dir] & _RB3_COLOR_BIT; +} + +static inline int rb3_is_red(struct rb3_head *head, int dir) +{ + return rb3_get_color_bit(head, dir) != 0; +} + +static inline void rb3_set_red(struct rb3_head *head, int dir) +{ + head->child[dir] |= _RB3_COLOR_BIT; +} + +static inline void rb3_set_black(struct rb3_head *head, int dir) +{ + head->child[dir] &= ~_RB3_COLOR_BIT; +} + +static inline void rb3_connect(struct rb3_head *head, int dir, struct rb3_head *child, int color) +{ + head->child[dir] = rb3_child_ptr(child, color); + child->parent = rb3_parent_ptr(head, dir); +} + +static inline void rb3_connect_null(struct rb3_head *head, int dir, struct rb3_head *child, int color) +{ + head->child[dir] = rb3_child_ptr(child, color); + if (child) + child->parent = rb3_parent_ptr(head, dir); +} + +struct rb3_tree *rb3_get_containing_tree(struct rb3_head *head) +{ + while (rb3_get_parent(head)) + head = rb3_get_parent(head); + return (struct rb3_tree *) ((char *) head - (offsetof(struct rb3_head, child[0]))); +} + +static struct rb3_head *rb3_get_minmax_in_subtree(struct rb3_head *head, int dir) +{ + if (!head) + return _RB3_NULL; + while (rb3_has_child(head, dir)) + head = rb3_get_child(head, dir); + return head; +} + +struct rb3_head *rb3_get_minmax(struct rb3_tree *tree, int dir) +{ + return rb3_get_minmax_in_subtree(rb3_get_root(tree), dir); +} + +struct rb3_head *rb3_get_prevnext_descendant(struct rb3_head *head, int dir) +{ + return rb3_get_minmax_in_subtree(rb3_get_child(head, dir), !dir); +} + +struct rb3_head *rb3_get_prevnext_ancestor(struct rb3_head *head, int dir) +{ + /* + * Note: the direction is "reversed" for our purposes here, since + * the bit indicates the direction from the parent to `head` + */ + while (head && rb3_get_parent_dir(head) == dir) { + head = rb3_get_parent(head); + } + if (head) { + head = rb3_get_parent(head); + if (!head || rb3_is_base(head)) + return _RB3_NULL; + return head; + } + return _RB3_NULL; +} + +struct rb3_head *rb3_get_prevnext(struct rb3_head *head, int dir) +{ + if (rb3_has_child(head, dir)) + return rb3_get_prevnext_descendant(head, dir); + else + return rb3_get_prevnext_ancestor(head, dir); +} + +void rb3_update_augment(struct rb3_head *head, rb3_augment_func *augment) +{ + while (!rb3_is_base(head)) { + augment(head); + head = rb3_get_parent(head); + } +} + +static void rb3_rebalance_after_link(struct rb3_head *head, rb3_augment_func *augment) +{ + struct rb3_head *pnt; + struct rb3_head *gpnt; + struct rb3_head *ggpnt; + int left; + int right; + int gdir; + int ggdir; + + if (!rb3_get_parent(rb3_get_parent(head))) { + rb3_set_black(rb3_get_parent(head), RB3_LEFT); + if (augment) + augment(head); + return; + } + + if (!rb3_is_red(rb3_get_parent(rb3_get_parent(head)), rb3_get_parent_dir(rb3_get_parent(head)))) { + /* parent is black */ + if (augment) + rb3_update_augment(head, augment); + return; + } + + /* + * Since parent is red parent can't be the root. + * So we have at least a grandparent node, and grand-grandparent + * is either a real node or the base head. + */ + pnt = rb3_get_parent(head); + gpnt = rb3_get_parent(pnt); + ggpnt = rb3_get_parent(gpnt); + left = rb3_get_parent_dir(head); + right = !rb3_get_parent_dir(head); + gdir = rb3_get_parent_dir(pnt); + ggdir = rb3_get_parent_dir(gpnt); + + if (rb3_is_red(gpnt, !gdir)) { + /* uncle and parent are both red */ + rb3_set_red(ggpnt, ggdir); + rb3_set_black(gpnt, RB3_LEFT); + rb3_set_black(gpnt, RB3_RIGHT); + if (augment) + rb3_update_augment(head, augment); + rb3_rebalance_after_link(gpnt, augment); + } else if (gdir == right) { + rb3_connect_null(pnt, left, rb3_get_black_child(head, right), _RB3_BLACK); + rb3_connect_null(gpnt, right, rb3_get_black_child(head, left), _RB3_BLACK); + rb3_connect(head, left, gpnt, _RB3_RED); + rb3_connect(head, right, pnt, _RB3_RED); + rb3_connect(ggpnt, ggdir, head, _RB3_BLACK); + if (augment) { + augment(pnt); + augment(gpnt); + rb3_update_augment(head, augment); + } + } else { + rb3_connect_null(gpnt, left, rb3_get_black_child(pnt, right), _RB3_BLACK); + rb3_connect(pnt, right, gpnt, _RB3_RED); + rb3_connect(ggpnt, ggdir, pnt, _RB3_BLACK); + if (augment) { + augment(gpnt); + rb3_update_augment(head, augment); + } + } +} + +static void rb3_rebalance_after_unlink(struct rb3_head *pnt, int pdir, rb3_augment_func *augment) +{ + struct rb3_head *gpnt; + struct rb3_head *sibling; + struct rb3_head *sleft; + struct rb3_head *sleftleft; + struct rb3_head *sleftright; + enum rb3_dir left; + enum rb3_dir right; + enum rb3_dir gdir; + + if (!rb3_get_parent(pnt)) + return; + + left = pdir; // define "left" as the direction from parent to deleted node + right = !pdir; + gpnt = rb3_get_parent(pnt); + gdir = rb3_get_parent_dir(pnt); + sibling = rb3_get_child(pnt, right); + sleft = rb3_get_child(sibling, left); + + if (rb3_is_red(pnt, right)) { + /* sibling is red */ + rb3_connect(pnt, right, sleft, _RB3_BLACK); + rb3_connect(sibling, left, pnt, _RB3_RED); + rb3_connect(gpnt, gdir, sibling, _RB3_BLACK); + if (augment) + augment(sleft); + rb3_rebalance_after_unlink(pnt, pdir, augment); + } else if (rb3_is_red(sibling, right)) { + /* outer child of sibling is red */ + rb3_connect_null(pnt, right, sleft, rb3_get_color_bit(sibling, left)); + rb3_connect(sibling, left, pnt, _RB3_BLACK); + rb3_connect(gpnt, gdir, sibling, rb3_get_color_bit(gpnt, gdir)); + if (augment) { + rb3_update_augment(pnt, augment); + } + rb3_set_black(sibling, right); + } else if (rb3_is_red(sibling, left)) { + /* inner child of sibling is red */ + sleftleft = rb3_get_child(sleft, left); + sleftright = rb3_get_child(sleft, right); + rb3_connect_null(pnt, right, sleftleft, _RB3_BLACK); + rb3_connect_null(sibling, left, sleftright, _RB3_BLACK); + rb3_connect(sleft, left, pnt, _RB3_BLACK); + rb3_connect(sleft, right, sibling, _RB3_BLACK); + rb3_connect(gpnt, gdir, sleft, rb3_get_color_bit(gpnt, gdir)); + if (augment) { + augment(sibling); + rb3_update_augment(pnt, augment); + } + } else if (rb3_is_red(gpnt, gdir)) { + /* parent is red */ + rb3_set_red(pnt, right); + rb3_set_black(gpnt, gdir); + if (augment) + rb3_update_augment(pnt, augment); + } else { + /* all relevant nodes are black */ + rb3_set_red(pnt, right); + if (augment) + augment(pnt); + rb3_rebalance_after_unlink(gpnt, gdir, augment); + } +} + +void rb3_link_and_rebalance_and_maybe_augment(struct rb3_head *head, struct rb3_head *parent, int dir, rb3_augment_func *augment) +{ + _RB3_ASSERT(dir == RB3_LEFT || dir == RB3_RIGHT); + _RB3_ASSERT(!rb3_has_child(parent, dir)); + + parent->child[dir] = rb3_child_ptr(head, _RB3_RED); + head->parent = rb3_parent_ptr(parent, dir); + head->child[RB3_LEFT] = rb3_child_ptr(_RB3_NULL, _RB3_BLACK); + head->child[RB3_RIGHT] = rb3_child_ptr(_RB3_NULL, _RB3_BLACK); + rb3_rebalance_after_link(head, augment); +} + +void rb3_replace_and_maybe_augment(struct rb3_head *head, struct rb3_head *newhead, rb3_augment_func *augment) +{ + struct rb3_head *left; + struct rb3_head *right; + struct rb3_head *parent; + int pdir; + int pcol; + + *newhead = *head; + + left = rb3_get_child(head, RB3_LEFT); + right = rb3_get_child(head, RB3_RIGHT); + parent = rb3_get_parent(head); + pdir = rb3_get_parent_dir(head); + pcol = rb3_get_color_bit(parent, pdir); + + if (left) + left->parent = rb3_parent_ptr(newhead, RB3_LEFT); + if (right) + right->parent = rb3_parent_ptr(newhead, RB3_RIGHT); + parent->child[pdir] = rb3_child_ptr(newhead, pcol); + + if (augment) + rb3_update_augment(newhead, augment); +} + +static void rb3_unlink_noninternal_and_rebalance_and_maybe_augment(struct rb3_head *head, rb3_augment_func *augment) +{ + struct rb3_head *pnt; + struct rb3_head *cld; + int pdir; + int dir; + + dir = rb3_get_child(head, RB3_RIGHT) ? RB3_RIGHT : RB3_LEFT; + pnt = rb3_get_parent(head); + cld = rb3_get_child(head, dir); + pdir = rb3_get_parent_dir(head); + + int mustRebalance = !rb3_is_red(pnt, pdir) && !rb3_is_red(head, dir); + + /* since we added the possibility for augmentation, + we need to remove `head` *before* the rebalancing that we do below. + (Otherwise the augmentation function would still see the to-be-deleted child). */ + rb3_connect_null(pnt, pdir, cld, _RB3_BLACK); + + if (mustRebalance) + /* To be deleted node is black (and child cannot be repainted) + * => height decreased */ + rb3_rebalance_after_unlink(pnt, pdir, augment); + else if (augment) + /* the augment wasn't done since we didn't rebalance. So we need to do it separately. + TODO: Could we restrict the augmentation done during rebalancing to just the + nodes that aren't not be augmented by a regular rb3_augment_ancestors(pnt, augment)? */ + rb3_update_augment(pnt, augment); +} + +static void rb3_unlink_internal_and_rebalance_and_maybe_augment(struct rb3_head *head, rb3_augment_func *augment) +{ + struct rb3_head *subst; + + subst = rb3_get_next_descendant(head); + rb3_unlink_noninternal_and_rebalance_and_maybe_augment(subst, augment); + rb3_replace_and_maybe_augment(head, subst, augment); +} + +void rb3_unlink_and_rebalance_and_maybe_augment(struct rb3_head *head, rb3_augment_func *augment) +{ + if (rb3_has_child(head, RB3_LEFT) && rb3_has_child(head, RB3_RIGHT)) + rb3_unlink_internal_and_rebalance_and_maybe_augment(head, augment); + else + rb3_unlink_noninternal_and_rebalance_and_maybe_augment(head, augment); +} + +struct rb3_head *rb3_find_parent_in_subtree(struct rb3_head *parent, int dir, rb3_cmp cmp, void *data, struct rb3_head **parent_out, int *dir_out) +{ + return rb3_INLINE_find(parent, dir, cmp, data, parent_out, dir_out); +} + +struct rb3_head *rb3_insert(struct rb3_tree *tree, struct rb3_head *head, rb3_cmp cmp, void *data) +{ + struct rb3_head *found; + struct rb3_head *parent; + int dir; + + parent = rb3_get_base(tree); + dir = RB3_LEFT; + found = rb3_find_parent_in_subtree(parent, dir, cmp, data, &parent, &dir); + if (found) + return found; + rb3_link_and_rebalance(head, parent, dir); + return _RB3_NULL; +} + +struct rb3_head *rb3_delete(struct rb3_tree *tree, rb3_cmp cmp, void *data) +{ + struct rb3_head *found; + + found = rb3_find(tree, cmp, data); + if (found) { + rb3_unlink_and_rebalance(found); + return found; + } + return _RB3_NULL; +} + +struct rb3_head *rb3_find_parent(struct rb3_tree *tree, rb3_cmp cmp, void *data, struct rb3_head **parent_out, int *dir_out) +{ + return rb3_find_parent_in_subtree(rb3_get_base(tree), RB3_LEFT, cmp, data, parent_out, dir_out); +} + +struct rb3_head *rb3_find(struct rb3_tree *tree, rb3_cmp cmp, void *data) +{ + return rb3_find_parent_in_subtree(rb3_get_base(tree), RB3_LEFT, cmp, data, _RB3_NULL, _RB3_NULL); +} + +void rb3_link_and_rebalance(struct rb3_head *head, struct rb3_head *parent, int dir) +{ + rb3_link_and_rebalance_and_maybe_augment(head, parent, dir, _RB3_NULL); +} + +void rb3_unlink_and_rebalance(struct rb3_head *head) +{ + rb3_unlink_and_rebalance_and_maybe_augment(head, _RB3_NULL); +} + +void rb3_replace(struct rb3_head *head, struct rb3_head *newhead) +{ + rb3_replace_and_maybe_augment(head, newhead, _RB3_NULL); +} + +void rb3_link_and_rebalance_and_augment(struct rb3_head *head, struct rb3_head *parent, int dir, rb3_augment_func *augment) +{ + rb3_link_and_rebalance_and_maybe_augment(head, parent, dir, augment); +} + +void rb3_unlink_and_rebalance_and_augment(struct rb3_head *head, rb3_augment_func *augment) +{ + rb3_unlink_and_rebalance_and_maybe_augment(head, augment); +} + +void rb3_replace_and_augment(struct rb3_head *head, struct rb3_head *newhead, rb3_augment_func *augment) +{ + rb3_replace_and_maybe_augment(head, newhead, augment); +} + + +/* DEBUG STUFF */ + +#include +static void visit_inorder_helper(struct rb3_head *head, int isred) +{ + if (!head) + return; + printf(" ("); + visit_inorder_helper(rb3_get_child(head, RB3_LEFT), rb3_is_red(head, RB3_LEFT)); + printf("%s", isred ? "R" : "B"); + visit_inorder_helper(rb3_get_child(head, RB3_RIGHT), rb3_is_red(head, RB3_RIGHT)); + printf(")"); +} + +static void visit_inorder(struct rb3_tree *tree) +{ + visit_inorder_helper(rb3_get_root(tree), 0); + printf("\n"); +} + +static int rb3_is_valid_tree_helper(struct rb3_head *head, int isred, int dir, int *depth) +{ + int i; + int depths[2] = { 1,1 }; + + *depth = 1; + + if (!head) { + if (isred) { + printf("red leaf child!\n"); + return 0; + } + return 1; + } + + if (rb3_get_parent_dir(head) != dir) { + printf("Directions messed up!\n"); + return 0; + } + + for (i = 0; i < 2; i++) { + if (isred && rb3_get_color_bit(head, i)) { + printf("two red in a row!\n"); + return 0; + } + if (!rb3_is_valid_tree_helper(rb3_get_child(head, i), + rb3_is_red(head, i), i, &depths[i])) + return 0; + } + if (depths[0] != depths[1]) { + printf("Unbalanced tree! got %d and %d\n", depths[0], depths[1]); + return 0; + } + *depth = depths[0] + !isred; + + return 1; +} + +int rb3_check_tree(struct rb3_tree *tree) +{ + int depth; + int valid; + + if (rb3_is_red(&tree->base, RB3_LEFT)) { + printf("Error! root is red.\n"); + return 0; + } + + valid = rb3_is_valid_tree_helper(rb3_get_root(tree), 0, 0, &depth); + if (!valid) + visit_inorder(tree); + return valid; +} diff --git a/contrib/rb3ptr/rb3ptr.h b/contrib/rb3ptr/rb3ptr.h new file mode 100644 index 0000000..1765113 --- /dev/null +++ b/contrib/rb3ptr/rb3ptr.h @@ -0,0 +1,474 @@ +/* rb3ptr -- Intrusively linked 3-pointer Red-black tree implementation */ + +/* Copyright (C) 2019, Jens Stimpfle */ + +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifdef RB3PTR_H_INCLUDED +#error rb3ptr.h included twice! +#endif +#define RB3PTR_H_INCLUDED + +#ifndef UINTPTR_MAX /* detect stdint.h */ +#include /* uintptr_t */ +#endif + +#ifndef _RB3_ASSERT +#ifndef assert +#include +#endif +#define _RB3_ASSERT(x) assert(x) +#endif + +#ifndef _RB3_NULL +#ifndef NULL +#include +#endif +#define _RB3_NULL NULL +#endif + + +#ifdef __cplusplus // not yet tested +extern "C" { +#endif + + +/** + * Directions for navigation in the tree. + */ +enum rb3_dir { + RB3_LEFT = 0, + RB3_RIGHT = 1, +}; + +/** + * This type is used to efficiently store a pointer (at least 4-byte aligned) + * and some more information in the unused low bits. + */ +typedef uintptr_t rb3_ptr; + +/** + * Node type for 3-pointer Red-black trees. + * Contains left, right, and parent pointers. + * The left and right pointers have additional color bits. + * The parent pointer contains a direction bit indicating the direction + * to this child. + */ +struct rb3_head { + rb3_ptr child[2]; + rb3_ptr parent; +}; + +/** + * Tree type. It's just a fake base head that is wrapped for type safety and + * future extensibility. + */ +struct rb3_tree { + struct rb3_head base; +}; + +/** + * User-provided comparison function. It is used during tree searches. + * At each visited node, the function is called with that node as first + * argument and some additional user-provided data. + * + * It should returns a value less than, equal to, or greater than, 0, + * depending on whether the node compares less than, equal to, or greater + * than, the user-provided data. + */ +typedef int rb3_cmp(struct rb3_head *head, void *data); + +/** + * User-provided augment function. Used to do recomputations when a child changed. + */ +typedef void rb3_augment_func(struct rb3_head *head /*, void *data */); + +/** + * Initialize an rb3_head. + * After initialization, rb3_is_head_linked() will return false. + */ +static inline void rb3_reset_head(struct rb3_head *head) +{ + head->child[RB3_LEFT] = 0; + head->child[RB3_RIGHT] = 0; + head->parent = 0; +} + +/** + * Initialize an rb3_tree. + */ +static inline void rb3_reset_tree(struct rb3_tree *tree) +{ + tree->base.child[RB3_LEFT] = 0; + /* ! see doc of rb3_is_base(). */ + tree->base.child[RB3_RIGHT] = 3; + tree->base.parent = 0; +} + +/** + * Get base head of tree. + * + * Warning: the base head is never embedded in a client payload structure. + * It's just a link to host the real root of the tree as its left child. + */ +static inline struct rb3_head *rb3_get_base(struct rb3_tree *tree) +{ + return &tree->base; +} + +/** + * Test if given head is base of tree. + */ +static inline int rb3_is_base(struct rb3_head *head) +{ + /* We could check for the parent pointer being null, but by having + * a special sentinel right child value instead, we can make this + * function distinguish the base from unlinked pointers as well. + * + * A side effect is that this breaks programs with trees that are not + * initialized with rb3_init(), which could be a good or a bad thing, + * I don't know. */ + return head->child[RB3_RIGHT] == 3; +} + +/** + * Check if a non-base head is linked in a (any) tree. + */ +static inline int rb3_is_head_linked(struct rb3_head *head) +{ + return head->parent != 0; +} + +/** + * Get child in given direction, or NULL if there is no such child. `dir` + * must be RB3_LEFT or RB3_RIGHT. + */ +static inline struct rb3_head *rb3_get_child(struct rb3_head *head, int dir) +{ + return (struct rb3_head *)((head->child[dir]) & ~3); +} + +/* + * Test if a (left or right) child exists. + * This is slightly more efficient than calling rb3_get_child() and comparing + * to NULL. + */ +static inline int rb3_has_child(struct rb3_head *head, int dir) +{ + return head->child[dir] != 0; +} + +/** + * Get direction from parent to child by testing the direction. + * + * Return RB3_LEFT or RB3_RIGHT, depending on whether this node is the left or + * right child of its parent node. If the given node is the root node, + * RB3_LEFT is returned. (Technically the root node is the left child of the + * base node). + * + * This is more convenient and (in theory) more efficient than getting the + * parent and testing its left and right child. + */ +static inline int rb3_get_parent_dir(struct rb3_head *head) +{ + return head->parent & 1; +} + +/** + * Get parent head, or NULL if given node is the base head. + * + * Note that normally you don't want to visit the base head but stop already + * at the root node. + */ +static inline struct rb3_head *rb3_get_parent(struct rb3_head *head) +{ + return (struct rb3_head *)(head->parent & ~3); +} + +/** + * Get topmost element of tree (or NULL if empty) + */ +static inline struct rb3_head *rb3_get_root(struct rb3_tree *tree) +{ + return rb3_get_child(&tree->base, RB3_LEFT); +} + +/** + * Check if tree is empty. + */ +static inline int rb3_is_empty(struct rb3_tree *tree) +{ + struct rb3_head *base = rb3_get_base(tree); + return !rb3_has_child(base, RB3_LEFT); +} + +/** + * Get minimum or maximum node in the tree, depending on the value of `dir` + * (RB3_LEFT or RB3_RIGHT) + * + * Time complexity: O(log n) + */ +extern struct rb3_head *rb3_get_minmax(struct rb3_tree *tree, int dir); + +/** + * Get minimum (leftmost) element, or NULL if tree is empty. + * + * Time complexity: O(log n) + */ +static inline struct rb3_head *rb3_get_min(struct rb3_tree *tree) +{ + return rb3_get_minmax(tree, RB3_LEFT); +} + +/** + * Get previous or next in-order descendant, depending on the value of `dir` + * (RB3_LEFT or RB3_RIGHT). + * + * Time complexity: O(log n) + */ +extern struct rb3_head *rb3_get_prevnext_descendant(struct rb3_head *head, int dir); + +/** + * Get previous or next in-order ancestor, depending on the value of `dir` + * (RB3_LEFT or RB3_RIGHT). + * + * Time complexity: O(log n) + */ +extern struct rb3_head *rb3_get_prevnext_ancestor(struct rb3_head *head, int dir); + +/** + * Get previous or next in-order node, depending on the value of `dir`. + * + * Time complexity: O(log n), amortized over sequential scan: O(1) + */ +extern struct rb3_head *rb3_get_prevnext(struct rb3_head *head, int dir); + +/** + * Get maximum (rightmost) element, or NULL if tree is empty + * + * Time complexity: O(log n) + */ +static inline struct rb3_head *rb3_get_max(struct rb3_tree *tree) +{ + return rb3_get_minmax(tree, RB3_RIGHT); +} + +/** + * Get previous in-order node (maximal node in the tree that sorts before the + * given element) or NULL if no such element is in the tree. + * + * Time complexity: O(log n), amortized over sequential scan: O(1) + */ +static inline struct rb3_head *rb3_get_prev(struct rb3_head *head) +{ + return rb3_get_prevnext(head, RB3_LEFT); +} + +/** + * Get next in-order node (minimal node in the tree that sorts after the given + * element) or NULL if no such element is in the tree. + * + * Time complexity: O(log n), amortized over sequential scan: O(1) + */ +static inline struct rb3_head *rb3_get_next(struct rb3_head *head) +{ + return rb3_get_prevnext(head, RB3_RIGHT); +} + +/** + * Get previous in-order descendant (maximal descendant node that sorts before + * the given element) or NULL if no such element is in the tree. + * + * Time complexity: O(log n) + */ +static inline struct rb3_head *rb3_get_prev_descendant(struct rb3_head *head) +{ + return rb3_get_prevnext_descendant(head, RB3_LEFT); +} + +/** + * Get next in-order descendant (minimal descendant node that sorts after the + * given element) or NULL if no such element is in the tree. + * + * Time complexity: O(log n) + */ +static inline struct rb3_head *rb3_get_next_descendant(struct rb3_head *head) +{ + return rb3_get_prevnext_descendant(head, RB3_RIGHT); +} + +/** + * Get previous in-order ancestor (maximal ancestor node that sorts before the + * given element) or NULL if no such element is in the tree. + * + * Time complexity: O(log n) + */ +static inline struct rb3_head *rb3_get_prev_ancestor(struct rb3_head *head) +{ + return rb3_get_prevnext_ancestor(head, RB3_LEFT); +} + +/** + * Get next in-order ancestor (minimal ancestor node that sorts after the + * given element) or NULL if no such element is in the tree. + * + * Time complexity: O(log n) + */ +static inline struct rb3_head *rb3_get_next_ancestor(struct rb3_head *head) +{ + return rb3_get_prevnext_ancestor(head, RB3_RIGHT); +} + +/** + * Find a node in `tree` using `cmp` to direct the search. At each visited + * node in the tree `cmp` is called with that node and `data` as arguments. + * If a node that compares equal is found, it is returned. Otherwise, NULL is + * returned. + * + * Time complexity: O(log n) + */ +extern struct rb3_head *rb3_find(struct rb3_tree *tree, rb3_cmp cmp, void *data); + +/** + * Find a suitable insertion point for a new node in `tree` using `cmp` and + * `data` to direct the search. At each visited node in the tree `cmp` is + * called with that node and `data` as arguments. If a node that compares + * equal is found, it is returned. Otherwise, NULL is returned and the + * insertion point is returned as parent node and child direction in + * `parent_out` and `dir_out`. + * + * Time complexity: O(log n) + */ +extern struct rb3_head *rb3_find_parent(struct rb3_tree *tree, rb3_cmp cmp, void *data, struct rb3_head **parent_out, int *dir_out); + +/** + * Link `head` into `tree` below another node in the given direction (RB3_LEFT + * or RB3_RIGHT). The new node must replace a leaf. You can use + * rb3_find_parent() to find the insertion point. + * + * `head` must not be linked into another tree when this function is called. + * + * Time complexity: O(log n) + */ +extern void rb3_link_and_rebalance(struct rb3_head *head, struct rb3_head *parent, int dir); + +/** + * Unlink `head` from its current tree. + * + * Time complexity: O(log n) + */ +extern void rb3_unlink_and_rebalance(struct rb3_head *head); + +/** + * Replace `head` with `newhead`. `head` must be linked in a tree and + * `newhead` must not be linked in a tree. + */ +extern void rb3_replace(struct rb3_head *head, struct rb3_head *newhead); + +/** + * Like rb3_link_and_rebalance(), but call an augmentation function for each + * subtree that has been changed. + */ +extern void rb3_link_and_rebalance_and_augment(struct rb3_head *head, struct rb3_head *parent, int dir, rb3_augment_func *augment); + +/** + * Like rb3_unlink_and_rebalance(), but call an augmentation function for each + * subtree that has been changed. + */ +extern void rb3_unlink_and_rebalance_and_augment(struct rb3_head *head, rb3_augment_func *augment); + +/** + * Like rb3_replace(), but call an augmentation function for each subtree that has changed. + */ +extern void rb3_replace_and_augment(struct rb3_head *head, struct rb3_head *newhead, rb3_augment_func *augment); + +/** + * Update by calling the augmentation func for `head` and all its ancestors. + */ +extern void rb3_update_augment(struct rb3_head *head, rb3_augment_func *augment); + +/** + * Find suitable insertion point for a new node in a subtree, directed by the + * given search function. The subtree is given by its parent node `parent` and + * child direction `dir`. The insertion point and its child direction are + * returned in `parent_out` and `dir_out`. + * + * If the searched node is already in the tree (the compare function returns + * 0), it is returned. In this case `parent_out` and `dir_out` are left + * untouched. Otherwise NULL is returned. + */ +extern struct rb3_head *rb3_find_parent_in_subtree(struct rb3_head *parent, int dir, rb3_cmp cmp, void *data, struct rb3_head **parent_out, int *dir_out); + +/** + * Insert `head` into `tree` using `cmp` and `data` to direct the search. At + * each visited node in the tree `cmp` is called with that node and `data` as + * arguments (in that order). If a node that compares equal is found, it is + * returned. Otherwise, `head` is inserted into the tree and NULL is + * returned. + * + * Time complexity: O(log n) + */ +extern struct rb3_head *rb3_insert(struct rb3_tree *tree, struct rb3_head *head, rb3_cmp cmp, void *data); + +/** + * Find and delete a node from `tree` using `cmp` to direct the search. At + * each visited node in the tree `cmp` is called with that node and `head` as + * arguments (in that order). If a node that compares equal is found, it is + * unlinked from the tree and returned. Otherwise, NULL is returned. + * + * Time complexity: O(log n) + */ +extern struct rb3_head *rb3_delete(struct rb3_tree *tree, rb3_cmp cmp, void *data); + +/** + * Given a node that is known to be linked in _some_ tree, find that tree. + * + * This involves a little hackery with offsetof(3) + */ +extern struct rb3_tree *rb3_get_containing_tree(struct rb3_head *head); + + +/* +XXX: is inlining the search function advantageous? +*/ +static inline struct rb3_head *rb3_INLINE_find(struct rb3_head *parent, int dir, rb3_cmp cmp, void *data, struct rb3_head **parent_out, int *dir_out) +{ + _RB3_ASSERT(parent != _RB3_NULL); + while (rb3_has_child(parent, dir)) { + parent = rb3_get_child(parent, dir); + int r = cmp(parent, data); + if (r == 0) + return parent; + dir = (r < 0) ? RB3_RIGHT : RB3_LEFT; + } + if (parent_out) + *parent_out = parent; + if (dir_out) + *dir_out = dir; + return _RB3_NULL; +} + +/**************** DEBUG STUFF *******************/ +int rb3_check_tree(struct rb3_tree *tree); +/************************************************/ + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/debian/copyright b/debian/copyright index 4b24acc..36d5bf9 100644 --- a/debian/copyright +++ b/debian/copyright @@ -3,27 +3,33 @@ Upstream-Name: SciTECO Upstream-Contact: robin.haberkorn@googlemail.com Source: https://sourceforge.net/projects/sciteco -Files: debian/* -Copyright: Copyright 2013-2017 Robin Haberkorn +Files: debian/* src/* */Makefile.am lib/* +Copyright: Copyright 2013-2021 Robin Haberkorn License: GPL-3+ /usr/share/common-licenses/GPL-3 -Files: sciteco/* -Copyright: Copyright 2012-2017 Robin Haberkorn -License: GPL-3+ - /usr/share/common-licenses/GPL-3 +Files: contrib/dlmalloc/*.c contrib/dlmalloc/*.h +License: CC0 -Files: sciteco/gtkflowbox.c sciteco/gtkflowbox.h -Copyright: - Copyright 2007-2010 Openismus GmbH - Copyright 2013 Red Hat, Inc. -License: LGPL-2+ - /usr/share/common-licenses/LGPL-2 - -Files: compat/bsd/* -Copyright: Copyright 1991, 1993 The Regents of the University of California -License: BSD - /usr/share/common-licenses/BSD +Files: contrib/rb3ptr/*.c contrib/rb3ptr/*.h +Copyright: Copyright 2019 Jens Stimpfle +License: + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Files: scintilla/* Copyright: Copyright 1998-2003 Neil Hodgson @@ -50,11 +56,11 @@ License: OR PERFORMANCE OF THIS SOFTWARE. Files: scintilla/scinterm/* -Copyright: Copyright 2012-2015 Mitchell +Copyright: Copyright 2012-2016 Mitchell License: The MIT License . - Copyright (c) 2012-2015 Mitchell + Copyright (c) 2012-2016 Mitchell . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index c348346..d4c466f 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -177,7 +177,7 @@ SHORT_NAMES = NO # description.) # The default value is: NO. -JAVADOC_AUTOBRIEF = NO +JAVADOC_AUTOBRIEF = YES # If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first # line (until the first dot) of a Qt-style comment as the brief description. If @@ -228,7 +228,7 @@ TAB_SIZE = 8 # "Side Effects:". You can put \n's in the value part of an alias to insert # newlines. -ALIASES = +ALIASES = "fixme=@bug" # This tag can be used to specify a number of word-keyword mappings (TCL only). # A mapping has the form "name=value". For example adding "class=itcl::class" @@ -242,7 +242,7 @@ TCL_SUBST = # members will be omitted, etc. # The default value is: NO. -OPTIMIZE_OUTPUT_FOR_C = NO +OPTIMIZE_OUTPUT_FOR_C = YES # Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or # Python sources only. Doxygen will then generate output that is more tailored @@ -398,7 +398,7 @@ INLINE_SIMPLE_STRUCTS = NO # types are typedef'ed and only the typedef is referenced, never the tag name. # The default value is: NO. -TYPEDEF_HIDES_STRUCT = NO +TYPEDEF_HIDES_STRUCT = YES # The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This # cache is used to resolve symbols given their name and scope. Since this can be @@ -560,7 +560,7 @@ INLINE_INFO = YES # name. If set to NO, the members will appear in declaration order. # The default value is: YES. -SORT_MEMBER_DOCS = YES +SORT_MEMBER_DOCS = NO # If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief # descriptions of file, namespace and class members alphabetically by member @@ -670,7 +670,7 @@ SHOW_FILES = YES # Folder Tree View (if specified). # The default value is: YES. -SHOW_NAMESPACES = YES +SHOW_NAMESPACES = NO # The FILE_VERSION_FILTER tag can be used to specify a program or script that # doxygen should invoke to get the current version for each file (typically from @@ -806,9 +806,7 @@ INPUT_ENCODING = UTF-8 # *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, # *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf. -FILE_PATTERNS = *.cpp \ - *.c \ - *.h +FILE_PATTERNS = *.c *.h # The RECURSIVE tag can be used to specify whether or not subdirectories should # be searched for input files as well. @@ -839,8 +837,7 @@ EXCLUDE_SYMLINKS = NO # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories for example use the pattern */test/* -EXCLUDE_PATTERNS = "*/symbols-*.cpp" \ - "*/gtkflowbox.[ch]" +EXCLUDE_PATTERNS = "*/symbols-*.c" # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names # (namespaces, classes, functions, etc.) that should be excluded from the @@ -1991,7 +1988,7 @@ ENABLE_PREPROCESSING = YES # The default value is: NO. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -MACRO_EXPANSION = NO +MACRO_EXPANSION = YES # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then # the macro expansion is limited to the macros specified with the PREDEFINED and @@ -2031,7 +2028,7 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = +PREDEFINED = "__attribute__(X)=" # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The @@ -2050,7 +2047,7 @@ EXPAND_AS_DEFINED = # The default value is: YES. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -SKIP_FUNCTION_MACROS = YES +SKIP_FUNCTION_MACROS = NO #--------------------------------------------------------------------------- # Configuration options related to external references diff --git a/doc/Makefile.am b/doc/Makefile.am index ad60d7c..1afe7ee 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -3,15 +3,15 @@ include $(top_srcdir)/bootstrap.am # tedoc is a code documentation tool for SciTECO commands -# and macros, extracting comments from SciTECO and C/C++ +# and macros, extracting comments from SciTECO and C # code. # It generates Troff manpage markup and acts as a Troff # preprocessor to manpage templates. dist_bin_SCRIPTS = tedoc.tes -%.in : %.template tedoc.tes @top_srcdir@/src/*.cpp +%.in : %.template tedoc.tes @top_srcdir@/src/*.c $(SCITECO_FULL) -m -- @srcdir@/tedoc.tes \ - -C $@ $< @top_srcdir@/src/*.cpp + -C $@ $< @top_srcdir@/src/*.c # grosciteco is a troff postprocessor similar to grotty # which can be used to produce SciTECO-friendly output @@ -28,12 +28,13 @@ dist_bin_SCRIPTS += grosciteco.tes dist_pkgdata_DATA = sciteco.tmac # Fix up the hash-bang line of installed SciTECO scripts upon -# installation to refer to the installed sciteco binary, -# or rather to the sciteco-wrapper script. +# installation to refer to the installed sciteco binary. # This takes --program-prefix into account. +# +# FIXME: This will patch the hash-bang line repeatedly. install-exec-hook: $(SCITECO_FULL) -e "@EB'$(DESTDIR)$(bindir)/*.tes' 1U* \ - EJ-1<%*^[ 0,L.@FR'#!^EM^Xsciteco'#!$(SCITECO_WRAPPER_INSTALLED)'> \ + EJ-1<%*^[ 0,L.@FR'#!^EM^Xsciteco'#!$(SCITECO_INSTALLED)'> \ :EX" womendir = $(scitecolibdir)/women @@ -97,9 +98,7 @@ SUFFIXES += .htbl .html # # Doxygen processing (do not install or distribute) # -if BUILD_DEVDOC -noinst_DATA = doxygen/ -endif +devdoc : doxygen/ .PHONY: doxygen/ doxygen/ : Doxyfile diff --git a/doc/sciteco.1.in b/doc/sciteco.1.in index ead8337..5441621 100644 --- a/doc/sciteco.1.in +++ b/doc/sciteco.1.in @@ -69,19 +69,8 @@ interpreter before the script's file name, so all required \*(ST options must be mangled into a single argument with their single-letter names. Passing option-like arguments (beginning with a dash) to scripts may cause problems because \*(ST might try to interpret these options. -Beginning with Glib 2.44, \*(ST thus stops parsing at the first non-option +\*(ST thus stops parsing at the first non-option argument (which will always be the munged file name in a script invocation). -For binaries linked against older versions of Glib, \*(ST works around this -issue by providing a wrapper script that can be used in place of the main -executable. -A portable Hash-Bang line should thus look like: -.RS -.EX -.SCITECO_TT -#!@libexecdir@/sciteco-wrapper -m -.SCITECO_TT_END -.EE -.RE . .LP .SCITECO_TOPIC argv arguments @@ -144,7 +133,7 @@ The interactive mode enables character rub-out and thus undoing of command side-effects. Therefore code runs significantly slower in interactive mode and all algorithms have non-constant memory requirements -as they will constantly accumulate \(lqundo tokens\(rqP. +as they will constantly accumulate \(lqundo tokens\(rq. Batch mode does not have these restrictions. .IP \(bu A few commands that modify the command line are only available @@ -175,7 +164,7 @@ option. . .IP "\fB-h\fR, \fB--help\fR" .SCITECO_TOPIC "-h" "--help" -Display a short help text on the console. +Display a short help text on the console. .IP "\fB-e\fR, \fB--eval\fR \fImacro" .SCITECO_TOPIC "-e" "--eval" Evaluate (execute) @@ -253,11 +242,11 @@ Initialization of this variable ensures that the \(lq$HOME\(rq Q-Register is available even on Windows and the home directory can always be re-configured. .TP -.SCITECO_TOPIC "$SHELL" "SHELL" "$COMSPEC" "COMSPEC" -.BR SHELL " or " COMSPEC +.SCITECO_TOPIC "$SHELL" "SHELL" "$ComSpec" "ComSpec" +.BR SHELL " or " ComSpec Path of the command interpreter used by \fBEG\fP and \fBEC\fP commands if UNIX98 shell emulation is \fIdisabled\fP. -\fBSHELL\fP is used on UNIX-like systems, while \fBCOMSPEC\fP +\fBSHELL\fP is used on UNIX-like systems, while \fBComSpec\fP is used on DOS-like systems (like Windows). Both variables are usually already set in the process environment but are initialized to \(lq/bin/sh\(rq or \(lqcmd.exe\(rq diff --git a/doc/sciteco.7.template b/doc/sciteco.7.template index 47100e9..dbd9392 100644 --- a/doc/sciteco.7.template +++ b/doc/sciteco.7.template @@ -734,8 +734,11 @@ This is useful for writing cross-platform \*(ST macros (see .LP Note that completions take place after string building and tilde-expansion is also performed by file name completions, -so for instance \(lq~/foo\(rq will complete a file -in the user's home directory. +so for instance both \(lq~/foo\(rq and \(lq^EQ[$HOME]/foo\(rq +will complete a file in the user's home directory. +Auto-completion also takes care of \fIquoting\fP string termination +and string building characters, which might be relevant especially +when autocompleting Q-Register names. . . .SH USER INTERFACE @@ -1249,8 +1252,10 @@ A command identifier (and Q-Register specification) may be followed by one or more string arguments. String arguments are terminated by Escape characters (27) by default, but this may be changed using modifiers. -All string arguments may contain special string building characters -for instance to embed other strings. +If enabled, string arguments may contain special string building characters +for instance to embed other strings or to quote the argument terminator. +The detection of the end of a string is aware of string building characters, +ie. string building constructs may contain the current terminator. String building may be enabled or disabled by default for a command. In interactive mode the command is often executed as soon as it has been completely specified and updates to the string arguments @@ -1302,6 +1307,10 @@ string arguments, as in: } .SCITECO_TT_END .EE +The termination character can be \fIquoted\fP if you want to handle +it like any regular character. +For instance, you could write \(lqS^Q\fB$$\fP\(rq to search for the +escape character itself. . .SH Q-REGISTERS .SCITECO_TOPIC Q-Register @@ -1605,8 +1614,8 @@ thus refers to the corresponding control code: .TQ .BI ^R c Escape character \fIc\fP. -The character is not handled as a string building character, -so for instance \(lq^Q^Q\(rq translates to \(lq^Q\(rq. +The character is not handled as a string building or string termination +character, so for instance \(lq^Q^Q\(rq translates to \(lq^Q\(rq. .TP .SCITECO_TOPIC ^V^V ^Vc lower .B ^V^V diff --git a/m4/m4_ax_check_enable_debug.m4 b/m4/m4_ax_check_enable_debug.m4 new file mode 100644 index 0000000..7bc7710 --- /dev/null +++ b/m4/m4_ax_check_enable_debug.m4 @@ -0,0 +1,124 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_check_enable_debug.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CHECK_ENABLE_DEBUG([enable by default=yes/info/profile/no], [ENABLE DEBUG VARIABLES ...], [DISABLE DEBUG VARIABLES NDEBUG ...], [IS-RELEASE]) +# +# DESCRIPTION +# +# Check for the presence of an --enable-debug option to configure, with +# the specified default value used when the option is not present. Return +# the value in the variable $ax_enable_debug. +# +# Specifying 'yes' adds '-g -O0' to the compilation flags for all +# languages. Specifying 'info' adds '-g' to the compilation flags. +# Specifying 'profile' adds '-g -pg' to the compilation flags and '-pg' to +# the linking flags. Otherwise, nothing is added. +# +# Define the variables listed in the second argument if debug is enabled, +# defaulting to no variables. Defines the variables listed in the third +# argument if debug is disabled, defaulting to NDEBUG. All lists of +# variables should be space-separated. +# +# If debug is not enabled, ensure AC_PROG_* will not add debugging flags. +# Should be invoked prior to any AC_PROG_* compiler checks. +# +# IS-RELEASE can be used to change the default to 'no' when making a +# release. Set IS-RELEASE to 'yes' or 'no' as appropriate. By default, it +# uses the value of $ax_is_release, so if you are using the AX_IS_RELEASE +# macro, there is no need to pass this parameter. +# +# AX_IS_RELEASE([git-directory]) +# AX_CHECK_ENABLE_DEBUG() +# +# LICENSE +# +# Copyright (c) 2011 Rhys Ulerich +# Copyright (c) 2014, 2015 Philip Withnall +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. + +#serial 9 + +AC_DEFUN([AX_CHECK_ENABLE_DEBUG],[ + AC_BEFORE([$0],[AC_PROG_CC])dnl + AC_BEFORE([$0],[AC_PROG_CXX])dnl + AC_BEFORE([$0],[AC_PROG_F77])dnl + AC_BEFORE([$0],[AC_PROG_FC])dnl + + AC_MSG_CHECKING(whether to enable debugging) + + ax_enable_debug_default=m4_tolower(m4_normalize(ifelse([$1],,[no],[$1]))) + ax_enable_debug_is_release=m4_tolower(m4_normalize(ifelse([$4],, + [$ax_is_release], + [$4]))) + + # If this is a release, override the default. + AS_IF([test "$ax_enable_debug_is_release" = "yes"], + [ax_enable_debug_default="no"]) + + m4_define(ax_enable_debug_vars,[m4_normalize(ifelse([$2],,,[$2]))]) + m4_define(ax_disable_debug_vars,[m4_normalize(ifelse([$3],,[NDEBUG],[$3]))]) + + AC_ARG_ENABLE(debug, + [AS_HELP_STRING([--enable-debug=]@<:@yes/info/profile/no@:>@,[compile with debugging])], + [],enable_debug=$ax_enable_debug_default) + + # empty mean debug yes + AS_IF([test "x$enable_debug" = "x"], + [enable_debug="yes"]) + + # case of debug + AS_CASE([$enable_debug], + [yes],[ + AC_MSG_RESULT(yes) + CFLAGS="${CFLAGS} -g -O0" + CXXFLAGS="${CXXFLAGS} -g -O0" + FFLAGS="${FFLAGS} -g -O0" + FCFLAGS="${FCFLAGS} -g -O0" + OBJCFLAGS="${OBJCFLAGS} -g -O0" + ], + [info],[ + AC_MSG_RESULT(info) + CFLAGS="${CFLAGS} -g" + CXXFLAGS="${CXXFLAGS} -g" + FFLAGS="${FFLAGS} -g" + FCFLAGS="${FCFLAGS} -g" + OBJCFLAGS="${OBJCFLAGS} -g" + ], + [profile],[ + AC_MSG_RESULT(profile) + CFLAGS="${CFLAGS} -g -pg" + CXXFLAGS="${CXXFLAGS} -g -pg" + FFLAGS="${FFLAGS} -g -pg" + FCFLAGS="${FCFLAGS} -g -pg" + OBJCFLAGS="${OBJCFLAGS} -g -pg" + LDFLAGS="${LDFLAGS} -pg" + ], + [ + AC_MSG_RESULT(no) + dnl Ensure AC_PROG_CC/CXX/F77/FC/OBJC will not enable debug flags + dnl by setting any unset environment flag variables + AS_IF([test "x${CFLAGS+set}" != "xset"], + [CFLAGS=""]) + AS_IF([test "x${CXXFLAGS+set}" != "xset"], + [CXXFLAGS=""]) + AS_IF([test "x${FFLAGS+set}" != "xset"], + [FFLAGS=""]) + AS_IF([test "x${FCFLAGS+set}" != "xset"], + [FCFLAGS=""]) + AS_IF([test "x${OBJCFLAGS+set}" != "xset"], + [OBJCFLAGS=""]) + ]) + + dnl Define various variables if debugging is disabled. + dnl assert.h is a NOP if NDEBUG is defined, so define it by default. + AS_IF([test "x$enable_debug" = "xyes"], + [m4_map_args_w(ax_enable_debug_vars, [AC_DEFINE(], [,[1],[Define if debugging is enabled])])], + [m4_map_args_w(ax_disable_debug_vars, [AC_DEFINE(], [,[1],[Define if debugging is disabled])])]) + ax_enable_debug=$enable_debug +]) diff --git a/src/Makefile.am b/src/Makefile.am index 3589fdf..a2990d8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -11,11 +11,17 @@ endif include $(top_srcdir)/bootstrap.am include $(top_srcdir)/scintilla.am -AM_CXXFLAGS = -Wall -Wno-char-subscripts +# FIXME: Common flags should be in configure.ac +AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value +AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr + +# NOTE: This may be necessary to ensure that malloc() overriding +# works. It may prevent elimination of unused functions, though. +AM_LDFLAGS = -rdynamic if STATIC_EXECUTABLES # AM_LDFLAGS are libtool flags, NOT compiler/linker flags -AM_LDFLAGS = -all-static +AM_LDFLAGS += -all-static endif BUILT_SOURCES = @@ -26,43 +32,55 @@ dist_noinst_SCRIPTS = symbols-extract.tes EXTRA_DIST = sciteco.html noinst_LTLIBRARIES = libsciteco-base.la -libsciteco_base_la_SOURCES = main.cpp sciteco.h \ - memory.cpp memory.h \ - string-utils.cpp string-utils.h \ - error.cpp error.h \ - cmdline.cpp cmdline.h \ - undo.cpp undo.h \ - expressions.cpp expressions.h \ - document.cpp document.h \ - eol.cpp eol.h \ - ioview.cpp ioview.h \ - qregisters.cpp qregisters.h \ - ring.cpp ring.h \ - parser.cpp parser.h \ - search.cpp search.h \ - spawn.cpp spawn.h \ - glob.cpp glob.h \ - goto.cpp goto.h \ - help.cpp help.h \ - rbtree.cpp rbtree.h \ - symbols.cpp symbols.h \ - interface.cpp interface.h +libsciteco_base_la_SOURCES = main.c sciteco.h list.h \ + memory.c memory.h \ + string-utils.c string-utils.h \ + file-utils.c file-utils.h \ + error.c error.h \ + cmdline.c cmdline.h \ + undo.c undo.h \ + expressions.c expressions.h \ + doc.c doc.h \ + eol.c eol.h \ + qreg.c qreg.h \ + qreg-commands.c qreg-commands.h \ + ring.c ring.h \ + parser.c parser.h \ + core-commands.c core-commands.h \ + search.c search.h \ + spawn.c spawn.h \ + glob.c glob.h \ + goto.c goto.h \ + goto-commands.c goto-commands.h \ + help.c help.h \ + rb3str.c rb3str.h \ + scintilla.c scintilla.h \ + view.c view.h \ + interface.c interface.h # NOTE: We cannot link in Scintilla (static library) into # a libtool convenience library -libsciteco_base_la_LIBADD = $(LIBSCITECO_INTERFACE) +libsciteco_base_la_LIBADD = $(LIBSCITECO_INTERFACE) \ + $(top_builddir)/contrib/dlmalloc/libdlmalloc.la \ + $(top_builddir)/contrib/rb3ptr/librb3ptr.la if BOOTSTRAP noinst_PROGRAMS = sciteco-minimal -symbols-scintilla.cpp symbols-scilexer.cpp : sciteco-minimal$(EXEEXT) +sciteco_minimal_SOURCES = +symbols-scintilla.c symbols-scilexer.c : sciteco-minimal$(EXEEXT) endif -sciteco_minimal_SOURCES = symbols-minimal.cpp sciteco_minimal_LDADD = libsciteco-base.la \ @SCINTILLA_PATH@/bin/scintilla.a +# Scintilla is unfortunately still written in C++, so we must force +# Automake to use the C++ linker when linking the binaries. +# The following hack is actually advocated in the Automake manual. +nodist_EXTRA_sciteco_minimal_SOURCES = fuck-this-shit.cpp bin_PROGRAMS = sciteco sciteco_SOURCES = -nodist_sciteco_SOURCES = symbols-scintilla.cpp symbols-scilexer.cpp +nodist_sciteco_SOURCES = symbols-scintilla.c symbols-scilexer.c sciteco_LDADD = $(sciteco_minimal_LDADD) +# see above +nodist_EXTRA_sciteco_SOURCES = fuck-this-shit.cpp # For MinGW: Compile in resource (contains the icon) if WIN32 @@ -72,28 +90,14 @@ sciteco_SOURCES += sciteco.rc endif CLEANFILES = $(BUILT_SOURCES) \ - symbols-scintilla.cpp symbols-scilexer.cpp + symbols-scintilla.c symbols-scilexer.c -symbols-scintilla.cpp : @SCINTILLA_PATH@/include/Scintilla.h \ - symbols-extract.tes +symbols-scintilla.c : @SCINTILLA_PATH@/include/Scintilla.h \ + symbols-extract.tes $(SCITECO_MINIMAL) -m -- @srcdir@/symbols-extract.tes \ - -p "SCI_" -n scintilla $@ $< + -p "SCI_" -n teco_symbol_list_scintilla $@ $< -symbols-scilexer.cpp : @SCINTILLA_PATH@/include/SciLexer.h \ - symbols-extract.tes +symbols-scilexer.c : @SCINTILLA_PATH@/include/SciLexer.h \ + symbols-extract.tes $(SCITECO_MINIMAL) -m -- @srcdir@/symbols-extract.tes \ - -p "SCLEX_,SCE_" -n scilexer $@ $< - -# This installs a wrapper script to libexecdir to be used as -# the SciTECO interpreter in Hash-Bang lines. -# It makes sure that option parsing is disabled for all -# script arguments which is necessary for builds against Glib < 2.44. -# NOTE: When we raise the Glib requirement to 2.44, the sciteco-wrapper -# workaround can be removed completely. -libexec_SCRIPTS = sciteco-wrapper -CLEANFILES += $(libexec_SCRIPTS) - -.PHONY: sciteco-wrapper -sciteco-wrapper: - printf '#!/bin/sh\nOPT=$$1\nshift\nexec %s "$$OPT" -- $$@' \ - "$(SCITECO_INSTALLED)" >$@ + -p "SCLEX_,SCE_" -n teco_symbol_list_scilexer $@ $< diff --git a/src/cmdline.c b/src/cmdline.c new file mode 100644 index 0000000..85cbbdd --- /dev/null +++ b/src/cmdline.c @@ -0,0 +1,1058 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#ifdef HAVE_MALLOC_H +#include +#endif +#ifdef HAVE_MALLOC_NP_H +#include +#endif + +#include +#include +#include + +#include "sciteco.h" +#include "string-utils.h" +#include "file-utils.h" +#include "interface.h" +#include "view.h" +#include "expressions.h" +#include "parser.h" +#include "core-commands.h" +#include "qreg-commands.h" +#include "qreg.h" +#include "ring.h" +#include "goto.h" +#include "help.h" +#include "undo.h" +#include "scintilla.h" +#include "spawn.h" +#include "eol.h" +#include "error.h" +#include "qreg.h" +#include "cmdline.h" + +#if defined(HAVE_MALLOC_TRIM) && !defined(HAVE_DECL_MALLOC_TRIM) +int malloc_trim(size_t pad); +#endif + +#define TECO_DEFAULT_BREAK_CHARS " \t\v\r\n\f<>,;@" + +teco_cmdline_t teco_cmdline = {}; + +/* + * FIXME: Should this be here? + * Should perhaps rather be in teco_machine_main_t or teco_cmdline_t. + */ +gboolean teco_quit_requested = FALSE; + +/** Last terminated command line */ +static teco_string_t teco_last_cmdline = {NULL, 0}; + +/** + * Insert string into command line and execute + * it immediately. + * It already handles command line replacement (TECO_ERROR_CMDLINE). + * + * @param data String to insert. + * NULL inserts a character from the previously + * rubbed out command line (rubin). + * @param len Length of string to insert. + * @param error A GError. + * @return FALSE to throw a GError + */ +/* + * FIXME: Passing data == NULL to perform a rubin is inelegant. + * Better make teco_cmdline_rubin() a proper function. + * FIXME: The inner loop should be factored out. + */ +gboolean +teco_cmdline_insert(const gchar *data, gsize len, GError **error) +{ + const teco_string_t src = {(gchar *)data, len}; + teco_string_t old_cmdline = {NULL, 0}; + guint repl_pc = 0; + + teco_cmdline.machine.macro_pc = teco_cmdline.pc = teco_cmdline.effective_len; + + if (!data) { + if (teco_cmdline.effective_len < teco_cmdline.str.len) + teco_cmdline.effective_len++; + } else { + if (!teco_string_cmp(&src, teco_cmdline.str.data + teco_cmdline.effective_len, + teco_cmdline.str.len - teco_cmdline.effective_len)) { + teco_cmdline.effective_len += len; + } else { + if (teco_cmdline.effective_len < teco_cmdline.str.len) + /* automatically disable immediate editing modifier */ + teco_cmdline.modifier_enabled = FALSE; + + teco_cmdline.str.len = teco_cmdline.effective_len; + teco_string_append(&teco_cmdline.str, data, len); + teco_cmdline.effective_len = teco_cmdline.str.len; + } + } + + /* + * Parse/execute characters, one at a time so + * undo tokens get emitted for the corresponding characters. + */ + while (teco_cmdline.pc < teco_cmdline.effective_len) { + g_autoptr(GError) tmp_error = NULL; + + if (!teco_machine_main_step(&teco_cmdline.machine, teco_cmdline.str.data, + teco_cmdline.pc+1, &tmp_error)) { + if (g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_CMDLINE)) { + /* + * Result of command line replacement (}): + * Exchange command lines, avoiding deep copying + */ + teco_qreg_t *cmdline_reg = teco_qreg_table_find(&teco_qreg_table_globals, "\e", 1); + teco_string_t new_cmdline; + + if (!cmdline_reg->vtable->get_string(cmdline_reg, &new_cmdline.data, &new_cmdline.len, error)) + return FALSE; + + /* + * Search for first differing character in old and + * new command line. This avoids unnecessary rubouts + * and insertions when the command line is updated. + */ + teco_cmdline.pc = teco_string_diff(&teco_cmdline.str, new_cmdline.data, new_cmdline.len); + + teco_undo_pop(teco_cmdline.pc); + + g_assert(old_cmdline.len == 0); + old_cmdline = teco_cmdline.str; + teco_cmdline.str = new_cmdline; + teco_cmdline.effective_len = new_cmdline.len; + teco_cmdline.machine.macro_pc = repl_pc = teco_cmdline.pc; + + continue; + } + + if (tmp_error->domain != TECO_ERROR || tmp_error->code < TECO_ERROR_CMDLINE) { + teco_error_add_frame_toplevel(); + teco_error_display_short(tmp_error); + + if (old_cmdline.len > 0) { + /* + * Error during command-line replacement. + * Replay previous command-line. + * This avoids deep copying. + */ + teco_undo_pop(repl_pc); + + teco_string_clear(&teco_cmdline.str); + teco_cmdline.str = old_cmdline; + teco_cmdline.machine.macro_pc = teco_cmdline.pc = repl_pc; + + /* rubout cmdline replacement command */ + teco_cmdline.effective_len--; + continue; + } + } + + /* error is handled in teco_cmdline_keypress_c() */ + g_propagate_error(error, g_steal_pointer(&tmp_error)); + return FALSE; + } + + teco_cmdline.pc++; + } + + return TRUE; +} + +gboolean +teco_cmdline_keypress_c(gchar key, GError **error) +{ + teco_machine_t *machine = &teco_cmdline.machine.parent; + g_autoptr(GError) tmp_error = NULL; + + /* + * Cleanup messages,etc... + */ + teco_interface_msg_clear(); + + /* + * Process immediate editing commands, inserting + * characters as necessary into the command line. + */ + if (!machine->current->process_edit_cmd_cb(machine, NULL, key, &tmp_error)) { + if (g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN)) { + /* + * Return from top-level macro, results + * in command line termination. + * The return "arguments" are currently + * ignored. + */ + g_assert(machine->current == &teco_state_start); + + teco_interface_popup_clear(); + + if (teco_quit_requested) { + /* cought by user interface */ + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, ""); + return FALSE; + } + + teco_undo_clear(); + /* also empties all Scintilla undo buffers */ + teco_ring_set_scintilla_undo(TRUE); + teco_view_set_scintilla_undo(teco_qreg_view, TRUE); + /* + * FIXME: Reset main machine? + */ + teco_goto_table_clear(&teco_cmdline.machine.goto_table); + teco_expressions_clear(); + g_array_remove_range(teco_loop_stack, 0, teco_loop_stack->len); + + teco_string_clear(&teco_last_cmdline); + teco_last_cmdline = teco_cmdline.str; + memset(&teco_cmdline.str, 0, sizeof(teco_cmdline.str)); + teco_cmdline.effective_len = 0; + } else { + /* + * NOTE: Error message already displayed in + * teco_cmdline_insert(). + * + * Undo tokens may have been emitted + * (or had to be) before the exception + * is thrown. They must be executed so + * as if the character had never been + * inserted. + */ + teco_undo_pop(teco_cmdline.pc); + teco_cmdline.effective_len = teco_cmdline.pc; + /* program counter could be messed up */ + teco_cmdline.machine.macro_pc = teco_cmdline.effective_len; + } + +#ifdef HAVE_MALLOC_TRIM + /* + * Undo stacks can grow very large - sometimes large enough to + * make the system swap and become unresponsive. + * This shrinks the program break after lots of memory has + * been freed, reducing the virtual memory size and aiding + * in recovering from swapping issues. + * + * This is particularily important with some memory limiting backends + * after hitting the memory limit* as otherwise the program's resident + * size won't shrink and it would be impossible to recover. + */ + if (g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN) || + g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_MEMLIMIT)) + malloc_trim(0); +#endif + } + + /* + * Echo command line + */ + teco_interface_cmdline_update(&teco_cmdline); + return TRUE; +} + +gboolean +teco_cmdline_fnmacro(const gchar *name, GError **error) +{ + /* + * NOTE: It should be safe to allocate on the stack since + * there are only a limited number of possible function key macros. + */ + gchar macro_name[1 + strlen(name)]; + macro_name[0] = TECO_CTL_KEY('F'); + memcpy(macro_name+1, name, sizeof(macro_name)-1); + + teco_qreg_t *macro_reg; + + if (teco_ed & TECO_ED_FNKEYS && + (macro_reg = teco_qreg_table_find(&teco_qreg_table_globals, macro_name, sizeof(macro_name)))) { + teco_int_t macro_mask; + if (!macro_reg->vtable->get_integer(macro_reg, ¯o_mask, error)) + return FALSE; + + if (macro_mask & teco_cmdline.machine.parent.current->fnmacro_mask) + return TRUE; + + g_auto(teco_string_t) macro_str = {NULL, 0}; + return macro_reg->vtable->get_string(macro_reg, ¯o_str.data, ¯o_str.len, error) && + teco_cmdline_keypress(macro_str.data, macro_str.len, error); + } + + /* + * Most function key macros have no default action, + * except "CLOSE" which quits the application + * (this may loose unsaved data but is better than + * not doing anything if the user closes the window). + * NOTE: Doing the check here is less efficient than + * doing it in the UI implementations, but defines + * the default actions centrally. + * Also, fnmacros are only handled after key presses. + */ + if (!strcmp(name, "CLOSE")) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, ""); + return FALSE; + } + + return TRUE; +} + +#ifndef NDEBUG +static void __attribute__((destructor)) +teco_cmdline_cleanup(void) +{ + teco_machine_main_clear(&teco_cmdline.machine); + teco_string_clear(&teco_cmdline.str); + teco_string_clear(&teco_last_cmdline); +} +#endif + +/* + * Commandline key processing. + * + * These are all the implementations of teco_state_process_edit_cmd_cb_t. + * It makes sense to use state callbacks for key processing, as it is + * largely state-dependant; but it defines interactive-mode-only + * behaviour which can be kept isolated from the rest of the states' + * implementation. + */ + +gboolean +teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + switch (key) { + case '\n': /* insert EOL sequence */ + teco_interface_popup_clear(); + + if (teco_ed & TECO_ED_AUTOEOL) { + if (!teco_cmdline_insert("\n", 1, error)) + return FALSE; + } else { + const gchar *eol = teco_eol_get_seq(teco_interface_ssm(SCI_GETEOLMODE, 0, 0)); + if (!teco_cmdline_insert(eol, strlen(eol), error)) + return FALSE; + } + return TRUE; + + case TECO_CTL_KEY('G'): /* toggle immediate editing modifier */ + teco_interface_popup_clear(); + + teco_cmdline.modifier_enabled = !teco_cmdline.modifier_enabled; + teco_interface_msg(TECO_MSG_INFO, + "Immediate editing modifier is now %s.", + teco_cmdline.modifier_enabled ? "enabled" : "disabled"); + return TRUE; + + case TECO_CTL_KEY('H'): /* rubout/reinsert character */ + teco_interface_popup_clear(); + + if (teco_cmdline.modifier_enabled) { + /* re-insert character */ + if (!teco_cmdline_rubin(error)) + return FALSE; + } else { + /* rubout character */ + teco_cmdline_rubout(); + } + return TRUE; + + case TECO_CTL_KEY('W'): /* rubout/reinsert command */ + teco_interface_popup_clear(); + + if (teco_cmdline.modifier_enabled) { + /* reinsert command */ + do { + if (!teco_cmdline_rubin(error)) + return FALSE; + } while (!ctx->current->is_start && + teco_cmdline.effective_len < teco_cmdline.str.len); + } else { + /* rubout command */ + do + teco_cmdline_rubout(); + while (!ctx->current->is_start); + } + return TRUE; + +#ifdef SIGTSTP + case TECO_CTL_KEY('Z'): + /* + * does not raise signal if handling of + * special characters temporarily disabled in terminal + * (Curses), or command-line is detached from + * terminal (GTK+). + * This does NOT change the state of the popup window. + */ + raise(SIGTSTP); + return TRUE; +#endif + } + + teco_interface_popup_clear(); + return teco_cmdline_insert(&key, sizeof(key), error); +} + +gboolean +teco_state_caseinsensitive_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + if (teco_ed & TECO_ED_AUTOCASEFOLD) + /* will not modify non-letter keys */ + key = g_ascii_islower(key) ? g_ascii_toupper(key) + : g_ascii_tolower(key); + + return teco_state_process_edit_cmd(ctx, parent_ctx, key, error); +} + +/* + * NOTE: The wordchars are null-terminated, so the null byte + * is always considered to be a non-wordchar. + */ +static inline gboolean +teco_is_wordchar(const gchar *wordchars, gchar c) +{ + return c != '\0' && strchr(wordchars, c); +} + +gboolean +teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx, + gchar key, GError **error) +{ + teco_state_t *current = ctx->parent.current; + + switch (key) { + case TECO_CTL_KEY('W'): { /* rubout/reinsert word */ + teco_interface_popup_clear(); + + g_autofree gchar *wchars = g_malloc(teco_interface_ssm(SCI_GETWORDCHARS, 0, 0)); + teco_interface_ssm(SCI_GETWORDCHARS, 0, (sptr_t)wchars); + + if (teco_cmdline.modifier_enabled) { + /* reinsert word chars */ + while (ctx->parent.current == current && + teco_cmdline.effective_len < teco_cmdline.str.len && + teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len])) + if (!teco_cmdline_rubin(error)) + return FALSE; + + /* reinsert non-word chars */ + while (ctx->parent.current == current && + teco_cmdline.effective_len < teco_cmdline.str.len && + !teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len])) + if (!teco_cmdline_rubin(error)) + return FALSE; + + return TRUE; + } + + /* + * FIXME: In parse-only mode (ctx->result == NULL), we only + * get the default behaviour of teco_state_process_edit_cmd(). + * This may not be a real-life issue serious enough to maintain + * a result string even in parse-only mode. + * + * FIXME: Does not properly rubout string-building commands at the + * start of the string argument -- ctx->result->len is not + * a valid indicator of argument emptyness. + * Since it chains to teco_state_process_edit_cmd() we will instead + * rubout the entire command. + */ + if (ctx->result && ctx->result->len > 0) { + gboolean is_wordchar = teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1]); + teco_cmdline_rubout(); + if (ctx->parent.current != current) { + /* rub out string building command */ + while (ctx->result->len > 0 && ctx->parent.current != current) + teco_cmdline_rubout(); + return TRUE; + } + + /* + * rubout non-word chars + * FIXME: This might rub out part of string building commands, e.g. "EQ[A] ^W" + */ + if (!is_wordchar) { + while (ctx->result->len > 0 && + !teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1])) + teco_cmdline_rubout(); + } + + /* rubout word chars */ + while (ctx->result->len > 0 && + teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1])) + teco_cmdline_rubout(); + + return TRUE; + } + + /* + * Otherwise, the entire string command will be rubbed out. + */ + break; + } + + case TECO_CTL_KEY('U'): /* rubout/reinsert entire string */ + teco_interface_popup_clear(); + + if (teco_cmdline.modifier_enabled) { + /* reinsert string */ + while (ctx->parent.current == current && + teco_cmdline.effective_len < teco_cmdline.str.len) + if (!teco_cmdline_rubin(error)) + return FALSE; + + return TRUE; + } + + /* + * FIXME: In parse only mode (ctx->result == NULL), + * this will chain to teco_state_process_edit_cmd() and rubout + * only a single character. + */ + if (ctx->result) { + /* rubout string */ + while (ctx->result->len > 0) + teco_cmdline_rubout(); + return TRUE; + } + + break; + + case '\t': { /* autocomplete file name */ + /* + * FIXME: Does not autocomplete in parse-only mode (ctx->result == NULL). + */ + if (!teco_cmdline.modifier_enabled || !ctx->result) + break; + + /* + * TODO: In insertion commands, we can autocomplete + * the string at the buffer cursor. + */ + if (teco_interface_popup_is_shown()) { + /* cycle through popup pages */ + teco_interface_popup_show(); + return TRUE; + } + + const gchar *filename = teco_string_last_occurrence(ctx->result, + TECO_DEFAULT_BREAK_CHARS); + g_auto(teco_string_t) new_chars, new_chars_escaped; + gboolean unambiguous = teco_file_auto_complete(filename, G_FILE_TEST_EXISTS, &new_chars); + teco_machine_stringbuilding_escape(ctx, new_chars.data, new_chars.len, &new_chars_escaped); + if (unambiguous) + teco_string_append_c(&new_chars_escaped, ' '); + + if (!teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error)) + return FALSE; + + /* may be reset if there was a rubbed out command line */ + teco_cmdline.modifier_enabled = TRUE; + + return TRUE; + } + } + + /* + * Chaining to the parent (embedding) state machine's handler + * makes sure that ^W at the beginning of the string argument + * rubs out the entire string command. + */ + return teco_state_process_edit_cmd(parent_ctx, NULL, key, error); +} + +gboolean +teco_state_stringbuilding_qreg_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx, + gchar chr, GError **error) +{ + g_assert(ctx->machine_qregspec != NULL); + /* We downcast since teco_machine_qregspec_t is private in qreg.c */ + teco_machine_t *machine = (teco_machine_t *)ctx->machine_qregspec; + return machine->current->process_edit_cmd_cb(machine, &ctx->parent, chr, error); +} + +gboolean +teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current; + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); +} + +gboolean +teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current; + + /* + * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(), + * as it would be hard to subclass/overwrite for different main machine states. + */ + if (!stringbuilding_current->is_start) + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); + + switch (key) { + case '\t': { /* insert indention */ + if (teco_cmdline.modifier_enabled || teco_interface_ssm(SCI_GETUSETABS, 0, 0)) + break; + + teco_interface_popup_clear(); + + /* insert soft tabs */ + gint spaces = teco_interface_ssm(SCI_GETTABWIDTH, 0, 0); + gint pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + spaces -= teco_interface_ssm(SCI_GETCOLUMN, pos, 0) % spaces; + + while (spaces--) + if (!teco_cmdline_insert(" ", 1, error)) + return FALSE; + + return TRUE; + } + } + + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); +} + +gboolean +teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current; + + /* + * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(), + * as it would be hard to subclass/overwrite for different main machine states. + */ + if (!stringbuilding_current->is_start) + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); + + switch (key) { + case TECO_CTL_KEY('W'): /* rubout/reinsert file names including directories */ + teco_interface_popup_clear(); + + if (teco_cmdline.modifier_enabled) { + /* reinsert one level of file name */ + while (stringbuilding_ctx->parent.current == stringbuilding_current && + teco_cmdline.effective_len < teco_cmdline.str.len && + !G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len])) + if (!teco_cmdline_rubin(error)) + return FALSE; + + /* reinsert final directory separator */ + if (stringbuilding_ctx->parent.current == stringbuilding_current && + teco_cmdline.effective_len < teco_cmdline.str.len && + G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len]) && + !teco_cmdline_rubin(error)) + return FALSE; + + return TRUE; + } + + if (ctx->expectstring.string.len > 0) { + /* rubout directory separator */ + if (G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len-1])) + teco_cmdline_rubout(); + + /* rubout one level of file name */ + while (ctx->expectstring.string.len > 0 && + !G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len-1])) + teco_cmdline_rubout(); + + return TRUE; + } + + /* + * Rub out entire command instead of rubbing out nothing. + */ + break; + + case '\t': { /* autocomplete file name */ + if (teco_cmdline.modifier_enabled) + break; + + if (teco_interface_popup_is_shown()) { + /* cycle through popup pages */ + teco_interface_popup_show(); + return TRUE; + } + + if (teco_string_contains(&ctx->expectstring.string, '\0')) + /* null-byte not allowed in file names */ + return TRUE; + + g_auto(teco_string_t) new_chars, new_chars_escaped; + gboolean unambiguous = teco_file_auto_complete(ctx->expectstring.string.data, G_FILE_TEST_EXISTS, &new_chars); + teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped); + if (unambiguous && ctx->expectstring.nesting == 1) + teco_string_append_c(&new_chars_escaped, + ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char); + + return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error); + } + } + + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); +} + +gboolean +teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current; + + /* + * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(), + * as it would be hard to subclass/overwrite for different main machine states. + */ + if (!stringbuilding_current->is_start) + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); + + switch (key) { + case '\t': { /* autocomplete directory */ + if (teco_cmdline.modifier_enabled) + break; + + if (teco_interface_popup_is_shown()) { + /* cycle through popup pages */ + teco_interface_popup_show(); + return TRUE; + } + + if (teco_string_contains(&ctx->expectstring.string, '\0')) + /* null-byte not allowed in file names */ + return TRUE; + + /* + * FIXME: We might terminate the command in case of leaf directories. + */ + g_auto(teco_string_t) new_chars, new_chars_escaped; + teco_file_auto_complete(ctx->expectstring.string.data, G_FILE_TEST_IS_DIR, &new_chars); + teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped); + + return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error); + } + } + + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); +} + +gboolean +teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + g_assert(ctx->expectqreg != NULL); + /* + * NOTE: teco_machine_qregspec_t is private, so we downcast to teco_machine_t. + * Otherwise, we'd have to move this callback into qreg.c. + */ + teco_state_t *expectqreg_current = ((teco_machine_t *)ctx->expectqreg)->current; + return expectqreg_current->process_edit_cmd_cb((teco_machine_t *)ctx->expectqreg, &ctx->parent, key, error); +} + +gboolean +teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + switch (key) { + case '\t': { /* autocomplete Q-Register name */ + if (teco_cmdline.modifier_enabled) + break; + + if (teco_interface_popup_is_shown()) { + /* cycle through popup pages */ + teco_interface_popup_show(); + return TRUE; + } + + /* + * NOTE: This is only for short Q-Register specifications, + * so there is no escaping. + */ + g_auto(teco_string_t) new_chars; + teco_machine_qregspec_auto_complete(ctx, &new_chars); + + return new_chars.len ? teco_cmdline_insert(new_chars.data, new_chars.len, error) : TRUE; + } + } + + /* + * We chain to the parent (embedding) state machine's handler + * since rubout could otherwise rubout the command, invalidating + * the state machine. In particular ^W would crash. + * This also makes sure that commands like are completely + * rub out via ^W. + */ + return teco_state_process_edit_cmd(parent_ctx, NULL, key, error); +} + +gboolean +teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = teco_machine_qregspec_get_stringbuilding(ctx); + teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current; + + /* + * NOTE: teco_machine_qregspec_t is private, so we downcast to teco_machine_t. + * Otherwise, we'd have to move this callback into qreg.c. + * + * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(), + * as it would be hard to subclass/overwrite for different main machine states. + */ + if (!stringbuilding_current->is_start) + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, (teco_machine_t *)ctx, key, error); + + switch (key) { + case '\t': { /* autocomplete Q-Register name */ + if (teco_cmdline.modifier_enabled) + break; + + if (teco_interface_popup_is_shown()) { + /* cycle through popup pages */ + teco_interface_popup_show(); + return TRUE; + } + + g_auto(teco_string_t) new_chars, new_chars_escaped; + gboolean unambiguous = teco_machine_qregspec_auto_complete(ctx, &new_chars); + teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped); + if (unambiguous) + teco_string_append_c(&new_chars_escaped, ']'); + + return new_chars_escaped.len ? teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error) : TRUE; + } + } + + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, (teco_machine_t *)ctx, key, error); +} + +gboolean +teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current; + + /* + * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(), + * as it would be hard to subclass/overwrite for different main machine states. + */ + if (!stringbuilding_current->is_start) + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); + + switch (key) { + case '\t': { /* autocomplete file name */ + if (teco_cmdline.modifier_enabled) + break; + + /* + * In the EC command, completes files just like ^T + * + * TODO: Implement shell-command completion by iterating + * executables in $PATH + */ + if (teco_interface_popup_is_shown()) { + /* cycle through popup pages */ + teco_interface_popup_show(); + return TRUE; + } + + const gchar *filename = teco_string_last_occurrence(&ctx->expectstring.string, + TECO_DEFAULT_BREAK_CHARS); + g_auto(teco_string_t) new_chars, new_chars_escaped; + gboolean unambiguous = teco_file_auto_complete(filename, G_FILE_TEST_EXISTS, &new_chars); + teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped); + if (unambiguous) + teco_string_append_c(&new_chars_escaped, ' '); + + return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error); + } + } + + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); +} + +gboolean +teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current; + + /* + * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(), + * as it would be hard to subclass/overwrite for different main machine states. + */ + if (!stringbuilding_current->is_start) + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); + + switch (key) { + case '\t': { /* autocomplete Scintilla symbol */ + if (teco_cmdline.modifier_enabled) + break; + + if (teco_interface_popup_is_shown()) { + /* cycle through popup pages */ + teco_interface_popup_show(); + return TRUE; + } + + const gchar *symbol = teco_string_last_occurrence(&ctx->expectstring.string, ","); + teco_symbol_list_t *list = symbol == ctx->expectstring.string.data + ? &teco_symbol_list_scintilla + : &teco_symbol_list_scilexer; + g_auto(teco_string_t) new_chars, new_chars_escaped; + gboolean unambiguous = teco_symbol_list_auto_complete(list, symbol, &new_chars); + /* + * FIXME: Does not escape `,`. Also, <^Q,> is not allowed currently? + */ + teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped); + if (unambiguous) + teco_string_append_c(&new_chars_escaped, ','); + + return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error); + } + } + + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); +} + +gboolean +teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current; + + /* + * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(), + * as it would be hard to subclass/overwrite for different main machine states. + */ + if (!stringbuilding_current->is_start) + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); + + switch (key) { + case '\t': { /* autocomplete goto label */ + if (teco_cmdline.modifier_enabled) + break; + + if (teco_interface_popup_is_shown()) { + /* cycle through popup pages */ + teco_interface_popup_show(); + return TRUE; + } + + teco_string_t label = ctx->expectstring.string; + gint i = teco_string_rindex(&label, ','); + if (i >= 0) { + label.data += i+1; + label.len -= i+1; + } + + g_auto(teco_string_t) new_chars, new_chars_escaped; + gboolean unambiguous = teco_goto_table_auto_complete(&ctx->goto_table, label.data, label.len, &new_chars); + /* + * FIXME: This does not escape `,`. Cannot be escaped via ^Q currently? + */ + teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped); + if (unambiguous) + teco_string_append_c(&new_chars_escaped, ','); + + return new_chars_escaped.len ? teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error) : TRUE; + } + } + + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); +} + +gboolean +teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current; + + /* + * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(), + * as it would be hard to subclass/overwrite for different main machine states. + */ + if (!stringbuilding_current->is_start) + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); + + switch (key) { + case '\t': { /* autocomplete help term */ + if (teco_cmdline.modifier_enabled) + break; + + if (teco_interface_popup_is_shown()) { + /* cycle through popup pages */ + teco_interface_popup_show(); + return TRUE; + } + + if (teco_string_contains(&ctx->expectstring.string, '\0')) + /* help term must not contain null-byte */ + return TRUE; + + g_auto(teco_string_t) new_chars, new_chars_escaped; + gboolean unambiguous = teco_help_auto_complete(ctx->expectstring.string.data, &new_chars); + teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped); + if (unambiguous && ctx->expectstring.nesting == 1) + teco_string_append_c(&new_chars_escaped, + ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char); + + return new_chars_escaped.len ? teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error) : TRUE; + } + } + + return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); +} + +/* + * Command states + */ + +static teco_state_t * +teco_state_save_cmdline_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + teco_state_expectqreg_reset(ctx); + + if (ctx->mode != TECO_MODE_NORMAL) + return &teco_state_start; + + if (!qreg->vtable->undo_set_string(qreg, error) || + !qreg->vtable->set_string(qreg, teco_last_cmdline.data, teco_last_cmdline.len, error)) + return NULL; + + return &teco_state_start; +} + +/*$ *q + * *q -- Save last command line + * + * Only at the very beginning of a command-line, this command + * may be used to save the last command line as a string in + * Q-Register . + */ +TECO_DEFINE_STATE_EXPECTQREG(teco_state_save_cmdline, + .expectqreg.type = TECO_QREG_OPTIONAL_INIT +); diff --git a/src/cmdline.cpp b/src/cmdline.cpp deleted file mode 100644 index 9262e27..0000000 --- a/src/cmdline.cpp +++ /dev/null @@ -1,1043 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#ifdef HAVE_MALLOC_H -#include -#endif - -#include -#include - -#include -#include -#include - -#include "sciteco.h" -#include "string-utils.h" -#include "interface.h" -#include "expressions.h" -#include "parser.h" -#include "qregisters.h" -#include "ring.h" -#include "ioview.h" -#include "goto.h" -#include "help.h" -#include "undo.h" -#include "symbols.h" -#include "spawn.h" -#include "glob.h" -#include "error.h" -#include "cmdline.h" - -extern "C" { -#if defined(HAVE_MALLOC_TRIM) && !HAVE_DECL_MALLOC_TRIM -int malloc_trim(size_t pad); -#endif -} - -namespace SciTECO { - -static gchar *filename_complete(const gchar *filename, gchar completed = ' ', - GFileTest file_test = G_FILE_TEST_EXISTS); -static gchar *symbol_complete(SymbolList &list, const gchar *symbol, - gchar completed = ' '); - -static const gchar *last_occurrence(const gchar *str, - const gchar *chars = " \t\v\r\n\f<>,;@"); -static inline gboolean filename_is_dir(const gchar *filename); - -/** Current command line. */ -Cmdline cmdline; - -/** Last terminated command line */ -static Cmdline last_cmdline; - -/** - * Specifies whether the immediate editing modifier - * is enabled/disabled. - * It can be toggled with the ^G immediate editing command - * and influences the undo/redo direction and function of the - * TAB key. - */ -static bool modifier_enabled = false; - -bool quit_requested = false; - -namespace States { - StateSaveCmdline save_cmdline; -} - -#if 0 -Cmdline * -copy(void) const -{ - Cmdline *c = new Cmdline(); - - if (str) - c->str = g_memdup(str, len+rubout_len); - c->len = len; - c->rubout_len = rubout_len; - - return c; -} -#endif - -/** - * Throws a command line based on the command line - * replacement register. - * It is catched by Cmdline::keypress() to actually - * perform the command line update. - */ -void -Cmdline::replace(void) -{ - QRegister *cmdline_reg = QRegisters::globals[CTL_KEY_ESC_STR]; - /* use heap object to avoid copy constructors etc. */ - Cmdline *new_cmdline = new Cmdline(); - - /* FIXME: does not handle null bytes */ - new_cmdline->str = cmdline_reg->get_string(); - new_cmdline->len = strlen(new_cmdline->str); - new_cmdline->rubout_len = 0; - - /* - * Search for first differing character in old and - * new command line. This avoids unnecessary rubouts - * and insertions when the command line is updated. - */ - for (new_cmdline->pc = 0; - new_cmdline->pc < len && new_cmdline->pc < new_cmdline->len && - str[new_cmdline->pc] == new_cmdline->str[new_cmdline->pc]; - new_cmdline->pc++); - - throw new_cmdline; -} - -/** - * Insert string into command line and execute - * it immediately. - * It already handles command line replacement and will - * only throw SciTECO::Error. - * - * @param src String to insert (null-terminated). - * NULL inserts a character from the previously - * rubbed out command line. - */ -void -Cmdline::insert(const gchar *src) -{ - Cmdline old_cmdline; - guint repl_pc = 0; - - macro_pc = pc = len; - - if (!src) { - if (rubout_len) { - len++; - rubout_len--; - } - } else { - size_t src_len = strlen(src); - - if (src_len <= rubout_len && !strncmp(str+len, src, src_len)) { - len += src_len; - rubout_len -= src_len; - } else { - if (rubout_len) - /* automatically disable immediate editing modifier */ - modifier_enabled = false; - - String::append(str, len, src); - len += src_len; - rubout_len = 0; - } - } - - /* - * Parse/execute characters, one at a time so - * undo tokens get emitted for the corresponding characters. - */ - while (pc < len) { - try { - Execute::step(str, pc+1); - } catch (Cmdline *new_cmdline) { - /* - * Result of command line replacement (}): - * Exchange command lines, avoiding - * deep copying - */ - undo.pop(new_cmdline->pc); - - old_cmdline = *this; - *this = *new_cmdline; - new_cmdline->str = NULL; - macro_pc = repl_pc = pc; - - delete new_cmdline; - continue; - } catch (Error &error) { - error.add_frame(new Error::ToplevelFrame()); - error.display_short(); - - if (old_cmdline.str) { - /* - * Error during command-line replacement. - * Replay previous command-line. - * This avoids deep copying. - */ - undo.pop(repl_pc); - - g_free(str); - *this = old_cmdline; - old_cmdline.str = NULL; - macro_pc = pc = repl_pc; - - /* rubout cmdline replacement command */ - len--; - rubout_len++; - continue; - } - - /* error is handled in Cmdline::keypress() */ - throw; - } - - pc++; - } -} - -void -Cmdline::keypress(gchar key) -{ - /* - * Cleanup messages,etc... - */ - interface.msg_clear(); - - /* - * Process immediate editing commands, inserting - * characters as necessary into the command line. - */ - try { - States::current->process_edit_cmd(key); - } catch (Return) { - /* - * Return from top-level macro, results - * in command line termination. - * The return "arguments" are currently - * ignored. - */ - g_assert(States::current == &States::start); - - interface.popup_clear(); - - if (quit_requested) - /* cought by user interface */ - throw Quit(); - - undo.clear(); - /* also empties all Scintilla undo buffers */ - ring.set_scintilla_undo(true); - QRegisters::view.set_scintilla_undo(true); - Goto::table->clear(); - expressions.clear(); - loop_stack.clear(); - - last_cmdline = *this; - str = NULL; - len = rubout_len = 0; - -#ifdef HAVE_MALLOC_TRIM - /* - * Glibc/Linux-only optimization: Undo stacks can grow very - * large - sometimes large enough to make the system - * swap and become unresponsive. - * This shrink the program break after lots of memory has - * been freed, reducing the virtual memory size and aiding - * in recovering from swapping issues. - */ - malloc_trim(0); -#endif - } catch (Error &error) { - /* - * NOTE: Error message already displayed in - * Cmdline::insert(). - * - * Undo tokens may have been emitted - * (or had to be) before the exception - * is thrown. They must be executed so - * as if the character had never been - * inserted. - */ - undo.pop(pc); - rubout_len += len-pc; - len = pc; - /* program counter could be messed up */ - macro_pc = len; - } - - /* - * Echo command line - */ - interface.cmdline_update(this); -} - -void -Cmdline::fnmacro(const gchar *name) -{ - gchar macro_name[1 + strlen(name) + 1]; - QRegister *reg; - gchar *macro; - - if (!(Flags::ed & Flags::ED_FNKEYS)) - /* function key macros disabled */ - goto default_action; - - macro_name[0] = CTL_KEY('F'); - g_strlcpy(macro_name + 1, name, sizeof(macro_name) - 1); - - reg = QRegisters::globals[macro_name]; - if (!reg) - /* macro undefined */ - goto default_action; - - if (reg->get_integer() & States::current->get_fnmacro_mask()) - return; - - macro = reg->get_string(); - try { - keypress(macro); - } catch (...) { - /* could be "Quit" for instance */ - g_free(macro); - throw; - } - g_free(macro); - - return; - - /* - * Most function key macros have no default action, - * except "CLOSE" which quits the application - * (this may loose unsaved data but is better than - * not doing anything if the user closes the window). - * NOTE: Doing the check here is less efficient than - * doing it in the UI implementations, but defines - * the default actions centrally. - * Also, fnmacros are only handled after key presses. - */ -default_action: - if (!strcmp(name, "CLOSE")) - throw Quit(); -} - -static gchar * -filename_complete(const gchar *filename, gchar completed, - GFileTest file_test) -{ - gchar *filename_expanded; - gsize filename_len; - gchar *dirname, *basename, dir_sep; - gsize dirname_len; - const gchar *cur_basename; - - GDir *dir; - GSList *files = NULL; - guint files_len = 0; - gchar *insert = NULL; - gsize prefix_len = 0; - - if (Globber::is_pattern(filename)) - return NULL; - - filename_expanded = expand_path(filename); - filename_len = strlen(filename_expanded); - - /* - * Derive base and directory names. - * We do not use g_path_get_basename() or g_path_get_dirname() - * since we need strict suffixes and prefixes of filename - * in order to construct paths of entries in dirname - * that are suitable for auto completion. - */ - dirname_len = file_get_dirname_len(filename_expanded); - dirname = g_strndup(filename_expanded, dirname_len); - basename = filename_expanded + dirname_len; - - dir = g_dir_open(dirname_len ? dirname : ".", 0, NULL); - if (!dir) { - g_free(dirname); - g_free(filename_expanded); - return NULL; - } - - /* - * On Windows, both forward and backslash - * directory separators are allowed in directory - * names passed to glib. - * To imitate glib's behaviour, we use - * the last valid directory separator in `filename_expanded` - * to generate new separators. - * This also allows forward-slash auto-completion - * on Windows. - */ - dir_sep = dirname_len ? dirname[dirname_len-1] - : G_DIR_SEPARATOR; - - while ((cur_basename = g_dir_read_name(dir))) { - gchar *cur_filename; - - if (!g_str_has_prefix(cur_basename, basename)) - continue; - - /* - * dirname contains any directory separator, - * so g_strconcat() works here. - */ - cur_filename = g_strconcat(dirname, cur_basename, NIL); - - /* - * NOTE: This avoids g_file_test() for G_FILE_TEST_EXISTS - * since the file we process here should always exist. - */ - if ((!*basename && !file_is_visible(cur_filename)) || - (file_test != G_FILE_TEST_EXISTS && - !g_file_test(cur_filename, file_test))) { - g_free(cur_filename); - continue; - } - - if (file_test == G_FILE_TEST_IS_DIR || - g_file_test(cur_filename, G_FILE_TEST_IS_DIR)) - String::append(cur_filename, dir_sep); - - files = g_slist_prepend(files, cur_filename); - - if (g_slist_next(files)) { - const gchar *other_file = (gchar *)g_slist_next(files)->data; - gsize len = String::diff(other_file + filename_len, - cur_filename + filename_len); - if (len < prefix_len) - prefix_len = len; - } else { - prefix_len = strlen(cur_filename + filename_len); - } - - files_len++; - } - if (prefix_len > 0) - insert = g_strndup((gchar *)files->data + filename_len, prefix_len); - - g_dir_close(dir); - g_free(dirname); - g_free(filename_expanded); - - if (!insert && files_len > 1) { - files = g_slist_sort(files, (GCompareFunc)g_strcmp0); - - for (GSList *file = files; file; file = g_slist_next(file)) { - InterfaceCurrent::PopupEntryType type; - bool is_buffer = false; - - if (filename_is_dir((gchar *)file->data)) { - type = InterfaceCurrent::POPUP_DIRECTORY; - } else { - type = InterfaceCurrent::POPUP_FILE; - /* FIXME: inefficient */ - is_buffer = ring.find((gchar *)file->data); - } - - interface.popup_add(type, (gchar *)file->data, - is_buffer); - } - - interface.popup_show(); - } else if (completed && files_len == 1 && - !filename_is_dir((gchar *)files->data)) { - /* - * FIXME: If we are completing only directories, - * we can theoretically insert the completed character - * after directories without subdirectories - */ - String::append(insert, completed); - } - - g_slist_free_full(files, g_free); - - return insert; -} - -static gchar * -symbol_complete(SymbolList &list, const gchar *symbol, gchar completed) -{ - GList *glist; - guint glist_len = 0; - gchar *insert = NULL; - gsize symbol_len; - gsize prefix_len = 0; - - if (!symbol) - symbol = ""; - symbol_len = strlen(symbol); - - glist = list.get_glist(); - if (!glist) - return NULL; - glist = g_list_copy(glist); - if (!glist) - return NULL; - /* NOTE: element data must not be freed */ - - for (GList *entry = g_list_first(glist), *next = g_list_next(entry); - entry != NULL; - entry = next, next = entry ? g_list_next(entry) : NULL) { - if (!g_str_has_prefix((gchar *)entry->data, symbol)) { - glist = g_list_delete_link(glist, entry); - continue; - } - - gsize len = String::diff((gchar *)glist->data + symbol_len, - (gchar *)entry->data + symbol_len); - if (!prefix_len || len < prefix_len) - prefix_len = len; - - glist_len++; - } - if (prefix_len > 0) - insert = g_strndup((gchar *)glist->data + symbol_len, prefix_len); - - if (!insert && glist_len > 1) { - for (GList *entry = g_list_first(glist); - entry != NULL; - entry = g_list_next(entry)) { - interface.popup_add(InterfaceCurrent::POPUP_PLAIN, - (gchar *)entry->data); - } - - interface.popup_show(); - } else if (glist_len == 1) { - String::append(insert, completed); - } - - g_list_free(glist); - - return insert; -} - -/* - * Commandline key processing. - * - * These are all the implementations of State::process_edit_cmd(). - * It makes sense to use virtual methods for key processing, as it is - * largely state-dependant; but it defines interactive-mode-only - * behaviour which can be kept isolated from the rest of the states' - * implementation. - */ - -void -State::process_edit_cmd(gchar key) -{ - switch (key) { - case '\n': /* insert EOL sequence */ - interface.popup_clear(); - - if (Flags::ed & Flags::ED_AUTOEOL) - cmdline.insert("\n"); - else - cmdline.insert(get_eol_seq(interface.ssm(SCI_GETEOLMODE))); - return; - - case CTL_KEY('G'): /* toggle immediate editing modifier */ - interface.popup_clear(); - - modifier_enabled = !modifier_enabled; - interface.msg(InterfaceCurrent::MSG_INFO, - "Immediate editing modifier is now %s.", - modifier_enabled ? "enabled" : "disabled"); - return; - - case CTL_KEY('H'): /* rubout/reinsert character */ - interface.popup_clear(); - - if (modifier_enabled) - /* re-insert character */ - cmdline.insert(); - else - /* rubout character */ - cmdline.rubout(); - return; - - case CTL_KEY('W'): /* rubout/reinsert command */ - interface.popup_clear(); - - if (modifier_enabled) { - /* reinsert command */ - do - cmdline.insert(); - while (!States::is_start() && cmdline.rubout_len); - } else { - /* rubout command */ - do - cmdline.rubout(); - while (!States::is_start()); - } - return; - -#ifdef SIGTSTP - case CTL_KEY('Z'): - /* - * does not raise signal if handling of - * special characters temporarily disabled in terminal - * (Curses), or command-line is detached from - * terminal (GTK+). - * This does NOT change the state of the popup window. - */ - raise(SIGTSTP); - return; -#endif - } - - interface.popup_clear(); - cmdline.insert(key); -} - -void -StateCaseInsensitive::process_edit_cmd(gchar key) -{ - if (Flags::ed & Flags::ED_AUTOCASEFOLD) - /* will not modify non-letter keys */ - key = g_ascii_islower(key) ? g_ascii_toupper(key) - : g_ascii_tolower(key); - - State::process_edit_cmd(key); -} - -void -StateExpectString::process_edit_cmd(gchar key) -{ - switch (key) { - case CTL_KEY('W'): { /* rubout/reinsert word */ - interface.popup_clear(); - - gchar wchars[interface.ssm(SCI_GETWORDCHARS)]; - interface.ssm(SCI_GETWORDCHARS, 0, (sptr_t)wchars); - - if (modifier_enabled) { - /* reinsert word chars */ - while (States::current == this && cmdline.rubout_len && - strchr(wchars, cmdline.str[cmdline.len])) - cmdline.insert(); - - /* reinsert non-word chars */ - while (States::current == this && cmdline.rubout_len && - !strchr(wchars, cmdline.str[cmdline.len])) - cmdline.insert(); - return; - } - - if (strings[0] && *strings[0]) { - /* rubout non-word chars */ - while (strings[0] && *strings[0] && - !strchr(wchars, cmdline.str[cmdline.len-1])) - cmdline.rubout(); - - /* rubout word chars */ - while (strings[0] && *strings[0] && - strchr(wchars, cmdline.str[cmdline.len-1])) - cmdline.rubout(); - return; - } - - /* - * Otherwise, the entire command string will - * be rubbed out. - */ - break; - } - - case CTL_KEY('U'): /* rubout/reinsert string */ - interface.popup_clear(); - - if (modifier_enabled) { - /* reinsert string */ - while (States::current == this && cmdline.rubout_len) - cmdline.insert(); - } else { - /* rubout string */ - while (strings[0] && *strings[0]) - cmdline.rubout(); - } - return; - - case '\t': /* autocomplete file name */ - if (modifier_enabled) { - /* - * TODO: In insertion commands, we can autocomplete - * the string at the buffer cursor. - */ - /* autocomplete filename using string argument */ - if (interface.popup_is_shown()) { - /* cycle through popup pages */ - interface.popup_show(); - return; - } - - const gchar *filename = last_occurrence(strings[0]); - gchar *new_chars = filename_complete(filename); - - if (new_chars) - cmdline.insert(new_chars); - g_free(new_chars); - - /* may be reset if there was a rubbed out command line */ - modifier_enabled = true; - return; - } - - if (machine.qregspec_machine) { - if (interface.popup_is_shown()) { - /* cycle through popup pages */ - interface.popup_show(); - return; - } - - gchar *new_chars = machine.qregspec_machine->auto_complete(); - - if (new_chars) - cmdline.insert(new_chars); - g_free(new_chars); - return; - } - break; - } - - State::process_edit_cmd(key); -} - -void -StateInsert::process_edit_cmd(gchar key) -{ - gint spaces; - - switch (key) { - case '\t': /* insert indention */ - if (modifier_enabled || interface.ssm(SCI_GETUSETABS)) - break; - - interface.popup_clear(); - - /* insert soft tabs */ - spaces = interface.ssm(SCI_GETTABWIDTH); - spaces -= interface.ssm(SCI_GETCOLUMN, - interface.ssm(SCI_GETCURRENTPOS)) % spaces; - - while (spaces--) - cmdline.insert(' '); - return; - } - - StateExpectString::process_edit_cmd(key); -} - -void -StateExpectFile::process_edit_cmd(gchar key) -{ - gchar *new_chars; - - switch (key) { - case CTL_KEY('W'): /* rubout/reinsert file names including directories */ - interface.popup_clear(); - - if (modifier_enabled) { - /* reinsert one level of file name */ - while (States::current == this && cmdline.rubout_len && - !G_IS_DIR_SEPARATOR(cmdline.str[cmdline.len])) - cmdline.insert(); - - /* reinsert final directory separator */ - if (States::current == this && cmdline.rubout_len && - G_IS_DIR_SEPARATOR(cmdline.str[cmdline.len])) - cmdline.insert(); - return; - } - - if (strings[0] && *strings[0]) { - /* rubout directory separator */ - if (strings[0] && *strings[0] && - G_IS_DIR_SEPARATOR(cmdline.str[cmdline.len-1])) - cmdline.rubout(); - - /* rubout one level of file name */ - while (strings[0] && *strings[0] && - !G_IS_DIR_SEPARATOR(cmdline.str[cmdline.len-1])) - cmdline.rubout(); - return; - } - - /* - * Rub out entire command instead of - * rubbing out nothing. - */ - break; - - case '\t': /* autocomplete file name */ - if (modifier_enabled) - break; - - if (interface.popup_is_shown()) { - /* cycle through popup pages */ - interface.popup_show(); - return; - } - - new_chars = filename_complete(strings[0], - escape_char == '{' ? '\0' : escape_char); - if (new_chars) - cmdline.insert(new_chars); - g_free(new_chars); - return; - } - - StateExpectString::process_edit_cmd(key); -} - -void -StateExpectDir::process_edit_cmd(gchar key) -{ - gchar *new_chars; - - switch (key) { - case '\t': /* autocomplete directory */ - if (modifier_enabled) - break; - - if (interface.popup_is_shown()) { - /* cycle through popup pages */ - interface.popup_show(); - return; - } - - new_chars = filename_complete(strings[0], '\0', - G_FILE_TEST_IS_DIR); - if (new_chars) - cmdline.insert(new_chars); - g_free(new_chars); - return; - } - - StateExpectFile::process_edit_cmd(key); -} - -void -StateExpectQReg::process_edit_cmd(gchar key) -{ - gchar *new_chars; - - switch (key) { - case '\t': /* autocomplete Q-Register name */ - if (modifier_enabled) - break; - - if (interface.popup_is_shown()) { - /* cycle through popup pages */ - interface.popup_show(); - return; - } - - new_chars = machine.auto_complete(); - if (new_chars) - cmdline.insert(new_chars); - g_free(new_chars); - return; - } - - State::process_edit_cmd(key); -} - -void -StateExecuteCommand::process_edit_cmd(gchar key) -{ - gchar *new_chars; - - switch (key) { - case '\t': /* autocomplete symbol or file name */ - if (modifier_enabled) - break; - - /* - * In the EC command, completes files just like ^T - * TODO: Implement shell-command completion by iterating - * executables in $PATH - */ - if (interface.popup_is_shown()) { - /* cycle through popup pages */ - interface.popup_show(); - return; - } - - new_chars = filename_complete(last_occurrence(strings[0])); - if (new_chars) - cmdline.insert(new_chars); - g_free(new_chars); - return; - } - - StateExpectString::process_edit_cmd(key); -} - -void -StateScintilla_symbols::process_edit_cmd(gchar key) -{ - switch (key) { - case '\t': { /* autocomplete Scintilla symbol */ - if (modifier_enabled) - break; - - if (interface.popup_is_shown()) { - /* cycle through popup pages */ - interface.popup_show(); - return; - } - - const gchar *symbol = last_occurrence(strings[0], ","); - SymbolList &list = symbol == strings[0] - ? Symbols::scintilla - : Symbols::scilexer; - gchar *new_chars = symbol_complete(list, symbol, ','); - - if (new_chars) - cmdline.insert(new_chars); - g_free(new_chars); - return; - } - } - - StateExpectString::process_edit_cmd(key); -} - -void -StateGotoCmd::process_edit_cmd(gchar key) -{ - switch (key) { - case '\t': { /* autocomplete goto label */ - if (modifier_enabled) - break; - - if (interface.popup_is_shown()) { - /* cycle through popup pages */ - interface.popup_show(); - return; - } - - const gchar *label = last_occurrence(strings[0], ","); - gchar *new_chars = Goto::table->auto_complete(label); - - if (new_chars) - cmdline.insert(new_chars); - g_free(new_chars); - return; - } - } - - StateExpectString::process_edit_cmd(key); -} - -void -StateGetHelp::process_edit_cmd(gchar key) -{ - switch (key) { - case '\t': { /* autocomplete help term */ - if (modifier_enabled) - break; - - if (interface.popup_is_shown()) { - /* cycle through popup pages */ - interface.popup_show(); - return; - } - - gchar complete = escape_char == '{' ? '\0' : escape_char; - gchar *new_chars = help_index.auto_complete(strings[0], complete); - - if (new_chars) - cmdline.insert(new_chars); - g_free(new_chars); - return; - } - } - - StateExpectString::process_edit_cmd(key); -} - -/* - * Command states - */ - -/*$ *q - * *q -- Save last command line - * - * Only at the very beginning of a command-line, this command - * may be used to save the last command line as a string in - * Q-Register . - */ -State * -StateSaveCmdline::got_register(QRegister *reg) -{ - machine.reset(); - - BEGIN_EXEC(&States::start); - reg->undo_set_string(); - reg->set_string(last_cmdline.str, last_cmdline.len); - return &States::start; -} - -/* - * Auxiliary functions - */ - -static const gchar * -last_occurrence(const gchar *str, const gchar *chars) -{ - if (!str) - return NULL; - - while (*chars) { - const gchar *p = strrchr(str, *chars++); - if (p) - str = p+1; - } - - return str; -} - -static inline gboolean -filename_is_dir(const gchar *filename) -{ - gchar c; - - if (!*filename) - return false; - - c = filename[strlen(filename)-1]; - return G_IS_DIR_SEPARATOR(c); -} - -} /* namespace SciTECO */ diff --git a/src/cmdline.h b/src/cmdline.h index 66e1829..0c61dea 100644 --- a/src/cmdline.h +++ b/src/cmdline.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,103 +14,84 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -#ifndef __CMDLINE_H -#define __CMDLINE_H +#pragma once #include -#include "memory.h" +#include "sciteco.h" +#include "string-utils.h" #include "parser.h" -#include "qregisters.h" #include "undo.h" -namespace SciTECO { +typedef struct { + /** + * State machine used for interactive mode (commandline macro). + * It is initialized on-demand in main.c. + * This is a global variable instead of being passed down the call stack + * since some process_edit_cmd_cb will be for nested state machines + * but we must "step" only the toplevel state machine. + */ + teco_machine_main_t machine; -/* - * NOTE: Some of the members (esp. insert() and rubout()) - * have to be public, so that State::process_edit_cmd() - * implementations can access it. - * Otherwise, we'd have to list all implementations as - * friend methods, which is inelegant. - */ -extern class Cmdline : public Object { -public: /** - * String containing the current command line. - * It is not null-terminated and contains the effective - * command-line up to cmdline_len followed by the recently rubbed-out - * command-line of length cmdline_rubout_len. + * String containing the current command line + * (both effective and rubbed out). */ - gchar *str; - /** Effective command line length */ - gsize len; - /** Length of the rubbed out command line */ - gsize rubout_len; + teco_string_t str; + /** + * Effective command line length. + * The length of the rubbed out part of the command line + * is (teco_cmdline.str.len - teco_cmdline.effective_len). + */ + gsize effective_len; + /** Program counter within the command-line macro */ guint pc; - Cmdline() : str(NULL), len(0), rubout_len(0), pc(0) {} - inline - ~Cmdline() - { - g_free(str); - } - - inline gchar - operator [](guint i) const - { - return str[i]; - } - - void keypress(gchar key); - inline void - keypress(const gchar *keys) - { - while (*keys) - keypress(*keys++); - } - - void fnmacro(const gchar *name); - - void replace(void) G_GNUC_NORETURN; - - inline void - rubout(void) - { - if (len) { - undo.pop(--len); - rubout_len++; - } - } - - void insert(const gchar *src = NULL); - inline void - insert(gchar key) - { - gchar src[] = {key, '\0'}; - insert(src); - } -} cmdline; - -extern bool quit_requested; + /** + * Specifies whether the immediate editing modifier + * is enabled/disabled. + * It can be toggled with the ^G immediate editing command + * and influences the undo/redo direction and function of the + * TAB key. + */ + gboolean modifier_enabled; +} teco_cmdline_t; -/* - * Command states - */ +extern teco_cmdline_t teco_cmdline; + +gboolean teco_cmdline_insert(const gchar *data, gsize len, GError **error); -class StateSaveCmdline : public StateExpectQReg { -public: - StateSaveCmdline() : StateExpectQReg(QREG_OPTIONAL_INIT) {} +static inline gboolean +teco_cmdline_rubin(GError **error) +{ + return teco_cmdline_insert(NULL, 0, error); +} -private: - State *got_register(QRegister *reg); -}; +gboolean teco_cmdline_keypress_c(gchar key, GError **error); -namespace States { - extern StateSaveCmdline save_cmdline; +static inline gboolean +teco_cmdline_keypress(const gchar *str, gsize len, GError **error) +{ + for (guint i = 0; i < len; i++) + if (!teco_cmdline_keypress_c(str[i], error)) + return FALSE; + return TRUE; } -} /* namespace SciTECO */ +gboolean teco_cmdline_fnmacro(const gchar *name, GError **error); + +static inline void +teco_cmdline_rubout(void) +{ + if (teco_cmdline.effective_len) + teco_undo_pop(--teco_cmdline.effective_len); +} + +extern gboolean teco_quit_requested; + +/* + * Command states + */ -#endif +TECO_DECLARE_STATE(teco_state_save_cmdline); diff --git a/src/core-commands.c b/src/core-commands.c new file mode 100644 index 0000000..4c5d176 --- /dev/null +++ b/src/core-commands.c @@ -0,0 +1,2510 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include + +#include "sciteco.h" +#include "string-utils.h" +#include "file-utils.h" +#include "interface.h" +#include "undo.h" +#include "expressions.h" +#include "ring.h" +#include "parser.h" +#include "scintilla.h" +#include "search.h" +#include "spawn.h" +#include "glob.h" +#include "help.h" +#include "cmdline.h" +#include "error.h" +#include "memory.h" +#include "eol.h" +#include "qreg.h" +#include "qreg-commands.h" +#include "goto-commands.h" +#include "core-commands.h" + +static teco_state_t *teco_state_control_input(teco_machine_main_t *ctx, gchar chr, GError **error); + +/* + * NOTE: This needs some extra code in teco_state_start_input(). + */ +static void +teco_state_start_mul(teco_machine_main_t *ctx, GError **error) +{ + teco_expressions_push_calc(TECO_OP_MUL, error); +} + +static void +teco_state_start_div(teco_machine_main_t *ctx, GError **error) +{ + teco_expressions_push_calc(TECO_OP_DIV, error); +} + +static void +teco_state_start_plus(teco_machine_main_t *ctx, GError **error) +{ + teco_expressions_push_calc(TECO_OP_ADD, error); +} + +static void +teco_state_start_minus(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_args()) + teco_set_num_sign(-teco_num_sign); + else + teco_expressions_push_calc(TECO_OP_SUB, error); +} + +static void +teco_state_start_and(teco_machine_main_t *ctx, GError **error) +{ + teco_expressions_push_calc(TECO_OP_AND, error); +} + +static void +teco_state_start_or(teco_machine_main_t *ctx, GError **error) +{ + teco_expressions_push_calc(TECO_OP_OR, error); +} + +static void +teco_state_start_brace_open(teco_machine_main_t *ctx, GError **error) +{ + if (teco_num_sign < 0) { + teco_set_num_sign(1); + if (!teco_expressions_eval(FALSE, error)) + return; + teco_expressions_push(-1); + if (!teco_expressions_push_calc(TECO_OP_MUL, error)) + return; + } + teco_expressions_brace_open(); +} + +static void +teco_state_start_brace_close(teco_machine_main_t *ctx, GError **error) +{ + teco_expressions_brace_close(error); +} + +static void +teco_state_start_comma(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + teco_expressions_push_op(TECO_OP_NEW); +} + +/*$ "." dot + * \&. -> dot -- Return buffer position + * + * \(lq.\(rq pushes onto the stack, the current + * position (also called ) of the currently + * selected buffer or Q-Register. + */ +static void +teco_state_start_dot(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + teco_expressions_push(teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0)); +} + +/*$ Z size + * Z -> size -- Return buffer size + * + * Pushes onto the stack, the size of the currently selected + * buffer or Q-Register. + * This is value is also the buffer position of the document's + * end. + */ +static void +teco_state_start_zed(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + teco_expressions_push(teco_interface_ssm(SCI_GETLENGTH, 0, 0)); +} + +/*$ H + * H -> 0,Z -- Return range for entire buffer + * + * Pushes onto the stack the integer 0 (position of buffer + * beginning) and the current buffer's size. + * It is thus often equivalent to the expression + * \(lq0,Z\(rq, or more generally \(lq(0,Z)\(rq. + */ +static void +teco_state_start_range(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + teco_expressions_push(0); + teco_expressions_push(teco_interface_ssm(SCI_GETLENGTH, 0, 0)); +} + +/*$ "\\" + * n\\ -- Insert or read ASCII numbers + * \\ -> n + * + * Backslash pops a value from the stack, formats it + * according to the current radix and inserts it in the + * current buffer or Q-Register at dot. + * If is omitted (empty stack), it does the reverse - + * it reads from the current buffer position an integer + * in the current radix and pushes it onto the stack. + * Dot is not changed when reading integers. + * + * In other words, the command serializes or deserializes + * integers as ASCII characters. + */ +static void +teco_state_start_backslash(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + + if (teco_expressions_args()) { + teco_int_t value; + + if (!teco_expressions_pop_num_calc(&value, 0, error)) + return; + + gchar buffer[TECO_EXPRESSIONS_FORMAT_LEN]; + gchar *str = teco_expressions_format(buffer, value); + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + teco_interface_ssm(SCI_ADDTEXT, strlen(str), (sptr_t)str); + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + teco_ring_dirtify(); + + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + } else { + uptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + gchar c = (gchar)teco_interface_ssm(SCI_GETCHARAT, pos, 0); + teco_int_t v = 0; + gint sign = 1; + + if (c == '-') { + pos++; + sign = -1; + } + + for (;;) { + c = teco_ascii_toupper((gchar)teco_interface_ssm(SCI_GETCHARAT, pos, 0)); + if (c >= '0' && c <= '0' + MIN(teco_radix, 10) - 1) + v = (v*teco_radix) + (c - '0'); + else if (c >= 'A' && + c <= 'A' + MIN(teco_radix - 10, 26) - 1) + v = (v*teco_radix) + 10 + (c - 'A'); + else + break; + + pos++; + } + + teco_expressions_push(sign * v); + } +} + +/* + * NOTE: This needs some extra code in teco_state_start_input(). + */ +static void +teco_state_start_loop_open(teco_machine_main_t *ctx, GError **error) +{ + teco_loop_context_t lctx; + if (!teco_expressions_eval(FALSE, error) || + !teco_expressions_pop_num_calc(&lctx.counter, -1, error)) + return; + lctx.pass_through = teco_machine_main_eval_colon(ctx); + + if (lctx.counter) { + /* + * Non-colon modified, we add implicit + * braces, so loop body won't see parameters. + * Colon modified, loop starts can be used + * to process stack elements which is symmetric + * to ":>". + */ + if (!lctx.pass_through) + teco_expressions_brace_open(); + + lctx.pc = ctx->macro_pc; + g_array_append_val(teco_loop_stack, lctx); + undo__remove_index__teco_loop_stack(teco_loop_stack->len-1); + } else { + /* skip to end of loop */ + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->mode = TECO_MODE_PARSE_ONLY_LOOP; + } +} + +/* + * NOTE: This needs some extra code in teco_state_start_input(). + */ +static void +teco_state_start_loop_close(teco_machine_main_t *ctx, GError **error) +{ + if (teco_loop_stack->len <= ctx->loop_stack_fp) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Loop end without corresponding " + "loop start command"); + return; + } + + teco_loop_context_t *lctx = &g_array_index(teco_loop_stack, teco_loop_context_t, teco_loop_stack->len-1); + gboolean colon_modified = teco_machine_main_eval_colon(ctx); + + /* + * Colon-modified loop ends can be used to + * aggregate values on the stack. + * A non-colon modified ">" behaves like ":>" + * for pass-through loop starts, though. + */ + if (!lctx->pass_through) { + if (colon_modified) { + if (!teco_expressions_eval(FALSE, error)) + return; + teco_expressions_push_op(TECO_OP_NEW); + } else if (!teco_expressions_discard_args(error)) { + return; + } + } + + if (lctx->counter == 1) { + /* this was the last loop iteration */ + if (!lctx->pass_through && + !teco_expressions_brace_close(error)) + return; + undo__insert_val__teco_loop_stack(teco_loop_stack->len-1, *lctx); + g_array_remove_index(teco_loop_stack, teco_loop_stack->len-1); + } else { + /* + * Repeat loop: + * NOTE: One undo token per iteration could + * be avoided by saving the original counter + * in the teco_loop_context_t. + * We do however optimize the case of infinite loops + * because the loop counter does not have to be + * updated. + */ + ctx->macro_pc = lctx->pc; + if (lctx->counter >= 0) { + if (ctx->parent.must_undo) + teco_undo_int(lctx->counter); + lctx->counter--; + } + } +} + +/*$ ";" break + * [bool]; -- Conditionally break from loop + * [bool]:; + * + * Breaks from the current inner-most loop if + * signifies failure (non-negative value). + * If colon-modified, breaks from the loop if + * signifies success (negative value). + * + * If the condition code cannot be popped from the stack, + * the global search register's condition integer + * is implied instead. + * This way, you may break on search success/failures + * without colon-modifying the search command (or at a + * later point). + * + * Executing \(lq;\(rq outside of iterations in the current + * macro invocation level yields an error. It is thus not + * possible to let a macro break a caller's loop. + */ +static void +teco_state_start_break(teco_machine_main_t *ctx, GError **error) +{ + if (teco_loop_stack->len <= ctx->loop_stack_fp) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "<;> only allowed in iterations"); + return; + } + + teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1); + g_assert(reg != NULL); + teco_int_t v; + if (!reg->vtable->get_integer(reg, &v, error)) + return; + + teco_bool_t rc; + if (!teco_expressions_pop_num_calc(&rc, v, error)) + return; + if (teco_machine_main_eval_colon(ctx)) + rc = ~rc; + + if (teco_is_success(rc)) + return; + + teco_loop_context_t lctx = g_array_index(teco_loop_stack, teco_loop_context_t, teco_loop_stack->len-1); + g_array_remove_index(teco_loop_stack, teco_loop_stack->len-1); + + if (!teco_expressions_discard_args(error)) + return; + if (!lctx.pass_through && + !teco_expressions_brace_close(error)) + return; + + undo__insert_val__teco_loop_stack(teco_loop_stack->len, lctx); + + /* skip to end of loop */ + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->mode = TECO_MODE_PARSE_ONLY_LOOP; +} + +/*$ "{" "}" + * { -- Edit command line + * } + * + * The opening curly bracket is a powerful command + * to edit command lines but has very simple semantics. + * It copies the current commandline into the global + * command line editing register (called Escape, i.e. + * ASCII 27) and edits this register. + * The curly bracket itself is not copied. + * + * The command line may then be edited using any + * \*(ST command or construct. + * You may switch between the command line editing + * register and other registers or buffers. + * The user will then usually reapply (called update) + * the current command-line. + * + * The closing curly bracket will update the current + * command-line with the contents of the global command + * line editing register. + * To do so it merely rubs-out the current command-line + * up to the first changed character and inserts + * all characters following from the updated command + * line into the command stream. + * To prevent the undesired rubout of the entire + * command-line, the replacement command ("}") is only + * allowed when the replacement register currently edited + * since it will otherwise be usually empty. + * + * .B Note: + * - Command line editing only works on command lines, + * but not arbitrary macros. + * It is therefore not available in batch mode and + * will yield an error if used. + * - Command line editing commands may be safely used + * from macro invocations. + * Such macros are called command line editing macros. + * - A command line update from a macro invocation will + * always yield to the outer-most macro level (i.e. + * the command line macro). + * Code following the update command in the macro + * will thus never be executed. + * - As a safe-guard against command line trashing due + * to erroneous changes at the beginning of command + * lines, a backup mechanism is implemented: + * If the updated command line yields an error at + * any command during the update, the original + * command line will be restored with an algorithm + * similar to command line updating and the update + * command will fail instead. + * That way it behaves like any other command that + * yields an error: + * The character resulting in the update is rejected + * by the command line input subsystem. + * - In the rare case that an aforementioned command line + * backup fails, the commands following the erroneous + * character will not be inserted again (will be lost). + */ +static void +teco_state_start_cmdline_push(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_undo_enabled) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Command-line editing only possible in " + "interactive mode"); + return; + } + + if (!teco_current_doc_undo_edit(error) || + !teco_qreg_table_edit_name(&teco_qreg_table_globals, "\e", 1, error)) + return; + + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + teco_interface_ssm(SCI_CLEARALL, 0, 0); + teco_interface_ssm(SCI_ADDTEXT, teco_cmdline.pc, (sptr_t)teco_cmdline.str.data); + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + + /* must always support undo on global register */ + undo__teco_interface_ssm(SCI_UNDO, 0, 0); +} + +static void +teco_state_start_cmdline_pop(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_undo_enabled) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Command-line editing only possible in " + "interactive mode"); + return; + } + if (teco_qreg_current != teco_qreg_table_find(&teco_qreg_table_globals, "\e", 1)) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Command-line replacement only allowed when " + "editing the replacement register"); + return; + } + + /* replace cmdline in the outer macro environment */ + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CMDLINE, ""); +} + +/*$ J jump + * [position]J -- Go to position in buffer + * [position]:J -> Success|Failure + * + * Sets dot to . + * If is omitted, 0 is implied and \(lqJ\(rq will + * go to the beginning of the buffer. + * + * If is outside the range of the buffer, the + * command yields an error. + * If colon-modified, the command will instead return a + * condition boolean signalling whether the position could + * be changed or not. + */ +static void +teco_state_start_jump(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, 0, error)) + return; + + if (teco_validate_pos(v)) { + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_GOTOPOS, + teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0); + teco_interface_ssm(SCI_GOTOPOS, v, 0); + + if (teco_machine_main_eval_colon(ctx)) + teco_expressions_push(TECO_SUCCESS); + } else if (teco_machine_main_eval_colon(ctx)) { + teco_expressions_push(TECO_FAILURE); + } else { + teco_error_move_set(error, "J"); + return; + } +} + +static teco_bool_t +teco_move_chars(teco_int_t n) +{ + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + + if (!teco_validate_pos(pos + n)) + return TECO_FAILURE; + + teco_interface_ssm(SCI_GOTOPOS, pos + n, 0); + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); + + return TECO_SUCCESS; +} + +/*$ C move + * [n]C -- Move dot characters + * -C + * [n]:C -> Success|Failure + * + * Adds to dot. 1 or -1 is implied if is omitted. + * Fails if would move dot off-page. + * The colon modifier results in a success-boolean being + * returned instead. + */ +static void +teco_state_start_move(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + teco_bool_t rc = teco_move_chars(v); + if (teco_machine_main_eval_colon(ctx)) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_move_set(error, "C"); + return; + } +} + +/*$ R reverse + * [n]R -- Move dot characters backwards + * -R + * [n]:R -> Success|Failure + * + * Subtracts from dot. + * It is equivalent to \(lq-nC\(rq. + */ +static void +teco_state_start_reverse(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + teco_bool_t rc = teco_move_chars(-v); + if (teco_machine_main_eval_colon(ctx)) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_move_set(error, "R"); + return; + } +} + +static teco_bool_t +teco_move_lines(teco_int_t n) +{ + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + sptr_t line = teco_interface_ssm(SCI_LINEFROMPOSITION, pos, 0) + n; + + if (!teco_validate_line(line)) + return TECO_FAILURE; + + teco_interface_ssm(SCI_GOTOLINE, line, 0); + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); + + return TECO_SUCCESS; +} + +/*$ L line + * [n]L -- Move dot lines forwards + * -L + * [n]:L -> Success|Failure + * + * Move dot to the beginning of the line specified + * relatively to the current line. + * Therefore a value of 0 for goes to the + * beginning of the current line, 1 will go to the + * next line, -1 to the previous line etc. + * If is omitted, 1 or -1 is implied depending on + * the sign prefix. + * + * If would move dot off-page, the command yields + * an error. + * The colon-modifer results in a condition boolean + * being returned instead. + */ +static void +teco_state_start_line(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + teco_bool_t rc = teco_move_lines(v); + if (teco_machine_main_eval_colon(ctx)) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_move_set(error, "L"); + return; + } +} + +/*$ B backwards + * [n]B -- Move dot lines backwards + * -B + * [n]:B -> Success|Failure + * + * Move dot to the beginning of the line + * lines before the current one. + * It is equivalent to \(lq-nL\(rq. + */ +static void +teco_state_start_back(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + teco_bool_t rc = teco_move_lines(-v); + if (teco_machine_main_eval_colon(ctx)) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_move_set(error, "B"); + return; + } +} + +/*$ W word + * [n]W -- Move dot by words + * -W + * [n]:W -> Success|Failure + * + * Move dot words forward. + * - If is positive, dot is positioned at the beginning + * of the word words after the current one. + * - If is negative, dot is positioned at the end + * of the word words before the current one. + * - If is zero, dot is not moved. + * + * \(lqW\(rq uses Scintilla's definition of a word as + * configurable using the + * .B SCI_SETWORDCHARS + * message. + * + * Otherwise, the command's behaviour is analogous to + * the \(lqC\(rq command. + */ +static void +teco_state_start_word(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + + /* + * FIXME: would be nice to do this with constant amount of + * editor messages. E.g. by using custom algorithm accessing + * the internal document buffer. + */ + unsigned int msg = SCI_WORDRIGHTEND; + if (v < 0) { + v *= -1; + msg = SCI_WORDLEFTEND; + } + while (v--) { + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_interface_ssm(msg, 0, 0); + if (pos == teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0)) + break; + } + if (v < 0) { + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); + if (teco_machine_main_eval_colon(ctx)) + teco_expressions_push(TECO_SUCCESS); + } else { + teco_interface_ssm(SCI_GOTOPOS, pos, 0); + if (!teco_machine_main_eval_colon(ctx)) { + teco_error_move_set(error, "W"); + return; + } + teco_expressions_push(TECO_FAILURE); + } +} + +static teco_bool_t +teco_delete_words(teco_int_t n) +{ + sptr_t pos, size; + + if (!n) + return TECO_SUCCESS; + + pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + size = teco_interface_ssm(SCI_GETLENGTH, 0, 0); + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + /* + * FIXME: would be nice to do this with constant amount of + * editor messages. E.g. by using custom algorithm accessing + * the internal document buffer. + */ + if (n > 0) { + while (n--) { + sptr_t size = teco_interface_ssm(SCI_GETLENGTH, 0, 0); + teco_interface_ssm(SCI_DELWORDRIGHTEND, 0, 0); + if (size == teco_interface_ssm(SCI_GETLENGTH, 0, 0)) + break; + } + } else { + n *= -1; + while (n--) { + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + //teco_interface_ssm(SCI_DELWORDLEFTEND, 0, 0); + teco_interface_ssm(SCI_WORDLEFTEND, 0, 0); + if (pos == teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0)) + break; + teco_interface_ssm(SCI_DELWORDRIGHTEND, 0, 0); + } + } + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + + if (n >= 0) { + if (size != teco_interface_ssm(SCI_GETLENGTH, 0, 0)) { + teco_interface_ssm(SCI_UNDO, 0, 0); + teco_interface_ssm(SCI_GOTOPOS, pos, 0); + } + return TECO_FAILURE; + } + + undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + teco_ring_dirtify(); + + return TECO_SUCCESS; +} + +/*$ V + * [n]V -- Delete words forward + * -V + * [n]:V -> Success|Failure + * + * Deletes the next words until the end of the + * n'th word after the current one. + * If is negative, deletes up to end of the + * n'th word before the current one. + * If is omitted, 1 or -1 is implied depending on the + * sign prefix. + * + * It uses Scintilla's definition of a word as configurable + * using the + * .B SCI_SETWORDCHARS + * message. + * + * If the words to delete extend beyond the range of the + * buffer, the command yields an error. + * If colon-modified it instead returns a condition code. + */ +static void +teco_state_start_delete_words(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + teco_bool_t rc = teco_delete_words(v); + if (teco_machine_main_eval_colon(ctx)) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_words_set(error, "V"); + return; + } +} + +/*$ Y + * [n]Y -- Delete word backwards + * -Y + * [n]:Y -> Success|Failure + * + * Delete words backward. + * Y is equivalent to \(lq-nV\(rq. + */ +static void +teco_state_start_delete_words_back(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + teco_bool_t rc = teco_delete_words(-v); + if (teco_machine_main_eval_colon(ctx)) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_words_set(error, "Y"); + return; + } +} + +/*$ "=" print + * = -- Show value as message + * + * Shows integer as a message in the message line and/or + * on the console. + * It is currently always formatted as a decimal integer and + * shown with the user-message severity. + * The command fails if is not given. + */ +/** + * @todo perhaps care about current radix + * @todo colon-modifier to suppress line-break on console? + */ +static void +teco_state_start_print(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + if (!teco_expressions_args()) { + teco_error_argexpected_set(error, "="); + return; + } + teco_int_t v; + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + teco_interface_msg(TECO_MSG_USER, "%" TECO_INT_FORMAT, v); +} + +static gboolean +teco_state_start_kill(teco_machine_main_t *ctx, const gchar *cmd, gboolean by_lines, GError **error) +{ + teco_bool_t rc; + teco_int_t from, len; + + if (!teco_expressions_eval(FALSE, error)) + return FALSE; + + if (teco_expressions_args() <= 1) { + from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + if (by_lines) { + teco_int_t line; + if (!teco_expressions_pop_num_calc(&line, teco_num_sign, error)) + return FALSE; + line += teco_interface_ssm(SCI_LINEFROMPOSITION, from, 0); + len = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0) - from; + rc = teco_bool(teco_validate_line(line)); + } else { + if (!teco_expressions_pop_num_calc(&len, teco_num_sign, error)) + return FALSE; + rc = teco_bool(teco_validate_pos(from + len)); + } + if (len < 0) { + len *= -1; + from -= len; + } + } else { + teco_int_t to = teco_expressions_pop_num(0); + from = teco_expressions_pop_num(0); + len = to - from; + rc = teco_bool(len >= 0 && teco_validate_pos(from) && + teco_validate_pos(to)); + } + + if (teco_machine_main_eval_colon(ctx)) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_range_set(error, cmd); + return FALSE; + } + + if (len == 0 || teco_is_failure(rc)) + return TRUE; + + if (teco_current_doc_must_undo()) { + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + } + + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + teco_interface_ssm(SCI_DELETERANGE, from, len); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + teco_ring_dirtify(); + + return TRUE; +} + +/*$ K kill + * [n]K -- Kill lines + * -K + * from,to K + * [n]:K -> Success|Failure + * from,to:K -> Success|Failure + * + * Deletes characters up to the beginning of the + * line lines after or before the current one. + * If is 0, \(lqK\(rq will delete up to the beginning + * of the current line. + * If is omitted, the sign prefix will be implied. + * So to delete the entire line regardless of the position + * in it, one can use \(lq0KK\(rq. + * + * If the deletion is beyond the buffer's range, the command + * will yield an error unless it has been colon-modified + * so it returns a condition code. + * + * If two arguments and are available, the + * command is synonymous to ,D. + */ +static void +teco_state_start_kill_lines(teco_machine_main_t *ctx, GError **error) +{ + teco_state_start_kill(ctx, "K", TRUE, error); +} + +/*$ D delete + * [n]D -- Delete characters + * -D + * from,to D + * [n]:D -> Success|Failure + * from,to:D -> Success|Failure + * + * If is positive, the next characters (up to and + * character .+) are deleted. + * If is negative, the previous characters are + * deleted. + * If is omitted, the sign prefix will be implied. + * + * If two arguments can be popped from the stack, the + * command will delete the characters with absolute + * position up to from the current buffer. + * + * If the character range to delete is beyond the buffer's + * range, the command will yield an error unless it has + * been colon-modified so it returns a condition code + * instead. + */ +static void +teco_state_start_delete_chars(teco_machine_main_t *ctx, GError **error) +{ + teco_state_start_kill(ctx, "D", FALSE, error); +} + +/*$ A + * [n]A -> code -- Get character code from buffer + * -A -> code + * + * Returns the character of the character + * relative to dot from the buffer. + * This can be an ASCII or Unicode codepoint + * depending on Scintilla's encoding of the current + * buffer. + * - If is 0, return the of the character + * pointed to by dot. + * - If is 1, return the of the character + * immediately after dot. + * - If is -1, return the of the character + * immediately preceding dot, ecetera. + * - If is omitted, the sign prefix is implied. + * + * If the position of the queried character is off-page, + * the command will yield an error. + */ +/** @todo does Scintilla really return code points??? */ +static void +teco_state_start_get(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + v += teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + /* + * NOTE: We cannot use teco_validate_pos() here since + * the end of the buffer is not a valid position for . + */ + if (v < 0 || v >= teco_interface_ssm(SCI_GETLENGTH, 0, 0)) { + teco_error_range_set(error, "A"); + return; + } + teco_expressions_push(teco_interface_ssm(SCI_GETCHARAT, v, 0)); +} + +static teco_state_t * +teco_state_start_input(teco_machine_main_t *ctx, gchar chr, GError **error) +{ + static teco_machine_main_transition_t transitions[] = { + /* + * Simple transitions + */ + ['$'] = {&teco_state_escape}, + ['!'] = {&teco_state_label}, + ['O'] = {&teco_state_goto}, + ['^'] = {&teco_state_control}, + ['F'] = {&teco_state_fcommand}, + ['"'] = {&teco_state_condcommand}, + ['E'] = {&teco_state_ecommand}, + ['I'] = {&teco_state_insert_building}, + ['?'] = {&teco_state_help}, + ['S'] = {&teco_state_search}, + ['N'] = {&teco_state_search_all}, + + ['['] = {&teco_state_pushqreg}, + [']'] = {&teco_state_popqreg}, + ['G'] = {&teco_state_getqregstring}, + ['Q'] = {&teco_state_queryqreg}, + ['U'] = {&teco_state_setqreginteger}, + ['%'] = {&teco_state_increaseqreg}, + ['M'] = {&teco_state_macro}, + ['X'] = {&teco_state_copytoqreg}, + + /* + * Arithmetics + */ + ['*'] = {&teco_state_start, teco_state_start_mul}, + ['/'] = {&teco_state_start, teco_state_start_div}, + ['+'] = {&teco_state_start, teco_state_start_plus}, + ['-'] = {&teco_state_start, teco_state_start_minus}, + ['&'] = {&teco_state_start, teco_state_start_and}, + ['#'] = {&teco_state_start, teco_state_start_or}, + ['('] = {&teco_state_start, teco_state_start_brace_open}, + [')'] = {&teco_state_start, teco_state_start_brace_close}, + [','] = {&teco_state_start, teco_state_start_comma}, + + ['.'] = {&teco_state_start, teco_state_start_dot}, + ['Z'] = {&teco_state_start, teco_state_start_zed}, + ['H'] = {&teco_state_start, teco_state_start_range}, + ['\\'] = {&teco_state_start, teco_state_start_backslash}, + + /* + * Control Structures (loops) + */ + ['<'] = {&teco_state_start, teco_state_start_loop_open}, + ['>'] = {&teco_state_start, teco_state_start_loop_close}, + [';'] = {&teco_state_start, teco_state_start_break}, + + /* + * Command-line Editing + */ + ['{'] = {&teco_state_start, teco_state_start_cmdline_push}, + ['}'] = {&teco_state_start, teco_state_start_cmdline_pop}, + + /* + * Commands + */ + ['J'] = {&teco_state_start, teco_state_start_jump}, + ['C'] = {&teco_state_start, teco_state_start_move}, + ['R'] = {&teco_state_start, teco_state_start_reverse}, + ['L'] = {&teco_state_start, teco_state_start_line}, + ['B'] = {&teco_state_start, teco_state_start_back}, + ['W'] = {&teco_state_start, teco_state_start_word}, + ['V'] = {&teco_state_start, teco_state_start_delete_words}, + ['Y'] = {&teco_state_start, teco_state_start_delete_words_back}, + ['='] = {&teco_state_start, teco_state_start_print}, + ['K'] = {&teco_state_start, teco_state_start_kill_lines}, + ['D'] = {&teco_state_start, teco_state_start_delete_chars}, + ['A'] = {&teco_state_start, teco_state_start_get} + }; + + switch (chr) { + /* + * No-ops: + * These are explicitly not handled in teco_state_control, + * so that we can potentially reuse the upcaret notations like ^J. + */ + case ' ': + case '\f': + case '\r': + case '\n': + case '\v': + return &teco_state_start; + + /*$ 0 1 2 3 4 5 6 7 8 9 digit number + * [n]0|1|2|3|4|5|6|7|8|9 -> n*Radix+X -- Append digit + * + * Integer constants in \*(ST may be thought of and are + * technically sequences of single-digit commands. + * These commands take one argument from the stack + * (0 is implied), multiply it with the current radix + * (2, 8, 10, 16, ...), add the digit's value and + * return the resultant integer. + * + * The command-like semantics of digits may be abused + * in macros, for instance to append digits to computed + * integers. + * It is not an error to append a digit greater than the + * current radix - this may be changed in the future. + */ + case '0' ... '9': + if (ctx->mode == TECO_MODE_NORMAL) + teco_expressions_add_digit(chr); + return &teco_state_start; + + case '*': + /* + * Special save last commandline command + * + * FIXME: Maybe, there should be a special teco_state_t + * for beginnings of command-lines? + * It could also be used for a corresponding FNMACRO mask. + */ + if (teco_cmdline.effective_len == 1 && teco_cmdline.str.data[0] == '*') + return &teco_state_save_cmdline; + break; + + case '<': + if (ctx->mode != TECO_MODE_PARSE_ONLY_LOOP) + break; + if (ctx->parent.must_undo) + teco_undo_gint(ctx->nest_level); + ctx->nest_level++; + return &teco_state_start; + + case '>': + if (ctx->mode != TECO_MODE_PARSE_ONLY_LOOP) + break; + if (!ctx->nest_level) { + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->mode = TECO_MODE_NORMAL; + } else { + if (ctx->parent.must_undo) + teco_undo_gint(ctx->nest_level); + ctx->nest_level--; + } + return &teco_state_start; + + /* + * Control Structures (conditionals) + */ + case '|': + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + if (ctx->mode == TECO_MODE_PARSE_ONLY_COND && !ctx->nest_level) + ctx->mode = TECO_MODE_NORMAL; + else if (ctx->mode == TECO_MODE_NORMAL) + /* skip to end of conditional; skip ELSE-part */ + ctx->mode = TECO_MODE_PARSE_ONLY_COND; + return &teco_state_start; + + case '\'': + switch (ctx->mode) { + case TECO_MODE_PARSE_ONLY_COND: + case TECO_MODE_PARSE_ONLY_COND_FORCE: + if (!ctx->nest_level) { + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->mode = TECO_MODE_NORMAL; + } else { + if (ctx->parent.must_undo) + teco_undo_gint(ctx->nest_level); + ctx->nest_level--; + } + break; + default: + break; + } + return &teco_state_start; + + /* + * Modifiers + */ + case '@': + /* + * @ modifier has syntactic significance, so set it even + * in PARSE_ONLY* modes + */ + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->modifier_at = TRUE; + return &teco_state_start; + + case ':': + if (ctx->mode == TECO_MODE_NORMAL) { + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->modifier_colon = TRUE; + } + return &teco_state_start; + + default: + /* + * commands implemented in teco_state_control + */ + if (TECO_IS_CTL(chr)) + return teco_state_control_input(ctx, TECO_CTL_ECHO(chr), error); + } + + return teco_machine_main_transition_input(ctx, transitions, G_N_ELEMENTS(transitions), + teco_ascii_toupper(chr), error); +} + +TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_start, + .end_of_macro_cb = NULL, /* Allowed at the end of a macro! */ + .is_start = TRUE, + .fnmacro_mask = TECO_FNMACRO_MASK_START +); + +/*$ F< + * F< -- Go to loop start or jump to beginning of macro + * + * Immediately jumps to the current loop's start. + * Also works from inside conditionals. + * + * Outside of loops \(em or in a macro without + * a loop \(em this jumps to the beginning of the macro. + */ +static void +teco_state_fcommand_loop_start(teco_machine_main_t *ctx, GError **error) +{ + /* FIXME: what if in brackets? */ + if (!teco_expressions_discard_args(error)) + return; + + ctx->macro_pc = teco_loop_stack->len > ctx->loop_stack_fp + ? g_array_index(teco_loop_stack, teco_loop_context_t, teco_loop_stack->len-1).pc : -1; +} + +/*$ F> continue + * F> -- Go to loop end + * :F> + * + * Jumps to the current loop's end. + * If the loop has remaining iterations or runs indefinitely, + * the jump is performed immediately just as if \(lq>\(rq + * had been executed. + * If the loop has reached its last iteration, \*(ST will + * parse until the loop end command has been found and control + * resumes after the end of the loop. + * + * In interactive mode, if the loop is incomplete and must + * be exited, you can type in the loop's remaining commands + * without them being executed (but they are parsed). + * + * When colon-modified, \fB:F>\fP behaves like \fB:>\fP + * and allows numbers to be aggregated on the stack. + * + * Calling \fBF>\fP outside of a loop at the current + * macro invocation level will throw an error. + */ +static void +teco_state_fcommand_loop_end(teco_machine_main_t *ctx, GError **error) +{ + guint old_len = teco_loop_stack->len; + + /* + * NOTE: This is almost identical to the normal + * loop end since we don't really want to or need to + * parse till the end of the loop. + */ + g_assert(error != NULL); + teco_state_start_loop_close(ctx, error); + if (*error) + return; + + if (teco_loop_stack->len < old_len) { + /* skip to end of loop */ + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->mode = TECO_MODE_PARSE_ONLY_LOOP; + } +} + +/*$ "F'" + * F\' -- Jump to end of conditional + */ +static void +teco_state_fcommand_cond_end(teco_machine_main_t *ctx, GError **error) +{ + /* skip to end of conditional, also including any else-clause */ + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->mode = TECO_MODE_PARSE_ONLY_COND_FORCE; +} + +/*$ F| + * F| -- Jump to else-part of conditional + * + * Jump to else-part of conditional or end of + * conditional (only if invoked from inside the + * condition's else-part). + */ +static void +teco_state_fcommand_cond_else(teco_machine_main_t *ctx, GError **error) +{ + /* skip to ELSE-part or end of conditional */ + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->mode = TECO_MODE_PARSE_ONLY_COND; +} + +static teco_state_t * +teco_state_fcommand_input(teco_machine_main_t *ctx, gchar chr, GError **error) +{ + static teco_machine_main_transition_t transitions[] = { + /* + * Simple transitions + */ + ['K'] = {&teco_state_search_kill}, + ['D'] = {&teco_state_search_delete}, + ['S'] = {&teco_state_replace}, + ['R'] = {&teco_state_replace_default}, + ['G'] = {&teco_state_changedir}, + + /* + * Loop Flow Control + */ + ['<'] = {&teco_state_start, teco_state_fcommand_loop_start}, + ['>'] = {&teco_state_start, teco_state_fcommand_loop_end}, + + /* + * Conditional Flow Control + */ + ['\''] = {&teco_state_start, teco_state_fcommand_cond_end}, + ['|'] = {&teco_state_start, teco_state_fcommand_cond_else} + }; + + return teco_machine_main_transition_input(ctx, transitions, G_N_ELEMENTS(transitions), + teco_ascii_toupper(chr), error); +} + +TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_fcommand); + +static void +teco_undo_change_dir_action(gchar **dir, gboolean run) +{ + /* + * Changing the directory on rub-out may fail. + * This is handled silently. + */ + if (run) + g_chdir(*dir); + g_free(*dir); +} + +void +teco_undo_change_dir_to_current(void) +{ + gchar **ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_change_dir_action, + sizeof(gchar *)); + if (ctx) + *ctx = g_get_current_dir(); +} + +static teco_state_t * +teco_state_changedir_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + g_autofree gchar *dir = teco_file_expand_path(str->data); + if (!*dir) { + teco_qreg_t *qreg = teco_qreg_table_find(&teco_qreg_table_globals, "$HOME", 5); + g_assert(qreg != NULL); + teco_string_t home; + if (!qreg->vtable->get_string(qreg, &home.data, &home.len, error)) + return NULL; + + /* + * Null-characters must not occur in file names. + */ + if (teco_string_contains(&home, '\0')) { + teco_string_clear(&home); + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Null-character not allowed in filenames"); + return NULL; + } + + g_free(dir); + dir = home.data; + } + + teco_undo_change_dir_to_current(); + + if (g_chdir(dir)) { + /* FIXME: Is errno usable on Windows here? */ + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Cannot change working directory to \"%s\"", dir); + return NULL; + } + + return &teco_state_start; +} + +/*$ FG cd change-dir folder-go + * FG[directory]$ -- Change working directory + * + * Changes the process' current working directory + * to which affects all subsequent + * operations on relative file names like + * tab-completions. + * It is also inherited by external processes spawned + * via \fBEC\fP and \fBEG\fP. + * + * If is omitted, the working directory + * is changed to the current user's home directory + * as set by the \fBHOME\fP environment variable + * (i.e. its corresponding \(lq$HOME\(rq environment + * register). + * This variable is always initialized by \*(ST + * (see \fBsciteco\fP(1)). + * Therefore the expression \(lqFG\fB$\fP\(rq is + * exactly equivalent to both \(lqFG~\fB$\fP\(rq and + * \(lqFG^EQ[$HOME]\fB$\fP\(rq. + * + * The current working directory is also mapped to + * the special global Q-Register \(lq$\(rq (dollar sign) + * which may be used retrieve the current working directory. + * + * String-building characters are enabled on this + * command and directories can be tab-completed. + */ +TECO_DEFINE_STATE_EXPECTDIR(teco_state_changedir); + +static teco_state_t * +teco_state_condcommand_input(teco_machine_main_t *ctx, gchar chr, GError **error) +{ + teco_int_t value = 0; + gboolean result = TRUE; + + switch (ctx->mode) { + case TECO_MODE_PARSE_ONLY_COND: + case TECO_MODE_PARSE_ONLY_COND_FORCE: + if (ctx->parent.must_undo) + teco_undo_gint(ctx->nest_level); + ctx->nest_level++; + break; + + case TECO_MODE_NORMAL: + if (!teco_expressions_eval(FALSE, error)) + return NULL; + + if (chr == '~') + /* don't pop value for ~ conditionals */ + break; + + if (!teco_expressions_args()) { + teco_error_argexpected_set(error, "\""); + return NULL; + } + if (!teco_expressions_pop_num_calc(&value, 0, error)) + return NULL; + break; + + default: + break; + } + + switch (teco_ascii_toupper(chr)) { + case '~': + if (ctx->mode == TECO_MODE_NORMAL) + result = !teco_expressions_args(); + break; + case 'A': + if (ctx->mode == TECO_MODE_NORMAL) + result = g_ascii_isalpha((gchar)value); + break; + case 'C': + if (ctx->mode == TECO_MODE_NORMAL) + result = g_ascii_isalnum((gchar)value) || + value == '.' || value == '$' || value == '_'; + break; + case 'D': + if (ctx->mode == TECO_MODE_NORMAL) + result = g_ascii_isdigit((gchar)value); + break; + case 'I': + if (ctx->mode == TECO_MODE_NORMAL) + result = G_IS_DIR_SEPARATOR((gchar)value); + break; + case 'S': + case 'T': + if (ctx->mode == TECO_MODE_NORMAL) + result = teco_is_success(value); + break; + case 'F': + case 'U': + if (ctx->mode == TECO_MODE_NORMAL) + result = teco_is_failure(value); + break; + case 'E': + case '=': + if (ctx->mode == TECO_MODE_NORMAL) + result = value == 0; + break; + case 'G': + case '>': + if (ctx->mode == TECO_MODE_NORMAL) + result = value > 0; + break; + case 'L': + case '<': + if (ctx->mode == TECO_MODE_NORMAL) + result = value < 0; + break; + case 'N': + if (ctx->mode == TECO_MODE_NORMAL) + result = value != 0; + break; + case 'R': + if (ctx->mode == TECO_MODE_NORMAL) + result = g_ascii_isalnum((gchar)value); + break; + case 'V': + if (ctx->mode == TECO_MODE_NORMAL) + result = g_ascii_islower((gchar)value); + break; + case 'W': + if (ctx->mode == TECO_MODE_NORMAL) + result = g_ascii_isupper((gchar)value); + break; + default: + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Invalid conditional type \"%c\"", chr); + return NULL; + } + + if (!result) { + /* skip to ELSE-part or end of conditional */ + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->mode = TECO_MODE_PARSE_ONLY_COND; + } + + return &teco_state_start; +} + +TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_condcommand); + +/*$ ^_ negate + * n^_ -> ~n -- Binary negation + * + * Binary negates (complements) and returns + * the result. + * Binary complements are often used to negate + * \*(ST booleans. + */ +static void +teco_state_control_negate(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_args()) { + teco_error_argexpected_set(error, "^_"); + return; + } + if (!teco_expressions_pop_num_calc(&v, 0, error)) + return; + teco_expressions_push(~v); +} + +static void +teco_state_control_pow(teco_machine_main_t *ctx, GError **error) +{ + teco_expressions_push_calc(TECO_OP_POW, error); +} + +static void +teco_state_control_mod(teco_machine_main_t *ctx, GError **error) +{ + teco_expressions_push_calc(TECO_OP_MOD, error); +} + +static void +teco_state_control_xor(teco_machine_main_t *ctx, GError **error) +{ + teco_expressions_push_calc(TECO_OP_XOR, error); +} + +/*$ ^C exit + * ^C -- Exit program immediately + * + * Lets the top-level macro return immediately + * regardless of the current macro invocation frame. + * This command is only allowed in batch mode, + * so it is not invoked accidentally when using + * the CTRL+C immediate editing command to + * interrupt long running operations. + * When using \fB^C\fP in a munged file, + * interactive mode is never started, so it behaves + * effectively just like \(lq-EX\fB$$\fP\(rq + * (when executed in the top-level macro at least). + * + * The \fBquit\fP hook is still executed. + */ +static void +teco_state_control_exit(teco_machine_main_t *ctx, GError **error) +{ + if (teco_undo_enabled) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "<^C> not allowed in interactive mode"); + return; + } + + teco_quit_requested = TRUE; + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, ""); +} + +/*$ ^O octal + * ^O -- Set radix to 8 (octal) + */ +static void +teco_state_control_octal(teco_machine_main_t *ctx, GError **error) +{ + teco_set_radix(8); +} + +/*$ ^D decimal + * ^D -- Set radix to 10 (decimal) + */ +static void +teco_state_control_decimal(teco_machine_main_t *ctx, GError **error) +{ + teco_set_radix(10); +} + +/*$ ^R radix + * radix^R -- Set and get radix + * ^R -> radix + * + * Set current radix to arbitrary value . + * If is omitted, the command instead + * returns the current radix. + */ +static void +teco_state_control_radix(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + if (!teco_expressions_args()) { + teco_expressions_push(teco_radix); + } else { + teco_int_t v; + if (!teco_expressions_pop_num_calc(&v, 0, error)) + return; + teco_set_radix(v); + } +} + +static teco_state_t * +teco_state_control_input(teco_machine_main_t *ctx, gchar chr, GError **error) +{ + static teco_machine_main_transition_t transitions[] = { + /* + * Simple transitions + */ + ['I'] = {&teco_state_insert_indent}, + ['U'] = {&teco_state_ctlucommand}, + ['^'] = {&teco_state_ascii}, + ['['] = {&teco_state_escape}, + + /* + * Additional numeric operations + */ + ['_'] = {&teco_state_start, teco_state_control_negate}, + ['*'] = {&teco_state_start, teco_state_control_pow}, + ['/'] = {&teco_state_start, teco_state_control_mod}, + ['#'] = {&teco_state_start, teco_state_control_xor}, + + /* + * Commands + */ + ['C'] = {&teco_state_start, teco_state_control_exit}, + ['O'] = {&teco_state_start, teco_state_control_octal}, + ['D'] = {&teco_state_start, teco_state_control_decimal}, + ['R'] = {&teco_state_start, teco_state_control_radix} + }; + + /* + * FIXME: Should we return a special syntax error in case of failure? + * Currently you get error messages like 'Syntax error "F"' for ^F. + * The easiest way around would be g_prefix_error(error, "Control command"); + */ + return teco_machine_main_transition_input(ctx, transitions, G_N_ELEMENTS(transitions), + teco_ascii_toupper(chr), error); +} + +TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_control); + +static teco_state_t * +teco_state_ascii_input(teco_machine_main_t *ctx, gchar chr, GError **error) +{ + if (ctx->mode == TECO_MODE_NORMAL) + teco_expressions_push(chr); + + return &teco_state_start; +} + +/*$ ^^ ^^c + * ^^c -> n -- Get ASCII code of character + * + * Returns the ASCII code of the character + * that is part of the command. + * Can be used in place of integer constants for improved + * readability. + * For instance ^^A will return 65. + * + * Note that this command can be typed CTRL+Caret or + * Caret-Caret. + */ +TECO_DEFINE_STATE(teco_state_ascii); + +/* + * The Escape state is special, as it implements + * a kind of "lookahead" for the ^[ command (discard all + * arguments). + * It is not executed immediately as usual in SciTECO + * but only if not followed by an escape character. + * This is necessary since $$ is the macro return + * and command-line termination command and it must not + * discard arguments. + * Deferred execution of ^[ is possible since it does + * not have any visible side-effects - its effects can + * only be seen when executing the following command. + */ +static teco_state_t * +teco_state_escape_input(teco_machine_main_t *ctx, gchar chr, GError **error) +{ + /*$ ^[^[ ^[$ $$ terminate return + * [a1,a2,...]$$ -- Terminate command line or return from macro + * [a1,a2,...]^[$ + * + * Returns from the current macro invocation. + * This will pass control to the calling macro immediately + * and is thus faster than letting control reach the macro's end. + * Also, direct arguments to \fB$$\fP will be left on the expression + * stack when the macro returns. + * \fB$$\fP closes loops automatically and is thus safe to call + * from loop bodies. + * Furthermore, it has defined semantics when executed + * from within braced expressions: + * All braces opened in the current macro invocation will + * be closed and their values discarded. + * Only the direct arguments to \fB$$\fP will be kept. + * + * Returning from the top-level macro in batch mode + * will exit the program or start up interactive mode depending + * on whether program exit has been requested. + * \(lqEX\fB$$\fP\(rq is thus a common idiom to exit + * prematurely. + * + * In interactive mode, returning from the top-level macro + * (i.e. typing \fB$$\fP at the command line) has the + * effect of command line termination. + * The arguments to \fB$$\fP are currently not used + * when terminating a command line \(em the new command line + * will always start with a clean expression stack. + * + * The first \fIescape\fP of \fB$$\fP may be typed either + * as an escape character (ASCII 27), in up-arrow mode + * (e.g. \fB^[$\fP) or as a dollar character \(em the + * second character must be either a real escape character + * or a dollar character. + */ + if (chr == '\e' || chr == '$') { + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + ctx->parent.current = &teco_state_start; + if (!teco_expressions_eval(FALSE, error)) + return NULL; + teco_error_return_set(error, teco_expressions_args()); + return NULL; + } + + /* + * Alternatives: ^[, , , $ (dollar) + */ + /*$ ^[ $ escape discard + * $ -- Discard all arguments + * ^[ + * + * Pops and discards all values from the stack that + * might otherwise be used as arguments to following + * commands. + * Therefore it stops popping on stack boundaries like + * they are introduced by arithmetic brackets or loops. + * + * Note that ^[ is usually typed using the Escape key. + * CTRL+[ however is possible as well and equivalent to + * Escape in every manner. + * The up-arrow notation however is processed like any + * ordinary command and only works at the begining of + * a command. + * Additionally, this command may be written as a single + * dollar character. + */ + if (ctx->mode == TECO_MODE_NORMAL && + !teco_expressions_discard_args(error)) + return NULL; + return teco_state_start_input(ctx, chr, error); +} + +static gboolean +teco_state_escape_end_of_macro(teco_machine_t *ctx, GError **error) +{ + /* + * Due to the deferred nature of ^[, + * it is valid to end in the "escape" state. + */ + return teco_expressions_discard_args(error); +} + +TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_escape, + .end_of_macro_cb = teco_state_escape_end_of_macro, + /* + * The state should behave like teco_state_start + * when it comes to function key macro masking. + */ + .is_start = TRUE, + .fnmacro_mask = TECO_FNMACRO_MASK_START +); + +/*$ EF close + * [bool]EF -- Remove buffer from ring + * -EF + * + * Removes buffer from buffer ring, effectively + * closing it. + * If the buffer is dirty (modified), EF will yield + * an error. + * may be a specified to enforce closing dirty + * buffers. + * If it is a Failure condition boolean (negative), + * the buffer will be closed unconditionally. + * If is absent, the sign prefix (1 or -1) will + * be implied, so \(lq-EF\(rq will always close the buffer. + * + * It is noteworthy that EF will be executed immediately in + * interactive mode but can be rubbed out at a later time + * to reopen the file. + * Closed files are kept in memory until the command line + * is terminated. + */ +static void +teco_state_ecommand_close(teco_machine_main_t *ctx, GError **error) +{ + if (teco_qreg_current) { + const teco_string_t *name = &teco_qreg_current->head.name; + g_autofree gchar *name_printable = teco_string_echo(name->data, name->len); + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Q-Register \"%s\" currently edited", name_printable); + return; + } + + teco_int_t v; + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + if (teco_is_failure(v) && teco_ring_current->dirty) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Buffer \"%s\" is dirty", + teco_ring_current->filename ? : "(Unnamed)"); + return; + } + + teco_ring_close(error); +} + +/*$ ED flags + * flags ED -- Set and get ED-flags + * [off,]on ED + * ED -> flags + * + * With arguments, the command will set the \fBED\fP flags. + * is a bitmap of flags to set. + * Specifying one argument to set the flags is a special + * case of specifying two arguments that allow to control + * which flags to enable/disable. + * is a bitmap of flags to disable (set to 0 in ED + * flags) and is a bitmap of flags that is ORed into + * the flags variable. + * If is omitted, the value 0^_ is implied. + * In otherwords, all flags are turned off before turning + * on the flags. + * Without any argument ED returns the current flags. + * + * Currently, the following flags are used by \*(ST: + * - 8: Enable/disable automatic folding of case-insensitive + * command characters during interactive key translation. + * The case of letter keys is inverted, so one or two + * character commands will typically be inserted upper-case, + * but you can still press Shift to insert lower-case letters. + * Case-insensitive Q-Register specifications are not + * case folded. + * This is thought to improve the readability of the command + * line macro. + * - 16: Enable/disable automatic translation of end of + * line sequences to and from line feed. + * Disabling this flag allows 8-bit clean loading and saving + * of files. + * - 32: Enable/Disable buffer editing hooks + * (via execution of macro in global Q-Register \(lqED\(rq) + * - 64: Enable/Disable function key macros + * - 128: Enable/Disable enforcement of UNIX98 + * \(lq/bin/sh\(rq emulation for operating system command + * executions + * - 256: Enable/Disable \fBxterm\fP(1) clipboard support. + * Should only be enabled if XTerm allows the + * \fIGetSelection\fP and \fISetSelection\fP window + * operations. + * + * The features controlled thus are discribed in other sections + * of this manual. + * + * The default value of the \fBED\fP flags is 16 + * (only automatic EOL translation enabled). + */ +static void +teco_state_ecommand_flags(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + if (!teco_expressions_args()) { + teco_expressions_push(teco_ed); + } else { + teco_int_t on, off; + if (!teco_expressions_pop_num_calc(&on, 0, error) || + !teco_expressions_pop_num_calc(&off, ~(teco_int_t)0, error)) + return; + teco_undo_int(teco_ed) = (teco_ed & ~off) | on; + } +} + +/*$ EJ properties + * [key]EJ -> value -- Get and set system properties + * -EJ -> value + * value,keyEJ + * rgb,color,3EJ + * + * This command may be used to get and set system + * properties. + * With one argument, it retrieves a numeric property + * identified by \fIkey\fP. + * If \fIkey\fP is omitted, the prefix sign is implied + * (1 or -1). + * With two arguments, it sets property \fIkey\fP to + * \fIvalue\fP and returns nothing. Some property \fIkeys\fP + * may require more than one value. Properties may be + * write-only or read-only. + * + * The following property keys are defined: + * .IP 0 4 + * The current user interface: 1 for Curses, 2 for GTK + * (\fBread-only\fP) + * .IP 1 + * The current numbfer of buffers: Also the numeric id + * of the last buffer in the ring. This is implied if + * no argument is given, so \(lqEJ\(rq returns the number + * of buffers in the ring. + * (\fBread-only\fP) + * .IP 2 + * The current memory limit in bytes. + * This limit helps to prevent dangerous out-of-memory + * conditions (e.g. resulting from infinite loops) by + * constantly sampling the memory requirements of \*(ST. + * Note that not all platforms support precise measurements + * of the current memory usage \(em \*(ST will fall back + * to an approximation which might be less than the actual + * usage on those platforms. + * Memory limiting is effective in batch and interactive mode. + * Commands which would exceed that limit will fail instead + * allowing users to recover in interactive mode, e.g. by + * terminating the command line. + * When getting, a zero value indicates that memory limiting is + * disabled. + * Setting a value less than or equal to 0 as in + * \(lq0,2EJ\(rq disables the limit. + * \fBWarning:\fP Disabling memory limiting may provoke + * out-of-memory errors in long running or infinite loops + * (interactive mode) that result in abnormal program + * termination. + * Setting a new limit may fail if the current memory + * requirements are too large for the new limit \(em if + * this happens you may have to clear your command-line + * first. + * Memory limiting is enabled by default. + * .IP 3 + * This \fBwrite-only\fP property allows redefining the + * first 16 entries of the terminal color palette \(em a + * feature required by some + * color schemes when using the Curses user interface. + * When setting this property, you are making a request + * to define the terminal \fIcolor\fP as the Scintilla-compatible + * RGB color value given in the \fIrgb\fP parameter. + * \fIcolor\fP must be a value between 0 and 15 + * corresponding to black, red, green, yellow, blue, magenta, + * cyan, white, bright black, bright red, etc. in that order. + * The \fIrgb\fP value has the format 0xBBGGRR, i.e. the red + * component is the least-significant byte and all other bytes + * are ignored. + * Note that on curses, RGB color values sent to Scintilla + * are actually mapped to these 16 colors by the Scinterm port + * and may represent colors with no resemblance to the \(lqRGB\(rq + * value used (depending on the current palette) \(em they should + * instead be viewed as placeholders for 16 standard terminal + * color codes. + * Please refer to the Scinterm manual for details on the allowed + * \(lqRGB\(rq values and how they map to terminal colors. + * This command provides a crude way to request exact RGB colors + * for the first 16 terminal colors. + * The color definition may be queued or be completely ignored + * on other user interfaces and no feedback is given + * if it fails. In fact feedback cannot be given reliably anyway. + * Note that on 8 color terminals, only the first 8 colors + * can be redefined (if you are lucky). + * Note that due to restrictions of most terminal emulators + * and some curses implementations, this command simply will not + * restore the original palette entry or request + * when rubbed out and should generally only be used in + * \fIbatch-mode\fP \(em typically when loading a color scheme. + * For the same reasons \(em even though \*(ST tries hard to + * restore the original palette on exit \(em palette changes may + * persist after \*(ST terminates on most terminal emulators on Unix. + * The only emulator which will restore their default palette + * on exit the author is aware of is \fBxterm\fP(1) and + * the Linux console driver. + * You have been warned. Good luck. + */ +static void +teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error) +{ + enum { + EJ_USER_INTERFACE = 0, + EJ_BUFFERS, + EJ_MEMORY_LIMIT, + EJ_INIT_COLOR + }; + + teco_int_t property; + if (!teco_expressions_eval(FALSE, error) || + !teco_expressions_pop_num_calc(&property, teco_num_sign, error)) + return; + + if (teco_expressions_args() > 0) { + /* + * Set property + */ + teco_int_t value, color; + if (!teco_expressions_pop_num_calc(&value, 0, error)) + return; + + switch (property) { + case EJ_MEMORY_LIMIT: + if (!teco_memory_set_limit(MAX(0, value), error)) + return; + break; + + case EJ_INIT_COLOR: + if (value < 0 || value >= 16) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Invalid color code %" TECO_INT_FORMAT " " + "specified for ", value); + return; + } + if (!teco_expressions_args()) { + teco_error_argexpected_set(error, "EJ"); + return; + } + if (!teco_expressions_pop_num_calc(&color, 0, error)) + return; + teco_interface_init_color((guint)value, (guint32)color); + break; + + default: + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Cannot set property %" TECO_INT_FORMAT " " + "for ", property); + return; + } + + return; + } + + /* + * Get property + */ + switch (property) { + case EJ_USER_INTERFACE: + /* + * FIXME: Replace INTERFACE_* macros with + * teco_interface_id()? + */ +#ifdef INTERFACE_CURSES + teco_expressions_push(1); +#elif defined(INTERFACE_GTK) + teco_expressions_push(2); +#else +#error Missing value for current interface! +#endif + break; + + case EJ_BUFFERS: + teco_expressions_push(teco_ring_get_id(teco_ring_last())); + break; + + case EJ_MEMORY_LIMIT: + teco_expressions_push(teco_memory_limit); + break; + + default: + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Invalid property %" TECO_INT_FORMAT " " + "for ", property); + return; + } +} + +/*$ EL eol + * 0EL -- Set or get End of Line mode + * 13,10:EL + * 1EL + * 13:EL + * 2EL + * 10:EL + * EL -> 0 | 1 | 2 + * :EL -> 13,10 | 13 | 10 + * + * Sets or gets the current document's End Of Line (EOL) mode. + * This is a thin wrapper around Scintilla's + * \fBSCI_SETEOLMODE\fP and \fBSCI_GETEOLMODE\fP messages but is + * shorter to type and supports restoring the EOL mode upon rubout. + * Like the Scintilla message, does \fBnot\fP change the + * characters in the current document. + * If automatic EOL translation is activated (which is the default), + * \*(ST will however use this information when saving files or + * writing to external processes. + * + * With one argument, the EOL mode is set according to these + * constants: + * .IP 0 4 + * Carriage return (ASCII 13), followed by line feed (ASCII 10). + * This is the default EOL mode on DOS/Windows. + * .IP 1 + * Carriage return (ASCII 13). + * The default EOL mode on old Mac OS systems. + * .IP 2 + * Line feed (ASCII 10). + * The default EOL mode on POSIX/UNIX systems. + * + * In its colon-modified form, the EOL mode is set according + * to the EOL characters on the expression stack. + * \*(ST will only pop as many values as are necessary to + * determine the EOL mode. + * + * Without arguments, the current EOL mode is returned. + * When colon-modified, the current EOL mode's character sequence + * is pushed onto the expression stack. + */ +static void +teco_state_ecommand_eol(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + + if (teco_expressions_args() > 0) { + teco_int_t eol_mode; + + if (teco_machine_main_eval_colon(ctx)) { + teco_int_t v1, v2; + if (!teco_expressions_pop_num_calc(&v1, 0, error)) + return; + + switch (v1) { + case '\r': + eol_mode = SC_EOL_CR; + break; + case '\n': + if (!teco_expressions_args()) { + eol_mode = SC_EOL_LF; + break; + } + if (!teco_expressions_pop_num_calc(&v2, 0, error)) + return; + if (v2 == '\r') { + eol_mode = SC_EOL_CRLF; + break; + } + /* fall through */ + default: + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Invalid EOL sequence for "); + return; + } + } else { + if (!teco_expressions_pop_num_calc(&eol_mode, 0, error)) + return; + switch (eol_mode) { + case SC_EOL_CRLF: + case SC_EOL_CR: + case SC_EOL_LF: + break; + default: + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Invalid EOL mode %" TECO_INT_FORMAT " for ", + eol_mode); + return; + } + } + + undo__teco_interface_ssm(SCI_SETEOLMODE, + teco_interface_ssm(SCI_GETEOLMODE, 0, 0), 0); + teco_interface_ssm(SCI_SETEOLMODE, eol_mode, 0); + } else if (teco_machine_main_eval_colon(ctx)) { + const gchar *eol_seq = teco_eol_get_seq(teco_interface_ssm(SCI_GETEOLMODE, 0, 0)); + teco_expressions_push(eol_seq); + } else { + teco_expressions_push(teco_interface_ssm(SCI_GETEOLMODE, 0, 0)); + } +} + +/*$ EX exit + * [bool]EX -- Exit program + * -EX + * :EX + * + * Exits \*(ST, or rather requests program termination + * at the end of the top-level macro. + * Therefore instead of exiting immediately which + * could be annoying in interactive mode, EX will + * result in program termination only when the command line + * is terminated. + * This allows EX to be rubbed out and used in macros. + * The usual command to exit \*(ST in interactive mode + * is thus \(lqEX\fB$$\fP\(rq. + * In batch mode EX will exit the program if control + * reaches the end of the munged file \(em instead of + * starting up interactive mode. + * + * If any buffer is dirty (modified), EX will yield + * an error. + * When specifying as a success/truth condition + * boolean, EX will not check whether there are modified + * buffers and will always succeed. + * If is omitted, the sign prefix is implied + * (1 or -1). + * In other words \(lq-EX\fB$$\fP\(rq is the usual + * interactive command sequence to discard all unsaved + * changes and exit. + * + * When colon-modified, is ignored and EX + * will instead immediately try to save all modified buffers \(em + * this can of course be reversed using rubout. + * Saving all buffers can fail, e.g. if the unnamed file + * is modified or if there is an IO error. + * \(lq:EX\fB$$\fP\(rq is nevertheless the usual interactive + * command sequence to exit while saving all modified + * buffers. + */ +/** @fixme what if changing file after EX? will currently still exit */ +static void +teco_state_ecommand_exit(teco_machine_main_t *ctx, GError **error) +{ + if (teco_machine_main_eval_colon(ctx)) { + if (!teco_ring_save_all_dirty_buffers(error)) + return; + } else { + teco_int_t v; + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + if (teco_is_failure(v) && teco_ring_is_any_dirty()) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Modified buffers exist"); + return; + } + } + + teco_undo_gboolean(teco_quit_requested) = TRUE; +} + +static teco_state_t * +teco_state_ecommand_input(teco_machine_main_t *ctx, gchar chr, GError **error) +{ + static teco_machine_main_transition_t transitions[] = { + /* + * Simple Transitions + */ + ['%'] = {&teco_state_epctcommand}, + ['B'] = {&teco_state_edit_file}, + ['C'] = {&teco_state_execute}, + ['G'] = {&teco_state_egcommand}, + ['I'] = {&teco_state_insert_nobuilding}, + ['M'] = {&teco_state_macrofile}, + ['N'] = {&teco_state_glob_pattern}, + ['S'] = {&teco_state_scintilla_symbols}, + ['Q'] = {&teco_state_eqcommand}, + ['U'] = {&teco_state_eucommand}, + ['W'] = {&teco_state_save_file}, + + /* + * Commands + */ + ['F'] = {&teco_state_start, teco_state_ecommand_close}, + ['D'] = {&teco_state_start, teco_state_ecommand_flags}, + ['J'] = {&teco_state_start, teco_state_ecommand_properties}, + ['L'] = {&teco_state_start, teco_state_ecommand_eol}, + ['X'] = {&teco_state_start, teco_state_ecommand_exit} + }; + + /* + * FIXME: Should we return a special syntax error in case of failure? + */ + return teco_machine_main_transition_input(ctx, transitions, G_N_ELEMENTS(transitions), + teco_ascii_toupper(chr), error); +} + +TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_ecommand); + +gboolean +teco_state_insert_initial(teco_machine_main_t *ctx, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return TRUE; + + if (!teco_expressions_eval(FALSE, error)) + return FALSE; + guint args = teco_expressions_args(); + if (!args) + return TRUE; + + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + for (int i = args; i > 0; i--) { + gchar chr = (gchar)teco_expressions_peek_num(i-1); + teco_interface_ssm(SCI_ADDTEXT, 1, (sptr_t)&chr); + } + for (int i = args; i > 0; i--) + if (!teco_expressions_pop_num_calc(NULL, 0, error)) + return FALSE; + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + teco_ring_dirtify(); + + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + + return TRUE; +} + +gboolean +teco_state_insert_process(teco_machine_main_t *ctx, const teco_string_t *str, + gsize new_chars, GError **error) +{ + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + teco_interface_ssm(SCI_ADDTEXT, new_chars, + (sptr_t)(str->data + str->len - new_chars)); + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + teco_ring_dirtify(); + + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + + return TRUE; +} + +/* + * NOTE: cannot support VideoTECO's I because + * beginning and end of strings must be determined + * syntactically + */ +/*$ I insert + * [c1,c2,...]I[text]$ -- Insert text with string building characters + * + * First inserts characters for all the values + * on the argument stack (interpreted as codepoints). + * It does so in the order of the arguments, i.e. + * is inserted before , ecetera. + * Secondly, the command inserts . + * In interactive mode, is inserted interactively. + * + * String building characters are \fBenabled\fP for the + * I command. + * When editing \*(ST macros, using the \fBEI\fP command + * may be better, since it has string building characters + * disabled. + */ +TECO_DEFINE_STATE_INSERT(teco_state_insert_building); + +/*$ EI + * [c1,c2,...]EI[text]$ -- Insert text without string building characters + * + * Inserts text at the current position in the current + * document. + * This command is identical to the \fBI\fP command, + * except that string building characters are \fBdisabled\fP. + * Therefore it may be beneficial when editing \*(ST + * macros. + */ +TECO_DEFINE_STATE_INSERT(teco_state_insert_nobuilding, + .expectstring.string_building = FALSE +); + +static gboolean +teco_state_insert_indent_initial(teco_machine_main_t *ctx, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return TRUE; + + if (!teco_state_insert_initial(ctx, error)) + return FALSE; + + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + if (teco_interface_ssm(SCI_GETUSETABS, 0, 0)) { + teco_interface_ssm(SCI_ADDTEXT, 1, (sptr_t)"\t"); + } else { + gint len = teco_interface_ssm(SCI_GETTABWIDTH, 0, 0); + + len -= teco_interface_ssm(SCI_GETCOLUMN, + teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0) % len; + + gchar spaces[len]; + + memset(spaces, ' ', sizeof(spaces)); + teco_interface_ssm(SCI_ADDTEXT, sizeof(spaces), (sptr_t)spaces); + } + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + teco_ring_dirtify(); + + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + + return TRUE; +} + +/* + * Alternatives: ^i, ^I, , + */ +/*$ ^I indent + * [char,...]^I[text]$ -- Insert with leading indention + * + * ^I (usually typed using the Tab key), first inserts + * all the chars on the stack into the buffer, then indention + * characters (one tab or multiple spaces) and eventually + * the optional is inserted interactively. + * It is thus a derivate of the \fBI\fP (insertion) command. + * + * \*(ST uses Scintilla settings to determine the indention + * characters. + * If tab use is enabled with the \fBSCI_SETUSETABS\fP message, + * a single tab character is inserted. + * Tab use is enabled by default. + * Otherwise, a number of spaces is inserted up to the + * next tab stop so that the command's argument + * is inserted at the beginning of the next tab stop. + * The size of the tab stops is configured by the + * \fBSCI_SETTABWIDTH\fP Scintilla message (8 by default). + * In combination with \*(ST's use of the tab key as an + * immediate editing command for all insertions, this + * implements support for different insertion styles. + * The Scintilla settings apply to the current Scintilla + * document and are thus local to the currently edited + * buffer or Q-Register. + * + * However for the same reason, the ^I command is not + * fully compatible with classic TECO which \fIalways\fP + * inserts a single tab character and should not be used + * for the purpose of inserting single tabs in generic + * macros. + * To insert a single tab character reliably, the idioms + * \(lq9I$\(rq or \(lqI^I$\(rq may be used. + * + * Like the I command, ^I has string building characters + * \fBenabled\fP. + */ +TECO_DEFINE_STATE_INSERT(teco_state_insert_indent, + .initial_cb = (teco_state_initial_cb_t)teco_state_insert_indent_initial +); diff --git a/src/core-commands.h b/src/core-commands.h new file mode 100644 index 0000000..c5a8ee0 --- /dev/null +++ b/src/core-commands.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#pragma once + +#include + +#include "sciteco.h" +#include "parser.h" +#include "string-utils.h" + +/* + * FIXME: Most of these states can probably be private/static + * as they are only referenced from teco_state_start. + */ +TECO_DECLARE_STATE(teco_state_start); +TECO_DECLARE_STATE(teco_state_fcommand); + +void teco_undo_change_dir_to_current(void); +TECO_DECLARE_STATE(teco_state_changedir); + +TECO_DECLARE_STATE(teco_state_condcommand); +TECO_DECLARE_STATE(teco_state_control); +TECO_DECLARE_STATE(teco_state_ascii); +TECO_DECLARE_STATE(teco_state_escape); +TECO_DECLARE_STATE(teco_state_ecommand); + +gboolean teco_state_insert_initial(teco_machine_main_t *ctx, GError **error); +gboolean teco_state_insert_process(teco_machine_main_t *ctx, const teco_string_t *str, + gsize new_chars, GError **error); + +/* in cmdline.c */ +gboolean teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar chr, GError **error); + +/** + * @class TECO_DEFINE_STATE_INSERT + * @implements TECO_DEFINE_STATE_EXPECTSTRING + * @ingroup states + * + * @note Also serves as a base class of the replace-insertion commands. + * @fixme Generating the done_cb could be avoided if there simply were a default. + */ +#define TECO_DEFINE_STATE_INSERT(NAME, ...) \ + static teco_state_t * \ + NAME##_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) \ + { \ + return &teco_state_start; /* nothing to be done when done */ \ + } \ + TECO_DEFINE_STATE_EXPECTSTRING(NAME, \ + .initial_cb = (teco_state_initial_cb_t)teco_state_insert_initial, \ + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_insert_process_edit_cmd, \ + .expectstring.process_cb = teco_state_insert_process, \ + ##__VA_ARGS__ \ + ) + +TECO_DECLARE_STATE(teco_state_insert_building); +TECO_DECLARE_STATE(teco_state_insert_nobuilding); +TECO_DECLARE_STATE(teco_state_insert_indent); diff --git a/src/doc.c b/src/doc.c new file mode 100644 index 0000000..41acf40 --- /dev/null +++ b/src/doc.c @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include + +#include "sciteco.h" +#include "view.h" +#include "undo.h" +#include "qreg.h" +#include "doc.h" + +static inline teco_doc_scintilla_t * +teco_doc_get_scintilla(teco_doc_t *ctx) +{ + if (G_UNLIKELY(!ctx->doc)) + ctx->doc = (teco_doc_scintilla_t *)teco_view_ssm(teco_qreg_view, SCI_CREATEDOCUMENT, 0, 0); + return ctx->doc; +} + +/** @memberof teco_doc_t */ +void +teco_doc_edit(teco_doc_t *ctx) +{ + /* + * FIXME: SCI_SETREPRESENTATION does not redraw + * the screen - also that would be very slow. + * Since SCI_SETDOCPOINTER resets the representation + * (this should probably be fixed in Scintilla), + * the screen is garbled since the layout cache + * is calculated with the default representations. + * We work around this by temporarily disabling the + * layout cache. + */ + gint old_mode = teco_view_ssm(teco_qreg_view, SCI_GETLAYOUTCACHE, 0, 0); + teco_view_ssm(teco_qreg_view, SCI_SETLAYOUTCACHE, SC_CACHE_NONE, 0); + + teco_view_ssm(teco_qreg_view, SCI_SETDOCPOINTER, 0, + (sptr_t)teco_doc_get_scintilla(ctx)); + teco_view_ssm(teco_qreg_view, SCI_SETFIRSTVISIBLELINE, ctx->first_line, 0); + teco_view_ssm(teco_qreg_view, SCI_SETXOFFSET, ctx->xoffset, 0); + teco_view_ssm(teco_qreg_view, SCI_SETSEL, ctx->anchor, (sptr_t)ctx->dot); + + /* + * Default TECO-style character representations. + * They are reset on EVERY SETDOCPOINTER call by Scintilla. + */ + teco_view_set_representations(teco_qreg_view); + + teco_view_ssm(teco_qreg_view, SCI_SETLAYOUTCACHE, old_mode, 0); +} + +/** @memberof teco_doc_t */ +void +teco_doc_undo_edit(teco_doc_t *ctx) +{ + /* + * FIXME: see above in teco_doc_edit() + */ + undo__teco_view_ssm(teco_qreg_view, SCI_SETLAYOUTCACHE, + teco_view_ssm(teco_qreg_view, SCI_GETLAYOUTCACHE, 0, 0), 0); + + undo__teco_view_set_representations(teco_qreg_view); + + undo__teco_view_ssm(teco_qreg_view, SCI_SETSEL, ctx->anchor, (sptr_t)ctx->dot); + undo__teco_view_ssm(teco_qreg_view, SCI_SETXOFFSET, ctx->xoffset, 0); + undo__teco_view_ssm(teco_qreg_view, SCI_SETFIRSTVISIBLELINE, ctx->first_line, 0); + undo__teco_view_ssm(teco_qreg_view, SCI_SETDOCPOINTER, 0, + (sptr_t)teco_doc_get_scintilla(ctx)); + + undo__teco_view_ssm(teco_qreg_view, SCI_SETLAYOUTCACHE, SC_CACHE_NONE, 0); +} + +/** @memberof teco_doc_t */ +void +teco_doc_set_string(teco_doc_t *ctx, const gchar *str, gsize len) +{ + if (teco_qreg_current) + teco_doc_update(&teco_qreg_current->string, teco_qreg_view); + + teco_doc_reset(ctx); + teco_doc_edit(ctx); + + teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0); + teco_view_ssm(teco_qreg_view, SCI_CLEARALL, 0, 0); + teco_view_ssm(teco_qreg_view, SCI_APPENDTEXT, len, (sptr_t)(str ? : "")); + teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0); + + if (teco_qreg_current) + teco_doc_edit(&teco_qreg_current->string); +} + +/** @memberof teco_doc_t */ +void +teco_doc_undo_set_string(teco_doc_t *ctx) +{ + /* + * Necessary, so that upon rubout the + * string's parameters are restored. + */ + teco_doc_update(ctx, teco_qreg_view); + + if (teco_qreg_current && teco_qreg_current->must_undo) // FIXME + teco_doc_undo_edit(&teco_qreg_current->string); + + teco_doc_undo_reset(ctx); + undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0); + + teco_doc_undo_edit(ctx); +} + +/** + * Get a document as a string. + * + * @param ctx The document. + * @param str Pointer to a variable to hold the return string. + * It can be NULL if you are interested only in the string's length. + * Strings must be freed via g_free(). + * @param len Where to store the string's length (mandatory). + * + * @see teco_qreg_vtable_t::get_string() + * @memberof teco_doc_t + */ +void +teco_doc_get_string(teco_doc_t *ctx, gchar **str, gsize *len) +{ + if (!ctx->doc) { + if (str) + *str = NULL; + *len = 0; + return; + } + + if (teco_qreg_current) + teco_doc_update(&teco_qreg_current->string, teco_qreg_view); + + teco_doc_edit(ctx); + + *len = teco_view_ssm(teco_qreg_view, SCI_GETLENGTH, 0, 0); + if (str) { + *str = g_malloc(*len + 1); + teco_view_ssm(teco_qreg_view, SCI_GETTEXT, *len + 1, (sptr_t)*str); + } + + if (teco_qreg_current) + teco_doc_edit(&teco_qreg_current->string); +} + +/** @memberof teco_doc_t */ +void +teco_doc_update_from_view(teco_doc_t *ctx, teco_view_t *from) +{ + ctx->anchor = teco_view_ssm(from, SCI_GETANCHOR, 0, 0); + ctx->dot = teco_view_ssm(from, SCI_GETCURRENTPOS, 0, 0); + ctx->first_line = teco_view_ssm(from, SCI_GETFIRSTVISIBLELINE, 0, 0); + ctx->xoffset = teco_view_ssm(from, SCI_GETXOFFSET, 0, 0); +} + +/** @memberof teco_doc_t */ +void +teco_doc_update_from_doc(teco_doc_t *ctx, const teco_doc_t *from) +{ + ctx->anchor = from->anchor; + ctx->dot = from->dot; + ctx->first_line = from->first_line; + ctx->xoffset = from->xoffset; +} + +/** + * Only for teco_qreg_stack_pop() which does some clever + * exchanging of document data (without any deep copying) + * + * @memberof teco_doc_t + */ +void +teco_doc_exchange(teco_doc_t *ctx, teco_doc_t *other) +{ + teco_doc_t temp; + memcpy(&temp, ctx, sizeof(temp)); + memcpy(ctx, other, sizeof(*ctx)); + memcpy(other, &temp, sizeof(*other)); +} + +/** @memberof teco_doc_t */ +void +teco_doc_clear(teco_doc_t *ctx) +{ + if (ctx->doc) + teco_view_ssm(teco_qreg_view, SCI_RELEASEDOCUMENT, 0, (sptr_t)ctx->doc); +} diff --git a/src/doc.h b/src/doc.h new file mode 100644 index 0000000..471d16a --- /dev/null +++ b/src/doc.h @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#pragma once + +#include + +#include + +#include + +#include "sciteco.h" +#include "view.h" +#include "undo.h" + +/** + * Scintilla document type. + * The struct is never defined and only exists for improved + * type safety. + */ +typedef struct teco_doc_scintilla_t teco_doc_scintilla_t; + +/** + * A Scintilla document. + * + * Also contains other attributes required to restore + * the overall editor state when loading it into a Scintilla view. + */ +typedef struct { + /** + * Underlying Scintilla document. + * It is created on demand in teco_doc_maybe_create_document(), + * so that we don't waste memory on integer-only Q-Registers. + */ + teco_doc_scintilla_t *doc; + + /* + * The so called "parameters". + * Updated/restored only when required + */ + gint anchor, dot; + gint first_line, xoffset; +} teco_doc_t; + +/** @memberof teco_doc_t */ +static inline void +teco_doc_init(teco_doc_t *ctx) +{ + memset(ctx, 0, sizeof(*ctx)); +} + +void teco_doc_edit(teco_doc_t *ctx); +void teco_doc_undo_edit(teco_doc_t *ctx); + +void teco_doc_set_string(teco_doc_t *ctx, const gchar *str, gsize len); +void teco_doc_undo_set_string(teco_doc_t *ctx); + +void teco_doc_get_string(teco_doc_t *ctx, gchar **str, gsize *len); + +void teco_doc_update_from_view(teco_doc_t *ctx, teco_view_t *from); +void teco_doc_update_from_doc(teco_doc_t *ctx, const teco_doc_t *from); + +/** @memberof teco_doc_t */ +#define teco_doc_update(CTX, FROM) \ + (_Generic((FROM), teco_view_t * : teco_doc_update_from_view, \ + teco_doc_t * : teco_doc_update_from_doc, \ + const teco_doc_t * : teco_doc_update_from_doc)((CTX), (FROM))) + +/** @memberof teco_doc_t */ +static inline void +teco_doc_reset(teco_doc_t *ctx) +{ + ctx->anchor = ctx->dot = 0; + ctx->first_line = ctx->xoffset = 0; +} + +/** @memberof teco_doc_t */ +static inline void +teco_doc_undo_reset(teco_doc_t *ctx) +{ + /* + * NOTE: Could be rolled into one function + * and called with teco_undo_call() if we really + * wanted to save more memory. + */ + teco_undo_gint(ctx->anchor); + teco_undo_gint(ctx->dot); + teco_undo_gint(ctx->first_line); + teco_undo_gint(ctx->xoffset); +} + +void teco_doc_exchange(teco_doc_t *ctx, teco_doc_t *other); + +/** @memberof teco_doc_t */ +static inline void +teco_doc_undo_exchange(teco_doc_t *ctx) +{ + teco_undo_ptr(ctx->doc); + teco_doc_undo_reset(ctx); +} + +void teco_doc_clear(teco_doc_t *ctx); diff --git a/src/document.cpp b/src/document.cpp deleted file mode 100644 index e3b183e..0000000 --- a/src/document.cpp +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include - -#include "sciteco.h" -#include "interface.h" -#include "undo.h" -#include "document.h" - -namespace SciTECO { - -void -Document::edit(ViewCurrent &view) -{ - /* - * FIXME: SCI_SETREPRESENTATION does not redraw - * the screen - also that would be very slow. - * Since SCI_SETDOCPOINTER resets the representation - * (this should probably be fixed in Scintilla), - * the screen is garbled since the layout cache - * is calculated with the default representations. - * We work around this by temporarily disabling the - * layout cache. - */ - gint old_mode = view.ssm(SCI_GETLAYOUTCACHE); - - maybe_create_document(); - - view.ssm(SCI_SETLAYOUTCACHE, SC_CACHE_NONE); - - view.ssm(SCI_SETDOCPOINTER, 0, (sptr_t)doc); - view.ssm(SCI_SETFIRSTVISIBLELINE, first_line); - view.ssm(SCI_SETXOFFSET, xoffset); - view.ssm(SCI_SETSEL, anchor, (sptr_t)dot); - - /* - * Default TECO-style character representations. - * They are reset on EVERY SETDOCPOINTER call by Scintilla. - */ - view.set_representations(); - - view.ssm(SCI_SETLAYOUTCACHE, old_mode); -} - -void -Document::undo_edit(ViewCurrent &view) -{ - maybe_create_document(); - - /* - * FIXME: see above in Document::edit() - */ - view.undo_ssm(SCI_SETLAYOUTCACHE, - view.ssm(SCI_GETLAYOUTCACHE)); - - view.undo_set_representations(); - - view.undo_ssm(SCI_SETSEL, anchor, (sptr_t)dot); - view.undo_ssm(SCI_SETXOFFSET, xoffset); - view.undo_ssm(SCI_SETFIRSTVISIBLELINE, first_line); - view.undo_ssm(SCI_SETDOCPOINTER, 0, (sptr_t)doc); - - view.undo_ssm(SCI_SETLAYOUTCACHE, SC_CACHE_NONE); -} - -void -Document::update(ViewCurrent &view) -{ - anchor = view.ssm(SCI_GETANCHOR); - dot = view.ssm(SCI_GETCURRENTPOS); - first_line = view.ssm(SCI_GETFIRSTVISIBLELINE); - xoffset = view.ssm(SCI_GETXOFFSET); -} - -/* - * Only for QRegisterStack::pop() which does some clever - * exchanging of document data (without any deep copying) - */ -void -Document::exchange(Document &other) -{ - SciDoc temp_doc = doc; - gint temp_anchor = anchor; - gint temp_dot = dot; - gint temp_first_line = first_line; - gint temp_xoffset = xoffset; - - doc = other.doc; - anchor = other.anchor; - dot = other.dot; - first_line = other.first_line; - xoffset = other.xoffset; - - other.doc = temp_doc; - other.anchor = temp_anchor; - other.dot = temp_dot; - other.first_line = temp_first_line; - other.xoffset = temp_xoffset; -} - -} /* namespace SciTECO */ diff --git a/src/document.h b/src/document.h deleted file mode 100644 index f132745..0000000 --- a/src/document.h +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifndef __DOCUMENT_H -#define __DOCUMENT_H - -#include - -#include - -#include "sciteco.h" -#include "memory.h" -#include "interface.h" -#include "undo.h" - -namespace SciTECO { - -/* - * Classes - */ - -class Document : public Object { - typedef const void *SciDoc; - SciDoc doc; - - /* - * The so called "parameters". - * Updated/restored only when required - */ - gint anchor, dot; - gint first_line, xoffset; - -public: - Document() : doc(NULL) - { - reset(); - } - virtual ~Document() - { - /* - * Cannot release document here, since we must - * do it on the same view that created it. - * We also cannot call get_create_document_view() - * since it is virtual. - * So we must demand that deriving classes call - * release_document() from their destructors. - */ - g_assert(doc == NULL); - } - - inline bool - is_initialized(void) - { - return doc != NULL; - } - - void edit(ViewCurrent &view); - void undo_edit(ViewCurrent &view); - - void update(ViewCurrent &view); - inline void - update(const Document &from) - { - anchor = from.anchor; - dot = from.dot; - first_line = from.first_line; - xoffset = from.xoffset; - } - - inline void - reset(void) - { - anchor = dot = 0; - first_line = xoffset = 0; - } - inline void - undo_reset(void) - { - undo.push_var(anchor); - undo.push_var(dot); - undo.push_var(first_line); - undo.push_var(xoffset); - } - - void exchange(Document &other); - inline void - undo_exchange(void) - { - undo.push_var(doc); - undo_reset(); - } - -protected: - inline void - release_document(void) - { - if (is_initialized()) { - ViewCurrent &view = get_create_document_view(); - view.ssm(SCI_RELEASEDOCUMENT, 0, (sptr_t)doc); - doc = NULL; - } - } - -private: - /* - * Must be implemented by derived class. - * Documents must be released on the same view - * as they were created. - * Since we do not want to save this view - * per document, it must instead be returned by - * this method. - */ - virtual ViewCurrent &get_create_document_view(void) = 0; - - inline void - maybe_create_document(void) - { - if (!is_initialized()) { - ViewCurrent &view = get_create_document_view(); - doc = (SciDoc)view.ssm(SCI_CREATEDOCUMENT); - } - } -}; - -} /* namespace SciTECO */ - -#endif diff --git a/src/eol.c b/src/eol.c new file mode 100644 index 0000000..44ad021 --- /dev/null +++ b/src/eol.c @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include + +#include + +#include "sciteco.h" +#include "eol.h" + +const gchar * +teco_eol_get_seq(gint eol_mode) +{ + switch (eol_mode) { + case SC_EOL_CRLF: + return "\r\n"; + case SC_EOL_CR: + return "\r"; + case SC_EOL_LF: + default: + return "\n"; + } +} + +static inline void +teco_eol_reader_init(teco_eol_reader_t *ctx) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->eol_style = -1; +} + +static GIOStatus +teco_eol_reader_read_gio(teco_eol_reader_t *ctx, gsize *read_len, GError **error) +{ + return g_io_channel_read_chars(ctx->gio.channel, ctx->gio.buffer, + sizeof(ctx->gio.buffer), + read_len, error); +} + +/** @memberof teco_eol_reader_t */ +void +teco_eol_reader_init_gio(teco_eol_reader_t *ctx, GIOChannel *channel) +{ + teco_eol_reader_init(ctx); + ctx->read_cb = teco_eol_reader_read_gio; + + teco_eol_reader_set_channel(ctx, channel); +} + +static GIOStatus +teco_eol_reader_read_mem(teco_eol_reader_t *ctx, gsize *read_len, GError **error) +{ + *read_len = ctx->mem.len; + ctx->mem.len = 0; + /* + * On the first call, returns G_IO_STATUS_NORMAL, + * later G_IO_STATUS_EOF. + */ + return *read_len != 0 ? G_IO_STATUS_NORMAL : G_IO_STATUS_EOF; +} + +/** @memberof teco_eol_reader_t */ +void +teco_eol_reader_init_mem(teco_eol_reader_t *ctx, gchar *buffer, gsize len) +{ + teco_eol_reader_init(ctx); + ctx->read_cb = teco_eol_reader_read_mem; + + ctx->mem.buffer = buffer; + ctx->mem.len = len; +} + +/** + * Read data with automatic EOL translation. + * + * This gets the next data block from the converter + * implementation, performs EOL translation (if enabled) + * in a more or less efficient manner and returns + * a chunk of EOL-normalized data. + * + * Since the underlying data source may have to be + * queried repeatedly and because the EOL Reader avoids + * reassembling the EOL-normalized data by returning + * references into the modified data source, it is + * necessary to call this function repeatedly until + * it returns G_IO_STATUS_EOF. + * + * @param ctx The EOL Reader object. + * @param ret Location to store a pointer to the converted chunk. + * The EOL-converted data is NOT null-terminated. + * @param data_len A pointer to the length of the converted chunk. + * @param error A GError. + * @return The status of the conversion. + * + * @memberof teco_eol_reader_t + */ +GIOStatus +teco_eol_reader_convert(teco_eol_reader_t *ctx, gchar **ret, gsize *data_len, GError **error) +{ + gchar *buffer = ctx->read_cb == teco_eol_reader_read_gio ? ctx->gio.buffer : ctx->mem.buffer; + + if (ctx->last_char < 0) { + /* a CRLF was last translated */ + ctx->block_len++; + ctx->last_char = '\n'; + } + ctx->offset += ctx->block_len; + + if (ctx->offset == ctx->read_len) { + ctx->offset = 0; + + switch (ctx->read_cb(ctx, &ctx->read_len, error)) { + case G_IO_STATUS_ERROR: + return G_IO_STATUS_ERROR; + + case G_IO_STATUS_EOF: + if (ctx->last_char == '\r') { + /* + * Very last character read is CR. + * If this is the only EOL so far, the + * EOL style is MAC. + * This is also executed if auto-eol is disabled + * but it doesn't hurt. + */ + if (ctx->eol_style < 0) + ctx->eol_style = SC_EOL_CR; + else if (ctx->eol_style != SC_EOL_CR) + ctx->eol_style_inconsistent = TRUE; + } + + return G_IO_STATUS_EOF; + + case G_IO_STATUS_NORMAL: + case G_IO_STATUS_AGAIN: + break; + } + + if (!(teco_ed & TECO_ED_AUTOEOL)) { + /* + * No EOL translation - always return entire + * buffer + */ + *data_len = ctx->block_len = ctx->read_len; + *ret = buffer; + return G_IO_STATUS_NORMAL; + } + } + + /* + * Return data with automatic EOL translation. + * Every EOL sequence is normalized to LF and + * the first sequence determines the documents + * EOL style. + * This loop is executed for every byte of the + * file/stream, so it was important to optimize + * it. Specifically, the number of returns + * is minimized by keeping a pointer to + * the beginning of a block of data in the buffer + * which already has LFs (offset). + * Mac EOLs can be converted to UNIX EOLs directly + * in the buffer. + * So if their EOLs are consistent, the function + * will return one block for the entire buffer. + * When reading a file with DOS EOLs, there will + * be one call per line which is significantly slower. + */ + for (guint i = ctx->offset; i < ctx->read_len; i++) { + switch (buffer[i]) { + case '\n': + if (ctx->last_char == '\r') { + if (ctx->eol_style < 0) + ctx->eol_style = SC_EOL_CRLF; + else if (ctx->eol_style != SC_EOL_CRLF) + ctx->eol_style_inconsistent = TRUE; + + /* + * Return block. CR has already + * been made LF in `buffer`. + */ + *data_len = ctx->block_len = i-ctx->offset; + /* next call will skip the CR */ + ctx->last_char = -1; + *ret = buffer + ctx->offset; + return G_IO_STATUS_NORMAL; + } + + if (ctx->eol_style < 0) + ctx->eol_style = SC_EOL_LF; + else if (ctx->eol_style != SC_EOL_LF) + ctx->eol_style_inconsistent = TRUE; + /* + * No conversion necessary and no need to + * return block yet. + */ + ctx->last_char = '\n'; + break; + + case '\r': + if (ctx->last_char == '\r') { + if (ctx->eol_style < 0) + ctx->eol_style = SC_EOL_CR; + else if (ctx->eol_style != SC_EOL_CR) + ctx->eol_style_inconsistent = TRUE; + } + + /* + * Convert CR to LF in `buffer`. + * This way more than one line using + * Mac EOLs can be returned at once. + */ + buffer[i] = '\n'; + ctx->last_char = '\r'; + break; + + default: + if (ctx->last_char == '\r') { + if (ctx->eol_style < 0) + ctx->eol_style = SC_EOL_CR; + else if (ctx->eol_style != SC_EOL_CR) + ctx->eol_style_inconsistent = TRUE; + } + ctx->last_char = buffer[i]; + break; + } + } + + /* + * Return remaining block. + * With UNIX/MAC EOLs, this will usually be the + * entire `buffer` + */ + *data_len = ctx->block_len = ctx->read_len-ctx->offset; + *ret = buffer + ctx->offset; + return G_IO_STATUS_NORMAL; +} + +/** @memberof teco_eol_reader_t */ +GIOStatus +teco_eol_reader_convert_all(teco_eol_reader_t *ctx, gchar **ret, gsize *out_len, GError **error) +{ + gsize buffer_len = ctx->read_cb == teco_eol_reader_read_gio + ? sizeof(ctx->gio.buffer) : ctx->mem.len; + + /* + * NOTE: Doesn't use teco_string_t to make use of GString's + * preallocation feature. + */ + GString *str = g_string_sized_new(buffer_len); + + for (;;) { + gchar *data; + gsize data_len; + + GIOStatus rc = teco_eol_reader_convert(ctx, &data, &data_len, error); + if (rc == G_IO_STATUS_ERROR) { + g_string_free(str, TRUE); + return G_IO_STATUS_ERROR; + } + if (rc == G_IO_STATUS_EOF) + break; + + g_string_append_len(str, data, data_len); + } + + if (out_len) + *out_len = str->len; + *ret = g_string_free(str, FALSE); + return G_IO_STATUS_NORMAL; +} + +/** @memberof teco_eol_reader_t */ +void +teco_eol_reader_clear(teco_eol_reader_t *ctx) +{ + if (ctx->read_cb == teco_eol_reader_read_gio && ctx->gio.channel) + g_io_channel_unref(ctx->gio.channel); +} + +static inline void +teco_eol_writer_init(teco_eol_writer_t *ctx, gint eol_mode) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->eol_seq = teco_eol_get_seq(eol_mode); + ctx->eol_seq_len = strlen(ctx->eol_seq); +} + +static gssize +teco_eol_writer_write_gio(teco_eol_writer_t *ctx, const gchar *buffer, gsize buffer_len, GError **error) +{ + gsize bytes_written; + + switch (g_io_channel_write_chars(ctx->gio.channel, buffer, buffer_len, + &bytes_written, error)) { + case G_IO_STATUS_ERROR: + return -1; + case G_IO_STATUS_EOF: + case G_IO_STATUS_NORMAL: + case G_IO_STATUS_AGAIN: + break; + } + + return bytes_written; +} + +/** @memberof teco_eol_writer_t */ +void +teco_eol_writer_init_gio(teco_eol_writer_t *ctx, gint eol_mode, GIOChannel *channel) +{ + teco_eol_writer_init(ctx, eol_mode); + ctx->write_cb = teco_eol_writer_write_gio; + teco_eol_writer_set_channel(ctx, channel); +} + +static gssize +teco_eol_writer_write_mem(teco_eol_writer_t *ctx, const gchar *buffer, gsize buffer_len, GError **error) +{ + g_string_append_len(ctx->mem.str, buffer, buffer_len); + return buffer_len; +} + +/** + * @note Currently uses GString instead of teco_string_t to allow making use + * of preallocation. + * On the other hand GString has a higher overhead. + * + * @memberof teco_eol_writer_t + */ +void +teco_eol_writer_init_mem(teco_eol_writer_t *ctx, gint eol_mode, GString *str) +{ + teco_eol_writer_init(ctx, eol_mode); + ctx->write_cb = teco_eol_writer_write_mem; + ctx->mem.str = str; +} + +/** + * Perform EOL-normalization on a buffer (if enabled) and + * pass it to the underlying data sink. + * + * This can be called repeatedly to transform a larger + * document - the buffer provided does not have to be + * well-formed with regard to EOL sequences. + * + * @param ctx The EOL Reader object. + * @param buffer The buffer to convert. + * @param buffer_len The length of the data in buffer. + * @param error A GError. + * @return The number of bytes consumed/converted from buffer. + * A value smaller than 0 is returned in case of errors. + * + * @memberof teco_eol_writer_t + */ +gssize +teco_eol_writer_convert(teco_eol_writer_t *ctx, const gchar *buffer, gsize buffer_len, GError **error) +{ + if (!(teco_ed & TECO_ED_AUTOEOL)) + /* + * Write without EOL-translation: + * `state` is not required + * NOTE: This throws in case of errors + */ + return ctx->write_cb(ctx, buffer, buffer_len, error); + + /* + * Write to stream with EOL-translation. + * The document's EOL mode tells us what was guessed + * when its content was read in (presumably from a file) + * but might have been changed manually by the user. + * NOTE: This code assumes that the output stream is + * buffered, since otherwise it would be slower + * (has been benchmarked). + * NOTE: The loop is executed for every character + * in `buffer` and has been optimized for minimal + * function (i.e. GIOChannel) calls. + */ + guint i = 0; + gsize bytes_written = 0; + if (ctx->state == TECO_EOL_STATE_WRITE_LF) { + /* complete writing a CRLF sequence */ + gssize rc = ctx->write_cb(ctx, "\n", 1, error); + if (rc < 1) + /* nothing written or error */ + return rc; + ctx->state = TECO_EOL_STATE_START; + bytes_written++; + i++; + } + + guint block_start = i; + gssize block_written; + while (i < buffer_len) { + switch (buffer[i]) { + case '\n': + if (ctx->last_c == '\r') { + /* EOL sequence already written */ + bytes_written++; + block_start = i+1; + break; + } + /* fall through */ + case '\r': + block_written = ctx->write_cb(ctx, buffer+block_start, i-block_start, error); + if (block_written < 0) + return -1; + bytes_written += block_written; + if (block_written < i-block_start) + return bytes_written; + + block_written = ctx->write_cb(ctx, ctx->eol_seq, ctx->eol_seq_len, error); + if (block_written < 0) + return -1; + if (block_written == 0) + return bytes_written; + if (block_written < ctx->eol_seq_len) { + /* incomplete EOL seq - we have written CR of CRLF */ + ctx->state = TECO_EOL_STATE_WRITE_LF; + return bytes_written; + } + bytes_written++; + + block_start = i+1; + break; + } + + ctx->last_c = buffer[i++]; + } + + /* + * Write out remaining block (i.e. line) + */ + gssize rc = ctx->write_cb(ctx, buffer+block_start, buffer_len-block_start, error); + return rc < 0 ? -1 : bytes_written + rc; +} + +/** @memberof teco_eol_writer_t */ +void +teco_eol_writer_clear(teco_eol_writer_t *ctx) +{ + if (ctx->write_cb == teco_eol_writer_write_gio && ctx->gio.channel) + g_io_channel_unref(ctx->gio.channel); +} diff --git a/src/eol.cpp b/src/eol.cpp deleted file mode 100644 index 2dea3ef..0000000 --- a/src/eol.cpp +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include "sciteco.h" -#include "error.h" -#include "eol.h" - -namespace SciTECO { - -/** - * Read data with automatic EOL translation. - * - * This gets the next data block from the converter - * implementation, performs EOL translation (if enabled) - * in a more or less efficient manner and returns - * a chunk of EOL-normalized data. - * - * Since the underlying data source may have to be - * queried repeatedly and because EOLReader avoids - * reassembling the EOL-normalized data by returning - * references into the modified data source, it is - * necessary to call this function repeatedly until - * it returns NULL. - * - * Errors reading the data source are propagated - * (as exceptions). - * - * @param data_len The length of the data chunk returned - * by this function. Set on return. - * @return A pointer to a chunk of EOL-normalized - * data of length data_len. - * It is NOT null-terminated. - * NULL is returned when all data has been converted. - */ -const gchar * -EOLReader::convert(gsize &data_len) -{ - if (last_char < 0) { - /* a CRLF was last translated */ - block_len++; - last_char = '\n'; - } - offset += block_len; - - if (offset == read_len) { - offset = 0; - - /* - * NOTE: This throws in case of errors - */ - if (!this->read(buffer, read_len)) { - /* EOF */ - if (last_char == '\r') { - /* - * Very last character read is CR. - * If this is the only EOL so far, the - * EOL style is MAC. - * This is also executed if auto-eol is disabled - * but it doesn't hurt. - */ - if (eol_style < 0) - eol_style = SC_EOL_CR; - else if (eol_style != SC_EOL_CR) - eol_style_inconsistent = TRUE; - } - - return NULL; - } - - if (!(Flags::ed & Flags::ED_AUTOEOL)) { - /* - * No EOL translation - always return entire - * buffer - */ - data_len = block_len = read_len; - return buffer; - } - } - - /* - * Return data with automatic EOL translation. - * Every EOL sequence is normalized to LF and - * the first sequence determines the documents - * EOL style. - * This loop is executed for every byte of the - * file/stream, so it was important to optimize - * it. Specifically, the number of returns - * is minimized by keeping a pointer to - * the beginning of a block of data in the buffer - * which already has LFs (offset). - * Mac EOLs can be converted to UNIX EOLs directly - * in the buffer. - * So if their EOLs are consistent, the function - * will return one block for the entire buffer. - * When reading a file with DOS EOLs, there will - * be one call per line which is significantly slower. - */ - for (guint i = offset; i < read_len; i++) { - switch (buffer[i]) { - case '\n': - if (last_char == '\r') { - if (eol_style < 0) - eol_style = SC_EOL_CRLF; - else if (eol_style != SC_EOL_CRLF) - eol_style_inconsistent = TRUE; - - /* - * Return block. CR has already - * been made LF in `buffer`. - */ - data_len = block_len = i-offset; - /* next call will skip the CR */ - last_char = -1; - return buffer + offset; - } - - if (eol_style < 0) - eol_style = SC_EOL_LF; - else if (eol_style != SC_EOL_LF) - eol_style_inconsistent = TRUE; - /* - * No conversion necessary and no need to - * return block yet. - */ - last_char = '\n'; - break; - - case '\r': - if (last_char == '\r') { - if (eol_style < 0) - eol_style = SC_EOL_CR; - else if (eol_style != SC_EOL_CR) - eol_style_inconsistent = TRUE; - } - - /* - * Convert CR to LF in `buffer`. - * This way more than one line using - * Mac EOLs can be returned at once. - */ - buffer[i] = '\n'; - last_char = '\r'; - break; - - default: - if (last_char == '\r') { - if (eol_style < 0) - eol_style = SC_EOL_CR; - else if (eol_style != SC_EOL_CR) - eol_style_inconsistent = TRUE; - } - last_char = buffer[i]; - break; - } - } - - /* - * Return remaining block. - * With UNIX/MAC EOLs, this will usually be the - * entire `buffer` - */ - data_len = block_len = read_len-offset; - return buffer + offset; -} - -bool -EOLReaderGIO::read(gchar *buffer, gsize &read_len) -{ - GError *error = NULL; - - switch (g_io_channel_read_chars(channel, buffer, - sizeof(EOLReaderGIO::buffer), - &read_len, &error)) { - case G_IO_STATUS_ERROR: - throw GlibError(error); - case G_IO_STATUS_EOF: - return false; - case G_IO_STATUS_NORMAL: - case G_IO_STATUS_AGAIN: - break; - } - - return true; -} - -bool -EOLReaderMem::read(gchar *buffer, gsize &read_len) -{ - read_len = buffer_len; - buffer_len = 0; - /* - * On the first call, returns true, - * later false (no more data). - */ - return read_len != 0; -} - -/* - * This could be in EOLReader as well, but this way, we - * make use of the buffer_len to avoid unnecessary allocations. - */ -gchar * -EOLReaderMem::convert_all(gsize *out_len) -{ - GString *str = g_string_sized_new(buffer_len); - const gchar *data; - gsize data_len; - - try { - while ((data = convert(data_len))) - g_string_append_len(str, data, data_len); - } catch (...) { - g_string_free(str, TRUE); - throw; /* forward */ - } - - if (out_len) - *out_len = str->len; - return g_string_free(str, FALSE); -} - -/** - * Perform EOL-normalization on a buffer (if enabled) and - * pass it to the underlying data sink. - * - * This can be called repeatedly to transform a larger - * document - the buffer provided does not have to be - * well-formed with regard to EOL sequences. - * - * @param buffer The buffer to convert. - * @param buffer_len The length of the data in buffer. - * @return The number of bytes consumed/converted from buffer. - */ -gsize -EOLWriter::convert(const gchar *buffer, gsize buffer_len) -{ - gsize bytes_written; - guint i = 0; - guint block_start; - gsize block_written; - - if (!(Flags::ed & Flags::ED_AUTOEOL)) - /* - * Write without EOL-translation: - * `state` is not required - * NOTE: This throws in case of errors - */ - return this->write(buffer, buffer_len); - - /* - * Write to stream with EOL-translation. - * The document's EOL mode tells us what was guessed - * when its content was read in (presumably from a file) - * but might have been changed manually by the user. - * NOTE: This code assumes that the output stream is - * buffered, since otherwise it would be slower - * (has been benchmarked). - * NOTE: The loop is executed for every character - * in `buffer` and has been optimized for minimal - * function (i.e. GIOChannel) calls. - */ - bytes_written = 0; - if (state == STATE_WRITE_LF) { - /* complete writing a CRLF sequence */ - if (this->write("\n", 1) < 1) - return 0; - state = STATE_START; - bytes_written++; - i++; - } - - block_start = i; - while (i < buffer_len) { - switch (buffer[i]) { - case '\n': - if (last_c == '\r') { - /* EOL sequence already written */ - bytes_written++; - block_start = i+1; - break; - } - /* fall through */ - case '\r': - block_written = this->write(buffer+block_start, i-block_start); - bytes_written += block_written; - if (block_written < i-block_start) - return bytes_written; - - block_written = this->write(eol_seq, eol_seq_len); - if (block_written == 0) - return bytes_written; - if (block_written < eol_seq_len) { - /* incomplete EOL seq - we have written CR of CRLF */ - state = STATE_WRITE_LF; - return bytes_written; - } - bytes_written++; - - block_start = i+1; - break; - } - - last_c = buffer[i++]; - } - - /* - * Write out remaining block (i.e. line) - */ - bytes_written += this->write(buffer+block_start, buffer_len-block_start); - return bytes_written; -} - -gsize -EOLWriterGIO::write(const gchar *buffer, gsize buffer_len) -{ - gsize bytes_written; - GError *error = NULL; - - switch (g_io_channel_write_chars(channel, buffer, buffer_len, - &bytes_written, &error)) { - case G_IO_STATUS_ERROR: - throw GlibError(error); - case G_IO_STATUS_EOF: - case G_IO_STATUS_NORMAL: - case G_IO_STATUS_AGAIN: - break; - } - - return bytes_written; -} - -gsize -EOLWriterMem::write(const gchar *buffer, gsize buffer_len) -{ - g_string_append_len(str, buffer, buffer_len); - return buffer_len; -} - -} /* namespace SciTECO */ diff --git a/src/eol.h b/src/eol.h index 7d6c527..4a0c144 100644 --- a/src/eol.h +++ b/src/eol.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,148 +14,106 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -#ifndef __EOL_H -#define __EOL_H - -#include +#pragma once #include #include "sciteco.h" -#include "memory.h" -namespace SciTECO { +const gchar *teco_eol_get_seq(gint eol_mode); + +typedef struct teco_eol_reader_t teco_eol_reader_t; -class EOLReader : public Object { - gchar *buffer; +struct teco_eol_reader_t { gsize read_len; guint offset; gsize block_len; gint last_char; -public: gint eol_style; gboolean eol_style_inconsistent; - EOLReader(gchar *_buffer) - : buffer(_buffer), - read_len(0), offset(0), block_len(0), - last_char(0), eol_style(-1), - eol_style_inconsistent(FALSE) {} - virtual ~EOLReader() {} - - const gchar *convert(gsize &data_len); - -protected: - virtual bool read(gchar *buffer, gsize &read_len) = 0; + GIOStatus (*read_cb)(teco_eol_reader_t *ctx, gsize *read_len, GError **error); + + /* + * NOTE: This wastes some bytes for "memory" readers, + * but avoids inheritance. + */ + union { + struct { + gchar buffer[1024]; + GIOChannel *channel; + } gio; + + struct { + gchar *buffer; + gsize len; + } mem; + }; }; -class EOLReaderGIO : public EOLReader { - gchar buffer[1024]; - GIOChannel *channel; - - bool read(gchar *buffer, gsize &read_len); - -public: - EOLReaderGIO(GIOChannel *_channel = NULL) - : EOLReader(buffer), channel(NULL) - { - set_channel(_channel); - } - - inline void - set_channel(GIOChannel *_channel = NULL) - { - if (channel) - g_io_channel_unref(channel); - channel = _channel; - if (channel) - g_io_channel_ref(channel); - } - - ~EOLReaderGIO() - { - set_channel(); - } -}; +void teco_eol_reader_init_gio(teco_eol_reader_t *ctx, GIOChannel *channel); +void teco_eol_reader_init_mem(teco_eol_reader_t *ctx, gchar *buffer, gsize len); -class EOLReaderMem : public EOLReader { - gsize buffer_len; +/** @memberof teco_eol_reader_t */ +static inline void +teco_eol_reader_set_channel(teco_eol_reader_t *ctx, GIOChannel *channel) +{ + if (ctx->gio.channel) + g_io_channel_unref(ctx->gio.channel); + ctx->gio.channel = channel; + if (ctx->gio.channel) + g_io_channel_ref(ctx->gio.channel); +} - bool read(gchar *buffer, gsize &read_len); +GIOStatus teco_eol_reader_convert(teco_eol_reader_t *ctx, gchar **ret, gsize *data_len, GError **error); +GIOStatus teco_eol_reader_convert_all(teco_eol_reader_t *ctx, gchar **ret, gsize *out_len, GError **error); -public: - EOLReaderMem(gchar *buffer, gsize _buffer_len) - : EOLReader(buffer), buffer_len(_buffer_len) {} +void teco_eol_reader_clear(teco_eol_reader_t *ctx); - gchar *convert_all(gsize *out_len = NULL); -}; +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_eol_reader_t, teco_eol_reader_clear); + +typedef struct teco_eol_writer_t teco_eol_writer_t; -class EOLWriter : public Object { +struct teco_eol_writer_t { enum { - STATE_START = 0, - STATE_WRITE_LF + TECO_EOL_STATE_START = 0, + TECO_EOL_STATE_WRITE_LF } state; gchar last_c; const gchar *eol_seq; gsize eol_seq_len; -public: - EOLWriter(gint eol_mode) : state(STATE_START), last_c('\0') - { - eol_seq = get_eol_seq(eol_mode); - eol_seq_len = strlen(eol_seq); - } - virtual ~EOLWriter() {} + gssize (*write_cb)(teco_eol_writer_t *ctx, const gchar *buffer, gsize buffer_len, GError **error); - gsize convert(const gchar *buffer, gsize buffer_len); + union { + struct { + GIOChannel *channel; + } gio; -protected: - virtual gsize write(const gchar *buffer, gsize buffer_len) = 0; + struct { + GString *str; + } mem; + }; }; -class EOLWriterGIO : public EOLWriter { - GIOChannel *channel; - - gsize write(const gchar *buffer, gsize buffer_len); - -public: - EOLWriterGIO(gint eol_mode) - : EOLWriter(eol_mode), channel(NULL) {} - - EOLWriterGIO(GIOChannel *_channel, gint eol_mode) - : EOLWriter(eol_mode), channel(NULL) - { - set_channel(_channel); - } - - inline void - set_channel(GIOChannel *_channel = NULL) - { - if (channel) - g_io_channel_unref(channel); - channel = _channel; - if (channel) - g_io_channel_ref(channel); - } - - ~EOLWriterGIO() - { - set_channel(); - } -}; +void teco_eol_writer_init_gio(teco_eol_writer_t *ctx, gint eol_mode, GIOChannel *channel); +void teco_eol_writer_init_mem(teco_eol_writer_t *ctx, gint eol_mode, GString *str); -class EOLWriterMem : public EOLWriter { - GString *str; +/** @memberof teco_eol_writer_t */ +static inline void +teco_eol_writer_set_channel(teco_eol_writer_t *ctx, GIOChannel *channel) +{ + if (ctx->gio.channel) + g_io_channel_unref(ctx->gio.channel); + ctx->gio.channel = channel; + if (ctx->gio.channel) + g_io_channel_ref(ctx->gio.channel); +} - gsize write(const gchar *buffer, gsize buffer_len); - -public: - EOLWriterMem(GString *_str, gint eol_mode) - : EOLWriter(eol_mode), str(_str) {} -}; +gssize teco_eol_writer_convert(teco_eol_writer_t *ctx, const gchar *buffer, + gsize buffer_len, GError **error); -} /* namespace SciTECO */ +void teco_eol_writer_clear(teco_eol_writer_t *ctx); -#endif +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_eol_writer_t, teco_eol_writer_clear); diff --git a/src/error.c b/src/error.c new file mode 100644 index 0000000..6a0e10f --- /dev/null +++ b/src/error.c @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include + +#include "sciteco.h" +#include "string-utils.h" +#include "interface.h" +#include "list.h" +#include "error.h" + +guint teco_error_return_args = 0; + +/* + * FIXME: Does this have to be stored in teco_machine_main_t? + * Probably becomes clear once we implement error handling by macros. + */ +guint teco_error_pos = 0, teco_error_line = 0, teco_error_column = 0; + +void +teco_error_set_coord(const gchar *str, guint pos) +{ + teco_error_pos = pos; + teco_string_get_coord(str, pos, &teco_error_line, &teco_error_column); +} + +typedef enum { + TECO_FRAME_QREG, + TECO_FRAME_FILE, + TECO_FRAME_EDHOOK, + TECO_FRAME_TOPLEVEL +} teco_frame_type_t; + +typedef struct teco_frame_t { + teco_stailq_entry_t entry; + + teco_frame_type_t type; + + guint pos, line, column; + + /* + * NOTE: This is currently sufficient to describe all + * frame types. Otherwise, add an union. + */ + gchar name[]; +} teco_frame_t; + +/** + * List of teco_frame_t describing the stack frames. + * + * Stack frames are collected deliberately unformatted + * since there are future applications where displaying + * a stack frame will not be necessary (e.g. error handled + * by SciTECO macro). + * Preformatting all stack frames would be very costly. + */ +static teco_stailq_head_t teco_frames = TECO_STAILQ_HEAD_INITIALIZER(&teco_frames); + +void +teco_error_display_short(const GError *error) +{ + teco_interface_msg(TECO_MSG_ERROR, "%s (at %d)", + error->message, teco_error_pos); +} + +void +teco_error_display_full(const GError *error) +{ + teco_interface_msg(TECO_MSG_ERROR, "%s", error->message); + + guint nr = 0; + + for (teco_stailq_entry_t *cur = teco_frames.first; cur != NULL; cur = cur->next) { + teco_frame_t *frame = (teco_frame_t *)cur; + + switch (frame->type) { + case TECO_FRAME_QREG: + teco_interface_msg(TECO_MSG_INFO, + "#%d in Q-Register \"%s\" at %d (%d:%d)", + nr, frame->name, frame->pos, frame->line, frame->column); + break; + case TECO_FRAME_FILE: + teco_interface_msg(TECO_MSG_INFO, + "#%d in file \"%s\" at %d (%d:%d)", + nr, frame->name, frame->pos, frame->line, frame->column); + break; + case TECO_FRAME_EDHOOK: + teco_interface_msg(TECO_MSG_INFO, + "#%d in \"%s\" hook execution", + nr, frame->name); + break; + case TECO_FRAME_TOPLEVEL: + teco_interface_msg(TECO_MSG_INFO, + "#%d in toplevel macro at %d (%d:%d)", + nr, frame->pos, frame->line, frame->column); + break; + } + + nr++; + } +} + +static teco_frame_t * +teco_error_add_frame(teco_frame_type_t type, gsize size) +{ + teco_frame_t *frame = g_malloc(sizeof(teco_frame_t) + size); + frame->type = type; + frame->pos = teco_error_pos; + frame->line = teco_error_line; + frame->column = teco_error_column; + teco_stailq_insert_tail(&teco_frames, &frame->entry); + + return frame; +} + +void +teco_error_add_frame_qreg(const gchar *name, gsize len) +{ + g_autofree gchar *name_printable = teco_string_echo(name, len); + teco_frame_t *frame = teco_error_add_frame(TECO_FRAME_QREG, strlen(name_printable) + 1); + strcpy(frame->name, name_printable); +} + +void +teco_error_add_frame_file(const gchar *name) +{ + teco_frame_t *frame = teco_error_add_frame(TECO_FRAME_FILE, strlen(name) + 1); + strcpy(frame->name, name); +} + +void +teco_error_add_frame_edhook(const gchar *type) +{ + teco_frame_t *frame = teco_error_add_frame(TECO_FRAME_EDHOOK, strlen(type) + 1); + strcpy(frame->name, type); +} + +void +teco_error_add_frame_toplevel(void) +{ + teco_error_add_frame(TECO_FRAME_TOPLEVEL, 0); +} + +#ifndef NDEBUG +__attribute__((destructor)) +#endif +void +teco_error_clear_frames(void) +{ + teco_stailq_entry_t *entry; + while ((entry = teco_stailq_remove_head(&teco_frames))) + g_free(entry); +} diff --git a/src/error.cpp b/src/error.cpp deleted file mode 100644 index f960a54..0000000 --- a/src/error.cpp +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include -#include - -#include "sciteco.h" -#include "interface.h" -#include "error.h" - -namespace SciTECO { - -Error::Frame * -Error::QRegFrame::copy() const -{ - Frame *frame = new QRegFrame(name); - - frame->pos = pos; - frame->line = line; - frame->column = column; - - return frame; -} - -void -Error::QRegFrame::display(gint nr) -{ - interface.msg(InterfaceCurrent::MSG_INFO, - "#%d in Q-Register \"%s\" at %d (%d:%d)", - nr, name, pos, line, column); -} - -Error::Frame * -Error::FileFrame::copy() const -{ - Frame *frame = new FileFrame(name); - - frame->pos = pos; - frame->line = line; - frame->column = column; - - return frame; -} - -void -Error::FileFrame::display(gint nr) -{ - interface.msg(InterfaceCurrent::MSG_INFO, - "#%d in file \"%s\" at %d (%d:%d)", - nr, name, pos, line, column); -} - -Error::Frame * -Error::EDHookFrame::copy() const -{ - /* coordinates do not matter */ - return new EDHookFrame(type); -} - -void -Error::EDHookFrame::display(gint nr) -{ - interface.msg(InterfaceCurrent::MSG_INFO, - "#%d in \"%s\" hook execution", - nr, type); -} - -Error::Frame * -Error::ToplevelFrame::copy() const -{ - Frame *frame = new ToplevelFrame(); - - frame->pos = pos; - frame->line = line; - frame->column = column; - - return frame; -} - -void -Error::ToplevelFrame::display(gint nr) -{ - interface.msg(InterfaceCurrent::MSG_INFO, - "#%d in toplevel macro at %d (%d:%d)", - nr, pos, line, column); -} - -Error::Error(const gchar *fmt, ...) - : frames(NULL), pos(0), line(0), column(0) -{ - va_list ap; - - va_start(ap, fmt); - description = g_strdup_vprintf(fmt, ap); - va_end(ap); -} - -Error::Error(const Error &inst) - : description(g_strdup(inst.description)), - pos(inst.pos), line(inst.line), column(inst.column) -{ - /* shallow copy of the frames */ - frames = g_slist_copy(inst.frames); - - for (GSList *cur = frames; cur; cur = g_slist_next(cur)) { - Frame *frame = (Frame *)cur->data; - cur->data = frame->copy(); - } -} - -void -Error::add_frame(Frame *frame) -{ - frame->pos = pos; - frame->line = line; - frame->column = column; - - frames = g_slist_prepend(frames, frame); -} - -void -Error::display_short(void) -{ - interface.msg(InterfaceCurrent::MSG_ERROR, - "%s (at %d)", description, pos); -} - -void -Error::display_full(void) -{ - gint nr = 0; - - interface.msg(InterfaceCurrent::MSG_ERROR, "%s", description); - - frames = g_slist_reverse(frames); - for (GSList *cur = frames; cur; cur = g_slist_next(cur)) { - Frame *frame = (Frame *)cur->data; - - frame->display(nr++); - } -} - -Error::~Error() -{ - g_free(description); - for (GSList *cur = frames; cur; cur = g_slist_next(cur)) - delete (Frame *)cur->data; - g_slist_free(frames); -} - -} /* namespace SciTECO */ diff --git a/src/error.h b/src/error.h index a12a76b..16136b9 100644 --- a/src/error.h +++ b/src/error.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,205 +14,135 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -#ifndef __ERROR_H -#define __ERROR_H - -#include -#include +#pragma once #include -#include #include "sciteco.h" -#include "memory.h" #include "string-utils.h" -namespace SciTECO { - -/** - * Thrown as exception to signify that program - * should be terminated. - */ -class Quit : public Object {}; - -/** - * Thrown as exception to cause a macro to - * return or a command-line termination. +/* + * FIXME: Introducing a second error quark might be useful to distinguish + * errors that can be cought by SciTECO macros from errors that must always + * propagate (TECO_ERROR_QUIT, TECO_ERROR_RETURN). + * On the other hand, these error codes will probably soon become obsolete + * when the SciTECO call stack no longer corresponds with the C callstack. */ -class Return : public Object { -public: - guint args; - - Return(guint _args = 0) : args(_args) {} -}; - -class Error : public Object { - GSList *frames; - -public: - gchar *description; - gint pos; - gint line, column; - - class Frame : public Object { - public: - gint pos; - gint line, column; - - virtual Frame *copy() const = 0; - virtual ~Frame() {} - - virtual void display(gint nr) = 0; - }; - - class QRegFrame : public Frame { - gchar *name; - - public: - QRegFrame(const gchar *_name) - : name(g_strdup(_name)) {} - - Frame *copy() const; - - ~QRegFrame() - { - g_free(name); - } - - void display(gint nr); - }; - - class FileFrame : public Frame { - gchar *name; - - public: - FileFrame(const gchar *_name) - : name(g_strdup(_name)) {} - - Frame *copy() const; - - ~FileFrame() - { - g_free(name); - } - - void display(gint nr); - }; - - class EDHookFrame : public Frame { - gchar *type; - - public: - EDHookFrame(const gchar *_type) - : type(g_strdup(_type)) {} - - Frame *copy() const; - - ~EDHookFrame() - { - g_free(type); - } - - void display(gint nr); - }; - - class ToplevelFrame : public Frame { - public: - Frame *copy() const; - - void display(gint nr); - }; - - Error(const gchar *fmt, ...) G_GNUC_PRINTF(2, 3); - Error(const Error &inst); - ~Error(); - - inline void - set_coord(const gchar *str, gint _pos) - { - pos = _pos; - String::get_coord(str, pos, line, column); - } - - void add_frame(Frame *frame); - - void display_short(void); - void display_full(void); -}; +#define TECO_ERROR (g_quark_from_static_string("sciteco-error-quark")) -class StdError : public Error { -public: - StdError(const gchar *type, const std::exception &error) - : Error("%s: %s", type, error.what()) {} - StdError(const std::exception &error) - : Error("%s: %s", typeid(error).name(), error.what()) {} -}; +typedef enum { + /** Default (catch-all) error code */ + TECO_ERROR_FAILED = 0, -class GlibError : public Error { -public: - /** - * Construct error for glib's GError. - * Ownership of the error's resources is passed - * the GlibError object. + /* + * FIXME: Subsume all these errors under TECO_ERROR_SYNTAX or TECO_ERROR_FAIL? + * They will mainly be different in their error message. */ - GlibError(GError *gerror) - : Error("%s", gerror->message) - { - g_error_free(gerror); - } -}; - -class SyntaxError : public Error { -public: - SyntaxError(gchar chr) - : Error("Syntax error \"%c\" (%d)", chr, chr) {} -}; - -class ArgExpectedError : public Error { -public: - ArgExpectedError(const gchar *cmd) - : Error("Argument expected for <%s>", cmd) {} - ArgExpectedError(gchar cmd) - : Error("Argument expected for <%c>", cmd) {} -}; - -class MoveError : public Error { -public: - MoveError(const gchar *cmd) - : Error("Attempt to move pointer off page with <%s>", - cmd) {} - MoveError(gchar cmd) - : Error("Attempt to move pointer off page with <%c>", - cmd) {} -}; - -class RangeError : public Error { -public: - RangeError(const gchar *cmd) - : Error("Invalid range specified for <%s>", cmd) {} - RangeError(gchar cmd) - : Error("Invalid range specified for <%c>", cmd) {} -}; - -class InvalidQRegError : public Error { -public: - InvalidQRegError(const gchar *name, bool local = false) - : Error("Invalid Q-Register \"%s%s\"", - local ? "." : "", name) {} - InvalidQRegError(gchar name, bool local = false) - : Error("Invalid Q-Register \"%s%c\"", - local ? "." : "", name) {} -}; - -class QRegOpUnsupportedError : public Error { -public: - QRegOpUnsupportedError(const gchar *name, bool local = false) - : Error("Operation unsupported on " - "Q-Register \"%s%s\"", - local ? "." : "", name) {} -}; - -} /* namespace SciTECO */ - -#endif + TECO_ERROR_SYNTAX, + TECO_ERROR_ARGEXPECTED, + TECO_ERROR_MOVE, + TECO_ERROR_WORDS, + TECO_ERROR_RANGE, + TECO_ERROR_INVALIDQREG, + TECO_ERROR_QREGOPUNSUPPORTED, + TECO_ERROR_QREGCONTAINSNULL, + TECO_ERROR_MEMLIMIT, + + /** Interrupt current operation */ + TECO_ERROR_INTERRUPTED, + + /** Thrown to signal command line replacement */ + TECO_ERROR_CMDLINE = 0x80, + /** Thrown as exception to cause a macro to return or a command-line termination. */ + TECO_ERROR_RETURN, + /** Thrown as exception to signify that program should be terminated. */ + TECO_ERROR_QUIT +} teco_error_t; + +static inline void +teco_error_syntax_set(GError **error, gchar chr) +{ + g_set_error(error, TECO_ERROR, TECO_ERROR_SYNTAX, + "Syntax error \"%c\" (%d)", chr, chr); +} + +static inline void +teco_error_argexpected_set(GError **error, const gchar *cmd) +{ + g_set_error(error, TECO_ERROR, TECO_ERROR_ARGEXPECTED, + "Argument expected for <%s>", cmd); +} + +static inline void +teco_error_move_set(GError **error, const gchar *cmd) +{ + g_set_error(error, TECO_ERROR, TECO_ERROR_MOVE, + "Attempt to move pointer off page with <%s>", cmd); +} + +static inline void +teco_error_words_set(GError **error, const gchar *cmd) +{ + g_set_error(error, TECO_ERROR, TECO_ERROR_MOVE, + "Not enough words to delete with <%s>", cmd); +} + +static inline void +teco_error_range_set(GError **error, const gchar *cmd) +{ + g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE, + "Invalid range specified for <%s>", cmd); +} + +static inline void +teco_error_invalidqreg_set(GError **error, const gchar *name, gsize len, gboolean local) +{ + g_autofree gchar *name_printable = teco_string_echo(name, len); + g_set_error(error, TECO_ERROR, TECO_ERROR_INVALIDQREG, + "Invalid %sQ-Register \"%s\"", local ? "local " : "", name_printable); +} + +static inline void +teco_error_qregopunsupported_set(GError **error, const gchar *name, gsize len, gboolean local) +{ + g_autofree gchar *name_printable = teco_string_echo(name, len); + g_set_error(error, TECO_ERROR, TECO_ERROR_QREGOPUNSUPPORTED, + "Operation unsupported on %sQ-Register \"%s\"", local ? "local " : "", name_printable); +} + +static inline void +teco_error_qregcontainsnull_set(GError **error, const gchar *name, gsize len, gboolean local) +{ + g_autofree gchar *name_printable = teco_string_echo(name, len); + g_set_error(error, TECO_ERROR, TECO_ERROR_QREGCONTAINSNULL, + "%sQ-Register \"%s\" contains null-byte", local ? "Local " : "", name_printable); +} + +static inline void +teco_error_interrupted_set(GError **error) +{ + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_INTERRUPTED, "Interrupted"); +} + +extern guint teco_error_return_args; + +static inline void +teco_error_return_set(GError **error, guint args) +{ + teco_error_return_args = args; + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_RETURN, ""); +} + +extern guint teco_error_pos, teco_error_line, teco_error_column; + +void teco_error_set_coord(const gchar *str, guint pos); + +void teco_error_display_short(const GError *error); +void teco_error_display_full(const GError *error); + +void teco_error_add_frame_qreg(const gchar *name, gsize len); +void teco_error_add_frame_file(const gchar *name); +void teco_error_add_frame_edhook(const gchar *type); +void teco_error_add_frame_toplevel(void); + +void teco_error_clear_frames(void); diff --git a/src/expressions.c b/src/expressions.c new file mode 100644 index 0000000..9a00fee --- /dev/null +++ b/src/expressions.c @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include "sciteco.h" +#include "error.h" +#include "undo.h" +#include "expressions.h" + +/* + * Number and operator stacks are static, so + * they can be passed to the undo token constructors. + * This is OK since we're currently singleton. + */ +static GArray *teco_numbers; + +TECO_DEFINE_ARRAY_UNDO_INSERT_VAL(teco_numbers, teco_int_t); +TECO_DEFINE_ARRAY_UNDO_REMOVE_INDEX(teco_numbers); + +static GArray *teco_operators; + +TECO_DEFINE_ARRAY_UNDO_INSERT_VAL(teco_operators, teco_operator_t); +TECO_DEFINE_ARRAY_UNDO_REMOVE_INDEX(teco_operators); + +static gboolean teco_expressions_calc(GError **error); + +static void __attribute__((constructor)) +teco_expressions_init(void) +{ + teco_numbers = g_array_sized_new(FALSE, FALSE, sizeof(teco_int_t), 1024); + teco_operators = g_array_sized_new(FALSE, FALSE, sizeof(teco_operator_t), 1024); +} + +/** Get operator precedence */ +static inline gint +teco_expressions_precedence(teco_operator_t op) +{ + return op >> 4; +} + +gint teco_num_sign = 1; +gint teco_radix = 10; + +void +teco_expressions_push_int(teco_int_t number) +{ + while (teco_operators->len > 0 && teco_expressions_peek_op(0) == TECO_OP_NEW) + teco_expressions_pop_op(0); + + teco_expressions_push_op(TECO_OP_NUMBER); + + if (teco_num_sign < 0) { + teco_set_num_sign(1); + number *= -1; + } + + g_array_append_val(teco_numbers, number); + undo__remove_index__teco_numbers(teco_numbers->len-1); +} + +teco_int_t +teco_expressions_peek_num(guint index) +{ + return g_array_index(teco_numbers, teco_int_t, teco_numbers->len - 1 - index); +} + +teco_int_t +teco_expressions_pop_num(guint index) +{ + teco_int_t n = 0; + teco_operator_t op = teco_expressions_pop_op(0); + + g_assert(op == TECO_OP_NUMBER); + + if (teco_numbers->len > 0) { + n = teco_expressions_peek_num(index); + undo__insert_val__teco_numbers(teco_numbers->len - 1 - index, n); + g_array_remove_index(teco_numbers, teco_numbers->len - 1 - index); + } + + return n; +} + +gboolean +teco_expressions_pop_num_calc(teco_int_t *ret, teco_int_t imply, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return FALSE; + if (teco_num_sign < 0) + teco_set_num_sign(1); + + teco_int_t v = teco_expressions_args() > 0 ? teco_expressions_pop_num(0) : imply; + if (ret) + *ret = v; + return TRUE; +} + +void +teco_expressions_add_digit(gchar digit) +{ + teco_int_t n = teco_expressions_args() > 0 ? teco_expressions_pop_num(0) : 0; + + teco_expressions_push(n*teco_radix + (n < 0 ? -1 : 1)*(digit - '0')); +} + +void +teco_expressions_push_op(teco_operator_t op) +{ + g_array_append_val(teco_operators, op); + undo__remove_index__teco_operators(teco_operators->len-1); +} + +gboolean +teco_expressions_push_calc(teco_operator_t op, GError **error) +{ + gint first = teco_expressions_first_op(); + + /* calculate if op has lower precedence than op on stack */ + if (first >= 0 && + teco_expressions_precedence(op) <= teco_expressions_precedence(teco_expressions_peek_op(first)) && + !teco_expressions_calc(error)) + return FALSE; + + teco_expressions_push_op(op); + return TRUE; +} + +teco_operator_t +teco_expressions_peek_op(guint index) +{ + return g_array_index(teco_operators, teco_operator_t, teco_operators->len - 1 - index); +} + +teco_operator_t +teco_expressions_pop_op(guint index) +{ + teco_operator_t op = TECO_OP_NIL; + + if (teco_operators->len > 0) { + op = teco_expressions_peek_op(index); + undo__insert_val__teco_operators(teco_operators->len - 1 - index, op); + g_array_remove_index(teco_operators, teco_operators->len - 1 - index); + } + + return op; +} + +static gboolean +teco_expressions_calc(GError **error) +{ + teco_int_t result; + + if (!teco_operators->len || teco_expressions_peek_op(0) != TECO_OP_NUMBER) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Missing right operand"); + return FALSE; + } + teco_int_t vright = teco_expressions_pop_num(0); + teco_operator_t op = teco_expressions_pop_op(0); + if (!teco_operators->len || teco_expressions_peek_op(0) != TECO_OP_NUMBER) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Missing left operand"); + return FALSE; + } + teco_int_t vleft = teco_expressions_pop_num(0); + + switch (op) { + case TECO_OP_POW: + for (result = 1; vright--; result *= vleft); + break; + case TECO_OP_MUL: + result = vleft * vright; + break; + case TECO_OP_DIV: + if (!vright) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Division by zero"); + return FALSE; + } + result = vleft / vright; + break; + case TECO_OP_MOD: + if (!vright) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Remainder of division by zero"); + return FALSE; + } + result = vleft % vright; + break; + case TECO_OP_ADD: + result = vleft + vright; + break; + case TECO_OP_SUB: + result = vleft - vright; + break; + case TECO_OP_AND: + result = vleft & vright; + break; + case TECO_OP_XOR: + result = vleft ^ vright; + break; + case TECO_OP_OR: + result = vleft | vright; + break; + default: + /* shouldn't happen */ + g_assert_not_reached(); + } + + teco_expressions_push(result); + return TRUE; +} + +gboolean +teco_expressions_eval(gboolean pop_brace, GError **error) +{ + for (;;) { + gint n = teco_expressions_first_op(); + if (n < 0) + break; + + teco_operator_t op = teco_expressions_peek_op(n); + if (op == TECO_OP_BRACE) { + if (pop_brace) + teco_expressions_pop_op(n); + break; + } + if (n < 1) + break; + + if (!teco_expressions_calc(error)) + return FALSE; + } + + return TRUE; +} + +guint +teco_expressions_args(void) +{ + guint n = 0; + + while (n < teco_operators->len && teco_expressions_peek_op(n) == TECO_OP_NUMBER) + n++; + + return n; +} + +gint +teco_expressions_first_op(void) +{ + for (guint i = 0; i < teco_operators->len; i++) { + switch (teco_expressions_peek_op(i)) { + case TECO_OP_NUMBER: + case TECO_OP_NEW: + break; + default: + return i; + } + } + + return -1; /* no operator */ +} + +gboolean +teco_expressions_discard_args(GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return FALSE; + for (guint i = teco_expressions_args(); i; i--) + if (!teco_expressions_pop_num_calc(NULL, 0, error)) + return FALSE; + return TRUE; +} + +/** The nesting level of braces */ +guint teco_brace_level = 0; + +void +teco_expressions_brace_open(void) +{ + teco_expressions_push_op(TECO_OP_BRACE); + teco_undo_guint(teco_brace_level)++; +} + +gboolean +teco_expressions_brace_return(guint keep_braces, guint args, GError **error) +{ + /* + * FIXME: Allocating on the stack might be dangerous. + */ + teco_int_t return_numbers[args]; + + for (guint i = args; i; i--) + return_numbers[i-1] = teco_expressions_pop_num(0); + + teco_undo_guint(teco_brace_level); + + while (teco_brace_level > keep_braces) { + if (!teco_expressions_discard_args(error) || + !teco_expressions_eval(TRUE, error)) + return FALSE; + teco_brace_level--; + } + + for (guint i = 0; i < args; i++) + teco_expressions_push(return_numbers[i]); + + return TRUE; +} + +gboolean +teco_expressions_brace_close(GError **error) +{ + if (!teco_brace_level) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Missing opening brace"); + return FALSE; + } + teco_undo_guint(teco_brace_level)--; + return teco_expressions_eval(TRUE, error); +} + +void +teco_expressions_clear(void) +{ + g_array_set_size(teco_numbers, 0); + g_array_set_size(teco_operators, 0); + teco_brace_level = 0; +} + +/** + * Format a TECO integer as the `\` command would. + * + * @param buffer The output buffer of at least TECO_EXPRESSIONS_FORMAT_LEN characters. + * The output string will be null-terminated. + * @param number The number to format. + * @return A pointer into buffer to the beginning of the formatted number. + */ +gchar * +teco_expressions_format(gchar *buffer, teco_int_t number) +{ + gchar *p = buffer + TECO_EXPRESSIONS_FORMAT_LEN; + + teco_int_t v = ABS(number); + + *--p = '\0'; + do { + *--p = '0' + (v % teco_radix); + if (*p > '9') + *p += 'A' - '9' - 1; + } while ((v /= teco_radix)); + if (number < 0) + *--p = '-'; + + return p; +} + +#ifndef NDEBUG +static void __attribute__((destructor)) +teco_expressions_cleanup(void) +{ + g_array_free(teco_numbers, TRUE); + g_array_free(teco_operators, TRUE); +} +#endif diff --git a/src/expressions.cpp b/src/expressions.cpp deleted file mode 100644 index 7ccdd31..0000000 --- a/src/expressions.cpp +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include "sciteco.h" -#include "error.h" -#include "expressions.h" - -namespace SciTECO { - -Expressions expressions; -Expressions::NumberStack Expressions::numbers; -Expressions::OperatorStack Expressions::operators; - -tecoInt -Expressions::push(tecoInt number) -{ - while (operators.items() && operators.peek() == OP_NEW) - pop_op(); - - push(OP_NUMBER); - - if (num_sign < 0) { - set_num_sign(1); - number *= -1; - } - - NumberStack::undo_pop(); - return numbers.push(number); -} - -tecoInt -Expressions::pop_num(guint index) -{ - tecoInt n = 0; - Operator op = pop_op(); - - g_assert(op == OP_NUMBER); - - if (numbers.items()) { - n = numbers.pop(index); - NumberStack::undo_push(n, index); - } - - return n; -} - -tecoInt -Expressions::pop_num_calc(guint index, tecoInt imply) -{ - eval(); - if (num_sign < 0) - set_num_sign(1); - - return args() > 0 ? pop_num(index) : imply; -} - -tecoInt -Expressions::add_digit(gchar digit) -{ - tecoInt n = args() > 0 ? pop_num() : 0; - - return push(n*radix + (n < 0 ? -1 : 1)*(digit - '0')); -} - -Expressions::Operator -Expressions::push(Expressions::Operator op) -{ - OperatorStack::undo_pop(); - return operators.push(op); -} - -Expressions::Operator -Expressions::push_calc(Expressions::Operator op) -{ - gint first = first_op(); - - /* calculate if op has lower precedence than op on stack */ - if (first >= 0 && - precedence(op) <= precedence(operators.peek(first))) - calc(); - - return push(op); -} - -Expressions::Operator -Expressions::pop_op(guint index) -{ - Operator op = OP_NIL; - - if (operators.items()) { - op = operators.pop(index); - OperatorStack::undo_push(op, index); - } - - return op; -} - -void -Expressions::calc(void) -{ - tecoInt result; - - tecoInt vright; - Operator op; - tecoInt vleft; - - if (!operators.items() || operators.peek() != OP_NUMBER) - throw Error("Missing right operand"); - vright = pop_num(); - op = pop_op(); - if (!operators.items() || operators.peek() != OP_NUMBER) - throw Error("Missing left operand"); - vleft = pop_num(); - - switch (op) { - case OP_POW: - for (result = 1; vright--; result *= vleft); - break; - case OP_MUL: - result = vleft * vright; - break; - case OP_DIV: - if (!vright) - throw Error("Division by zero"); - result = vleft / vright; - break; - case OP_MOD: - if (!vright) - throw Error("Remainder of division by zero"); - result = vleft % vright; - break; - case OP_ADD: - result = vleft + vright; - break; - case OP_SUB: - result = vleft - vright; - break; - case OP_AND: - result = vleft & vright; - break; - case OP_XOR: - result = vleft ^ vright; - break; - case OP_OR: - result = vleft | vright; - break; - default: - /* shouldn't happen */ - g_assert_not_reached(); - } - - push(result); -} - -void -Expressions::eval(bool pop_brace) -{ - for (;;) { - gint n = first_op(); - Operator op; - - if (n < 0) - break; - - op = operators.peek(n); - if (op == OP_BRACE) { - if (pop_brace) - pop_op(n); - break; - } - if (n < 1) - break; - - calc(); - } -} - -guint -Expressions::args(void) -{ - guint n = 0; - guint items = operators.items(); - - while (n < items && operators.peek(n) == OP_NUMBER) - n++; - - return n; -} - -gint -Expressions::first_op(void) -{ - guint items = operators.items(); - - for (guint i = 0; i < items; i++) { - switch (operators.peek(i)) { - case OP_NUMBER: - case OP_NEW: - break; - default: - return i; - } - } - - return -1; /* no operator */ -} - -void -Expressions::discard_args(void) -{ - eval(); - for (guint i = args(); i; i--) - pop_num_calc(); -} - -void -Expressions::brace_return(guint keep_braces, guint args) -{ - tecoInt return_numbers[args]; - - for (guint i = args; i; i--) - return_numbers[i-1] = pop_num(); - - undo.push_var(brace_level); - - while (brace_level > keep_braces) { - discard_args(); - eval(true); - brace_level--; - } - - for (guint i = 0; i < args; i++) - push(return_numbers[i]); -} - -const gchar * -Expressions::format(tecoInt number) -{ - /* maximum length if radix = 2 */ - static gchar buf[1+sizeof(number)*8+1]; - gchar *p = buf + sizeof(buf); - - tecoInt v = ABS(number); - - *--p = '\0'; - do { - *--p = '0' + (v % radix); - if (*p > '9') - *p += 'A' - '9' - 1; - } while ((v /= radix)); - if (number < 0) - *--p = '-'; - - return p; -} - -} /* namespace SciTECO */ diff --git a/src/expressions.h b/src/expressions.h index bdd683c..6ff8af4 100644 --- a/src/expressions.h +++ b/src/expressions.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,300 +14,149 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -#ifndef __EXPRESSIONS_H -#define __EXPRESSIONS_H +#pragma once #include -#include "memory.h" +#include "sciteco.h" #include "undo.h" -#include "error.h" - -namespace SciTECO { - -template -class ValueStack : public Object { - /* - * NOTE: Since value stacks are usually singleton, - * we pass them as a template parameter, saving space - * in the undo token. - */ - template &stack> - class UndoTokenPush : public UndoToken { - Type value; - guint index; - - public: - UndoTokenPush(Type _value, guint _index = 0) - : value(_value), index(_index) {} - - void - run(void) - { - stack.push(value, index); - } - }; - - template &stack> - class UndoTokenPop : public UndoToken { - guint index; - - public: - UndoTokenPop(guint _index = 0) - : index(_index) {} - - void - run(void) - { - stack.pop(index); - } - }; - - /** Beginning of stack area */ - Type *stack; - /** End of stack area */ - Type *stack_top; - - /** Pointer to top element on stack */ - Type *sp; - -public: - ValueStack(gsize size = 1024) - { - stack = new Type[size]; - /* stack grows to smaller addresses */ - sp = stack_top = stack+size; - } - - ~ValueStack() - { - delete[] stack; - } - - inline guint - items(void) - { - return stack_top - sp; - } - - inline Type & - push(Type value, guint index = 0) - { - if (G_UNLIKELY(sp == stack)) - throw Error("Stack overflow"); - - /* reserve space for new element */ - sp--; - g_assert(items() > index); - - /* move away elements after index (index > 0) */ - for (guint i = 0; i < index; i++) - sp[i] = sp[i+1]; - - return sp[index] = value; - } - - template &stack> - static inline void - undo_push(Type value, guint index = 0) - { - undo.push>(value, index); - } - - inline Type - pop(guint index = 0) - { - /* peek() already asserts */ - Type v = peek(index); - - /* elements after index are moved to index (index > 0) */ - while (index--) - sp[index+1] = sp[index]; - - /* free space of element to pop */ - sp++; - - return v; - } - - template &stack> - static inline void - undo_pop(guint index = 0) - { - undo.push>(index); - } - - inline Type & - peek(guint index = 0) - { - g_assert(items() > index); - - return sp[index]; - } - - /** Clear all but `keep_items` items. */ - inline void - clear(guint keep_items = 0) - { - g_assert(keep_items <= items()); - - sp = stack_top - keep_items; - } -}; /** - * Arithmetic expression stacks + * Defines a function undo__insert_val__ARRAY() to insert a value into + * a fixed GArray. + * + * @note + * This optimizes undo token memory consumption under the assumption + * that ARRAY is a global object that does not have to be stored in + * the undo tokens. + * Otherwise, you could simply undo__g_array_insert_val(...). + * + * @fixme + * If we only ever use INDEX == ARRAY->len, we might simplify this + * to undo__append_val__ARRAY(). */ -extern class Expressions : public Object { -public: - /** - * Operator type. - * The enumeration value divided by 16 represents - * its precedence (small values mean low precedence). - * In other words, the value's lower nibble is - * reserved for enumerating operators of the - * same precedence. - */ - enum Operator { - /* - * Pseudo operators - */ - OP_NIL = 0x00, - OP_NEW, - OP_BRACE, - OP_NUMBER, - /* - * Real operators - */ - OP_POW = 0x60, // ^* - OP_MOD = 0x50, // ^/ - OP_DIV, // / - OP_MUL, // * - OP_SUB = 0x40, // - - OP_ADD, // + - OP_AND = 0x30, // & - OP_XOR = 0x20, // ^# - OP_OR = 0x10 // # - }; +#define TECO_DEFINE_ARRAY_UNDO_INSERT_VAL(ARRAY, TYPE) \ + static inline void \ + insert_val__##ARRAY(guint index, TYPE value) \ + { \ + g_array_insert_val(ARRAY, index, value); \ + } \ + TECO_DEFINE_UNDO_CALL(insert_val__##ARRAY, guint, TYPE) -private: - /** Get operator precedence */ - inline gint - precedence(Operator op) - { - return op >> 4; - } +/** + * Defines a function undo__remove_index__ARRAY() to remove a value from + * a fixed GArray. + * + * @note + * See TECO_DEFINE_ARRAY_UNDO_INSERT_VAL(). + * undo__g_array_remove_index(...) would also be possible. + * + * @fixme + * If we only ever use INDEX == ARRAY->len-1, we might simplify this + * to undo__pop__ARRAY(). + */ +#define TECO_DEFINE_ARRAY_UNDO_REMOVE_INDEX(ARRAY) \ + static inline void \ + remove_index__##ARRAY(guint index) \ + { \ + g_array_remove_index(ARRAY, index); \ + } \ + TECO_DEFINE_UNDO_CALL(remove_index__##ARRAY, guint) +/** + * Operator type. + * The enumeration value divided by 16 represents + * its precedence (small values mean low precedence). + * In other words, the value's lower nibble is + * reserved for enumerating operators of the + * same precedence. + */ +typedef enum { /* - * Number and operator stacks are static, so - * they can be passed to the undo token constructors. - * This is OK since Expression is singleton. + * Pseudo operators */ - typedef ValueStack NumberStack; - static NumberStack numbers; - - typedef ValueStack OperatorStack; - static OperatorStack operators; - -public: - Expressions() : num_sign(1), radix(10), brace_level(0) {} - - gint num_sign; - inline void - set_num_sign(gint sign) - { - undo.push_var(num_sign) = sign; - } - - gint radix; - inline void - set_radix(gint r) - { - undo.push_var(radix) = r; - } - - tecoInt push(tecoInt number); - - /** - * Push characters of a C-string. - * Could be overloaded on push(tecoInt) - * but this confuses GCC. + TECO_OP_NIL = 0x00, + TECO_OP_NEW, + TECO_OP_BRACE, + TECO_OP_NUMBER, + /* + * Real operators */ - inline void - push_str(const gchar *str) - { - while (*str) - push(*str++); - } - - inline tecoInt - peek_num(guint index = 0) - { - return numbers.peek(index); - } - tecoInt pop_num(guint index = 0); - tecoInt pop_num_calc(guint index, tecoInt imply); - inline tecoInt - pop_num_calc(guint index = 0) - { - return pop_num_calc(index, num_sign); - } + TECO_OP_POW = 0x60, // ^* + TECO_OP_MOD = 0x50, // ^/ + TECO_OP_DIV, // / + TECO_OP_MUL, // * + TECO_OP_SUB = 0x40, // - + TECO_OP_ADD, // + + TECO_OP_AND = 0x30, // & + TECO_OP_XOR = 0x20, // ^# + TECO_OP_OR = 0x10 // # +} teco_operator_t; + +extern gint teco_num_sign; + +static inline void +teco_set_num_sign(gint sign) +{ + teco_undo_gint(teco_num_sign) = sign; +} + +extern gint teco_radix; + +static inline void +teco_set_radix(gint r) +{ + teco_undo_gint(teco_radix) = r; +} + +void teco_expressions_push_int(teco_int_t number); + +/** Push characters of a C-string. */ +static inline void +teco_expressions_push_str(const gchar *str) +{ + while (*str) + teco_expressions_push_int(*str++); +} + +teco_int_t teco_expressions_peek_num(guint index); +teco_int_t teco_expressions_pop_num(guint index); +gboolean teco_expressions_pop_num_calc(teco_int_t *ret, teco_int_t imply, GError **error); + +void teco_expressions_add_digit(gchar digit); + +void teco_expressions_push_op(teco_operator_t op); +gboolean teco_expressions_push_calc(teco_operator_t op, GError **error); - tecoInt add_digit(gchar digit); - - Operator push(Operator op); - Operator push_calc(Operator op); - inline Operator - peek_op(guint index = 0) - { - return operators.peek(index); - } - Operator pop_op(guint index = 0); - - void eval(bool pop_brace = false); - - guint args(void); - - void discard_args(void); +/* + * FIXME: Does not work for TECO_OP_* constants as they are treated like int. + */ +#define teco_expressions_push(X) \ + (_Generic((X), default : teco_expressions_push_int, \ + char * : teco_expressions_push_str, \ + const char * : teco_expressions_push_str)(X)) - /** The nesting level of braces */ - guint brace_level; +teco_operator_t teco_expressions_peek_op(guint index); +teco_operator_t teco_expressions_pop_op(guint index); - inline void - brace_open(void) - { - push(OP_BRACE); - undo.push_var(brace_level)++; - } +gboolean teco_expressions_eval(gboolean pop_brace, GError **error); - void brace_return(guint keep_braces, guint args = 0); +guint teco_expressions_args(void); - inline void - brace_close(void) - { - if (!brace_level) - throw Error("Missing opening brace"); - undo.push_var(brace_level)--; - eval(true); - } +gint teco_expressions_first_op(void); - inline void - clear(void) - { - numbers.clear(); - operators.clear(); - brace_level = 0; - } +gboolean teco_expressions_discard_args(GError **error); - const gchar *format(tecoInt number); +extern guint teco_brace_level; -private: - void calc(void); +void teco_expressions_brace_open(void); +gboolean teco_expressions_brace_return(guint keep_braces, guint args, GError **error); +gboolean teco_expressions_brace_close(GError **error); - gint first_op(void); -} expressions; +void teco_expressions_clear(void); -} /* namespace SciTECO */ +/** Maximum size required to format a number if teco_radix == 2 */ +#define TECO_EXPRESSIONS_FORMAT_LEN \ + (1 + sizeof(teco_int_t)*8 + 1) -#endif +gchar *teco_expressions_format(gchar *buffer, teco_int_t number); diff --git a/src/file-utils.c b/src/file-utils.c new file mode 100644 index 0000000..4948787 --- /dev/null +++ b/src/file-utils.c @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_WINDOWS_H +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#include +#include + +#include "sciteco.h" +#include "qreg.h" +#include "glob.h" +#include "interface.h" +#include "string-utils.h" +#include "file-utils.h" + +#ifdef G_OS_WIN32 + +/* + * NOTE: File attributes are represented as DWORDs in the Win32 API + * which should be equivalent to guint32. + */ +G_STATIC_ASSERT(sizeof(DWORD) == sizeof(teco_file_attributes_t)); +/* + * NOTE: Invalid file attributes should be represented by 0xFFFFFFFF. + */ +G_STATIC_ASSERT(INVALID_FILE_ATTRIBUTES == TECO_FILE_INVALID_ATTRIBUTES); + +teco_file_attributes_t +teco_file_get_attributes(const gchar *filename) +{ + return GetFileAttributes((LPCTSTR)filename); +} + +void +teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs) +{ + SetFileAttributes((LPCTSTR)filename, attrs); +} + +gchar * +teco_file_get_absolute_path(const gchar *path) +{ + TCHAR buf[MAX_PATH]; + return path && GetFullPathName(path, sizeof(buf), buf, NULL) ? g_strdup(buf) : NULL; +} + +gboolean +teco_file_is_visible(const gchar *path) +{ + return !(GetFileAttributes((LPCTSTR)path) & FILE_ATTRIBUTE_HIDDEN); +} + +#else /* !G_OS_WIN32 */ + +teco_file_attributes_t +teco_file_get_attributes(const gchar *filename) +{ + struct stat buf; + return g_stat(filename, &buf) ? TECO_FILE_INVALID_ATTRIBUTES : buf.st_mode; +} + +void +teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs) +{ + g_chmod(filename, attrs); +} + +#ifdef G_OS_UNIX + +gchar * +teco_file_get_absolute_path(const gchar *path) +{ + gchar buf[PATH_MAX]; + + if (!path) + return NULL; + if (realpath(path, buf)) + return g_strdup(buf); + if (g_path_is_absolute(path)) + return g_strdup(path); + + g_autofree gchar *cwd = g_get_current_dir(); + return g_build_filename(cwd, path, NULL); +} + +gboolean +teco_file_is_visible(const gchar *path) +{ + g_autofree gchar *basename = g_path_get_basename(path); + return *basename != '.'; +} + +#else /* !G_OS_UNIX */ + +#if GLIB_CHECK_VERSION(2,58,0) + +/* + * FIXME: This should perhaps be preferred on any platform. + * But it will complicate preprocessing. + */ +gchar * +teco_file_get_absolute_path(const gchar *path) +{ + return g_canonicalize_filename(path, NULL); +} + +#else /* !GLIB_CHECK_VERSION(2,58,0) */ + +/* + * This will never canonicalize relative paths. + * I.e. the absolute path will often contain + * relative components, even if `path` exists. + * The only exception would be a simple filename + * not containing any "..". + */ +gchar * +teco_file_get_absolute_path(const gchar *path) +{ + if (!path) + return NULL; + if (g_path_is_absolute(path)) + return g_strdup(path); + + g_autofree gchar *cwd = g_get_current_dir(); + return g_build_filename(cwd, path, NULL); +} + +#endif /* !GLIB_CHECK_VERSION(2,58,0) */ + +/* + * There's no platform-independent way to determine if a file + * is visible/hidden, so we just assume that all files are + * visible. + */ +gboolean +teco_file_is_visible(const gchar *path) +{ + return TRUE; +} + +#endif /* !G_OS_UNIX */ + +#endif /* !G_OS_WIN32 */ + +/** + * Perform tilde expansion on a file name or path. + * + * This supports only strings with a "~" prefix. + * A user name after "~" is not supported. + * The $HOME environment variable/register is used to retrieve + * the current user's home directory. + */ +gchar * +teco_file_expand_path(const gchar *path) +{ + if (!path) + return g_strdup(""); + + if (path[0] != '~' || (path[1] && !G_IS_DIR_SEPARATOR(path[1]))) + return g_strdup(path); + + /* + * $HOME should not have a trailing directory separator since + * it is canonicalized to an absolute path at startup, + * but this ensures that a proper path is constructed even if + * it does (e.g. $HOME is changed later on). + * + * FIXME: In the future, it might be possible to remove the entire register. + */ + teco_qreg_t *qreg = teco_qreg_table_find(&teco_qreg_table_globals, "$HOME", 5); + g_assert(qreg != NULL); + + /* + * Getting the string should not possible to fail. + * The $HOME register should not contain any null-bytes on startup, + * but it may have been changed later on. + */ + g_auto(teco_string_t) home = {NULL, 0}; + if (!qreg->vtable->get_string(qreg, &home.data, &home.len, NULL) || + teco_string_contains(&home, '\0')) + return g_strdup(path); + + return g_build_filename(home.data, path+1, NULL); +} + +/** + * Auto-complete a filename/directory. + * + * @param filename The filename to auto-complete or NULL. + * @param file_test Restrict completion to files matching the test. + * If G_FILE_TEST_EXISTS, both files and directories are completed. + * If G_FILE_TEST_IS_DIR, only directories will be completed. + * @param insert String to initialize with the autocompletion. + * @return TRUE if the completion was unambiguous (eg. command can be terminated). + */ +gboolean +teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_t *insert) +{ + memset(insert, 0, sizeof(*insert)); + + if (teco_globber_is_pattern(filename)) + return FALSE; + + g_autofree gchar *filename_expanded = teco_file_expand_path(filename); + gsize filename_len = strlen(filename_expanded); + + /* + * Derive base and directory names. + * We do not use g_path_get_basename() or g_path_get_dirname() + * since we need strict suffixes and prefixes of filename + * in order to construct paths of entries in dirname + * that are suitable for auto completion. + */ + gsize dirname_len = teco_file_get_dirname_len(filename_expanded); + g_autofree gchar *dirname = g_strndup(filename_expanded, dirname_len); + gchar *basename = filename_expanded + dirname_len; + + g_autoptr(GDir) dir = g_dir_open(dirname_len ? dirname : ".", 0, NULL); + if (!dir) + return FALSE; + + /* + * On Windows, both forward and backslash + * directory separators are allowed in directory + * names passed to glib. + * To imitate glib's behaviour, we use + * the last valid directory separator in `filename_expanded` + * to generate new separators. + * This also allows forward-slash auto-completion + * on Windows. + */ + const gchar *dir_sep = dirname_len ? dirname + dirname_len - 1 + : G_DIR_SEPARATOR_S; + + GSList *files = NULL; + guint files_len = 0; + gsize prefix_len = 0; + + const gchar *cur_basename; + while ((cur_basename = g_dir_read_name(dir))) { + if (!g_str_has_prefix(cur_basename, basename)) + continue; + + /* + * NOTE: `dirname` contains any directory separator, so strcat() works here. + * Reserving one byte at the end of the filename ensures we can easily + * append the directory separator without reallocations. + */ + gchar *cur_filename = g_malloc(strlen(dirname)+strlen(cur_basename)+2); + strcat(strcpy(cur_filename, dirname), cur_basename); + + /* + * NOTE: This avoids g_file_test() for G_FILE_TEST_EXISTS + * since the file we process here should always exist. + */ + if ((!*basename && !teco_file_is_visible(cur_filename)) || + (file_test != G_FILE_TEST_EXISTS && + !g_file_test(cur_filename, file_test))) { + g_free(cur_filename); + continue; + } + + if (file_test == G_FILE_TEST_IS_DIR || + g_file_test(cur_filename, G_FILE_TEST_IS_DIR)) + strcat(cur_filename, dir_sep); + + files = g_slist_prepend(files, cur_filename); + + if (g_slist_next(files)) { + teco_string_t other_file; + other_file.data = (gchar *)g_slist_next(files)->data + filename_len; + other_file.len = strlen(other_file.data); + + gsize len = teco_string_diff(&other_file, cur_filename + filename_len, + strlen(cur_filename) - filename_len); + if (len < prefix_len) + prefix_len = len; + } else { + prefix_len = strlen(cur_filename + filename_len); + } + + files_len++; + } + + if (prefix_len > 0) { + teco_string_init(insert, (gchar *)files->data + filename_len, prefix_len); + } else if (files_len > 1) { + files = g_slist_sort(files, (GCompareFunc)g_strcmp0); + + for (GSList *file = files; file; file = g_slist_next(file)) { + teco_popup_entry_type_t type = TECO_POPUP_DIRECTORY; + gboolean is_buffer = FALSE; + + if (!teco_file_is_dir((gchar *)file->data)) { + type = TECO_POPUP_FILE; + /* FIXME: inefficient */ + is_buffer = teco_ring_find((gchar *)file->data) != NULL; + } + + teco_interface_popup_add(type, (gchar *)file->data, + strlen((gchar *)file->data), is_buffer); + } + + teco_interface_popup_show(); + } + + /* + * FIXME: If we are completing only directories, + * we can theoretically insert the completed character + * after directories without subdirectories. + */ + gboolean unambiguous = files_len == 1 && !teco_file_is_dir((gchar *)files->data); + g_slist_free_full(files, g_free); + return unambiguous; +} diff --git a/src/file-utils.h b/src/file-utils.h new file mode 100644 index 0000000..496e881 --- /dev/null +++ b/src/file-utils.h @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#pragma once + +#include + +#include + +#include "sciteco.h" +#include "string-utils.h" + +typedef guint32 teco_file_attributes_t; +#define TECO_FILE_INVALID_ATTRIBUTES G_MAXUINT32 + +teco_file_attributes_t teco_file_get_attributes(const gchar *filename); +void teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs); + +/** + * Get absolute/full version of a possibly relative path. + * The path is tried to be canonicalized so it does + * not contain relative components. + * Works with existing and non-existing paths (in the latter case, + * heuristics may be applied). + * Depending on platform and existence of the path, + * canonicalization might fail, but the path returned is + * always absolute. + * + * @param path Possibly relative path name. + * @return Newly-allocated absolute path name. + */ +gchar *teco_file_get_absolute_path(const gchar *path); + +/** + * Normalize path or file name. + * + * This changes the directory separators + * to forward slash (on platforms that support + * different directory separator styles). + * + * @param path The path to normalize. + * It is changed in place. + * @return Returns `path`. The return value + * may be ignored. + */ +static inline gchar * +teco_file_normalize_path(gchar *path) +{ +#if G_DIR_SEPARATOR != '/' + return g_strdelimit(path, G_DIR_SEPARATOR_S, '/'); +#else + return path; +#endif +} + +gboolean teco_file_is_visible(const gchar *path); + +gchar *teco_file_expand_path(const gchar *path); + +/** + * This gets the length of a file name's directory + * component including any trailing directory separator. + * It returns 0 if the file name does not have a directory + * separator. + * This is useful when constructing file names in the same + * directory as an existing one, keeping the exact same + * directory component (globbing, tab completion...). + * Also if it returns non-0, this can be used to look up + * the last used directory separator in the file name. + */ +static inline gsize +teco_file_get_dirname_len(const gchar *path) +{ + gsize len = 0; + + for (const gchar *p = path; *p; p++) + if (G_IS_DIR_SEPARATOR(*p)) + len = p - path + 1; + + return len; +} + +static inline gboolean +teco_file_is_dir(const gchar *filename) +{ + if (!*filename) + return FALSE; + + gchar c = filename[strlen(filename)-1]; + return G_IS_DIR_SEPARATOR(c); +} + +gboolean teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_t *insert); diff --git a/src/glob.c b/src/glob.c new file mode 100644 index 0000000..f6810c2 --- /dev/null +++ b/src/glob.c @@ -0,0 +1,573 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include +#include + +#include "sciteco.h" +#include "string-utils.h" +#include "file-utils.h" +#include "interface.h" +#include "parser.h" +#include "core-commands.h" +#include "expressions.h" +#include "qreg.h" +#include "ring.h" +#include "error.h" +#include "glob.h" + +/* + * FIXME: This state could be static. + */ +TECO_DECLARE_STATE(teco_state_glob_filename); + +/** @memberof teco_globber_t */ +void +teco_globber_init(teco_globber_t *ctx, const gchar *pattern, GFileTest test) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->test = test; + + /* + * This finds the directory component including + * any trailing directory separator + * without making up a directory if it is missing + * (as g_path_get_dirname() does). + * Important since it allows us to construct + * file names with the exact same directory + * prefix as the input pattern. + */ + gsize dirname_len = teco_file_get_dirname_len(pattern); + ctx->dirname = g_strndup(pattern, dirname_len); + + ctx->dir = g_dir_open(*ctx->dirname ? ctx->dirname : ".", 0, NULL); + /* if dirname does not exist, the result may be NULL */ + + ctx->pattern = teco_globber_compile_pattern(pattern + dirname_len); +} + +/** @memberof teco_globber_t */ +gchar * +teco_globber_next(teco_globber_t *ctx) +{ + const gchar *basename; + + if (!ctx->dir) + return NULL; + + while ((basename = g_dir_read_name(ctx->dir))) { + if (!g_regex_match(ctx->pattern, basename, 0, NULL)) + continue; + + /* + * As dirname includes the directory separator, + * we can simply concatenate dirname with basename. + */ + gchar *filename = g_strconcat(ctx->dirname, basename, NULL); + + /* + * No need to perform file test for EXISTS since + * g_dir_read_name() will only return existing entries + */ + if (ctx->test == G_FILE_TEST_EXISTS || g_file_test(filename, ctx->test)) + return filename; + + g_free(filename); + } + + return NULL; +} + +/** @memberof teco_globber_t */ +void +teco_globber_clear(teco_globber_t *ctx) +{ + if (ctx->pattern) + g_regex_unref(ctx->pattern); + if (ctx->dir) + g_dir_close(ctx->dir); + g_free(ctx->dirname); +} + +/** @static @memberof teco_globber_t */ +gchar * +teco_globber_escape_pattern(const gchar *pattern) +{ + gsize escaped_len = 1; + gchar *escaped, *pout; + + /* + * NOTE: The exact size of the escaped string is easy to calculate + * in O(n) just like strlen(pattern), so we can just as well + * do that. + */ + for (const gchar *pin = pattern; *pin; pin++) { + switch (*pin) { + case '*': + case '?': + case '[': + escaped_len += 3; + break; + default: + escaped_len++; + break; + } + } + pout = escaped = g_malloc(escaped_len); + + while (*pattern) { + switch (*pattern) { + case '*': + case '?': + case '[': + *pout++ = '['; + *pout++ = *pattern; + *pout++ = ']'; + break; + default: + *pout++ = *pattern; + break; + } + + pattern++; + } + *pout = '\0'; + + return escaped; +} + +/** + * Compile a fnmatch(3)-compatible glob pattern to + * a PCRE regular expression. + * + * There is GPattern, but it only supports the + * "*" and "?" wildcards which most importantly + * do not allow escaping. + * + * @param pattern The pattern to compile. + * @return A new compiled regular expression object. + * Always non-NULL. Unref after use. + * + * @static @memberof teco_globber_t + */ +GRegex * +teco_globber_compile_pattern(const gchar *pattern) +{ + enum { + STATE_WILDCARD, + STATE_CLASS_START, + STATE_CLASS_NEGATE, + STATE_CLASS + } state = STATE_WILDCARD; + + /* + * NOTE: The conversion to regex needs at most two + * characters per input character and the regex pattern + * is required only temporarily, so we use a fixed size + * buffer avoiding reallocations but wasting a few bytes + * (determining the exact required space would be tricky). + * It is not allocated on the stack though since pattern + * might be arbitrary user input and we must avoid + * stack overflows at all costs. + */ + g_autofree gchar *pattern_regex = g_malloc(strlen(pattern)*2 + 1 + 1); + gchar *pout = pattern_regex; + + while (*pattern) { + if (state == STATE_WILDCARD) { + /* + * Outside a character class/set. + */ + switch (*pattern) { + case '*': + *pout++ = '.'; + *pout++ = '*'; + break; + case '?': + *pout++ = '.'; + break; + case '[': + /* + * The special case of an unclosed character + * class is allowed in fnmatch(3) but invalid + * in PCRE, so we must check for it explicitly. + * FIXME: This is sort of inefficient... + */ + if (strchr(pattern, ']')) { + state = STATE_CLASS_START; + *pout++ = '['; + break; + } + /* fall through */ + default: + /* + * For simplicity, all non-alphanumeric + * characters are escaped since they could + * be PCRE magic characters. + * g_regex_escape_string() is inefficient. + * character anyway. + */ + if (!g_ascii_isalnum(*pattern)) + *pout++ = '\\'; + *pout++ = *pattern; + break; + } + } else { + /* + * Within a character class/set. + */ + switch (*pattern) { + case '!': + /* + * fnmatch(3) allows ! instead of ^ immediately + * after the opening bracket. + */ + if (state > STATE_CLASS_START) { + state = STATE_CLASS; + *pout++ = '!'; + break; + } + /* fall through */ + case '^': + state = state == STATE_CLASS_START + ? STATE_CLASS_NEGATE : STATE_CLASS; + *pout++ = '^'; + break; + case ']': + /* + * fnmatch(3) allows the closing bracket as the + * first character to include it in the set, while + * PCRE requires it to be escaped. + */ + if (state == STATE_CLASS) { + state = STATE_WILDCARD; + *pout++ = ']'; + break; + } + /* fall through */ + default: + if (!g_ascii_isalnum(*pattern)) + *pout++ = '\\'; + /* fall through */ + case '-': + state = STATE_CLASS; + *pout++ = *pattern; + break; + } + } + + pattern++; + } + *pout++ = '$'; + *pout = '\0'; + + GRegex *pattern_compiled = g_regex_new(pattern_regex, + G_REGEX_DOTALL | G_REGEX_ANCHORED, 0, NULL); + /* + * Since the regex is generated from patterns that are + * always valid, there must be no syntactic error. + */ + g_assert(pattern_compiled != NULL); + + return pattern_compiled; +} + +/* + * Command States + */ + +static teco_state_t * +teco_state_glob_pattern_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_glob_filename; + + if (str->len > 0) { + g_autofree gchar *filename = teco_file_expand_path(str->data); + + teco_qreg_t *glob_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1); + g_assert(glob_reg != NULL); + if (!glob_reg->vtable->undo_set_string(glob_reg, error) || + !glob_reg->vtable->set_string(glob_reg, filename, strlen(filename), error)) + return NULL; + } + + return &teco_state_glob_filename; +} + +/*$ EN glob + * [type]EN[pattern]$[filename]$ -- Glob files or match filename and check file type + * [type]:EN[pattern]$[filename]$ -> Success|Failure + * + * EN is a powerful command for performing various tasks + * given a glob \fIpattern\fP. + * For a description of the glob pattern syntax, refer to the section + * .B Glob Patterns + * for details. + * + * \fIpattern\fP may be omitted, in which case it defaults + * to the pattern saved in the search and glob register \(lq_\(rq. + * If it is specified, it overwrites the contents of the register + * \(lq_\(rq with \fIpattern\fP. + * This behaviour is similar to the search and replace commands + * and allows for repeated globbing/matching with the same + * pattern. + * Therefoe you should also save the \(lq_\(rq register on the + * Q-Register stack when calling EN from portable macros. + * + * If \fIfilename\fP is omitted (empty), EN may be used to expand + * a glob \fIpattern\fP to a list of matching file names. + * This is similar to globbing + * on UNIX but not as powerful and may be used e.g. for + * iterating over directory contents. + * E.g. \(lqEN*.c\fB$$\fP\(rq expands to all \(lq.c\(rq files + * in the current directory. + * The resulting file names have the exact same directory + * component as \fIpattern\fP (if any). + * Without \fIfilename\fP, EN will currently only match files + * in the file name component + * of \fIpattern\fP, not on each component of the path name + * separately. + * In other words, EN only looks through the directory + * of \fIpattern\fP \(em you cannot effectively match + * multiple directories. + * + * If \fIfilename\fP is specified, \fIpattern\fP will only + * be matched against that single file name. + * If it matches, \fIfilename\fP is used verbatim. + * In this form, \fIpattern\fP is matched against the entire + * file name, so it is possible to match directory components + * as well. + * \fIfilename\fP does not necessarily have to exist in the + * file system for the match to succeed (unless a file type check + * is also specified). + * For instance, \(lqENf??/\[**].c\fB$\fPfoo/bar.c\fB$\fP\(rq will + * always match and the string \(lqfoo/bar.c\(rq will be inserted + * (see below). + * + * By default, if EN is not colon-modified, the result of + * globbing or file name matching is inserted into the current + * document, at the current position. + * The file names will be separated by line feeds, i.e. + * every matching file will be on its own line. + * + * EN may be colon-modified to avoid any text insertion. + * Instead, a boolean is returned that signals whether + * any file matched \fIpattern\fP. + * E.g. \(lq:EN*.c\fB$$\fP\(rq returns success (-1) if + * there is at least one \(lq.c\(rq file in the current directory. + * + * The results of EN may be filtered by specifying a numeric file + * \fItype\fP check argument. + * This argument may be omitted (as in the examples above) and defaults + * to 0, i.e. no additional checking. + * The following file type check values are currently defined: + * .IP 0 4 + * No file type checking is performed. + * Note however, that when globbing only directory contents + * (of any type) are used, so without the \fIfilename\fP + * argument, the value 0 is equivalent to 5. + * .IP 1 + * Only match \fIregular files\fP (no directories). + * Will also match symlinks to regular files (on platforms + * supporting symlinks). + * .IP 2 + * Only match \fIsymlinks\fP. + * On platforms without symlinks (non-UNIX), this will never + * match anything. + * .IP 3 + * Only match \fIdirectories\fP. + * .IP 4 + * Only match \fIexecutables\fP. + * On UNIX, the executable flag is evaluated, while on + * Windows only the file name is checked. + * .IP 5 + * Only match existing files or directories. + * When globbing, this check makes no sense and is + * equivalent to no check at all. + * It may however be used to test that a filename refers + * to an existing file. + * + * For instance, \(lq3EN*\fB$$\fP\(rq will expand to + * all subdirectories in the current directory. + * The following idiom may be used to check whether + * a given filename refers to a regular file: + * 1:EN*\fB$\fIfilename\fB$\fR + * + * Note that both without colon and colon modified + * forms of EN save the success or failure of the + * operation in the numeric part of the glob register + * \(lq_\(rq (i.e. the same value that the colon modified + * form would return). + * The command itself never fails because of failure + * in matching any files. + * E.g. if \(lqEN*.c\fB$$\fP\(rq does not match any + * files, the EN command is still successful but does + * not insert anything. A failure boolean would be saved + * in \(lq_\(rq, though. + * + * String-building characters are enabled for EN and + * both string arguments are considered file names + * with regard to auto-completions. + */ +/* + * NOTE: This does not work like classic TECO's + * EN command (iterative globbing), since the + * position in the directory cannot be reasonably + * reset on rubout with glib's API. + * If we have to perform all the globbing on initialization + * we can just as well return all the results at once. + * And we can add them to the current document since + * when they should be in a register, the user will + * have to edit that register anyway. + */ +TECO_DEFINE_STATE_EXPECTFILE(teco_state_glob_pattern, + .expectstring.last = FALSE +); + +static teco_state_t * +teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + GFileTest file_flags = G_FILE_TEST_EXISTS; + + gboolean matching = FALSE; + gboolean colon_modified = teco_machine_main_eval_colon(ctx); + + teco_int_t teco_test_mode; + + if (!teco_expressions_eval(FALSE, error) || + !teco_expressions_pop_num_calc(&teco_test_mode, 0, error)) + return NULL; + switch (teco_test_mode) { + /* + * 0 means, no file testing. + * file_flags will still be G_FILE_TEST_EXISTS which + * is equivalent to no testing when using the Globber class. + */ + case 0: break; + case 1: file_flags = G_FILE_TEST_IS_REGULAR; break; + case 2: file_flags = G_FILE_TEST_IS_SYMLINK; break; + case 3: file_flags = G_FILE_TEST_IS_DIR; break; + case 4: file_flags = G_FILE_TEST_IS_EXECUTABLE; break; + case 5: file_flags = G_FILE_TEST_EXISTS; break; + default: + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Invalid file test %" TECO_INT_FORMAT " for ", + teco_test_mode); + return NULL; + } + + teco_qreg_t *glob_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1); + g_assert(glob_reg != NULL); + g_auto(teco_string_t) pattern_str = {NULL, 0}; + if (!glob_reg->vtable->get_string(glob_reg, &pattern_str.data, &pattern_str.len, error)) + return NULL; + if (teco_string_contains(&pattern_str, '\0')) { + teco_error_qregcontainsnull_set(error, "_", 1, FALSE); + return NULL; + } + + if (str->len > 0) { + /* + * Match pattern against provided file name + */ + g_autofree gchar *filename = teco_file_expand_path(str->data); + g_autoptr(GRegex) pattern = teco_globber_compile_pattern(pattern_str.data); + + if (g_regex_match(pattern, filename, 0, NULL) && + (teco_test_mode == 0 || g_file_test(filename, file_flags))) { + if (!colon_modified) { + /* + * FIXME: Filenames may contain linefeeds. + * But if we add them null-terminated, they will be relatively hard to parse. + */ + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + teco_interface_ssm(SCI_ADDTEXT, strlen(filename), + (sptr_t)filename); + teco_interface_ssm(SCI_ADDTEXT, 1, (sptr_t)"\n"); + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + } + + matching = TRUE; + } + } else if (colon_modified) { + /* + * Match pattern against directory contents (globbing), + * returning TECO_SUCCESS if at least one file matches + */ + g_auto(teco_globber_t) globber; + + teco_globber_init(&globber, pattern_str.data, file_flags); + g_autofree gchar *globbed_filename = teco_globber_next(&globber); + + matching = globbed_filename != NULL; + } else { + /* + * Match pattern against directory contents (globbing), + * inserting all matching file names (null-byte-terminated) + */ + g_auto(teco_globber_t) globber; + teco_globber_init(&globber, pattern_str.data, file_flags); + + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + + gchar *globbed_filename; + while ((globbed_filename = teco_globber_next(&globber))) { + /* + * FIXME: Filenames may contain linefeeds. + * But if we add them null-terminated, they will be relatively hard to parse. + */ + teco_interface_ssm(SCI_ADDTEXT, strlen(globbed_filename), + (sptr_t)globbed_filename); + teco_interface_ssm(SCI_ADDTEXT, 1, (sptr_t)"\n"); + + g_free(globbed_filename); + matching = TRUE; + } + + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + } + + if (colon_modified) { + teco_expressions_push(teco_bool(matching)); + } else if (matching) { + /* text has been inserted */ + teco_ring_dirtify(); + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + } + + if (!glob_reg->vtable->undo_set_integer(glob_reg, error) || + !glob_reg->vtable->set_integer(glob_reg, teco_bool(matching), error)) + return NULL; + + return &teco_state_start; +} + +TECO_DEFINE_STATE_EXPECTFILE(teco_state_glob_filename); diff --git a/src/glob.cpp b/src/glob.cpp deleted file mode 100644 index e6b5bd4..0000000 --- a/src/glob.cpp +++ /dev/null @@ -1,554 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include -#include -#include - -#include "sciteco.h" -#include "interface.h" -#include "parser.h" -#include "expressions.h" -#include "qregisters.h" -#include "ring.h" -#include "ioview.h" -#include "glob.h" - -namespace SciTECO { - -namespace States { - StateGlob_pattern glob_pattern; - StateGlob_filename glob_filename; -} - -Globber::Globber(const gchar *pattern, GFileTest _test) - : test(_test) -{ - gsize dirname_len; - - /* - * This finds the directory component including - * any trailing directory separator - * without making up a directory if it is missing - * (as g_path_get_dirname() does). - * Important since it allows us to construct - * file names with the exact same directory - * prefix as the input pattern. - */ - dirname_len = file_get_dirname_len(pattern); - dirname = g_strndup(pattern, dirname_len); - - dir = g_dir_open(*dirname ? dirname : ".", 0, NULL); - /* if dirname does not exist, dir may be NULL */ - - Globber::pattern = compile_pattern(pattern + dirname_len); -} - -gchar * -Globber::next(void) -{ - const gchar *basename; - - if (!dir) - return NULL; - - while ((basename = g_dir_read_name(dir))) { - gchar *filename; - - if (!g_regex_match(pattern, basename, (GRegexMatchFlags)0, NULL)) - continue; - - /* - * As dirname includes the directory separator, - * we can simply concatenate dirname with basename. - */ - filename = g_strconcat(dirname, basename, NIL); - - /* - * No need to perform file test for EXISTS since - * g_dir_read_name() will only return existing entries - */ - if (test == G_FILE_TEST_EXISTS || g_file_test(filename, test)) - return filename; - - g_free(filename); - } - - return NULL; -} - -Globber::~Globber() -{ - if (pattern) - g_regex_unref(pattern); - if (dir) - g_dir_close(dir); - g_free(dirname); -} - -gchar * -Globber::escape_pattern(const gchar *pattern) -{ - gsize escaped_len = 1; - gchar *escaped, *pout; - - /* - * NOTE: The exact size of the escaped string is easy to calculate - * in O(n) just like strlen(pattern), so we can just as well - * do that. - */ - for (const gchar *pin = pattern; *pin; pin++) { - switch (*pin) { - case '*': - case '?': - case '[': - escaped_len += 3; - break; - default: - escaped_len++; - break; - } - } - pout = escaped = (gchar *)g_malloc(escaped_len); - - while (*pattern) { - switch (*pattern) { - case '*': - case '?': - case '[': - *pout++ = '['; - *pout++ = *pattern; - *pout++ = ']'; - break; - default: - *pout++ = *pattern; - break; - } - - pattern++; - } - *pout = '\0'; - - return escaped; -} - -/** - * Compile a fnmatch(3)-compatible glob pattern to - * a PCRE regular expression. - * - * There is GPattern, but it only supports the - * "*" and "?" wildcards which most importantly - * do not allow escaping. - * - * @param pattern The pattern to compile. - * @return A new compiled regular expression object. - * Always non-NULL. Unref after use. - */ -GRegex * -Globber::compile_pattern(const gchar *pattern) -{ - gchar *pattern_regex, *pout; - GRegex *pattern_compiled; - - enum { - STATE_WILDCARD, - STATE_CLASS_START, - STATE_CLASS_NEGATE, - STATE_CLASS - } state = STATE_WILDCARD; - - /* - * NOTE: The conversion to regex needs at most two - * characters per input character and the regex pattern - * is required only temporarily, so we use a fixed size - * buffer avoiding reallocations but wasting a few bytes - * (determining the exact required space would be tricky). - * It is not allocated on the stack though since pattern - * might be arbitrary user input and we must avoid - * stack overflows at all costs. - */ - pout = pattern_regex = (gchar *)g_malloc(strlen(pattern)*2 + 1 + 1); - - while (*pattern) { - if (state == STATE_WILDCARD) { - /* - * Outside a character class/set. - */ - switch (*pattern) { - case '*': - *pout++ = '.'; - *pout++ = '*'; - break; - case '?': - *pout++ = '.'; - break; - case '[': - /* - * The special case of an unclosed character - * class is allowed in fnmatch(3) but invalid - * in PCRE, so we must check for it explicitly. - * FIXME: This is sort of inefficient... - */ - if (strchr(pattern, ']')) { - state = STATE_CLASS_START; - *pout++ = '['; - break; - } - /* fall through */ - default: - /* - * For simplicity, all non-alphanumeric - * characters are escaped since they could - * be PCRE magic characters. - * g_regex_escape_string() is inefficient. - * character anyway. - */ - if (!g_ascii_isalnum(*pattern)) - *pout++ = '\\'; - *pout++ = *pattern; - break; - } - } else { - /* - * Within a character class/set. - */ - switch (*pattern) { - case '!': - /* - * fnmatch(3) allows ! instead of ^ immediately - * after the opening bracket. - */ - if (state > STATE_CLASS_START) { - state = STATE_CLASS; - *pout++ = '!'; - break; - } - /* fall through */ - case '^': - state = state == STATE_CLASS_START - ? STATE_CLASS_NEGATE : STATE_CLASS; - *pout++ = '^'; - break; - case ']': - /* - * fnmatch(3) allows the closing bracket as the - * first character to include it in the set, while - * PCRE requires it to be escaped. - */ - if (state == STATE_CLASS) { - state = STATE_WILDCARD; - *pout++ = ']'; - break; - } - /* fall through */ - default: - if (!g_ascii_isalnum(*pattern)) - *pout++ = '\\'; - /* fall through */ - case '-': - state = STATE_CLASS; - *pout++ = *pattern; - break; - } - } - - pattern++; - } - *pout++ = '$'; - *pout = '\0'; - - pattern_compiled = g_regex_new(pattern_regex, - (GRegexCompileFlags)(G_REGEX_DOTALL | G_REGEX_ANCHORED), - (GRegexMatchFlags)0, NULL); - /* - * Since the regex is generated from patterns that are - * always valid, there must be no syntactic error. - */ - g_assert(pattern_compiled != NULL); - - g_free(pattern_regex); - return pattern_compiled; -} - -/* - * Command States - */ - -/*$ EN glob - * [type]EN[pattern]$[filename]$ -- Glob files or match filename and check file type - * [type]:EN[pattern]$[filename]$ -> Success|Failure - * - * EN is a powerful command for performing various tasks - * given a glob \fIpattern\fP. - * For a description of the glob pattern syntax, refer to the section - * .B Glob Patterns - * for details. - * - * \fIpattern\fP may be omitted, in which case it defaults - * to the pattern saved in the search and glob register \(lq_\(rq. - * If it is specified, it overwrites the contents of the register - * \(lq_\(rq with \fIpattern\fP. - * This behaviour is similar to the search and replace commands - * and allows for repeated globbing/matching with the same - * pattern. - * Therefoe you should also save the \(lq_\(rq register on the - * Q-Register stack when calling EN from portable macros. - * - * If \fIfilename\fP is omitted (empty), EN may be used to expand - * a glob \fIpattern\fP to a list of matching file names. - * This is similar to globbing - * on UNIX but not as powerful and may be used e.g. for - * iterating over directory contents. - * E.g. \(lqEN*.c\fB$$\fP\(rq expands to all \(lq.c\(rq files - * in the current directory. - * The resulting file names have the exact same directory - * component as \fIpattern\fP (if any). - * Without \fIfilename\fP, EN will currently only match files - * in the file name component - * of \fIpattern\fP, not on each component of the path name - * separately. - * In other words, EN only looks through the directory - * of \fIpattern\fP \(em you cannot effectively match - * multiple directories. - * - * If \fIfilename\fP is specified, \fIpattern\fP will only - * be matched against that single file name. - * If it matches, \fIfilename\fP is used verbatim. - * In this form, \fIpattern\fP is matched against the entire - * file name, so it is possible to match directory components - * as well. - * \fIfilename\fP does not necessarily have to exist in the - * file system for the match to succeed (unless a file type check - * is also specified). - * For instance, \(lqENf??/\[**].c\fB$\fPfoo/bar.c\fB$\fP\(rq will - * always match and the string \(lqfoo/bar.c\(rq will be inserted - * (see below). - * - * By default, if EN is not colon-modified, the result of - * globbing or file name matching is inserted into the current - * document, at the current position. - * A linefeed is inserted after every file name, i.e. - * every matching file will be on its own line. - * - * EN may be colon-modified to avoid any text insertion. - * Instead, a boolean is returned that signals whether - * any file matched \fIpattern\fP. - * E.g. \(lq:EN*.c\fB$$\fP\(rq returns success (-1) if - * there is at least one \(lq.c\(rq file in the current directory. - * - * The results of EN may be filtered by specifying a numeric file - * \fItype\fP check argument. - * This argument may be omitted (as in the examples above) and defaults - * to 0, i.e. no additional checking. - * The following file type check values are currently defined: - * .IP 0 4 - * No file type checking is performed. - * Note however, that when globbing only directory contents - * (of any type) are used, so without the \fIfilename\fP - * argument, the value 0 is equivalent to 5. - * .IP 1 - * Only match \fIregular files\fP (no directories). - * Will also match symlinks to regular files (on platforms - * supporting symlinks). - * .IP 2 - * Only match \fIsymlinks\fP. - * On platforms without symlinks (non-UNIX), this will never - * match anything. - * .IP 3 - * Only match \fIdirectories\fP. - * .IP 4 - * Only match \fIexecutables\fP. - * On UNIX, the executable flag is evaluated, while on - * Windows only the file name is checked. - * .IP 5 - * Only match existing files or directories. - * When globbing, this check makes no sense and is - * equivalent to no check at all. - * It may however be used to test that a filename refers - * to an existing file. - * - * For instance, \(lq3EN*\fB$$\fP\(rq will expand to - * all subdirectories in the current directory. - * The following idiom may be used to check whether - * a given filename refers to a regular file: - * 1:EN*\fB$\fIfilename\fB$\fR - * - * Note that both without colon and colon modified - * forms of EN save the success or failure of the - * operation in the numeric part of the glob register - * \(lq_\(rq (i.e. the same value that the colon modified - * form would return). - * The command itself never fails because of failure - * in matching any files. - * E.g. if \(lqEN*.c\fB$$\fP\(rq does not match any - * files, the EN command is still successful but does - * not insert anything. A failure boolean would be saved - * in \(lq_\(rq, though. - * - * String-building characters are enabled for EN and - * both string arguments are considered file names - * with regard to auto-completions. - */ -/* - * NOTE: This does not work like classic TECO's - * EN command (iterative globbing), since the - * position in the directory cannot be reasonably - * reset on rubout with glib's API. - * If we have to perform all the globbing on initialization - * we can just as well return all the results at once. - * And we can add them to the current document since - * when they should be in a register, the user will - * have to edit that register anyway. - */ -State * -StateGlob_pattern::got_file(const gchar *filename) -{ - BEGIN_EXEC(&States::glob_filename); - - if (*filename) { - QRegister *glob_reg = QRegisters::globals["_"]; - - glob_reg->undo_set_string(); - glob_reg->set_string(filename); - } - - return &States::glob_filename; -} - -State * -StateGlob_filename::got_file(const gchar *filename) -{ - BEGIN_EXEC(&States::start); - - tecoInt teco_test_mode; - GFileTest file_flags = G_FILE_TEST_EXISTS; - - bool matching = false; - bool colon_modified = eval_colon(); - - QRegister *glob_reg = QRegisters::globals["_"]; - gchar *pattern_str; - - expressions.eval(); - teco_test_mode = expressions.pop_num_calc(0, 0); - switch (teco_test_mode) { - /* - * 0 means, no file testing. - * file_flags will still be G_FILE_TEST_EXISTS which - * is equivalent to no testing when using the Globber class. - */ - case 0: break; - case 1: file_flags = G_FILE_TEST_IS_REGULAR; break; - case 2: file_flags = G_FILE_TEST_IS_SYMLINK; break; - case 3: file_flags = G_FILE_TEST_IS_DIR; break; - case 4: file_flags = G_FILE_TEST_IS_EXECUTABLE; break; - case 5: file_flags = G_FILE_TEST_EXISTS; break; - default: - throw Error("Invalid file test %" TECO_INTEGER_FORMAT - " for ", teco_test_mode); - } - - pattern_str = glob_reg->get_string(); - - if (*filename) { - /* - * Match pattern against provided file name - */ - GRegex *pattern = Globber::compile_pattern(pattern_str); - - if (g_regex_match(pattern, filename, (GRegexMatchFlags)0, NULL) && - (!teco_test_mode || g_file_test(filename, file_flags))) { - if (!colon_modified) { - interface.ssm(SCI_BEGINUNDOACTION); - interface.ssm(SCI_ADDTEXT, strlen(filename), - (sptr_t)filename); - interface.ssm(SCI_ADDTEXT, 1, (sptr_t)"\n"); - interface.ssm(SCI_SCROLLCARET); - interface.ssm(SCI_ENDUNDOACTION); - } - - matching = true; - } - - g_regex_unref(pattern); - } else if (colon_modified) { - /* - * Match pattern against directory contents (globbing), - * returning SUCCESS if at least one file matches - */ - Globber globber(pattern_str, file_flags); - gchar *globbed_filename = globber.next(); - - matching = globbed_filename != NULL; - - g_free(globbed_filename); - } else { - /* - * Match pattern against directory contents (globbing), - * inserting all matching file names (linefeed-terminated) - */ - Globber globber(pattern_str, file_flags); - - gchar *globbed_filename; - - interface.ssm(SCI_BEGINUNDOACTION); - - while ((globbed_filename = globber.next())) { - size_t len = strlen(globbed_filename); - /* overwrite trailing null */ - globbed_filename[len] = '\n'; - - /* - * FIXME: Once we're 8-bit clean, we should - * add the filenames null-terminated - * (there may be linebreaks in filename). - */ - interface.ssm(SCI_ADDTEXT, len+1, - (sptr_t)globbed_filename); - - g_free(globbed_filename); - matching = true; - } - - interface.ssm(SCI_SCROLLCARET); - interface.ssm(SCI_ENDUNDOACTION); - } - - g_free(pattern_str); - - if (colon_modified) { - expressions.push(TECO_BOOL(matching)); - } else if (matching) { - /* text has been inserted */ - ring.dirtify(); - if (current_doc_must_undo()) - interface.undo_ssm(SCI_UNDO); - } - - glob_reg->undo_set_integer(); - glob_reg->set_integer(TECO_BOOL(matching)); - - return &States::start; -} - -} /* namespace SciTECO */ diff --git a/src/glob.h b/src/glob.h index aca1a30..db17c99 100644 --- a/src/glob.h +++ b/src/glob.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,66 +14,40 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -#ifndef __GLOB_H -#define __GLOB_H +#pragma once #include #include -#include #include "sciteco.h" -#include "memory.h" #include "parser.h" -namespace SciTECO { - -class Globber : public Object { +typedef struct { GFileTest test; gchar *dirname; GDir *dir; GRegex *pattern; +} teco_globber_t; -public: - Globber(const gchar *pattern, - GFileTest test = G_FILE_TEST_EXISTS); - ~Globber(); +void teco_globber_init(teco_globber_t *ctx, const gchar *pattern, GFileTest test); +gchar *teco_globber_next(teco_globber_t *ctx); +void teco_globber_clear(teco_globber_t *ctx); - gchar *next(void); +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_globber_t, teco_globber_clear); - static inline bool - is_pattern(const gchar *str) - { - return str && strpbrk(str, "*?[") != NULL; - } +/** @static @memberof teco_globber_t */ +static inline gboolean +teco_globber_is_pattern(const gchar *str) +{ + return str && strpbrk(str, "*?[") != NULL; +} - static gchar *escape_pattern(const gchar *pattern); - static GRegex *compile_pattern(const gchar *pattern); -}; +gchar *teco_globber_escape_pattern(const gchar *pattern); +GRegex *teco_globber_compile_pattern(const gchar *pattern); /* * Command states */ -class StateGlob_pattern : public StateExpectFile { -public: - StateGlob_pattern() : StateExpectFile(true, false) {} - -private: - State *got_file(const gchar *filename); -}; - -class StateGlob_filename : public StateExpectFile { -private: - State *got_file(const gchar *filename); -}; - -namespace States { - extern StateGlob_pattern glob_pattern; - extern StateGlob_filename glob_filename; -} - -} /* namespace SciTECO */ - -#endif +TECO_DECLARE_STATE(teco_state_glob_pattern); diff --git a/src/goto-commands.c b/src/goto-commands.c new file mode 100644 index 0000000..792b4e3 --- /dev/null +++ b/src/goto-commands.c @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include + +#include "sciteco.h" +#include "string-utils.h" +#include "expressions.h" +#include "parser.h" +#include "core-commands.h" +#include "undo.h" +#include "goto.h" +#include "goto-commands.h" + +teco_string_t teco_goto_skip_label = {NULL, 0}; + +static gboolean +teco_state_label_initial(teco_machine_main_t *ctx, GError **error) +{ + memset(&ctx->goto_label, 0, sizeof(ctx->goto_label)); + return TRUE; +} + +/* + * NOTE: The comma is theoretically not allowed in a label + * (see syntax), but is accepted anyway since labels + * are historically used as comments. + * + * TODO: Add support for "true" comments of the form !* ... *! + * This would be almost trivial to implement, but if we don't + * want any (even temporary) overhead for comments at all, we need + * to add a new parser state. + * I'm unsure whether !-signs should be allowed within comments. + */ +static teco_state_t * +teco_state_label_input(teco_machine_main_t *ctx, gchar chr, GError **error) +{ + if (chr == '!') { + /* + * NOTE: If the label already existed, its PC will be restored + * on rubout. + * Otherwise, the label will be removed (PC == -1). + */ + gint existing_pc = teco_goto_table_set(&ctx->goto_table, ctx->goto_label.data, + ctx->goto_label.len, ctx->macro_pc); + if (ctx->parent.must_undo) + teco_goto_table_undo_set(&ctx->goto_table, ctx->goto_label.data, ctx->goto_label.len, existing_pc); + + if (teco_goto_skip_label.len > 0 && + !teco_string_cmp(&ctx->goto_label, teco_goto_skip_label.data, teco_goto_skip_label.len)) { + teco_undo_string_own(teco_goto_skip_label); + memset(&teco_goto_skip_label, 0, sizeof(teco_goto_skip_label)); + + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->mode = TECO_MODE_NORMAL; + } + + if (ctx->parent.must_undo) + teco_undo_string_own(ctx->goto_label); + else + teco_string_clear(&ctx->goto_label); + return &teco_state_start; + } + + if (ctx->parent.must_undo) + undo__teco_string_truncate(&ctx->goto_label, ctx->goto_label.len); + teco_string_append_c(&ctx->goto_label, chr); + return &teco_state_label; +} + +TECO_DEFINE_STATE(teco_state_label, + .initial_cb = (teco_state_initial_cb_t)teco_state_label_initial +); + +static teco_state_t * +teco_state_goto_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + teco_int_t value; + if (!teco_expressions_pop_num_calc(&value, 1, error)) + return NULL; + + /* + * Find the comma-separated substring in str indexed by `value`. + */ + teco_string_t label = {NULL, 0}; + while (value > 0) { + label.data = label.data ? label.data+label.len+1 : str->data; + const gchar *p = memchr(label.data, ',', str->len - (label.data - str->data)); + label.len = p ? p - label.data : str->len - (label.data - str->data); + + value--; + + if (!p) + break; + } + + if (value == 0) { + gint pc = teco_goto_table_find(&ctx->goto_table, label.data, label.len); + + if (pc >= 0) { + ctx->macro_pc = pc; + } else { + /* skip till label is defined */ + g_assert(teco_goto_skip_label.len == 0); + undo__teco_string_truncate(&teco_goto_skip_label, 0); + teco_string_init(&teco_goto_skip_label, label.data, label.len); + if (ctx->parent.must_undo) + teco_undo_guint(ctx->__flags); + ctx->mode = TECO_MODE_PARSE_ONLY_GOTO; + } + } + + return &teco_state_start; +} + +/* in cmdline.c */ +gboolean teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar chr, GError **error); + +/*$ O + * Olabel$ -- Go to label + * [n]Olabel1[,label2,...]$ + * + * Go to . - */ - if (v < 0 || v >= interface.ssm(SCI_GETLENGTH)) - throw RangeError("A"); - expressions.push(interface.ssm(SCI_GETCHARAT, v)); - break; - - default: - throw SyntaxError(chr); - } - - return this; -} - -StateFCommand::StateFCommand() -{ - transitions['\0'] = this; - transitions['K'] = &States::searchkill; - transitions['D'] = &States::searchdelete; - transitions['S'] = &States::replace; - transitions['R'] = &States::replacedefault; - transitions['G'] = &States::changedir; -} - -State * -StateFCommand::custom(gchar chr) -{ - switch (chr) { - /* - * loop flow control - */ - /*$ F< - * F< -- Go to loop start or jump to beginning of macro - * - * Immediately jumps to the current loop's start. - * Also works from inside conditionals. - * - * Outside of loops \(em or in a macro without - * a loop \(em this jumps to the beginning of the macro. - */ - case '<': - BEGIN_EXEC(&States::start); - /* FIXME: what if in brackets? */ - expressions.discard_args(); - - macro_pc = loop_stack.items() > loop_stack_fp - ? loop_stack.peek().pc : -1; - break; - - /*$ F> continue - * F> -- Go to loop end - * :F> - * - * Jumps to the current loop's end. - * If the loop has remaining iterations or runs indefinitely, - * the jump is performed immediately just as if \(lq>\(rq - * had been executed. - * If the loop has reached its last iteration, \*(ST will - * parse until the loop end command has been found and control - * resumes after the end of the loop. - * - * In interactive mode, if the loop is incomplete and must - * be exited, you can type in the loop's remaining commands - * without them being executed (but they are parsed). - * - * When colon-modified, \fB:F>\fP behaves like \fB:>\fP - * and allows numbers to be aggregated on the stack. - * - * Calling \fBF>\fP outside of a loop at the current - * macro invocation level will throw an error. - */ - /* - * NOTE: This is almost identical to the normal - * loop end since we don't really want to or need to - * parse till the end of the loop. - */ - case '>': { - BEGIN_EXEC(&States::start); - - if (loop_stack.items() <= loop_stack_fp) - throw Error("Jump to loop end without corresponding " - "loop start command"); - LoopContext &ctx = loop_stack.peek(); - bool colon_modified = eval_colon(); - - if (!ctx.pass_through) { - if (colon_modified) { - expressions.eval(); - expressions.push(Expressions::OP_NEW); - } else { - expressions.discard_args(); - } - } - - if (ctx.counter == 1) { - /* this was the last loop iteration */ - if (!ctx.pass_through) - expressions.brace_close(); - LoopStack::undo_push(loop_stack.pop()); - /* skip to end of loop */ - undo.push_var(mode) = MODE_PARSE_ONLY_LOOP; - } else { - /* repeat loop */ - macro_pc = ctx.pc; - ctx.counter = MAX(ctx.counter - 1, -1); - } - break; - } - - /* - * conditional flow control - */ - /*$ "F'" - * F\' -- Jump to end of conditional - */ - case '\'': - BEGIN_EXEC(&States::start); - /* skip to end of conditional */ - undo.push_var(mode); - mode = MODE_PARSE_ONLY_COND; - undo.push_var(skip_else); - skip_else = true; - break; - - /*$ F| - * F| -- Jump to else-part of conditional - * - * Jump to else-part of conditional or end of - * conditional (only if invoked from inside the - * condition's else-part). - */ - case '|': - BEGIN_EXEC(&States::start); - /* skip to ELSE-part or end of conditional */ - undo.push_var(mode); - mode = MODE_PARSE_ONLY_COND; - break; - - default: - throw SyntaxError(chr); - } - - return &States::start; -} - -void -UndoTokenChangeDir::run(void) -{ - /* - * Changing the directory on rub-out may fail. - * This is handled silently. - */ - g_chdir(dir); -} - -/*$ FG cd change-dir folder-go - * FG[directory]$ -- Change working directory - * - * Changes the process' current working directory - * to which affects all subsequent - * operations on relative file names like - * tab-completions. - * It is also inherited by external processes spawned - * via \fBEC\fP and \fBEG\fP. - * - * If is omitted, the working directory - * is changed to the current user's home directory - * as set by the \fBHOME\fP environment variable - * (i.e. its corresponding \(lq$HOME\(rq environment - * register). - * This variable is always initialized by \*(ST - * (see \fBsciteco\fP(1)). - * Therefore the expression \(lqFG\fB$\fP\(rq is - * exactly equivalent to both \(lqFG~\fB$\fP\(rq and - * \(lqFG^EQ[$HOME]\fB$\fP\(rq. - * - * The current working directory is also mapped to - * the special global Q-Register \(lq$\(rq (dollar sign) - * which may be used retrieve the current working directory. - * - * String-building characters are enabled on this - * command and directories can be tab-completed. - */ -State * -StateChangeDir::got_file(const gchar *filename) -{ - gchar *dir; - - BEGIN_EXEC(&States::start); - - /* passes ownership of string to undo token object */ - undo.push_own(g_get_current_dir()); - - dir = *filename ? g_strdup(filename) - : QRegisters::globals["$HOME"]->get_string(); - - if (g_chdir(dir)) { - /* FIXME: Is errno usable on Windows here? */ - Error err("Cannot change working directory " - "to \"%s\"", dir); - g_free(dir); - throw err; - } - - g_free(dir); - return &States::start; -} - -StateCondCommand::StateCondCommand() -{ - transitions['\0'] = this; -} - -State * -StateCondCommand::custom(gchar chr) -{ - tecoInt value = 0; - bool result; - - switch (mode) { - case MODE_PARSE_ONLY_COND: - undo.push_var(nest_level)++; - break; - - case MODE_NORMAL: - expressions.eval(); - - if (chr == '~') - /* don't pop value for ~ conditionals */ - break; - - if (!expressions.args()) - throw ArgExpectedError('"'); - value = expressions.pop_num_calc(); - break; - - default: - break; - } - - switch (String::toupper(chr)) { - case '~': - BEGIN_EXEC(&States::start); - result = !expressions.args(); - break; - case 'A': - BEGIN_EXEC(&States::start); - result = g_ascii_isalpha((gchar)value); - break; - case 'C': - BEGIN_EXEC(&States::start); - result = g_ascii_isalnum((gchar)value) || - value == '.' || value == '$' || value == '_'; - break; - case 'D': - BEGIN_EXEC(&States::start); - result = g_ascii_isdigit((gchar)value); - break; - case 'I': - BEGIN_EXEC(&States::start); - result = G_IS_DIR_SEPARATOR((gchar)value); - break; - case 'S': - case 'T': - BEGIN_EXEC(&States::start); - result = IS_SUCCESS(value); - break; - case 'F': - case 'U': - BEGIN_EXEC(&States::start); - result = IS_FAILURE(value); - break; - case 'E': - case '=': - BEGIN_EXEC(&States::start); - result = value == 0; - break; - case 'G': - case '>': - BEGIN_EXEC(&States::start); - result = value > 0; - break; - case 'L': - case '<': - BEGIN_EXEC(&States::start); - result = value < 0; - break; - case 'N': - BEGIN_EXEC(&States::start); - result = value != 0; - break; - case 'R': - BEGIN_EXEC(&States::start); - result = g_ascii_isalnum((gchar)value); - break; - case 'V': - BEGIN_EXEC(&States::start); - result = g_ascii_islower((gchar)value); - break; - case 'W': - BEGIN_EXEC(&States::start); - result = g_ascii_isupper((gchar)value); - break; - default: - throw Error("Invalid conditional type \"%c\"", chr); - } - - if (!result) - /* skip to ELSE-part or end of conditional */ - undo.push_var(mode) = MODE_PARSE_ONLY_COND; - - return &States::start; -} - -StateControl::StateControl() -{ - transitions['\0'] = this; - transitions['I'] = &States::insert_indent; - transitions['U'] = &States::ctlucommand; - transitions['^'] = &States::ascii; - transitions['['] = &States::escape; -} - -State * -StateControl::custom(gchar chr) -{ - switch (String::toupper(chr)) { - /*$ ^C exit - * ^C -- Exit program immediately - * - * Lets the top-level macro return immediately - * regardless of the current macro invocation frame. - * This command is only allowed in batch mode, - * so it is not invoked accidentally when using - * the CTRL+C immediate editing command to - * interrupt long running operations. - * When using \fB^C\fP in a munged file, - * interactive mode is never started, so it behaves - * effectively just like \(lq-EX\fB$$\fP\(rq - * (when executed in the top-level macro at least). - * - * The \fBquit\fP hook is still executed. - */ - case 'C': - BEGIN_EXEC(&States::start); - if (undo.enabled) - throw Error("<^C> not allowed in interactive mode"); - quit_requested = true; - throw Quit(); - - /*$ ^O octal - * ^O -- Set radix to 8 (octal) - */ - case 'O': - BEGIN_EXEC(&States::start); - expressions.set_radix(8); - break; - - /*$ ^D decimal - * ^D -- Set radix to 10 (decimal) - */ - case 'D': - BEGIN_EXEC(&States::start); - expressions.set_radix(10); - break; - - /*$ ^R radix - * radix^R -- Set and get radix - * ^R -> radix - * - * Set current radix to arbitrary value . - * If is omitted, the command instead - * returns the current radix. - */ - case 'R': - BEGIN_EXEC(&States::start); - expressions.eval(); - if (!expressions.args()) - expressions.push(expressions.radix); - else - expressions.set_radix(expressions.pop_num_calc()); - break; - - /* - * Additional numeric operations - */ - /*$ ^_ negate - * n^_ -> ~n -- Binary negation - * - * Binary negates (complements) and returns - * the result. - * Binary complements are often used to negate - * \*(ST booleans. - */ - case '_': - BEGIN_EXEC(&States::start); - expressions.push(~expressions.pop_num_calc()); - break; - - case '*': - BEGIN_EXEC(&States::start); - expressions.push_calc(Expressions::OP_POW); - break; - - case '/': - BEGIN_EXEC(&States::start); - expressions.push_calc(Expressions::OP_MOD); - break; - - case '#': - BEGIN_EXEC(&States::start); - expressions.push_calc(Expressions::OP_XOR); - break; - - default: - throw Error("Unsupported command <^%c>", chr); - } - - return &States::start; -} - -/*$ ^^ ^^c - * ^^c -> n -- Get ASCII code of character - * - * Returns the ASCII code of the character - * that is part of the command. - * Can be used in place of integer constants for improved - * readability. - * For instance ^^A will return 65. - * - * Note that this command can be typed CTRL+Caret or - * Caret-Caret. - */ -StateASCII::StateASCII() : State() -{ - transitions['\0'] = this; -} - -State * -StateASCII::custom(gchar chr) -{ - BEGIN_EXEC(&States::start); - - expressions.push(chr); - - return &States::start; -} - -/* - * The Escape state is special, as it implements - * a kind of "lookahead" for the ^[ command (discard all - * arguments). - * It is not executed immediately as usual in SciTECO - * but only if not followed by an escape character. - * This is necessary since $$ is the macro return - * and command-line termination command and it must not - * discard arguments. - * Deferred execution of ^[ is possible since it does - * not have any visible side-effects - its effects can - * only be seen when executing the following command. - */ -StateEscape::StateEscape() -{ - transitions['\0'] = this; -} - -State * -StateEscape::custom(gchar chr) -{ - /*$ ^[^[ ^[$ $$ terminate return - * [a1,a2,...]$$ -- Terminate command line or return from macro - * [a1,a2,...]^[$ - * - * Returns from the current macro invocation. - * This will pass control to the calling macro immediately - * and is thus faster than letting control reach the macro's end. - * Also, direct arguments to \fB$$\fP will be left on the expression - * stack when the macro returns. - * \fB$$\fP closes loops automatically and is thus safe to call - * from loop bodies. - * Furthermore, it has defined semantics when executed - * from within braced expressions: - * All braces opened in the current macro invocation will - * be closed and their values discarded. - * Only the direct arguments to \fB$$\fP will be kept. - * - * Returning from the top-level macro in batch mode - * will exit the program or start up interactive mode depending - * on whether program exit has been requested. - * \(lqEX\fB$$\fP\(rq is thus a common idiom to exit - * prematurely. - * - * In interactive mode, returning from the top-level macro - * (i.e. typing \fB$$\fP at the command line) has the - * effect of command line termination. - * The arguments to \fB$$\fP are currently not used - * when terminating a command line \(em the new command line - * will always start with a clean expression stack. - * - * The first \fIescape\fP of \fB$$\fP may be typed either - * as an escape character (ASCII 27), in up-arrow mode - * (e.g. \fB^[$\fP) or as a dollar character \(em the - * second character must be either a real escape character - * or a dollar character. - */ - if (chr == CTL_KEY_ESC || chr == '$') { - BEGIN_EXEC(&States::start); - States::current = &States::start; - expressions.eval(); - throw Return(expressions.args()); - } - - /* - * Alternatives: ^[, , , $ (dollar) - */ - /*$ ^[ $ escape discard - * $ -- Discard all arguments - * ^[ - * - * Pops and discards all values from the stack that - * might otherwise be used as arguments to following - * commands. - * Therefore it stops popping on stack boundaries like - * they are introduced by arithmetic brackets or loops. - * - * Note that ^[ is usually typed using the Escape key. - * CTRL+[ however is possible as well and equivalent to - * Escape in every manner. - * The up-arrow notation however is processed like any - * ordinary command and only works at the begining of - * a command. - * Additionally, this command may be written as a single - * dollar character. - */ - if (mode == MODE_NORMAL) - expressions.discard_args(); - return States::start.get_next_state(chr); -} - -void -StateEscape::end_of_macro(void) -{ - /* - * Due to the deferred nature of ^[, - * it is valid to end in the "escape" state. - */ - expressions.discard_args(); -} - -StateECommand::StateECommand() -{ - transitions['\0'] = this; - transitions['%'] = &States::epctcommand; - transitions['B'] = &States::editfile; - transitions['C'] = &States::executecommand; - transitions['G'] = &States::egcommand; - transitions['I'] = &States::insert_nobuilding; - transitions['M'] = &States::macro_file; - transitions['N'] = &States::glob_pattern; - transitions['S'] = &States::scintilla_symbols; - transitions['Q'] = &States::eqcommand; - transitions['U'] = &States::eucommand; - transitions['W'] = &States::savefile; -} - -State * -StateECommand::custom(gchar chr) -{ - switch (String::toupper(chr)) { - /*$ EF close - * [bool]EF -- Remove buffer from ring - * -EF - * - * Removes buffer from buffer ring, effectively - * closing it. - * If the buffer is dirty (modified), EF will yield - * an error. - * may be a specified to enforce closing dirty - * buffers. - * If it is a Failure condition boolean (negative), - * the buffer will be closed unconditionally. - * If is absent, the sign prefix (1 or -1) will - * be implied, so \(lq-EF\(rq will always close the buffer. - * - * It is noteworthy that EF will be executed immediately in - * interactive mode but can be rubbed out at a later time - * to reopen the file. - * Closed files are kept in memory until the command line - * is terminated. - */ - case 'F': - BEGIN_EXEC(&States::start); - if (QRegisters::current) - throw Error("Q-Register currently edited"); - - if (IS_FAILURE(expressions.pop_num_calc()) && - ring.current->dirty) - throw Error("Buffer \"%s\" is dirty", - ring.current->filename ? : "(Unnamed)"); - - ring.close(); - break; - - /*$ ED flags - * flags ED -- Set and get ED-flags - * [off,]on ED - * ED -> flags - * - * With arguments, the command will set the \fBED\fP flags. - * is a bitmap of flags to set. - * Specifying one argument to set the flags is a special - * case of specifying two arguments that allow to control - * which flags to enable/disable. - * is a bitmap of flags to disable (set to 0 in ED - * flags) and is a bitmap of flags that is ORed into - * the flags variable. - * If is omitted, the value 0^_ is implied. - * In otherwords, all flags are turned off before turning - * on the flags. - * Without any argument ED returns the current flags. - * - * Currently, the following flags are used by \*(ST: - * - 8: Enable/disable automatic folding of case-insensitive - * command characters during interactive key translation. - * The case of letter keys is inverted, so one or two - * character commands will typically be inserted upper-case, - * but you can still press Shift to insert lower-case letters. - * Case-insensitive Q-Register specifications are not - * case folded. - * This is thought to improve the readability of the command - * line macro. - * - 16: Enable/disable automatic translation of end of - * line sequences to and from line feed. - * - 32: Enable/Disable buffer editing hooks - * (via execution of macro in global Q-Register \(lqED\(rq) - * - 64: Enable/Disable function key macros - * - 128: Enable/Disable enforcement of UNIX98 - * \(lq/bin/sh\(rq emulation for operating system command - * executions - * - 256: Enable/Disable \fBxterm\fP(1) clipboard support. - * Should only be enabled if XTerm allows the - * \fIGetSelection\fP and \fISetSelection\fP window - * operations. - * - * The features controlled thus are discribed in other sections - * of this manual. - * - * The default value of the \fBED\fP flags is 16 - * (only automatic EOL translation enabled). - */ - case 'D': - BEGIN_EXEC(&States::start); - expressions.eval(); - if (!expressions.args()) { - expressions.push(Flags::ed); - } else { - tecoInt on = expressions.pop_num_calc(); - tecoInt off = expressions.pop_num_calc(0, ~(tecoInt)0); - - undo.push_var(Flags::ed); - Flags::ed = (Flags::ed & ~off) | on; - } - break; - - /*$ EJ properties - * [key]EJ -> value -- Get and set system properties - * -EJ -> value - * value,keyEJ - * rgb,color,3EJ - * - * This command may be used to get and set system - * properties. - * With one argument, it retrieves a numeric property - * identified by \fIkey\fP. - * If \fIkey\fP is omitted, the prefix sign is implied - * (1 or -1). - * With two arguments, it sets property \fIkey\fP to - * \fIvalue\fP and returns nothing. Some property \fIkeys\fP - * may require more than one value. Properties may be - * write-only or read-only. - * - * The following property keys are defined: - * .IP 0 4 - * The current user interface: 1 for Curses, 2 for GTK - * (\fBread-only\fP) - * .IP 1 - * The current numbfer of buffers: Also the numeric id - * of the last buffer in the ring. This is implied if - * no argument is given, so \(lqEJ\(rq returns the number - * of buffers in the ring. - * (\fBread-only\fP) - * .IP 2 - * The current memory limit in bytes. - * This limit helps to prevent dangerous out-of-memory - * conditions (e.g. resulting from infinite loops) by - * constantly sampling the memory requirements of \*(ST. - * Note that not all platforms support precise measurements - * of the current memory usage \(em \*(ST will fall back - * to an approximation which might be less than the actual - * usage on those platforms. - * Memory limiting is effective in batch and interactive mode. - * Commands which would exceed that limit will fail instead - * allowing users to recover in interactive mode, e.g. by - * terminating the command line. - * When getting, a zero value indicates that memory limiting is - * disabled. - * Setting a value less than or equal to 0 as in - * \(lq0,2EJ\(rq disables the limit. - * \fBWarning:\fP Disabling memory limiting may provoke - * out-of-memory errors in long running or infinite loops - * (interactive mode) that result in abnormal program - * termination. - * Setting a new limit may fail if the current memory - * requirements are too large for the new limit \(em if - * this happens you may have to clear your command-line - * first. - * Memory limiting is enabled by default. - * .IP 3 - * This \fBwrite-only\fP property allows redefining the - * first 16 entries of the terminal color palette \(em a - * feature required by some - * color schemes when using the Curses user interface. - * When setting this property, you are making a request - * to define the terminal \fIcolor\fP as the Scintilla-compatible - * RGB color value given in the \fIrgb\fP parameter. - * \fIcolor\fP must be a value between 0 and 15 - * corresponding to black, red, green, yellow, blue, magenta, - * cyan, white, bright black, bright red, etc. in that order. - * The \fIrgb\fP value has the format 0xBBGGRR, i.e. the red - * component is the least-significant byte and all other bytes - * are ignored. - * Note that on curses, RGB color values sent to Scintilla - * are actually mapped to these 16 colors by the Scinterm port - * and may represent colors with no resemblance to the \(lqRGB\(rq - * value used (depending on the current palette) \(em they should - * instead be viewed as placeholders for 16 standard terminal - * color codes. - * Please refer to the Scinterm manual for details on the allowed - * \(lqRGB\(rq values and how they map to terminal colors. - * This command provides a crude way to request exact RGB colors - * for the first 16 terminal colors. - * The color definition may be queued or be completely ignored - * on other user interfaces and no feedback is given - * if it fails. In fact feedback cannot be given reliably anyway. - * Note that on 8 color terminals, only the first 8 colors - * can be redefined (if you are lucky). - * Note that due to restrictions of most terminal emulators - * and some curses implementations, this command simply will not - * restore the original palette entry or request - * when rubbed out and should generally only be used in - * \fIbatch-mode\fP \(em typically when loading a color scheme. - * For the same reasons \(em even though \*(ST tries hard to - * restore the original palette on exit \(em palette changes may - * persist after \*(ST terminates on most terminal emulators on Unix. - * The only emulator which will restore their default palette - * on exit the author is aware of is \fBxterm\fP(1) and - * the Linux console driver. - * You have been warned. Good luck. - */ - case 'J': { - BEGIN_EXEC(&States::start); - - enum { - EJ_USER_INTERFACE = 0, - EJ_BUFFERS, - EJ_MEMORY_LIMIT, - EJ_INIT_COLOR - }; - tecoInt property; - - expressions.eval(); - property = expressions.pop_num_calc(); - if (expressions.args() > 0) { - /* set property */ - tecoInt value = expressions.pop_num_calc(); - - switch (property) { - case EJ_MEMORY_LIMIT: - memlimit.set_limit(MAX(0, value)); - break; - - case EJ_INIT_COLOR: - if (value < 0 || value >= 16) - throw Error("Invalid color code %" TECO_INTEGER_FORMAT - " specified for ", value); - if (!expressions.args()) - throw ArgExpectedError("EJ"); - interface.init_color((guint)value, - (guint32)expressions.pop_num_calc()); - break; - - default: - throw Error("Cannot set property %" TECO_INTEGER_FORMAT - " for ", property); - } - - break; - } - - switch (property) { - case EJ_USER_INTERFACE: -#ifdef INTERFACE_CURSES - expressions.push(1); -#elif defined(INTERFACE_GTK) - expressions.push(2); -#else -#error Missing value for current interface! -#endif - break; - - case EJ_BUFFERS: - expressions.push(ring.get_id(ring.last())); - break; - - case EJ_MEMORY_LIMIT: - expressions.push(memlimit.limit); - break; - - default: - throw Error("Invalid property %" TECO_INTEGER_FORMAT - " for ", property); - } - break; - } - - /*$ EL eol - * 0EL -- Set or get End of Line mode - * 13,10:EL - * 1EL - * 13:EL - * 2EL - * 10:EL - * EL -> 0 | 1 | 2 - * :EL -> 13,10 | 13 | 10 - * - * Sets or gets the current document's End Of Line (EOL) mode. - * This is a thin wrapper around Scintilla's - * \fBSCI_SETEOLMODE\fP and \fBSCI_GETEOLMODE\fP messages but is - * shorter to type and supports restoring the EOL mode upon rubout. - * Like the Scintilla message, does \fBnot\fP change the - * characters in the current document. - * If automatic EOL translation is activated (which is the default), - * \*(ST will however use this information when saving files or - * writing to external processes. - * - * With one argument, the EOL mode is set according to these - * constants: - * .IP 0 4 - * Carriage return (ASCII 13), followed by line feed (ASCII 10). - * This is the default EOL mode on DOS/Windows. - * .IP 1 - * Carriage return (ASCII 13). - * The default EOL mode on old Mac OS systems. - * .IP 2 - * Line feed (ASCII 10). - * The default EOL mode on POSIX/UNIX systems. - * - * In its colon-modified form, the EOL mode is set according - * to the EOL characters on the expression stack. - * \*(ST will only pop as many values as are necessary to - * determine the EOL mode. - * - * Without arguments, the current EOL mode is returned. - * When colon-modified, the current EOL mode's character sequence - * is pushed onto the expression stack. - */ - case 'L': - BEGIN_EXEC(&States::start); - - expressions.eval(); - if (expressions.args() > 0) { - gint eol_mode; - - if (eval_colon()) { - switch (expressions.pop_num_calc()) { - case '\r': - eol_mode = SC_EOL_CR; - break; - case '\n': - if (!expressions.args()) { - eol_mode = SC_EOL_LF; - break; - } - if (expressions.pop_num_calc() == '\r') { - eol_mode = SC_EOL_CRLF; - break; - } - /* fall through */ - default: - throw Error("Invalid EOL sequence for "); - } - } else { - eol_mode = expressions.pop_num_calc(); - switch (eol_mode) { - case SC_EOL_CRLF: - case SC_EOL_CR: - case SC_EOL_LF: - break; - default: - throw Error("Invalid EOL mode %d for ", - eol_mode); - } - } - - interface.undo_ssm(SCI_SETEOLMODE, - interface.ssm(SCI_GETEOLMODE)); - interface.ssm(SCI_SETEOLMODE, eol_mode); - } else if (eval_colon()) { - expressions.push_str(get_eol_seq(interface.ssm(SCI_GETEOLMODE))); - } else { - expressions.push(interface.ssm(SCI_GETEOLMODE)); - } - break; - - /*$ EX exit - * [bool]EX -- Exit program - * -EX - * :EX - * - * Exits \*(ST, or rather requests program termination - * at the end of the top-level macro. - * Therefore instead of exiting immediately which - * could be annoying in interactive mode, EX will - * result in program termination only when the command line - * is terminated. - * This allows EX to be rubbed out and used in macros. - * The usual command to exit \*(ST in interactive mode - * is thus \(lqEX\fB$$\fP\(rq. - * In batch mode EX will exit the program if control - * reaches the end of the munged file \(em instead of - * starting up interactive mode. - * - * If any buffer is dirty (modified), EX will yield - * an error. - * When specifying as a success/truth condition - * boolean, EX will not check whether there are modified - * buffers and will always succeed. - * If is omitted, the sign prefix is implied - * (1 or -1). - * In other words \(lq-EX\fB$$\fP\(rq is the usual - * interactive command sequence to discard all unsaved - * changes and exit. - * - * When colon-modified, is ignored and EX - * will instead immediately try to save all modified buffers \(em - * this can of course be reversed using rubout. - * Saving all buffers can fail, e.g. if the unnamed file - * is modified or if there is an IO error. - * \(lq:EX\fB$$\fP\(rq is nevertheless the usual interactive - * command sequence to exit while saving all modified - * buffers. - */ - /** @bug what if changing file after EX? will currently still exit */ - case 'X': - BEGIN_EXEC(&States::start); - - if (eval_colon()) - ring.save_all_dirty_buffers(); - else if (IS_FAILURE(expressions.pop_num_calc()) && - ring.is_any_dirty()) - throw Error("Modified buffers exist"); - - undo.push_var(quit_requested) = true; - break; - - default: - throw SyntaxError(chr); - } - - return &States::start; -} - -static struct ScintillaMessage { - unsigned int iMessage; - uptr_t wParam; - sptr_t lParam; -} scintilla_message = {0, 0, 0}; - -/*$ ES scintilla message - * -- Send Scintilla message - * [lParam[,wParam]]ESmessage[,wParam]$[lParam]$ -> result - * - * Send Scintilla message with code specified by symbolic - * name , and . - * may be symbolic when specified as part of the - * first string argument. - * If not it is popped from the stack. - * may be specified as a constant string whose - * pointer is passed to Scintilla if specified as the second - * string argument. - * If the second string argument is empty, is popped - * from the stack instead. - * Parameters popped from the stack may be omitted, in which - * case 0 is implied. - * The message's return value is pushed onto the stack. - * - * All messages defined by Scintilla (as C macros) can be - * used by passing their name as a string to ES - * (e.g. ESSCI_LINESONSCREEN...). - * The \(lqSCI_\(rq prefix may be omitted and message symbols - * are case-insensitive. - * Only the Scintilla lexer symbols (SCLEX_..., SCE_...) - * may be used symbolically with the ES command as , - * other values must be passed as integers on the stack. - * In interactive mode, symbols may be auto-completed by - * pressing Tab. - * String-building characters are by default interpreted - * in the string arguments. - * - * .BR Warning : - * Almost all Scintilla messages may be dispatched using - * this command. - * \*(ST does not keep track of the editor state changes - * performed by these commands and cannot undo them. - * You should never use it to change the editor state - * (position changes, deletions, etc.) or otherwise - * rub out will result in an inconsistent editor state. - * There are however exceptions: - * - In the editor profile and batch mode in general, - * the ES command may be used freely. - * - In the ED hook macro (register \(lqED\(rq), - * when a file is added to the ring, most destructive - * operations can be performed since rubbing out the - * EB command responsible for the hook execution also - * removes the buffer from the ring again. - */ -State * -StateScintilla_symbols::done(const gchar *str) -{ - BEGIN_EXEC(&States::scintilla_lparam); - - undo.push_var(scintilla_message); - if (*str) { - gchar **symbols = g_strsplit(str, ",", -1); - tecoInt v; - - if (!symbols[0]) - goto cleanup; - if (*symbols[0]) { - v = Symbols::scintilla.lookup(symbols[0], "SCI_"); - if (v < 0) - throw Error("Unknown Scintilla message symbol \"%s\"", - symbols[0]); - scintilla_message.iMessage = v; - } - - if (!symbols[1]) - goto cleanup; - if (*symbols[1]) { - v = Symbols::scilexer.lookup(symbols[1]); - if (v < 0) - throw Error("Unknown Scintilla Lexer symbol \"%s\"", - symbols[1]); - scintilla_message.wParam = v; - } - - if (!symbols[2]) - goto cleanup; - if (*symbols[2]) { - v = Symbols::scilexer.lookup(symbols[2]); - if (v < 0) - throw Error("Unknown Scintilla Lexer symbol \"%s\"", - symbols[2]); - scintilla_message.lParam = v; - } - -cleanup: - g_strfreev(symbols); - } - - expressions.eval(); - if (!scintilla_message.iMessage) { - if (!expressions.args()) - throw Error(" command requires at least a message code"); - - scintilla_message.iMessage = expressions.pop_num_calc(0, 0); - } - if (!scintilla_message.wParam) - scintilla_message.wParam = expressions.pop_num_calc(0, 0); - - return &States::scintilla_lparam; -} - -State * -StateScintilla_lParam::done(const gchar *str) -{ - BEGIN_EXEC(&States::start); - - if (!scintilla_message.lParam) - scintilla_message.lParam = *str ? (sptr_t)str - : expressions.pop_num_calc(0, 0); - - expressions.push(interface.ssm(scintilla_message.iMessage, - scintilla_message.wParam, - scintilla_message.lParam)); - - undo.push_var(scintilla_message); - memset(&scintilla_message, 0, sizeof(scintilla_message)); - - return &States::start; -} - -/* - * NOTE: cannot support VideoTECO's I because - * beginning and end of strings must be determined - * syntactically - */ -/*$ I insert - * [c1,c2,...]I[text]$ -- Insert text with string building characters - * - * First inserts characters for all the values - * on the argument stack (interpreted as codepoints). - * It does so in the order of the arguments, i.e. - * is inserted before , ecetera. - * Secondly, the command inserts . - * In interactive mode, is inserted interactively. - * - * String building characters are \fBenabled\fP for the - * I command. - * When editing \*(ST macros, using the \fBEI\fP command - * may be better, since it has string building characters - * disabled. - */ -/*$ EI - * [c1,c2,...]EI[text]$ -- Insert text without string building characters - * - * Inserts text at the current position in the current - * document. - * This command is identical to the \fBI\fP command, - * except that string building characters are \fBdisabled\fP. - * Therefore it may be beneficial when editing \*(ST - * macros. - */ -void -StateInsert::initial(void) -{ - guint args; - - expressions.eval(); - args = expressions.args(); - if (!args) - return; - - interface.ssm(SCI_BEGINUNDOACTION); - for (int i = args; i > 0; i--) { - gchar chr = (gchar)expressions.peek_num(i-1); - interface.ssm(SCI_ADDTEXT, 1, (sptr_t)&chr); - } - for (int i = args; i > 0; i--) - expressions.pop_num_calc(); - interface.ssm(SCI_SCROLLCARET); - interface.ssm(SCI_ENDUNDOACTION); - ring.dirtify(); - - if (current_doc_must_undo()) - interface.undo_ssm(SCI_UNDO); -} - -void -StateInsert::process(const gchar *str, gint new_chars) -{ - interface.ssm(SCI_BEGINUNDOACTION); - interface.ssm(SCI_ADDTEXT, new_chars, - (sptr_t)(str + strlen(str) - new_chars)); - interface.ssm(SCI_SCROLLCARET); - interface.ssm(SCI_ENDUNDOACTION); - ring.dirtify(); - - if (current_doc_must_undo()) - interface.undo_ssm(SCI_UNDO); -} - -State * -StateInsert::done(const gchar *str) -{ - /* nothing to be done when done */ - return &States::start; -} - -/* - * Alternatives: ^i, ^I, , - */ -/*$ ^I indent - * [char,...]^I[text]$ -- Insert with leading indention - * - * ^I (usually typed using the Tab key), first inserts - * all the chars on the stack into the buffer, then indention - * characters (one tab or multiple spaces) and eventually - * the optional is inserted interactively. - * It is thus a derivate of the \fBI\fP (insertion) command. - * - * \*(ST uses Scintilla settings to determine the indention - * characters. - * If tab use is enabled with the \fBSCI_SETUSETABS\fP message, - * a single tab character is inserted. - * Tab use is enabled by default. - * Otherwise, a number of spaces is inserted up to the - * next tab stop so that the command's argument - * is inserted at the beginning of the next tab stop. - * The size of the tab stops is configured by the - * \fBSCI_SETTABWIDTH\fP Scintilla message (8 by default). - * In combination with \*(ST's use of the tab key as an - * immediate editing command for all insertions, this - * implements support for different insertion styles. - * The Scintilla settings apply to the current Scintilla - * document and are thus local to the currently edited - * buffer or Q-Register. - * - * However for the same reason, the ^I command is not - * fully compatible with classic TECO which \fIalways\fP - * inserts a single tab character and should not be used - * for the purpose of inserting single tabs in generic - * macros. - * To insert a single tab character reliably, the idioms - * \(lq9I$\(rq or \(lqI^I$\(rq may be used. - * - * Like the I command, ^I has string building characters - * \fBenabled\fP. - */ -void -StateInsertIndent::initial(void) -{ - StateInsert::initial(); - - interface.ssm(SCI_BEGINUNDOACTION); - if (interface.ssm(SCI_GETUSETABS)) { - interface.ssm(SCI_ADDTEXT, 1, (sptr_t)"\t"); - } else { - gint len = interface.ssm(SCI_GETTABWIDTH); - - len -= interface.ssm(SCI_GETCOLUMN, - interface.ssm(SCI_GETCURRENTPOS)) % len; - - gchar spaces[len]; - - memset(spaces, ' ', sizeof(spaces)); - interface.ssm(SCI_ADDTEXT, sizeof(spaces), (sptr_t)spaces); - } - interface.ssm(SCI_SCROLLCARET); - interface.ssm(SCI_ENDUNDOACTION); - ring.dirtify(); - - if (current_doc_must_undo()) - interface.undo_ssm(SCI_UNDO); -} - -} /* namespace SciTECO */ diff --git a/src/parser.h b/src/parser.h index 9255268..b594edc 100644 --- a/src/parser.h +++ b/src/parser.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,47 +14,130 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -#ifndef __PARSER_H -#define __PARSER_H - -#include +#pragma once #include +#include + #include "sciteco.h" -#include "memory.h" -#include "undo.h" -#include "error.h" -#include "expressions.h" - -namespace SciTECO { - -/* TECO uses only lower 7 bits for commands */ -#define MAX_TRANSITIONS 127 - -class State : public Object { -protected: - /* static transitions */ - State *transitions[MAX_TRANSITIONS]; - - inline void - init(const gchar *chars, State &state) - { - while (*chars) - transitions[(int)*chars++] = &state; - } - inline void - init(const gchar *chars) - { - init(chars, *this); - } +#include "string-utils.h" +#include "goto.h" +#include "qreg.h" + +/* + * Forward Declarations + */ +typedef const struct teco_state_t teco_state_t; +typedef struct teco_machine_t teco_machine_t; +typedef struct teco_machine_main_t teco_machine_main_t; + +typedef struct { + /** how many iterations are left */ + teco_int_t counter; + /** Program counter of loop start command */ + guint pc : sizeof(guint)*8 - 1; + /** + * Whether the loop represents an argument + * barrier or not (it "passes through" + * stack arguments). + * + * Since the program counter is usually + * a signed integer, it's ok steal one + * bit for the pass_through flag. + */ + gboolean pass_through : 1; +} teco_loop_context_t; + +extern GArray *teco_loop_stack; + +void undo__insert_val__teco_loop_stack(guint, teco_loop_context_t); +void undo__remove_index__teco_loop_stack(guint); + +/** + * @defgroup states Parser states + * + * Parser states are defined as global constants using the TECO_DEFINE_STATE() + * macro, allowing individual fields and callbacks to be overwritten. + * Derived macros are defined to factor out common fields and settings. + * States therefore form a hierarchy, which is documented using + * \@interface and \@implements tags. + * + * @{ + */ -public: - State(); +/* + * FIXME: Remove _cb from all callback names. See qreg.h. + * FIXME: Maybe use TECO_DECLARE_VTABLE_METHOD()? + */ +typedef const struct { + gboolean string_building : 1; + gboolean last : 1; - static void input(gchar chr); - State *get_next_state(gchar chr); + /** + * Called repeatedly to process chunks of input and give interactive feedback. + * + * Can be NULL if no interactive feedback is required. + */ + gboolean (*process_cb)(teco_machine_main_t *ctx, const teco_string_t *str, + gsize new_chars, GError **error); + + /** + * Called at the end of the string argument to determine the next state. + * Commands that don't give interactive feedback can use this callback + * to perform their main processing. + */ + teco_state_t *(*done_cb)(teco_machine_main_t *ctx, const teco_string_t *str, GError **error); +} teco_state_expectstring_t; + +typedef const struct { + teco_qreg_type_t type; + + /** Called when a register specification has been successfully parsed. */ + teco_state_t *(*got_register_cb)(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error); +} teco_state_expectqreg_t; + +typedef gboolean (*teco_state_initial_cb_t)(teco_machine_t *ctx, GError **error); +typedef teco_state_t *(*teco_state_input_cb_t)(teco_machine_t *ctx, gchar chr, GError **error); +typedef gboolean (*teco_state_refresh_cb_t)(teco_machine_t *ctx, GError **error); +typedef gboolean (*teco_state_end_of_macro_cb_t)(teco_machine_t *ctx, GError **error); +typedef gboolean (*teco_state_process_edit_cmd_cb_t)(teco_machine_t *ctx, teco_machine_t *parent_ctx, + gchar key, GError **error); + +typedef enum { + TECO_FNMACRO_MASK_START = (1 << 0), + TECO_FNMACRO_MASK_STRING = (1 << 1), + TECO_FNMACRO_MASK_DEFAULT = ~((1 << 2)-1) +} teco_fnmacro_mask_t; + +/** + * A teco_machine_t state. + * These are declared as constants using TECO_DEFINE_STATE() and friends. + * + * @note Unless you don't want to manually "upcast" the teco_machine_t* in + * callback implementations, you will have to cast your callback types when initializing + * the teco_state_t vtables. + * Casting to functions of different signature is theoretically undefined behavior, + * but works on all major platforms including Emscripten, as long as they differ only + * in pointer types. + */ +struct teco_state_t { + /** + * Called the first time this state is entered. + * Theoretically, you can use teco_machine_main_transition_t instead, + * but this callback improves reusability. + * + * It can be NULL if not required. + */ + teco_state_initial_cb_t initial_cb; + + /** + * Get next state given an input character. + * + * This is a mandatory field. + */ + teco_state_input_cb_t input_cb; /** * Provide interactive feedback. @@ -63,32 +146,20 @@ public: * immediate interactive feedback should provide that * feedback; allowing them to optimize batch mode, * macro and many other cases. + * + * It can be NULL if not required. */ - virtual void refresh(void) {} + teco_state_refresh_cb_t refresh_cb; /** * Called at the end of a macro. * Most states/commands are not allowed to end unterminated * at the end of a macro. + * + * It can be NULL if not required. */ - virtual void - end_of_macro(void) - { - throw Error("Unterminated command"); - } + teco_state_end_of_macro_cb_t end_of_macro_cb; -protected: - static bool eval_colon(void); - - /** Get next state given an input character */ - virtual State * - custom(gchar chr) - { - throw SyntaxError(chr); - return NULL; - } - -public: /** * Process editing command (or key press). * @@ -97,383 +168,411 @@ public: * editing commands (behaviour on key press). * * By implementing this method, sub-states can either - * handle a key and return or chain to the - * parent's process_edit_cmd() implementation. + * handle a key and return, chain to the + * parent's process_edit_cmd() implementation or even + * to the parent state machine's handler. * * All implementations of this method are defined in - * cmdline.cpp. + * cmdline.c. + * + * This is a mandatory field. */ - virtual void process_edit_cmd(gchar key); - - enum fnmacroMask { - FNMACRO_MASK_START = (1 << 0), - FNMACRO_MASK_STRING = (1 << 1), - FNMACRO_MASK_DEFAULT = ~((1 << 2)-1) - }; + teco_state_process_edit_cmd_cb_t process_edit_cmd_cb; /** - * Get the function key macro mask this - * state refers to. + * Whether this state is a start state (ie. not within any + * escape sequence etc.). + * This is separate of TECO_FNMACRO_MASK_START which is set + * only in the main machine's start states. + */ + gboolean is_start : 1; + /** + * Function key macro mask. + * This is not a bitmask since it is compared with values set + * from TECO, so the bitorder needs to be defined. * - * Could also be modelled as a State member. + * @fixme If we intend to "forward" masks from other state machines like + * teco_machine_stringbuilding_t, this should probably be a callback. */ - virtual fnmacroMask - get_fnmacro_mask(void) const - { - return FNMACRO_MASK_DEFAULT; - } -}; + teco_fnmacro_mask_t fnmacro_mask : 8; -/** - * Base class of states with case-insenstive input. - * - * This is meant for states accepting command characters - * that can possibly be case-folded. - */ -class StateCaseInsensitive : public State { -protected: - /* in cmdline.cpp */ - void process_edit_cmd(gchar key); -}; - -template -class MicroStateMachine : public Object { -protected: - /* label pointers */ - typedef const void *MicroState; - const MicroState StateStart; -#define MICROSTATE_START G_STMT_START { \ - if (this->state != StateStart) \ - goto *this->state; \ -} G_STMT_END - - MicroState state; - - inline void - set(MicroState next) - { - if (next != state) - undo.push_var(state) = next; - } - -public: - MicroStateMachine() : StateStart(NULL), state(StateStart) {} - virtual ~MicroStateMachine() {} - - virtual inline void - reset(void) - { - set(StateStart); - } - - virtual bool input(gchar chr, Type &result) = 0; + /** + * Additional state-dependent callbacks and settings. + * This wastes some bytes compared to other techniques for extending teco_state_t + * but this is acceptable since there is only a limited number of constant instances. + * The main advantage of this approach is that we can use a single + * TECO_DEFINE_STATE() for defining and deriving all defaults. + */ + union { + teco_state_expectstring_t expectstring; + teco_state_expectqreg_t expectqreg; + }; }; -/* avoid circular dependency on qregisters.h */ -class QRegSpecMachine; - -class StringBuildingMachine : public MicroStateMachine { - enum Mode { - MODE_NORMAL, - MODE_UPPER, - MODE_LOWER - } mode; - - bool toctl; - -public: - QRegSpecMachine *qregspec_machine; +/** @} */ - StringBuildingMachine() : MicroStateMachine(), - mode(MODE_NORMAL), toctl(false), - qregspec_machine(NULL) {} - ~StringBuildingMachine(); +gboolean teco_state_end_of_macro(teco_machine_t *ctx, GError **error); - void reset(void); +/* in cmdline.c */ +gboolean teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gchar chr, GError **error); - bool input(gchar chr, gchar *&result); -}; - -/* - * Super-class for states accepting string arguments - * Opaquely cares about alternative-escape characters, - * string building commands and accumulation into a string +/** + * @interface TECO_DEFINE_STATE + * @implements teco_state_t + * @ingroup states + * + * @todo Should we eliminate required callbacks, this could be turned into a + * struct initializer TECO_INIT_STATE() and TECO_DECLARE_STATE() would become pointless. + * This would also ease declaring static states. */ -class StateExpectString : public State { - gsize insert_len; - - gint nesting; - - bool string_building; - bool last; - - StringBuildingMachine machine; - -public: - StateExpectString(bool _building = true, bool _last = true) - : insert_len(0), nesting(1), - string_building(_building), last(_last) {} - -private: - State *custom(gchar chr); - void refresh(void); - - virtual fnmacroMask - get_fnmacro_mask(void) const - { - return FNMACRO_MASK_STRING; +#define TECO_DEFINE_STATE(NAME, ...) \ + /** @ingroup states */ \ + teco_state_t NAME = { \ + .initial_cb = NULL, /* do nothing */ \ + .input_cb = (teco_state_input_cb_t)NAME##_input, /* always required */ \ + .refresh_cb = NULL, /* do nothing */ \ + .end_of_macro_cb = teco_state_end_of_macro, \ + .process_edit_cmd_cb = teco_state_process_edit_cmd, \ + .is_start = FALSE, \ + .fnmacro_mask = TECO_FNMACRO_MASK_DEFAULT, \ + ##__VA_ARGS__ \ } -protected: - virtual void initial(void) {} - virtual void process(const gchar *str, gint new_chars) {} - virtual State *done(const gchar *str) = 0; - - /* in cmdline.cpp */ - void process_edit_cmd(gchar key); -}; - -class StateExpectFile : public StateExpectString { -public: - StateExpectFile(bool _building = true, bool _last = true) - : StateExpectString(_building, _last) {} - -private: - State *done(const gchar *str); - -protected: - virtual State *got_file(const gchar *filename) = 0; +/** @ingroup states */ +#define TECO_DECLARE_STATE(NAME) \ + extern teco_state_t NAME - /* in cmdline.cpp */ - void process_edit_cmd(gchar key); -}; +/* in cmdline.c */ +gboolean teco_state_caseinsensitive_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gchar chr, GError **error); -class StateExpectDir : public StateExpectFile { -public: - StateExpectDir(bool _building = true, bool _last = true) - : StateExpectFile(_building, _last) {} +/** + * @interface TECO_DEFINE_STATE_CASEINSENSITIVE + * @implements TECO_DEFINE_STATE + * @ingroup states + * + * Base class of states with case-insenstive input. + * + * This is meant for states accepting command characters + * that can possibly be case-folded. + */ +#define TECO_DEFINE_STATE_CASEINSENSITIVE(NAME, ...) \ + TECO_DEFINE_STATE(NAME, \ + .process_edit_cmd_cb = teco_state_caseinsensitive_process_edit_cmd, \ + ##__VA_ARGS__ \ + ) -protected: - /* in cmdline.cpp */ - void process_edit_cmd(gchar key); +/** + * Base class of state machine. + * + * @note On extending teco_machine_t: + * There is `-fplan9-extensions`, but Clang doesn't support it. + * There is `-fms-extensions`, but that would require type-unsafe + * casting to teco_machine_t*. + * It's possible to portably implement typesafe inheritance by using + * an anonymous union of an anonymous struct and a named struct, but it's + * not really worth the trouble in our flat "class" hierachy. + */ +struct teco_machine_t { + teco_state_t *current; + /** + * Whether side effects must be reverted on rubout. + * State machines created within macro calls don't have to + * even in interactive mode. + */ + gboolean must_undo; }; -class StateStart : public StateCaseInsensitive { -public: - StateStart(); +static inline void +teco_machine_init(teco_machine_t *ctx, teco_state_t *initial, gboolean must_undo) +{ + ctx->current = initial; + ctx->must_undo = must_undo; +} -private: - void insert_integer(tecoInt v); - tecoInt read_integer(void); +static inline void +teco_machine_reset(teco_machine_t *ctx, teco_state_t *initial) +{ + if (ctx->current != initial) + teco_undo_ptr(ctx->current) = initial; +} - tecoBool move_chars(tecoInt n); - tecoBool move_lines(tecoInt n); +gboolean teco_machine_input(teco_machine_t *ctx, gchar chr, GError **error); - tecoBool delete_words(tecoInt n); +typedef enum { + TECO_STRINGBUILDING_MODE_NORMAL = 0, + TECO_STRINGBUILDING_MODE_UPPER, + TECO_STRINGBUILDING_MODE_LOWER +} teco_stringbuilding_mode_t; - State *custom(gchar chr); +/** + * A stringbuilding state machine. + * + * @fixme Should contain the escape char (currently in teco_machine_expectstring_t), + * so that we can escape it via ^Q. + * + * @extends teco_machine_t + */ +typedef struct teco_machine_stringbuilding_t { + teco_machine_t parent; - void end_of_macro(void) {} + /** + * A teco_stringbuilding_mode_t. + * This is still a guint, so you can call teco_undo_guint(). + */ + guint mode; - fnmacroMask - get_fnmacro_mask(void) const - { - return FNMACRO_MASK_START; - } -}; + /** + * The escape/termination character. + * + * If this is `[` or `{`, it is assumed that `]` and `}` must + * be escaped as well by teco_machine_stringbuilding_escape(). + */ + gchar escape_char; -class StateControl : public StateCaseInsensitive { -public: - StateControl(); + /** + * Q-Register table for local registers. + * This is stored here only to be passed to the Q-Reg spec machine. + */ + teco_qreg_table_t *qreg_table_locals; -private: - State *custom(gchar chr); -}; + /** + * A QRegister specification parser. + * It is allocated since it in turn contains a string building machine. + */ + teco_machine_qregspec_t *machine_qregspec; -class StateASCII : public State { -public: - StateASCII(); + /** + * A string to append characters to or NULL in parse-only mode. + * + * @bug As a side-effect, rubbing out in parse-only mode is severely limited + * (see teco_state_stringbuilding_start_process_edit_cmd()). + */ + teco_string_t *result; +} teco_machine_stringbuilding_t; -private: - State *custom(gchar chr); -}; +void teco_machine_stringbuilding_init(teco_machine_stringbuilding_t *ctx, gchar escape_char, + teco_qreg_table_t *locals, gboolean must_undo); -class StateEscape : public StateCaseInsensitive { -public: - StateEscape(); +void teco_machine_stringbuilding_reset(teco_machine_stringbuilding_t *ctx); -private: - State *custom(gchar chr); +/** + * Parse a string building character. + * + * @param ctx The string building machine. + * @param chr The character to parse. + * @param result String to append characters to or NULL in parse-only mode. + * @param error GError. + * @return FALSE in case of error. + */ +static inline gboolean +teco_machine_stringbuilding_input(teco_machine_stringbuilding_t *ctx, gchar chr, + teco_string_t *result, GError **error) +{ + ctx->result = result; + return teco_machine_input(&ctx->parent, chr, error); +} - void end_of_macro(void); +void teco_machine_stringbuilding_escape(teco_machine_stringbuilding_t *ctx, const gchar *str, gsize len, + teco_string_t *target); - /* - * The state should behave like StateStart - * when it comes to function key macro masking. - */ - fnmacroMask - get_fnmacro_mask(void) const - { - return FNMACRO_MASK_START; - } -}; +void teco_machine_stringbuilding_clear(teco_machine_stringbuilding_t *ctx); -class StateFCommand : public StateCaseInsensitive { -public: - StateFCommand(); +/** + * Peristent state for teco_state_expectstring_input(). + * + * This is part of the main machine instead of being a global variable, + * so that parsers can be run in parallel. + * + * Since it will also be part of a macro invocation frame, it will allow + * for tricks like macro-hooks while in "expectstring" states or calling + * macros as part of string building characters or macro string arguments. + */ +typedef struct { + teco_string_t string; + gsize insert_len; + gint nesting; -private: - State *custom(gchar chr); -}; + teco_machine_stringbuilding_t machine; +} teco_machine_expectstring_t; -class UndoTokenChangeDir : public UndoToken { - gchar *dir; +/** + * Scintilla message for collection by ES commands. + * + * @fixme This is a "forward" declaration, so that we don't introduce cyclic + * header dependencies. + * Could presumably be avoided by splitting parser.h in two. + */ +typedef struct { + unsigned int iMessage; + uptr_t wParam; + sptr_t lParam; +} teco_machine_scintilla_t; + +typedef enum { + /** Normal parsing - ie. execute while parsing */ + TECO_MODE_NORMAL = 0, + /** Parse, but don't execute until reaching not-yet-defined Goto-label */ + TECO_MODE_PARSE_ONLY_GOTO, + /** Parse, but don't execute until reaching end of loop */ + TECO_MODE_PARSE_ONLY_LOOP, + /** Parse, but don't execute until reaching end of conditional or its else-clause */ + TECO_MODE_PARSE_ONLY_COND, + /** Parse, but don't execute until reaching the very end of conditional */ + TECO_MODE_PARSE_ONLY_COND_FORCE +} teco_mode_t; + +/** @extends teco_machine_t */ +struct teco_machine_main_t { + teco_machine_t parent; + + gint macro_pc; -public: /** - * Construct undo token. - * - * This passes ownership of the directory string - * to the undo token object. + * Aliases bitfield with an integer. + * This allows teco_undo_guint(__flags), + * while still supporting easy-to-access flags. */ - UndoTokenChangeDir(gchar *_dir) : dir(_dir) {} - ~UndoTokenChangeDir() - { - g_free(dir); - } - - void run(void); -}; + union { + struct { + teco_mode_t mode : 8; + + gboolean modifier_colon : 1; + gboolean modifier_at : 1; + }; + guint __flags; + }; -class StateChangeDir : public StateExpectDir { -private: - State *got_file(const gchar *filename); -}; + /** The nesting level of braces */ + guint brace_level; + /** The nesting level of loops and control structures */ + gint nest_level; + /** + * Loop frame pointer: The number of elements on + * the loop stack when a macro invocation frame is + * created. + * This is used to perform checks for flow control + * commands to avoid jumping with invalid PCs while + * not creating a new stack per macro frame. + */ + guint loop_stack_fp; -class StateCondCommand : public StateCaseInsensitive { -public: - StateCondCommand(); + teco_goto_table_t goto_table; + teco_qreg_table_t *qreg_table_locals; -private: - State *custom(gchar chr); + /* + * teco_state_t-dependent state. + * + * Some of these cannot be used concurrently and are therefore + * grouped into unions. + * We could further optimize memory usage by dynamically allocating + * some of these structures on demand. + */ + teco_machine_expectstring_t expectstring; + union { + teco_string_t goto_label; + teco_machine_qregspec_t *expectqreg; + teco_machine_scintilla_t scintilla; + }; }; -class StateECommand : public StateCaseInsensitive { -public: - StateECommand(); - -private: - State *custom(gchar chr); -}; +void teco_machine_main_init(teco_machine_main_t *ctx, + teco_qreg_table_t *qreg_table_locals, + gboolean must_undo); -class StateScintilla_symbols : public StateExpectString { -public: - StateScintilla_symbols() : StateExpectString(true, false) {} +gboolean teco_machine_main_eval_colon(teco_machine_main_t *ctx); -private: - State *done(const gchar *str); +gboolean teco_machine_main_step(teco_machine_main_t *ctx, + const gchar *macro, gint stop_pos, GError **error); -protected: - /* in cmdline.cpp */ - void process_edit_cmd(gchar key); -}; +gboolean teco_execute_macro(const gchar *macro, gsize macro_len, + teco_qreg_table_t *qreg_table_locals, GError **error); +gboolean teco_execute_file(const gchar *filename, teco_qreg_table_t *qreg_table_locals, GError **error); -class StateScintilla_lParam : public StateExpectString { -private: - State *done(const gchar *str); -}; +typedef const struct { + teco_state_t *next; + void (*transition_cb)(teco_machine_main_t *ctx, GError **error); +} teco_machine_main_transition_t; /* - * also serves as base class for replace-insertion states + * FIXME: There should probably be a teco_state_plain with + * the transitions and their length being stored in + * teco_state_t::transitions. + * This does not exclude the possibility of overwriting input_cb. */ -class StateInsert : public StateExpectString { -public: - StateInsert(bool building = true) - : StateExpectString(building) {} - -protected: - void initial(void); - void process(const gchar *str, gint new_chars); - State *done(const gchar *str); - - /* in cmdline.cpp */ - void process_edit_cmd(gchar key); -}; - -class StateInsertIndent : public StateInsert { -protected: - void initial(void); -}; - -namespace States { - extern StateStart start; - extern StateControl control; - extern StateASCII ascii; - extern StateEscape escape; - extern StateFCommand fcommand; - extern StateChangeDir changedir; - extern StateCondCommand condcommand; - extern StateECommand ecommand; - extern StateScintilla_symbols scintilla_symbols; - extern StateScintilla_lParam scintilla_lparam; - extern StateInsert insert_building; - extern StateInsert insert_nobuilding; - extern StateInsertIndent insert_indent; - - extern State *current; - - static inline bool - is_start(void) - { - /* - * StateEscape should behave very much like StateStart. - */ - return current == &start || current == &escape; - } -} +teco_state_t *teco_machine_main_transition_input(teco_machine_main_t *ctx, + teco_machine_main_transition_t *transitions, + guint len, gchar chr, GError **error); -extern enum Mode { - MODE_NORMAL = 0, - MODE_PARSE_ONLY_GOTO, - MODE_PARSE_ONLY_LOOP, - MODE_PARSE_ONLY_COND -} mode; +void teco_machine_main_clear(teco_machine_main_t *ctx); -#define BEGIN_EXEC(STATE) G_STMT_START { \ - if (mode > MODE_NORMAL) \ - return STATE; \ -} G_STMT_END +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_machine_main_t, teco_machine_main_clear); -extern gint macro_pc; +teco_state_t *teco_state_expectstring_input(teco_machine_main_t *ctx, gchar chr, GError **error); +gboolean teco_state_expectstring_refresh(teco_machine_main_t *ctx, GError **error); -extern gchar *strings[2]; -extern gchar escape_char; +/* in cmdline.c */ +gboolean teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error); -struct LoopContext { - /** how many iterations are left */ - tecoInt counter; - /** Program counter of loop start command */ - guint pc : sizeof(guint)*8 - 1; - /** - * Whether the loop represents an argument - * barrier or not (it "passes through" - * stack arguments). - * - * Since the program counter is usually - * a signed integer, it's ok steal one - * bit for the pass_through flag. - */ - bool pass_through : 1; -}; -typedef ValueStack LoopStack; -extern LoopStack loop_stack; +/** + * @interface TECO_DEFINE_STATE_EXPECTSTRING + * @implements TECO_DEFINE_STATE + * @ingroup states + * + * Super-class for states accepting string arguments + * Opaquely cares about alternative-escape characters, + * string building commands and accumulation into a string + * + * @note Generating the input_cb could be avoided if there were a default + * implementation. + */ +#define TECO_DEFINE_STATE_EXPECTSTRING(NAME, ...) \ + static teco_state_t * \ + NAME##_input(teco_machine_main_t *ctx, gchar chr, GError **error) \ + { \ + return teco_state_expectstring_input(ctx, chr, error); \ + } \ + TECO_DEFINE_STATE(NAME, \ + .refresh_cb = (teco_state_refresh_cb_t)teco_state_expectstring_refresh, \ + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ + teco_state_expectstring_process_edit_cmd, \ + .fnmacro_mask = TECO_FNMACRO_MASK_STRING, \ + .expectstring.string_building = TRUE, \ + .expectstring.last = TRUE, \ + .expectstring.process_cb = NULL, /* do nothing */ \ + .expectstring.done_cb = NAME##_done, /* always required */ \ + ##__VA_ARGS__ \ + ) + +gboolean teco_state_expectfile_process(teco_machine_main_t *ctx, const teco_string_t *str, + gsize new_chars, GError **error); + +/* in cmdline.c */ +gboolean teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error); -namespace Execute { - void step(const gchar *macro, gint stop_pos); - void macro(const gchar *macro, bool locals = true); - void file(const gchar *filename, bool locals = true); -} +/** + * @interface TECO_DEFINE_STATE_EXPECTFILE + * @implements TECO_DEFINE_STATE_EXPECTSTRING + * @ingroup states + */ +#define TECO_DEFINE_STATE_EXPECTFILE(NAME, ...) \ + TECO_DEFINE_STATE_EXPECTSTRING(NAME, \ + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ + teco_state_expectfile_process_edit_cmd, \ + .expectstring.process_cb = teco_state_expectfile_process, \ + ##__VA_ARGS__ \ + ) -} /* namespace SciTECO */ +/* in cmdline.c */ +gboolean teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error); -#endif +/** + * @interface TECO_DEFINE_STATE_EXPECTDIR + * @implements TECO_DEFINE_STATE_EXPECTFILE + * @ingroup states + */ +#define TECO_DEFINE_STATE_EXPECTDIR(NAME, ...) \ + TECO_DEFINE_STATE_EXPECTFILE(NAME, \ + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ + teco_state_expectdir_process_edit_cmd, \ + ##__VA_ARGS__ \ + ) diff --git a/src/qreg-commands.c b/src/qreg-commands.c new file mode 100644 index 0000000..35508d7 --- /dev/null +++ b/src/qreg-commands.c @@ -0,0 +1,760 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include + +#include "sciteco.h" +#include "error.h" +#include "file-utils.h" +#include "expressions.h" +#include "interface.h" +#include "ring.h" +#include "parser.h" +#include "core-commands.h" +#include "qreg.h" +#include "qreg-commands.h" + +gboolean +teco_state_expectqreg_initial(teco_machine_main_t *ctx, GError **error) +{ + teco_state_t *current = ctx->parent.current; + + /* + * NOTE: We have to allocate a new instance always since `expectqreg` + * is part of an union. + */ + ctx->expectqreg = teco_machine_qregspec_new(current->expectqreg.type, ctx->qreg_table_locals, + ctx->parent.must_undo); + if (ctx->parent.must_undo) + undo__teco_machine_qregspec_free(ctx->expectqreg); + return TRUE; +} + +teco_state_t * +teco_state_expectqreg_input(teco_machine_main_t *ctx, gchar chr, GError **error) +{ + teco_state_t *current = ctx->parent.current; + + teco_qreg_t *qreg; + teco_qreg_table_t *table; + + switch (teco_machine_qregspec_input(ctx->expectqreg, chr, + ctx->mode == TECO_MODE_NORMAL ? &qreg : NULL, &table, error)) { + case TECO_MACHINE_QREGSPEC_ERROR: + return NULL; + case TECO_MACHINE_QREGSPEC_MORE: + return current; + case TECO_MACHINE_QREGSPEC_DONE: + break; + } + + /* + * NOTE: ctx->expectqreg is preserved since we may want to query it from follow-up + * states. This means, it must usually be stored manually in got_register_cb() via: + * teco_state_expectqreg_reset(ctx); + */ + return current->expectqreg.got_register_cb(ctx, qreg, table, error); +} + +static teco_state_t * +teco_state_pushqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + teco_state_expectqreg_reset(ctx); + + return ctx->mode == TECO_MODE_NORMAL && + !teco_qreg_stack_push(qreg, error) ? NULL : &teco_state_start; +} + +/*$ "[" "[q" push + * [q -- Save Q-Register + * + * Save Q-Register contents on the global Q-Register push-down + * stack. + */ +TECO_DEFINE_STATE_EXPECTQREG(teco_state_pushqreg); + +static teco_state_t * +teco_state_popqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + teco_state_expectqreg_reset(ctx); + + return ctx->mode == TECO_MODE_NORMAL && + !teco_qreg_stack_pop(qreg, error) ? NULL : &teco_state_start; +} + +/*$ "]" "]q" pop + * ]q -- Restore Q-Register + * + * Restore Q-Register by replacing its contents + * with the contents of the register saved on top of + * the Q-Register push-down stack. + * The stack entry is popped. + * + * In interactive mode, the original contents of + * are not immediately reclaimed but are kept in memory + * to support rubbing out the command. + * Memory is reclaimed on command-line termination. + */ +TECO_DEFINE_STATE_EXPECTQREG(teco_state_popqreg, + .expectqreg.type = TECO_QREG_OPTIONAL_INIT +); + +static teco_state_t * +teco_state_eqcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + /* + * NOTE: We will query ctx->expectqreg later in teco_state_loadqreg_done(). + */ + return &teco_state_loadqreg; +} + +TECO_DEFINE_STATE_EXPECTQREG(teco_state_eqcommand, + .expectqreg.type = TECO_QREG_OPTIONAL_INIT +); + +static teco_state_t * +teco_state_loadqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + teco_qreg_t *qreg; + teco_qreg_table_t *table; + + teco_machine_qregspec_get_results(ctx->expectqreg, &qreg, &table); + teco_state_expectqreg_reset(ctx); + + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + if (str->len > 0) { + /* Load file into Q-Register */ + g_autofree gchar *filename = teco_file_expand_path(str->data); + if (!teco_qreg_load(qreg, filename, error)) + return NULL; + } else { + /* Edit Q-Register */ + if (!teco_current_doc_undo_edit(error) || + !teco_qreg_table_edit(table, qreg, error)) + return NULL; + } + + return &teco_state_start; +} + +/*$ EQ EQq + * EQq$ -- Edit or load Q-Register + * EQq[file]$ + * + * When specified with an empty string argument, + * EQ makes the currently edited Q-Register. + * Otherwise, when is specified, it is the + * name of a file to read into Q-Register . + * When loading a file, the currently edited + * buffer/register is not changed and the edit position + * of register is reset to 0. + * + * Undefined Q-Registers will be defined. + * The command fails if could not be read. + */ +TECO_DEFINE_STATE_EXPECTFILE(teco_state_loadqreg); + +static teco_state_t * +teco_state_epctcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + /* + * NOTE: We will query ctx->expectqreg later in teco_state_saveqreg_done(). + */ + return &teco_state_saveqreg; +} + +TECO_DEFINE_STATE_EXPECTQREG(teco_state_epctcommand); + +static teco_state_t * +teco_state_saveqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + teco_qreg_t *qreg; + + teco_machine_qregspec_get_results(ctx->expectqreg, &qreg, NULL); + teco_state_expectqreg_reset(ctx); + + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + g_autofree gchar *filename = teco_file_expand_path(str->data); + return teco_qreg_save(qreg, filename, error) ? &teco_state_start : NULL; +} + +/*$ E% E%q + * E%q$ -- Save Q-Register string to file + * + * Saves the string contents of Q-Register to + * . + * The must always be specified, as Q-Registers + * have no notion of associated file names. + * + * In interactive mode, the E% command may be rubbed out, + * restoring the previous state of . + * This follows the same rules as with the \fBEW\fP command. + * + * File names may also be tab-completed and string building + * characters are enabled by default. + */ +TECO_DEFINE_STATE_EXPECTFILE(teco_state_saveqreg); + +static gboolean +teco_state_queryqreg_initial(teco_machine_main_t *ctx, GError **error) +{ + /* + * This prevents teco_state_queryqreg_got_register() from having to check + * for Q-Register existence, resulting in better error messages in case of + * required Q-Registers. + * In parse-only mode, the type does not matter. + */ + teco_qreg_type_t type = ctx->modifier_colon ? TECO_QREG_OPTIONAL : TECO_QREG_REQUIRED; + + /* + * NOTE: We have to allocate a new instance always since `expectqreg` + * is part of an union. + */ + ctx->expectqreg = teco_machine_qregspec_new(type, ctx->qreg_table_locals, + ctx->parent.must_undo); + if (ctx->parent.must_undo) + undo__teco_machine_qregspec_free(ctx->expectqreg); + return TRUE; +} + +static teco_state_t * +teco_state_queryqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + teco_state_expectqreg_reset(ctx); + + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + if (!teco_expressions_eval(FALSE, error)) + return NULL; + + if (teco_machine_main_eval_colon(ctx)) { + /* Query Q-Register's existence or string size */ + if (qreg) { + gsize len; + + if (!qreg->vtable->get_string(qreg, NULL, &len, error)) + return NULL; + teco_expressions_push(len); + } else { + teco_expressions_push(-1); + } + + return &teco_state_start; + } + + if (teco_expressions_args() > 0) { + /* Query character from Q-Register string */ + teco_int_t pos; + if (!teco_expressions_pop_num_calc(&pos, 0, error)) + return NULL; + if (pos < 0) { + teco_error_range_set(error, "Q"); + return NULL; + } + + gint c = qreg->vtable->get_character(qreg, pos, error); + if (c < 0) + return NULL; + + teco_expressions_push(c); + } else { + /* Query integer */ + teco_int_t value; + + if (!qreg->vtable->get_integer(qreg, &value, error)) + return NULL; + teco_expressions_push(value); + } + + return &teco_state_start; +} + +/*$ Q Qq query + * Qq -> n -- Query Q-Register existence, its integer or string characters + * Qq -> character + * :Qq -> -1 | size + * + * Without any arguments, get and return the integer-part of + * Q-Register . + * + * With one argument, return the code at + * from the string-part of Q-Register . + * Positions are handled like buffer positions \(em they + * begin at 0 up to the length of the string minus 1. + * An error is thrown for invalid positions. + * Both non-colon-modified forms of Q require register + * to be defined and fail otherwise. + * + * When colon-modified, Q does not pop any arguments from + * the expression stack and returns the of the string + * in Q-Register if register exists (i.e. is defined). + * Naturally, for empty strings, 0 is returned. + * When colon-modified and Q-Register is undefined, + * -1 is returned instead. + * Therefore checking the return value \fB:Q\fP for values smaller + * 0 allows checking the existence of a register. + * Note that if exists, its string part is not initialized, + * so \fB:Q\fP may be used to handle purely numeric data structures + * without creating Scintilla documents by accident. + * These semantics allow the useful idiom \(lq:Q\fIq\fP">\(rq for + * checking whether a Q-Register exists and has a non-empty string. + * Note also that the return value of \fB:Q\fP may be interpreted + * as a condition boolean that represents the non-existence of . + * If is undefined, it returns \fIsuccess\fP, else a \fIfailure\fP + * boolean. + */ +TECO_DEFINE_STATE_EXPECTQREG(teco_state_queryqreg, + .initial_cb = (teco_state_initial_cb_t)teco_state_queryqreg_initial +); + +static teco_state_t * +teco_state_ctlucommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + /* + * NOTE: We will query ctx->expectqreg later in teco_state_setqregstring_nobuilding_done(). + */ + return &teco_state_setqregstring_nobuilding; +} + +TECO_DEFINE_STATE_EXPECTQREG(teco_state_ctlucommand, + .expectqreg.type = TECO_QREG_OPTIONAL_INIT +); + +static teco_state_t * +teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx, + const teco_string_t *str, GError **error) +{ + teco_qreg_t *qreg; + + teco_machine_qregspec_get_results(ctx->expectqreg, &qreg, NULL); + teco_state_expectqreg_reset(ctx); + + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + gboolean colon_modified = teco_machine_main_eval_colon(ctx); + + if (!teco_expressions_eval(FALSE, error)) + return NULL; + gint args = teco_expressions_args(); + + if (args > 0) { + g_autofree gchar *buffer = g_malloc(args); + + for (gint i = args; i > 0; i--) { + teco_int_t v; + if (!teco_expressions_pop_num_calc(&v, 0, error)) + return NULL; + buffer[i-1] = (gchar)v; + } + + if (colon_modified) { + /* append to register */ + if (!qreg->vtable->undo_append_string(qreg, error) || + !qreg->vtable->append_string(qreg, buffer, args, error)) + return NULL; + } else { + /* set register */ + if (!qreg->vtable->undo_set_string(qreg, error) || + !qreg->vtable->set_string(qreg, buffer, args, error)) + return NULL; + } + } + + if (args > 0 || colon_modified) { + /* append to register */ + if (!qreg->vtable->undo_append_string(qreg, error) || + !qreg->vtable->append_string(qreg, str->data, str->len, error)) + return NULL; + } else { + /* set register */ + if (!qreg->vtable->undo_set_string(qreg, error) || + !qreg->vtable->set_string(qreg, str->data, str->len, error)) + return NULL; + } + + return &teco_state_start; +} + +/*$ ^Uq + * [c1,c2,...]^Uq[string]$ -- Set or append to Q-Register string without string building + * [c1,c2,...]:^Uq[string]$ + * + * If not colon-modified, it first fills the Q-Register + * with all the values on the expression stack (interpreted as + * codepoints). + * It does so in the order of the arguments, i.e. + * will be the first character in , the second, etc. + * Eventually the argument is appended to the + * register. + * Any existing string value in is overwritten by this operation. + * + * In the colon-modified form ^U does not overwrite existing + * contents of but only appends to it. + * + * If is undefined, it will be defined. + * + * String-building characters are \fBdisabled\fP for ^U + * commands. + * Therefore they are especially well-suited for defining + * \*(ST macros, since string building characters in the + * desired Q-Register contents do not have to be escaped. + * The \fBEU\fP command may be used where string building + * is desired. + */ +TECO_DEFINE_STATE_EXPECTSTRING(teco_state_setqregstring_nobuilding, + .expectstring.string_building = FALSE +); + +static teco_state_t * +teco_state_eucommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + /* + * NOTE: We will query ctx->expectqreg later in teco_state_setqregstring_building_done(). + */ + return &teco_state_setqregstring_building; +} + +TECO_DEFINE_STATE_EXPECTQREG(teco_state_eucommand, + .expectqreg.type = TECO_QREG_OPTIONAL_INIT +); + +static teco_state_t * +teco_state_setqregstring_building_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + return teco_state_setqregstring_nobuilding_done(ctx, str, error); +} + +/*$ EU EUq + * [c1,c2,...]EUq[string]$ -- Set or append to Q-Register string with string building characters + * [c1,c2,...]:EUq[string]$ + * + * This command sets or appends to the contents of + * Q-Register \fIq\fP. + * It is identical to the \fB^U\fP command, except + * that this form of the command has string building + * characters \fBenabled\fP. + */ +TECO_DEFINE_STATE_EXPECTSTRING(teco_state_setqregstring_building, + .expectstring.string_building = TRUE +); + +static teco_state_t * +teco_state_getqregstring_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + teco_state_expectqreg_reset(ctx); + + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + g_auto(teco_string_t) str = {NULL, 0}; + + if (!qreg->vtable->get_string(qreg, &str.data, &str.len, error)) + return NULL; + + if (str.len > 0) { + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + teco_interface_ssm(SCI_ADDTEXT, str.len, (sptr_t)str.data); + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + teco_ring_dirtify(); + + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + } + + return &teco_state_start; +} + +/*$ G Gq get + * Gq -- Insert Q-Register string + * + * Inserts the string of Q-Register into the buffer + * at its current position. + * Specifying an undefined yields an error. + */ +TECO_DEFINE_STATE_EXPECTQREG(teco_state_getqregstring); + +static teco_state_t * +teco_state_setqreginteger_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + teco_state_expectqreg_reset(ctx); + + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + if (!teco_expressions_eval(FALSE, error)) + return NULL; + if (teco_expressions_args() || teco_num_sign < 0) { + teco_int_t v; + if (!teco_expressions_pop_num_calc(&v, 0, error) || + !qreg->vtable->undo_set_integer(qreg, error) || + !qreg->vtable->set_integer(qreg, v, error)) + return NULL; + + if (teco_machine_main_eval_colon(ctx)) + teco_expressions_push(TECO_SUCCESS); + } else if (teco_machine_main_eval_colon(ctx)) { + teco_expressions_push(TECO_FAILURE); + } else { + teco_error_argexpected_set(error, "U"); + return NULL; + } + + return &teco_state_start; +} + +/*$ U Uq + * nUq -- Set Q-Register integer + * -Uq + * [n]:Uq -> Success|Failure + * + * Sets the integer-part of Q-Register to . + * \(lq-U\(rq is equivalent to \(lq-1U\(rq, otherwise + * the command fails if is missing. + * + * If the command is colon-modified, it returns a success + * boolean if or \(lq-\(rq is given. + * Otherwise it returns a failure boolean and does not + * modify . + * + * The register is defined if it does not exist. + */ +TECO_DEFINE_STATE_EXPECTQREG(teco_state_setqreginteger, + .expectqreg.type = TECO_QREG_OPTIONAL_INIT +); + +static teco_state_t * +teco_state_increaseqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + teco_state_expectqreg_reset(ctx); + + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + teco_int_t value, add; + + if (!qreg->vtable->undo_set_integer(qreg, error) || + !qreg->vtable->get_integer(qreg, &value, error) || + !teco_expressions_pop_num_calc(&add, teco_num_sign, error) || + !qreg->vtable->set_integer(qreg, value += add, error)) + return NULL; + teco_expressions_push(value); + + return &teco_state_start; +} + +/*$ % %q increment + * [n]%q -> q+n -- Increase Q-Register integer + * + * Add to the integer part of register , returning + * its new value. + * will be defined if it does not exist. + */ +TECO_DEFINE_STATE_EXPECTQREG(teco_state_increaseqreg, + .expectqreg.type = TECO_QREG_OPTIONAL_INIT +); + +static teco_state_t * +teco_state_macro_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + teco_state_expectqreg_reset(ctx); + + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + if (teco_machine_main_eval_colon(ctx)) { + /* don't create new local Q-Registers if colon modifier is given */ + if (!teco_qreg_execute(qreg, ctx->qreg_table_locals, error)) + return NULL; + } else { + g_auto(teco_qreg_table_t) table; + teco_qreg_table_init(&table, FALSE); + if (!teco_qreg_execute(qreg, &table, error)) + return NULL; + } + + return &teco_state_start; +} + +/*$ M Mq eval + * Mq -- Execute macro + * :Mq + * + * Execute macro stored in string of Q-Register . + * The command itself does not push or pop and arguments from the stack + * but the macro executed might well do so. + * The new macro invocation level will contain its own go-to label table + * and local Q-Register table. + * Except when the command is colon-modified - in this case, local + * Q-Registers referenced in the macro refer to the parent macro-level's + * local Q-Register table (or whatever level defined one last). + * + * Errors during the macro execution will propagate to the M command. + * In other words if a command in the macro fails, the M command will fail + * and this failure propagates until the top-level macro (e.g. + * the command-line macro). + * + * Note that the string of will be copied upon macro execution, + * so subsequent changes to Q-Register from inside the macro do + * not modify the executed code. + */ +TECO_DEFINE_STATE_EXPECTQREG(teco_state_macro); + +static teco_state_t * +teco_state_macrofile_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + g_autofree gchar *filename = teco_file_expand_path(str->data); + + if (teco_machine_main_eval_colon(ctx)) { + /* don't create new local Q-Registers if colon modifier is given */ + if (!teco_execute_file(filename, ctx->qreg_table_locals, error)) + return NULL; + } else { + g_auto(teco_qreg_table_t) table; + teco_qreg_table_init(&table, FALSE); + if (!teco_execute_file(filename, &table, error)) + return NULL; + } + + return &teco_state_start; +} + +/*$ EM + * EMfile$ -- Execute macro from file + * :EMfile$ + * + * Read the file with name into memory and execute its contents + * as a macro. + * It is otherwise similar to the \(lqM\(rq command. + * + * If could not be read, the command yields an error. + */ +TECO_DEFINE_STATE_EXPECTFILE(teco_state_macrofile); + +static teco_state_t * +teco_state_copytoqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + teco_state_expectqreg_reset(ctx); + + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + teco_int_t from, len; + + if (!teco_expressions_eval(FALSE, error)) + return NULL; + if (teco_expressions_args() <= 1) { + teco_int_t line; + + from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + if (!teco_expressions_pop_num_calc(&line, teco_num_sign, error)) + return NULL; + line += teco_interface_ssm(SCI_LINEFROMPOSITION, from, 0); + + if (!teco_validate_line(line)) { + teco_error_range_set(error, "X"); + return NULL; + } + + len = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0) - from; + + if (len < 0) { + from += len; + len *= -1; + } + } else { + teco_int_t to = teco_expressions_pop_num(0); + from = teco_expressions_pop_num(0); + + len = to - from; + + if (len < 0 || !teco_validate_pos(from) || !teco_validate_pos(to)) { + teco_error_range_set(error, "X"); + return NULL; + } + } + + g_autofree gchar *str = g_malloc(len + 1); + + struct Sci_TextRange text_range = { + .chrg = {.cpMin = from, .cpMax = from + len}, + .lpstrText = str + }; + teco_interface_ssm(SCI_GETTEXTRANGE, 0, (sptr_t)&text_range); + + if (teco_machine_main_eval_colon(ctx)) { + if (!qreg->vtable->undo_append_string(qreg, error) || + !qreg->vtable->append_string(qreg, str, len, error)) + return NULL; + } else { + if (!qreg->vtable->undo_set_string(qreg, error) || + !qreg->vtable->set_string(qreg, str, len, error)) + return NULL; + } + + return &teco_state_start; +} + +/*$ X Xq + * [lines]Xq -- Copy into or append to Q-Register + * -Xq + * from,toXq + * [lines]:Xq + * -:Xq + * from,to:Xq + * + * Copy the next or previous number of from the buffer + * into the Q-Register string. + * If is omitted, the sign prefix is implied. + * If two arguments are specified, the characters beginning + * at position up to the character at position + * are copied. + * The semantics of the arguments is analogous to the K + * command's arguments. + * If the command is colon-modified, the characters will be + * appended to the end of register instead. + * + * Register will be created if it is undefined. + */ +TECO_DEFINE_STATE_EXPECTQREG(teco_state_copytoqreg, + .expectqreg.type = TECO_QREG_OPTIONAL_INIT +); diff --git a/src/qreg-commands.h b/src/qreg-commands.h new file mode 100644 index 0000000..e91b19a --- /dev/null +++ b/src/qreg-commands.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#pragma once + +#include + +#include "sciteco.h" +#include "parser.h" +#include "qreg.h" + +static inline void +teco_state_expectqreg_reset(teco_machine_main_t *ctx) +{ + if (ctx->parent.must_undo) + teco_undo_qregspec_own(ctx->expectqreg); + else + teco_machine_qregspec_free(ctx->expectqreg); +} + +gboolean teco_state_expectqreg_initial(teco_machine_main_t *ctx, GError **error); + +teco_state_t *teco_state_expectqreg_input(teco_machine_main_t *ctx, gchar chr, GError **error); + +/* in cmdline.c */ +gboolean teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error); + +/** + * @interface TECO_DEFINE_STATE_EXPECTQREG + * @implements TECO_DEFINE_STATE + * @ingroup states + * + * Super class for states accepting Q-Register specifications. + */ +#define TECO_DEFINE_STATE_EXPECTQREG(NAME, ...) \ + static teco_state_t * \ + NAME##_input(teco_machine_main_t *ctx, gchar chr, GError **error) \ + { \ + return teco_state_expectqreg_input(ctx, chr, error); \ + } \ + TECO_DEFINE_STATE(NAME, \ + .initial_cb = (teco_state_initial_cb_t)teco_state_expectqreg_initial, \ + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ + teco_state_expectqreg_process_edit_cmd, \ + .expectqreg.type = TECO_QREG_REQUIRED, \ + .expectqreg.got_register_cb = NAME##_got_register, /* always required */ \ + ##__VA_ARGS__ \ + ) + +/* + * FIXME: Some of these states are referenced only in qreg-commands.c, + * so they should be moved there? + */ +TECO_DECLARE_STATE(teco_state_pushqreg); +TECO_DECLARE_STATE(teco_state_popqreg); + +TECO_DECLARE_STATE(teco_state_eqcommand); +TECO_DECLARE_STATE(teco_state_loadqreg); + +TECO_DECLARE_STATE(teco_state_epctcommand); +TECO_DECLARE_STATE(teco_state_saveqreg); + +TECO_DECLARE_STATE(teco_state_queryqreg); + +TECO_DECLARE_STATE(teco_state_ctlucommand); +TECO_DECLARE_STATE(teco_state_setqregstring_nobuilding); +TECO_DECLARE_STATE(teco_state_eucommand); +TECO_DECLARE_STATE(teco_state_setqregstring_building); + +TECO_DECLARE_STATE(teco_state_getqregstring); +TECO_DECLARE_STATE(teco_state_setqreginteger); +TECO_DECLARE_STATE(teco_state_increaseqreg); + +TECO_DECLARE_STATE(teco_state_macro); +TECO_DECLARE_STATE(teco_state_macrofile); + +TECO_DECLARE_STATE(teco_state_copytoqreg); diff --git a/src/qreg.c b/src/qreg.c new file mode 100644 index 0000000..5c39409 --- /dev/null +++ b/src/qreg.c @@ -0,0 +1,1542 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include + +#include + +#include "sciteco.h" +#include "string-utils.h" +#include "file-utils.h" +#include "interface.h" +#include "cmdline.h" +#include "view.h" +#include "undo.h" +#include "parser.h" +#include "core-commands.h" +#include "expressions.h" +#include "doc.h" +#include "ring.h" +#include "eol.h" +#include "error.h" +#include "qreg.h" + +/** + * View used for editing Q-Registers. + * Initialized in main.c after the interface. + */ +teco_view_t *teco_qreg_view = NULL; +/** Currently edited Q-Register */ +teco_qreg_t *teco_qreg_current = NULL; + +/** + * Table for global Q-Registers. + * Initialized in main.c after the interface. + */ +teco_qreg_table_t teco_qreg_table_globals; + +/** @private @static @memberof teco_qreg_t */ +static teco_qreg_t * +teco_qreg_new(teco_qreg_vtable_t *vtable, const gchar *name, gsize len) +{ + /* + * FIXME: Test with g_slice_new()... + * It could however cause problems upon command-line termination + * and may not be measurably faster. + */ + teco_qreg_t *qreg = g_new0(teco_qreg_t, 1); + qreg->vtable = vtable; + /* + * NOTE: This does not use GStringChunk/teco_string_init_chunk() + * since we want to implement Q-Register removing soon. + * Even without that, individual Q-Regs can be removed on rubout. + */ + teco_string_init(&qreg->head.name, name, len); + teco_doc_init(&qreg->string); + return qreg; +} + +/** @memberof teco_qreg_t */ +gboolean +teco_qreg_execute(teco_qreg_t *qreg, teco_qreg_table_t *qreg_table_locals, GError **error) +{ + g_auto(teco_string_t) macro = {NULL, 0}; + + if (!qreg->vtable->get_string(qreg, ¯o.data, ¯o.len, error) || + !teco_execute_macro(macro.data, macro.len, qreg_table_locals, error)) { + teco_error_add_frame_qreg(qreg->head.name.data, qreg->head.name.len); + return FALSE; + } + + return TRUE; +} + +/** @memberof teco_qreg_t */ +void +teco_qreg_undo_set_eol_mode(teco_qreg_t *qreg) +{ + if (!qreg->must_undo) + return; + + /* + * Necessary, so that upon rubout the + * string's parameters are restored. + */ + teco_doc_update(&qreg->string, teco_qreg_view); + + if (teco_qreg_current && teco_qreg_current->must_undo) // FIXME + teco_doc_undo_edit(&teco_qreg_current->string); + + undo__teco_view_ssm(teco_qreg_view, SCI_SETEOLMODE, + teco_view_ssm(teco_qreg_view, SCI_GETEOLMODE, 0, 0), 0); + + teco_doc_undo_edit(&qreg->string); +} + +/** @memberof teco_qreg_t */ +void +teco_qreg_set_eol_mode(teco_qreg_t *qreg, gint mode) +{ + if (teco_qreg_current) + teco_doc_update(&teco_qreg_current->string, teco_qreg_view); + + teco_doc_edit(&qreg->string); + teco_view_ssm(teco_qreg_view, SCI_SETEOLMODE, mode, 0); + + if (teco_qreg_current) + teco_doc_edit(&teco_qreg_current->string); +} + +/** @memberof teco_qreg_t */ +gboolean +teco_qreg_load(teco_qreg_t *qreg, const gchar *filename, GError **error) +{ + if (!qreg->vtable->undo_set_string(qreg, error)) + return FALSE; + + if (teco_qreg_current) + teco_doc_update(&teco_qreg_current->string, teco_qreg_view); + + teco_doc_edit(&qreg->string); + teco_doc_reset(&qreg->string); + + /* + * teco_view_load() might change the EOL style. + */ + teco_qreg_undo_set_eol_mode(qreg); + + /* + * undo_set_string() pushes undo tokens that restore + * the previous document in the view. + * So if loading fails, teco_qreg_current will be + * made the current document again. + */ + if (!teco_view_load(teco_qreg_view, filename, error)) + return FALSE; + + if (teco_qreg_current) + teco_doc_edit(&teco_qreg_current->string); + + return TRUE; +} + +/** @memberof teco_qreg_t */ +gboolean +teco_qreg_save(teco_qreg_t *qreg, const gchar *filename, GError **error) +{ + if (teco_qreg_current) + teco_doc_update(&teco_qreg_current->string, teco_qreg_view); + + teco_doc_edit(&qreg->string); + + if (!teco_view_save(teco_qreg_view, filename, error)) { + if (teco_qreg_current) + teco_doc_edit(&teco_qreg_current->string); + return FALSE; + } + + if (teco_qreg_current) + teco_doc_edit(&teco_qreg_current->string); + + return TRUE; +} + +static gboolean +teco_qreg_plain_set_integer(teco_qreg_t *qreg, teco_int_t value, GError **error) +{ + qreg->integer = value; + return TRUE; +} + +static gboolean +teco_qreg_plain_undo_set_integer(teco_qreg_t *qreg, GError **error) +{ + if (qreg->must_undo) // FIXME + teco_undo_int(qreg->integer); + return TRUE; +} + +static gboolean +teco_qreg_plain_get_integer(teco_qreg_t *qreg, teco_int_t *ret, GError **error) +{ + *ret = qreg->integer; + return TRUE; +} + +static gboolean +teco_qreg_plain_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error) +{ + teco_doc_set_string(&qreg->string, str, len); + return TRUE; +} + +static gboolean +teco_qreg_plain_undo_set_string(teco_qreg_t *qreg, GError **error) +{ + if (qreg->must_undo) // FIXME + teco_doc_undo_set_string(&qreg->string); + return TRUE; +} + +static gboolean +teco_qreg_plain_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error) +{ + /* + * NOTE: Will not create undo action if string is empty. + * Also, appending preserves the string's parameters. + */ + if (!len) + return TRUE; + + if (teco_qreg_current) + teco_doc_update(&teco_qreg_current->string, teco_qreg_view); + + teco_doc_edit(&qreg->string); + + teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0); + teco_view_ssm(teco_qreg_view, SCI_APPENDTEXT, len, (sptr_t)str); + teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0); + + if (teco_qreg_current) + teco_doc_edit(&teco_qreg_current->string); + return TRUE; +} + +static gboolean +teco_qreg_plain_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error) +{ + teco_doc_get_string(&qreg->string, str, len); + return TRUE; +} + +static gint +teco_qreg_plain_get_character(teco_qreg_t *qreg, guint position, GError **error) +{ + gint ret = -1; + + if (teco_qreg_current) + teco_doc_update(&teco_qreg_current->string, teco_qreg_view); + + teco_doc_edit(&qreg->string); + + if (position < teco_view_ssm(teco_qreg_view, SCI_GETLENGTH, 0, 0)) + ret = teco_view_ssm(teco_qreg_view, SCI_GETCHARAT, position, 0); + else + g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE, + "Position %u out of range", position); + /* make sure we still restore the current Q-Register */ + + if (teco_qreg_current) + teco_doc_edit(&teco_qreg_current->string); + + return ret; +} + +static gboolean +teco_qreg_plain_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error) +{ + teco_doc_exchange(&qreg->string, src); + return TRUE; +} + +static gboolean +teco_qreg_plain_undo_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error) +{ + if (qreg->must_undo) // FIXME + teco_doc_undo_exchange(&qreg->string); + teco_doc_undo_exchange(src); + return TRUE; +} + +static gboolean +teco_qreg_plain_edit(teco_qreg_t *qreg, GError **error) +{ + if (teco_qreg_current) + teco_doc_update(&teco_qreg_current->string, teco_qreg_view); + + teco_doc_edit(&qreg->string); + teco_interface_show_view(teco_qreg_view); + teco_interface_info_update(qreg); + + return TRUE; +} + +static gboolean +teco_qreg_plain_undo_edit(teco_qreg_t *qreg, GError **error) +{ + /* + * We might be switching the current document + * to a buffer. + */ + teco_doc_update(&qreg->string, teco_qreg_view); + + if (!qreg->must_undo) // FIXME + return TRUE; + + undo__teco_interface_info_update_qreg(qreg); + teco_doc_undo_edit(&qreg->string); + undo__teco_interface_show_view(teco_qreg_view); + return TRUE; +} + +#define TECO_INIT_QREG(...) { \ + .set_integer = teco_qreg_plain_set_integer, \ + .undo_set_integer = teco_qreg_plain_undo_set_integer, \ + .get_integer = teco_qreg_plain_get_integer, \ + .set_string = teco_qreg_plain_set_string, \ + .undo_set_string = teco_qreg_plain_undo_set_string, \ + .append_string = teco_qreg_plain_append_string, \ + .undo_append_string = teco_qreg_plain_undo_set_string, \ + .get_string = teco_qreg_plain_get_string, \ + .get_character = teco_qreg_plain_get_character, \ + .exchange_string = teco_qreg_plain_exchange_string, \ + .undo_exchange_string = teco_qreg_plain_undo_exchange_string, \ + .edit = teco_qreg_plain_edit, \ + .undo_edit = teco_qreg_plain_undo_edit, \ + ##__VA_ARGS__ \ +} + +/** @static @memberof teco_qreg_t */ +teco_qreg_t * +teco_qreg_plain_new(const gchar *name, gsize len) +{ + static teco_qreg_vtable_t vtable = TECO_INIT_QREG(); + + return teco_qreg_new(&vtable, name, len); +} + +/* + * NOTE: The integer-component is currently unused on the "*" special register. + */ +static gboolean +teco_qreg_bufferinfo_set_integer(teco_qreg_t *qreg, teco_int_t value, GError **error) +{ + return teco_ring_edit(value, error); +} + +static gboolean +teco_qreg_bufferinfo_undo_set_integer(teco_qreg_t *qreg, GError **error) +{ + return teco_current_doc_undo_edit(error); +} + +static gboolean +teco_qreg_bufferinfo_get_integer(teco_qreg_t *qreg, teco_int_t *ret, GError **error) +{ + *ret = teco_ring_get_id(teco_ring_current); + return TRUE; +} + +/* + * FIXME: These operations can and should be implemented. + * Setting the "*" register could for instance rename the file. + */ +static gboolean +teco_qreg_bufferinfo_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error) +{ + teco_error_qregopunsupported_set(error, qreg->head.name.data, qreg->head.name.len, FALSE); + return FALSE; +} + +static gboolean +teco_qreg_bufferinfo_undo_set_string(teco_qreg_t *qreg, GError **error) +{ + return TRUE; +} + +static gboolean +teco_qreg_bufferinfo_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error) +{ + teco_error_qregopunsupported_set(error, qreg->head.name.data, qreg->head.name.len, FALSE); + return FALSE; +} + +static gboolean +teco_qreg_bufferinfo_undo_append_string(teco_qreg_t *qreg, GError **error) +{ + return TRUE; +} + +/* + * NOTE: The `string` component is currently unused on the "*" register. + */ +static gboolean +teco_qreg_bufferinfo_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error) +{ + /* + * On platforms with a default non-forward-slash directory + * separator (i.e. Windows), Buffer::filename will have + * the wrong separator. + * To make the life of macros that evaluate "*" easier, + * the directory separators are normalized to "/" here. + */ + if (str) + *str = teco_file_normalize_path(g_strdup(teco_ring_current->filename ? : "")); + /* + * NOTE: teco_file_normalize_path() does not change the size of the string. + */ + *len = teco_ring_current->filename ? strlen(teco_ring_current->filename) : 0; + return TRUE; +} + +static gint +teco_qreg_bufferinfo_get_character(teco_qreg_t *qreg, guint position, GError **error) +{ + gsize max_len; + + if (!teco_qreg_bufferinfo_get_string(qreg, NULL, &max_len, error)) + return -1; + + if (position >= max_len) { + g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE, + "Position %u out of range", position); + return -1; + } + + return teco_ring_current->filename[position]; +} + +static gboolean +teco_qreg_bufferinfo_edit(teco_qreg_t *qreg, GError **error) +{ + if (!teco_qreg_plain_edit(qreg, error)) + return FALSE; + + g_auto(teco_string_t) str = {NULL, 0}; + + if (!teco_qreg_bufferinfo_get_string(qreg, &str.data, &str.len, error)) + return FALSE; + + teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0); + teco_view_ssm(teco_qreg_view, SCI_CLEARALL, 0, 0); + teco_view_ssm(teco_qreg_view, SCI_ADDTEXT, str.len, (sptr_t)str.data); + teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0); + + undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0); + return TRUE; +} + +/** @static @memberof teco_qreg_t */ +teco_qreg_t * +teco_qreg_bufferinfo_new(void) +{ + static teco_qreg_vtable_t vtable = TECO_INIT_QREG( + .set_integer = teco_qreg_bufferinfo_set_integer, + .undo_set_integer = teco_qreg_bufferinfo_undo_set_integer, + .get_integer = teco_qreg_bufferinfo_get_integer, + .set_string = teco_qreg_bufferinfo_set_string, + .undo_set_string = teco_qreg_bufferinfo_undo_set_string, + .append_string = teco_qreg_bufferinfo_append_string, + .undo_append_string = teco_qreg_bufferinfo_undo_append_string, + .get_string = teco_qreg_bufferinfo_get_string, + .get_character = teco_qreg_bufferinfo_get_character, + .edit = teco_qreg_bufferinfo_edit + ); + + return teco_qreg_new(&vtable, "*", 1); +} + +static gboolean +teco_qreg_workingdir_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error) +{ + /* + * NOTE: Makes sure that `dir` will be null-terminated as str[len] may not be '\0'. + */ + g_auto(teco_string_t) dir; + teco_string_init(&dir, str, len); + + if (teco_string_contains(&dir, '\0')) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Directory contains null-character"); + return FALSE; + } + + int ret = g_chdir(dir.data); + if (ret) { + /* FIXME: Is errno usable on Windows here? */ + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Cannot change working directory to \"%s\"", dir.data); + return FALSE; + } + + return TRUE; +} + +static gboolean +teco_qreg_workingdir_undo_set_string(teco_qreg_t *qreg, GError **error) +{ + teco_undo_change_dir_to_current(); + return TRUE; +} + +/* + * FIXME: Redundant with teco_qreg_bufferinfo_append_string()... + * Best solution would be to simply implement them. + */ +static gboolean +teco_qreg_workingdir_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error) +{ + teco_error_qregopunsupported_set(error, qreg->head.name.data, qreg->head.name.len, FALSE); + return FALSE; +} + +static gboolean +teco_qreg_workingdir_undo_append_string(teco_qreg_t *qreg, GError **error) +{ + return TRUE; +} + +static gboolean +teco_qreg_workingdir_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error) +{ + /* + * On platforms with a default non-forward-slash directory + * separator (i.e. Windows), teco_buffer_t::filename will have + * the wrong separator. + * To make the life of macros that evaluate "$" easier, + * the directory separators are normalized to "/" here. + * This does not change the size of the string, so + * the return value for str == NULL is still correct. + */ + gchar *dir = g_get_current_dir(); + *len = strlen(dir); + if (str) + *str = teco_file_normalize_path(dir); + else + g_free(dir); + + return TRUE; +} + +static gint +teco_qreg_workingdir_get_character(teco_qreg_t *qreg, guint position, GError **error) +{ + g_auto(teco_string_t) str = {NULL, 0}; + + if (!teco_qreg_workingdir_get_string(qreg, &str.data, &str.len, error)) + return -1; + + if (position >= str.len) { + g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE, + "Position %u out of range", position); + return -1; + } + + return str.data[position]; +} + +static gboolean +teco_qreg_workingdir_edit(teco_qreg_t *qreg, GError **error) +{ + g_auto(teco_string_t) str = {NULL, 0}; + + if (!teco_qreg_plain_edit(qreg, error) || + !teco_qreg_workingdir_get_string(qreg, &str.data, &str.len, error)) + return FALSE; + + teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0); + teco_view_ssm(teco_qreg_view, SCI_CLEARALL, 0, 0); + teco_view_ssm(teco_qreg_view, SCI_ADDTEXT, str.len, (sptr_t)str.data); + teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0); + + undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0); + return TRUE; +} + +static gboolean +teco_qreg_workingdir_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error) +{ + g_auto(teco_string_t) other_str, own_str = {NULL, 0}; + + teco_doc_get_string(src, &other_str.data, &other_str.len); + + if (!teco_qreg_workingdir_get_string(qreg, &own_str.data, &own_str.len, error) || + /* FIXME: Why is teco_qreg_plain_set_string() sufficient? */ + !teco_qreg_plain_set_string(qreg, other_str.data, other_str.len, error)) + return FALSE; + + teco_doc_set_string(src, own_str.data, own_str.len); + return TRUE; +} + +static gboolean +teco_qreg_workingdir_undo_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error) +{ + teco_undo_change_dir_to_current(); + if (qreg->must_undo) // FIXME + teco_doc_undo_set_string(src); + return TRUE; +} + +/** @static @memberof teco_qreg_t */ +teco_qreg_t * +teco_qreg_workingdir_new(void) +{ + static teco_qreg_vtable_t vtable = TECO_INIT_QREG( + .set_string = teco_qreg_workingdir_set_string, + .undo_set_string = teco_qreg_workingdir_undo_set_string, + .append_string = teco_qreg_workingdir_append_string, + .undo_append_string = teco_qreg_workingdir_undo_append_string, + .get_string = teco_qreg_workingdir_get_string, + .get_character = teco_qreg_workingdir_get_character, + .edit = teco_qreg_workingdir_edit, + .exchange_string = teco_qreg_workingdir_exchange_string, + .undo_exchange_string = teco_qreg_workingdir_undo_exchange_string + ); + + /* + * FIXME: Dollar is not the best name for it since it is already + * heavily overloaded in the language and easily confused with Escape and + * the "\e" register also exists. + * Not to mention that environment variable regs also start with dollar. + * Perhaps "~" would be a better choice, although it is also already used? + * Most logical would be ".", but this should probably map to to Dot and + * is also ugly to write in practice. + * Perhaps "@"... + */ + return teco_qreg_new(&vtable, "$", 1); +} + +static gboolean +teco_qreg_clipboard_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error) +{ + g_assert(!teco_string_contains(&qreg->head.name, '\0')); + const gchar *clipboard_name = qreg->head.name.data + 1; + + if (teco_ed & TECO_ED_AUTOEOL) { + /* + * NOTE: Currently uses GString instead of teco_string_t to make use of + * preallocation. + * On the other hand GString has a higher overhead. + */ + g_autoptr(GString) str_converted = g_string_sized_new(len); + + /* + * This will convert to the Q-Register view's EOL mode. + */ + g_auto(teco_eol_writer_t) writer; + teco_eol_writer_init_mem(&writer, teco_view_ssm(teco_qreg_view, SCI_GETEOLMODE, 0, 0), + str_converted); + + gssize bytes_written = teco_eol_writer_convert(&writer, str, len, error); + if (bytes_written < 0) + return FALSE; + g_assert(bytes_written == len); + + return teco_interface_set_clipboard(clipboard_name, str_converted->str, + str_converted->len, error); + } else { + /* + * No EOL conversion necessary. The teco_eol_writer_t can handle + * this as well, but will result in unnecessary allocations. + */ + return teco_interface_set_clipboard(clipboard_name, str, len, error); + } + + /* should not be reached */ + return TRUE; +} + +/* + * FIXME: Redundant with teco_qreg_bufferinfo_append_string()... + * Best solution would be to simply implement them. + */ +static gboolean +teco_qreg_clipboard_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error) +{ + teco_error_qregopunsupported_set(error, qreg->head.name.data, qreg->head.name.len, FALSE); + return FALSE; +} + +static gboolean +teco_qreg_clipboard_undo_append_string(teco_qreg_t *qreg, GError **error) +{ + return TRUE; +} + +static gboolean +teco_qreg_clipboard_undo_set_string(teco_qreg_t *qreg, GError **error) +{ + /* + * Upon rubout, the current contents of the clipboard are + * restored. + * We are checking for teco_undo_enabled instead of relying on + * teco_undo_push(), since getting the clipboard + * is an expensive operation that we want to avoid. + */ + if (!teco_undo_enabled) + return TRUE; + + g_assert(!teco_string_contains(&qreg->head.name, '\0')); + const gchar *clipboard_name = qreg->head.name.data + 1; + + /* + * Ownership of str is passed to the undo token. + * This avoids any EOL translation as that would be cumbersome + * and could also modify the clipboard in unexpected ways. + */ + teco_string_t str; + if (!teco_interface_get_clipboard(clipboard_name, &str.data, &str.len, error)) + return FALSE; + teco_interface_undo_set_clipboard(clipboard_name, str.data, str.len); + return TRUE; +} + +static gboolean +teco_qreg_clipboard_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error) +{ + g_assert(!teco_string_contains(&qreg->head.name, '\0')); + const gchar *clipboard_name = qreg->head.name.data + 1; + + if (!(teco_ed & TECO_ED_AUTOEOL)) + /* + * No auto-eol conversion - avoid unnecessary copying and allocations. + */ + return teco_interface_get_clipboard(clipboard_name, str, len, error); + + g_auto(teco_string_t) temp = {NULL, 0}; + if (!teco_interface_get_clipboard(clipboard_name, &temp.data, &temp.len, error)) + return FALSE; + + g_auto(teco_eol_reader_t) reader; + teco_eol_reader_init_mem(&reader, temp.data, temp.len); + + /* + * FIXME: Could be simplified if teco_eol_reader_convert_all() had the + * same conventions for passing NULL pointers. + */ + teco_string_t str_converted; + if (teco_eol_reader_convert_all(&reader, &str_converted.data, + &str_converted.len, error) == G_IO_STATUS_ERROR) + return FALSE; + + if (str) + *str = str_converted.data; + else + teco_string_clear(&str_converted); + *len = str_converted.len; + + return TRUE; +} + +static gint +teco_qreg_clipboard_get_character(teco_qreg_t *qreg, guint position, GError **error) +{ + g_auto(teco_string_t) str = {NULL, 0}; + + if (!teco_qreg_clipboard_get_string(qreg, &str.data, &str.len, error)) + return -1; + + if (position >= str.len) { + g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE, + "Position %u out of range", position); + return -1; + } + + return str.data[position]; +} + +static gboolean +teco_qreg_clipboard_edit(teco_qreg_t *qreg, GError **error) +{ + if (!teco_qreg_plain_edit(qreg, error)) + return FALSE; + + g_auto(teco_string_t) str = {NULL, 0}; + + if (!teco_qreg_clipboard_get_string(qreg, &str.data, &str.len, error)) + return FALSE; + + teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0); + teco_view_ssm(teco_qreg_view, SCI_CLEARALL, 0, 0); + teco_view_ssm(teco_qreg_view, SCI_APPENDTEXT, str.len, (sptr_t)str.data); + teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0); + + undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0); + return TRUE; +} + +/* + * FIXME: Very similar to teco_qreg_workingdir_exchange_string(). + */ +static gboolean +teco_qreg_clipboard_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error) +{ + g_auto(teco_string_t) other_str, own_str = {NULL, 0}; + + teco_doc_get_string(src, &other_str.data, &other_str.len); + + if (!teco_qreg_clipboard_get_string(qreg, &own_str.data, &own_str.len, error) || + /* FIXME: Why is teco_qreg_plain_set_string() sufficient? */ + !teco_qreg_plain_set_string(qreg, other_str.data, other_str.len, error)) + return FALSE; + + teco_doc_set_string(src, own_str.data, own_str.len); + return TRUE; +} + +/* + * FIXME: Very similar to teco_qreg_workingdir_undo_exchange_string(). + */ +static gboolean +teco_qreg_clipboard_undo_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error) +{ + if (!teco_qreg_clipboard_undo_set_string(qreg, error)) + return FALSE; + if (qreg->must_undo) // FIXME + teco_doc_undo_set_string(src); + return TRUE; +} + +/** @static @memberof teco_qreg_t */ +teco_qreg_t * +teco_qreg_clipboard_new(const gchar *name) +{ + static teco_qreg_vtable_t vtable = TECO_INIT_QREG( + .set_string = teco_qreg_clipboard_set_string, + .undo_set_string = teco_qreg_clipboard_undo_set_string, + .append_string = teco_qreg_clipboard_append_string, + .undo_append_string = teco_qreg_clipboard_undo_append_string, + .get_string = teco_qreg_clipboard_get_string, + .get_character = teco_qreg_clipboard_get_character, + .edit = teco_qreg_clipboard_edit, + .exchange_string = teco_qreg_clipboard_exchange_string, + .undo_exchange_string = teco_qreg_clipboard_undo_exchange_string + ); + + teco_qreg_t *qreg = teco_qreg_new(&vtable, "~", 1); + teco_string_append(&qreg->head.name, name, strlen(name)); + return qreg; +} + +/** @memberof teco_qreg_table_t */ +void +teco_qreg_table_init(teco_qreg_table_t *table, gboolean must_undo) +{ + rb3_reset_tree(&table->tree); + table->must_undo = must_undo; + + /* general purpose registers */ + for (gchar q = 'A'; q <= 'Z'; q++) + teco_qreg_table_insert(table, teco_qreg_plain_new(&q, sizeof(q))); + for (gchar q = '0'; q <= '9'; q++) + teco_qreg_table_insert(table, teco_qreg_plain_new(&q, sizeof(q))); +} + +static inline void +teco_qreg_table_remove(teco_qreg_t *reg) +{ + rb3_unlink_and_rebalance(®->head.head); + teco_qreg_free(reg); +} +TECO_DEFINE_UNDO_CALL(teco_qreg_table_remove, teco_qreg_t *); + +static inline void +teco_qreg_table_undo_remove(teco_qreg_t *qreg) +{ + if (qreg->must_undo) + undo__teco_qreg_table_remove(qreg); +} + +/** @memberof teco_qreg_table_t */ +teco_qreg_t * +teco_qreg_table_edit_name(teco_qreg_table_t *table, const gchar *name, gsize len, GError **error) +{ + teco_qreg_t *qreg = teco_qreg_table_find(table, name, len); + if (!qreg) { + g_autofree gchar *name_printable = teco_string_echo(name, len); + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Q-Register \"%s\" not found", name_printable); + return NULL; + } + + return teco_qreg_table_edit(table, qreg, error) ? qreg : NULL; +} + +/** + * Import process environment into table + * by setting environment registers for every + * environment variable. + * It is assumed that the table does not yet + * contain any environment register. + * + * In general this method is only safe to call + * at startup. + * + * @memberof teco_qreg_table_t + */ +gboolean +teco_qreg_table_set_environ(teco_qreg_table_t *table, GError **error) +{ + /* + * NOTE: Using g_get_environ() would be more efficient, + * but it appears to be broken, at least on Windows 2000. + */ + g_auto(GStrv) env = g_listenv(); + + for (gchar **key = env; *key; key++) { + g_autofree gchar *name = g_strconcat("$", *key, NULL); + + /* + * FIXME: It might be a good idea to wrap this into + * a convenience function. + */ + teco_qreg_t *qreg = teco_qreg_plain_new(name, strlen(name)); + teco_qreg_t *found = teco_qreg_table_insert(table, qreg); + if (found) { + teco_qreg_free(qreg); + qreg = found; + } + + const gchar *value = g_getenv(*key); + g_assert(value != NULL); + if (!qreg->vtable->set_string(qreg, value, strlen(value), error)) + return FALSE; + } + + return TRUE; +} + +/** + * Export environment registers as a list of environment + * variables compatible with `g_get_environ()`. + * + * @return Zero-terminated list of strings in the form + * `NAME=VALUE`. Should be freed with `g_strfreev()`. + * NULL in case of errors. + * + * @memberof teco_qreg_table_t + */ +gchar ** +teco_qreg_table_get_environ(teco_qreg_table_t *table, GError **error) +{ + teco_qreg_t *first = (teco_qreg_t *)teco_rb3str_nfind(&table->tree, TRUE, "$", 1); + + gint envp_len = 1; + + /* + * Iterate over all registers beginning with "$" to + * guess the size required for the environment array. + * This may waste a few bytes because not __every__ + * register beginning with "$" is an environment + * register. + */ + for (teco_qreg_t *cur = first; + cur && cur->head.name.data[0] == '$'; + cur = (teco_qreg_t *)teco_rb3str_get_next(&cur->head)) + envp_len++; + + gchar **envp, **p; + p = envp = g_new(gchar *, envp_len); + + for (teco_qreg_t *cur = first; + cur && cur->head.name.data[0] == '$'; + cur = (teco_qreg_t *)teco_rb3str_get_next(&cur->head)) { + const teco_string_t *name = &cur->head.name; + + /* + * Ignore the "$" register (not an environment + * variable register) and registers whose + * name contains "=" or null (not allowed in environment + * variable names). + */ + if (name->len == 1 || + teco_string_contains(name, '=') || teco_string_contains(name, '\0')) + continue; + + g_auto(teco_string_t) value = {NULL, 0}; + if (!cur->vtable->get_string(cur, &value.data, &value.len, error)) { + g_strfreev(envp); + return NULL; + } + if (teco_string_contains(&value, '\0')) { + g_strfreev(envp); + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Environment register \"%s\" must not contain null characters", + name->data); + return NULL; + } + + /* more efficient than g_environ_setenv() */ + *p++ = g_strconcat(name->data+1, "=", value.data, NULL); + } + + *p = NULL; + + return envp; +} + +/** + * Empty Q-Register table except the currently edited register. + * If the table contains the currently edited register, it will + * throw an error and the table might be left half-emptied. + * + * @memberof teco_qreg_table_t + */ +gboolean +teco_qreg_table_empty(teco_qreg_table_t *table, GError **error) +{ + struct rb3_head *cur; + + while ((cur = rb3_get_root(&table->tree))) { + if ((teco_qreg_t *)cur == teco_qreg_current) { + const teco_string_t *name = &teco_qreg_current->head.name; + g_autofree gchar *name_printable = teco_string_echo(name->data, name->len); + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Currently edited Q-Register \"%s\" cannot be discarded", name_printable); + return FALSE; + } + + rb3_unlink_and_rebalance(cur); + teco_qreg_free((teco_qreg_t *)cur); + } + + return TRUE; +} + +/** @memberof teco_qreg_table_t */ +void +teco_qreg_table_clear(teco_qreg_table_t *table) +{ + struct rb3_head *cur; + + while ((cur = rb3_get_root(&table->tree))) { + rb3_unlink_and_rebalance(cur); + teco_qreg_free((teco_qreg_t *)cur); + } +} + +typedef struct { + teco_int_t integer; + teco_doc_t string; +} teco_qreg_stack_entry_t; + +static inline void +teco_qreg_stack_entry_clear(teco_qreg_stack_entry_t *entry) +{ + teco_doc_clear(&entry->string); +} + +static GArray *teco_qreg_stack; + +static void __attribute__((constructor)) +teco_qreg_stack_init(void) +{ + teco_qreg_stack = g_array_sized_new(FALSE, FALSE, sizeof(teco_qreg_stack_entry_t), 1024); +} + +static inline void +teco_qreg_stack_remove_last(void) +{ + teco_qreg_stack_entry_clear(&g_array_index(teco_qreg_stack, teco_qreg_stack_entry_t, + teco_qreg_stack->len-1)); + g_array_remove_index(teco_qreg_stack, teco_qreg_stack->len-1); +} +TECO_DEFINE_UNDO_CALL(teco_qreg_stack_remove_last); + +gboolean +teco_qreg_stack_push(teco_qreg_t *qreg, GError **error) +{ + teco_qreg_stack_entry_t entry; + g_auto(teco_string_t) string = {NULL, 0}; + + if (!qreg->vtable->get_integer(qreg, &entry.integer, error) || + !qreg->vtable->get_string(qreg, &string.data, &string.len, error)) + return FALSE; + teco_doc_init(&entry.string); + teco_doc_set_string(&entry.string, string.data, string.len); + teco_doc_update(&entry.string, &qreg->string); + + /* pass ownership of entry to teco_qreg_stack */ + g_array_append_val(teco_qreg_stack, entry); + undo__teco_qreg_stack_remove_last(); + return TRUE; +} + +static void +teco_qreg_stack_entry_action(teco_qreg_stack_entry_t *entry, gboolean run) +{ + if (run) + g_array_append_val(teco_qreg_stack, *entry); + else + teco_qreg_stack_entry_clear(entry); +} + +static void +teco_undo_qreg_stack_push_own(teco_qreg_stack_entry_t *entry) +{ + teco_qreg_stack_entry_t *ctx = teco_undo_push(teco_qreg_stack_entry); + if (ctx) + memcpy(ctx, entry, sizeof(*ctx)); + else + teco_qreg_stack_entry_clear(entry); +} + +gboolean +teco_qreg_stack_pop(teco_qreg_t *qreg, GError **error) +{ + if (!teco_qreg_stack->len) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Q-Register stack empty"); + return FALSE; + } + + teco_qreg_stack_entry_t *entry; + entry = &g_array_index(teco_qreg_stack, teco_qreg_stack_entry_t, teco_qreg_stack->len-1); + + if (!qreg->vtable->undo_set_integer(qreg, error) || + !qreg->vtable->set_integer(qreg, entry->integer, error)) + return FALSE; + + /* exchange document ownership between stack entry and Q-Register */ + if (!qreg->vtable->undo_exchange_string(qreg, &entry->string, error) || + !qreg->vtable->exchange_string(qreg, &entry->string, error)) + return FALSE; + + /* pass entry ownership to undo stack. */ + teco_undo_qreg_stack_push_own(entry); + + g_array_remove_index(teco_qreg_stack, teco_qreg_stack->len-1); + return TRUE; +} + +#ifndef NDEBUG +static void __attribute__((destructor)) +teco_qreg_stack_clear(void) +{ + g_array_set_clear_func(teco_qreg_stack, (GDestroyNotify)teco_qreg_stack_entry_clear); + g_array_free(teco_qreg_stack, TRUE); +} +#endif + +gboolean +teco_ed_hook(teco_ed_hook_t type, GError **error) +{ + if (!(teco_ed & TECO_ED_HOOKS)) + return TRUE; + + /* + * NOTE: It is crucial to declare this before the first goto, + * since it runs all destructors. + */ + g_auto(teco_qreg_table_t) locals; + teco_qreg_table_init(&locals, FALSE); + + teco_qreg_t *qreg = teco_qreg_table_find(&teco_qreg_table_globals, "ED", 2); + if (!qreg) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Undefined ED-hook register (\"ED\")"); + goto error_add_frame; + } + + /* + * ED-hook execution should not see any + * integer parameters but the hook type. + * Such parameters could confuse the ED macro + * and macro authors do not expect side effects + * of ED macros on the expression stack. + * Also make sure it does not leave behind + * additional arguments on the stack. + * + * So this effectively executes: + * (typeM[ED]^[) + * + * FIXME: Temporarily stashing away the expression + * stack may be a more elegant solution. + */ + teco_expressions_brace_open(); + teco_expressions_push_int(type); + + if (!teco_qreg_execute(qreg, &locals, error)) + goto error_add_frame; + + return teco_expressions_discard_args(error) && + teco_expressions_brace_close(error); + + static const gchar *type2name[] = { + [TECO_ED_HOOK_ADD-1] = "ADD", + [TECO_ED_HOOK_EDIT-1] = "EDIT", + [TECO_ED_HOOK_CLOSE-1] = "CLOSE", + [TECO_ED_HOOK_QUIT-1] = "QUIT" + }; + +error_add_frame: + g_assert(0 <= type-1 && type-1 < G_N_ELEMENTS(type2name)); + teco_error_add_frame_edhook(type2name[type-1]); + return FALSE; +} + +/** @extends teco_machine_t */ +struct teco_machine_qregspec_t { + teco_machine_t parent; + + /** + * Aliases bitfield with an integer. + * This allows teco_undo_guint(__flags), + * while still supporting easy-to-access flags. + */ + union { + struct { + teco_qreg_type_t type : 8; + gboolean parse_only : 1; + }; + guint __flags; + }; + + /** Local Q-Register table of the macro invocation frame. */ + teco_qreg_table_t *qreg_table_locals; + + teco_machine_stringbuilding_t machine_stringbuilding; + /* + * FIXME: Does it make sense to allow nested braces? + * Perhaps it's sufficient to support ^Q]. + */ + gint nesting; + teco_string_t name; + + teco_qreg_t *result; + teco_qreg_table_t *result_table; +}; + +/* + * FIXME: All teco_state_qregspec_* states could be static? + */ +TECO_DECLARE_STATE(teco_state_qregspec_start); +TECO_DECLARE_STATE(teco_state_qregspec_start_global); +TECO_DECLARE_STATE(teco_state_qregspec_firstchar); +TECO_DECLARE_STATE(teco_state_qregspec_secondchar); +TECO_DECLARE_STATE(teco_state_qregspec_string); + +static teco_state_t *teco_state_qregspec_start_global_input(teco_machine_qregspec_t *ctx, + gchar chr, GError **error); + +static teco_state_t * +teco_state_qregspec_done(teco_machine_qregspec_t *ctx, GError **error) +{ + if (ctx->parse_only) + return &teco_state_qregspec_start; + + ctx->result = teco_qreg_table_find(ctx->result_table, ctx->name.data, ctx->name.len); + + switch (ctx->type) { + case TECO_QREG_REQUIRED: + if (!ctx->result) { + teco_error_invalidqreg_set(error, ctx->name.data, ctx->name.len, + ctx->result_table != &teco_qreg_table_globals); + return NULL; + } + break; + + case TECO_QREG_OPTIONAL: + break; + + case TECO_QREG_OPTIONAL_INIT: + if (!ctx->result) { + ctx->result = teco_qreg_plain_new(ctx->name.data, ctx->name.len); + teco_qreg_table_insert(ctx->result_table, ctx->result); + teco_qreg_table_undo_remove(ctx->result); + } + break; + } + + return &teco_state_qregspec_start; +} + +static teco_state_t * +teco_state_qregspec_start_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error) +{ + /* + * FIXME: We're using teco_state_qregspec_start as a success condition, + * so either '.' goes into its own state or we re-introduce a state attribute. + */ + if (chr == '.') { + if (ctx->parent.must_undo) + teco_undo_ptr(ctx->result_table); + ctx->result_table = ctx->qreg_table_locals; + return &teco_state_qregspec_start_global; + } + + return teco_state_qregspec_start_global_input(ctx, chr, error); +} + +/* in cmdline.c */ +gboolean teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error); + +TECO_DEFINE_STATE(teco_state_qregspec_start, + .is_start = TRUE, + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd +); + +static teco_state_t * +teco_state_qregspec_start_global_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error) +{ + /* + * FIXME: Disallow space characters? + */ + switch (chr) { + case '#': + return &teco_state_qregspec_firstchar; + + case '[': + if (ctx->parent.must_undo) + teco_undo_gint(ctx->nesting); + ctx->nesting++; + return &teco_state_qregspec_string; + } + + if (!ctx->parse_only) { + if (ctx->parent.must_undo) + undo__teco_string_truncate(&ctx->name, ctx->name.len); + teco_string_append_c(&ctx->name, g_ascii_toupper(chr)); + } + return teco_state_qregspec_done(ctx, error); +} + +/* + * NOTE: This state mainly exists so that we don't have to go back to teco_state_qregspec_start after + * an initial `.` -- this is currently used in teco_machine_qregspec_input() to check for completeness. + * Alternatively, we'd have to introduce a teco_machine_qregspec_t::status attribute. + * Or even better, why not use special pointers like ((teco_state_t *)"teco_state_qregspec_done")? + */ +TECO_DEFINE_STATE(teco_state_qregspec_start_global, + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd +); + +static teco_state_t * +teco_state_qregspec_firstchar_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error) +{ + /* + * FIXME: Disallow space characters? + */ + if (!ctx->parse_only) { + if (ctx->parent.must_undo) + undo__teco_string_truncate(&ctx->name, ctx->name.len); + teco_string_append_c(&ctx->name, g_ascii_toupper(chr)); + } + return &teco_state_qregspec_secondchar; +} + +TECO_DEFINE_STATE(teco_state_qregspec_firstchar, + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd +); + +static teco_state_t * +teco_state_qregspec_secondchar_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error) +{ + /* + * FIXME: Disallow space characters? + */ + if (!ctx->parse_only) { + if (ctx->parent.must_undo) + undo__teco_string_truncate(&ctx->name, ctx->name.len); + teco_string_append_c(&ctx->name, g_ascii_toupper(chr)); + } + return teco_state_qregspec_done(ctx, error); +} + +TECO_DEFINE_STATE(teco_state_qregspec_secondchar, + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd +); + +static teco_state_t * +teco_state_qregspec_string_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error) +{ + /* + * Makes sure that braces within string building constructs do not have to be + * escaped and that ^Q/^R can be used to escape braces. + * + * FIXME: Perhaps that's sufficient and we don't have to keep track of nesting? + */ + if (ctx->machine_stringbuilding.parent.current->is_start) { + switch (chr) { + case '[': + if (ctx->parent.must_undo) + teco_undo_gint(ctx->nesting); + ctx->nesting++; + break; + case ']': + if (ctx->parent.must_undo) + teco_undo_gint(ctx->nesting); + ctx->nesting--; + if (!ctx->nesting) + return teco_state_qregspec_done(ctx, error); + break; + } + } + + if (!ctx->parse_only && ctx->parent.must_undo) + undo__teco_string_truncate(&ctx->name, ctx->name.len); + + /* + * NOTE: machine_stringbuilding gets notified about parse-only mode by passing NULL + * as the target string. + */ + if (!teco_machine_stringbuilding_input(&ctx->machine_stringbuilding, chr, + ctx->parse_only ? NULL : &ctx->name, error)) + return NULL; + + return &teco_state_qregspec_string; +} + +/* in cmdline.c */ +gboolean teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, + gchar key, GError **error); + +TECO_DEFINE_STATE(teco_state_qregspec_string, + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_string_process_edit_cmd +); + +/** @static @memberof teco_machine_qregspec_t */ +teco_machine_qregspec_t * +teco_machine_qregspec_new(teco_qreg_type_t type, teco_qreg_table_t *locals, gboolean must_undo) +{ + /* + * FIXME: Allocate via g_slice? + */ + teco_machine_qregspec_t *ctx = g_new0(teco_machine_qregspec_t, 1); + teco_machine_init(&ctx->parent, &teco_state_qregspec_start, must_undo); + ctx->type = type; + ctx->qreg_table_locals = locals; + teco_machine_stringbuilding_init(&ctx->machine_stringbuilding, '[', locals, must_undo); + ctx->result_table = &teco_qreg_table_globals; + return ctx; +} + +/** @memberof teco_machine_qregspec_t */ +void +teco_machine_qregspec_reset(teco_machine_qregspec_t *ctx) +{ + teco_machine_reset(&ctx->parent, &teco_state_qregspec_start); + teco_machine_stringbuilding_reset(&ctx->machine_stringbuilding); + if (ctx->parent.must_undo) { + teco_undo_string_own(ctx->name); + teco_undo_gint(ctx->nesting); + teco_undo_guint(ctx->__flags); + } else { + teco_string_clear(&ctx->name); + } + memset(&ctx->name, 0, sizeof(ctx->name)); + ctx->nesting = 0; + ctx->result_table = &teco_qreg_table_globals; +} + +/** @memberof teco_machine_qregspec_t */ +teco_machine_stringbuilding_t * +teco_machine_qregspec_get_stringbuilding(teco_machine_qregspec_t *ctx) +{ + return &ctx->machine_stringbuilding; +} + +/** + * Pass a character to the QRegister specification machine. + * + * @param ctx QRegister specification machine. + * @param chr Character to parse. + * @param result Pointer to QRegister or NULL in parse-only mode. + * If non-NULL it will be set once a specification is successfully parsed. + * @param result_table Pointer to QRegister table. May be NULL in parse-only mode. + * @param error GError or NULL. + * @return Returns TECO_MACHINE_QREGSPEC_DONE in case of complete specs. + * + * @memberof teco_machine_qregspec_t + */ +teco_machine_qregspec_status_t +teco_machine_qregspec_input(teco_machine_qregspec_t *ctx, gchar chr, + teco_qreg_t **result, teco_qreg_table_t **result_table, GError **error) +{ + ctx->parse_only = result == NULL; + + if (!teco_machine_input(&ctx->parent, chr, error)) + return TECO_MACHINE_QREGSPEC_ERROR; + + teco_machine_qregspec_get_results(ctx, result, result_table); + return ctx->parent.current == &teco_state_qregspec_start + ? TECO_MACHINE_QREGSPEC_DONE : TECO_MACHINE_QREGSPEC_MORE; +} + +/** @memberof teco_machine_qregspec_t */ +void +teco_machine_qregspec_get_results(teco_machine_qregspec_t *ctx, + teco_qreg_t **result, teco_qreg_table_t **result_table) +{ + if (result) + *result = ctx->result; + if (result_table) + *result_table = ctx->result_table; +} + +/** @memberof teco_machine_qregspec_t */ +gboolean +teco_machine_qregspec_auto_complete(teco_machine_qregspec_t *ctx, teco_string_t *insert) +{ + gsize restrict_len = 0; + + /* + * NOTE: We could have separate process_edit_cmd_cb() for + * teco_state_qregspec_firstchar/teco_state_qregspec_secondchar + * and pass down restrict_len instead. + */ + if (ctx->parent.current == &teco_state_qregspec_start || + ctx->parent.current == &teco_state_qregspec_start_global) + /* single-letter Q-Reg */ + restrict_len = 1; + else if (ctx->parent.current != &teco_state_qregspec_string) + /* two-letter Q-Reg */ + restrict_len = 2; + + return teco_rb3str_auto_complete(&ctx->result_table->tree, !restrict_len, + ctx->name.data, ctx->name.len, restrict_len, insert) && + ctx->nesting == 1; +} + +/** @memberof teco_machine_qregspec_t */ +void +teco_machine_qregspec_free(teco_machine_qregspec_t *ctx) +{ + teco_machine_stringbuilding_clear(&ctx->machine_stringbuilding); + teco_string_clear(&ctx->name); + g_free(ctx); +} + +TECO_DEFINE_UNDO_CALL(teco_machine_qregspec_free, teco_machine_qregspec_t *); +TECO_DEFINE_UNDO_OBJECT_OWN(qregspec, teco_machine_qregspec_t *, teco_machine_qregspec_free); diff --git a/src/qreg.h b/src/qreg.h new file mode 100644 index 0000000..4797a01 --- /dev/null +++ b/src/qreg.h @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#pragma once + +#include + +#include "sciteco.h" +#include "view.h" +#include "doc.h" +#include "undo.h" +#include "string-utils.h" +#include "rb3str.h" + +/* + * Forward declarations. + */ +typedef struct teco_qreg_t teco_qreg_t; +/* Could be avoided by moving teco_qreg_execute() to the end of the file instead... */ +typedef struct teco_qreg_table_t teco_qreg_table_t; + +extern teco_view_t *teco_qreg_view; + +/* + * NOTE: This is not "hidden" in qreg.c, so that we won't need wrapper + * functions for every vtable method. + * + * FIXME: Use TECO_DECLARE_VTABLE_METHOD(gboolean, teco_qreg, set_integer, teco_qreg_t *, teco_int_t, GError **); + * ... + * teco_qreg_set_integer_t set_integer; + */ +typedef const struct { + gboolean (*set_integer)(teco_qreg_t *qreg, teco_int_t value, GError **error); + gboolean (*undo_set_integer)(teco_qreg_t *qreg, GError **error); + gboolean (*get_integer)(teco_qreg_t *qreg, teco_int_t *ret, GError **error); + + gboolean (*set_string)(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error); + gboolean (*undo_set_string)(teco_qreg_t *qreg, GError **error); + gboolean (*append_string)(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error); + gboolean (*undo_append_string)(teco_qreg_t *qreg, GError **error); + + gboolean (*get_string)(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error); + gint (*get_character)(teco_qreg_t *qreg, guint position, GError **error); + + /* + * These callbacks exist only to optimize teco_qreg_stack_push|pop() + * for plain Q-Registers making [q and ]q quite efficient operations even on rubout. + * On the other hand, this unnecessarily complicates teco_qreg_t derivations. + */ + gboolean (*exchange_string)(teco_qreg_t *qreg, teco_doc_t *src, GError **error); + gboolean (*undo_exchange_string)(teco_qreg_t *qreg, teco_doc_t *src, GError **error); + + gboolean (*edit)(teco_qreg_t *qreg, GError **error); + gboolean (*undo_edit)(teco_qreg_t *qreg, GError **error); +} teco_qreg_vtable_t; + +/** @extends teco_rb3str_head_t */ +struct teco_qreg_t { + /* + * NOTE: Must be the first member since we "upcast" to teco_qreg_t + */ + teco_rb3str_head_t head; + + teco_qreg_vtable_t *vtable; + + teco_int_t integer; + teco_doc_t string; + + /* + * Whether to generate undo tokens (unnecessary for registers + * in local qreg tables in macro invocations). + * + * FIXME: Every QRegister has this field, but it only differs + * between local and global QRegisters. This wastes space. + * Or by deferring any decision about undo token creation to a layer + * that knows which table it is accessing. + * On the other hand, we will need another flag like + * teco_qreg_current_must_undo. + * + * Otherwise, it might be possible to use a least significant bit + * in one of the pointers... + */ + gboolean must_undo; +}; + +teco_qreg_t *teco_qreg_plain_new(const gchar *name, gsize len); +teco_qreg_t *teco_qreg_bufferinfo_new(void); +teco_qreg_t *teco_qreg_workingdir_new(void); +teco_qreg_t *teco_qreg_clipboard_new(const gchar *name); + +gboolean teco_qreg_execute(teco_qreg_t *qreg, teco_qreg_table_t *qreg_table_locals, GError **error); + +void teco_qreg_undo_set_eol_mode(teco_qreg_t *qreg); +void teco_qreg_set_eol_mode(teco_qreg_t *qreg, gint mode); + +/* + * Load and save already care about undo token + * creation. + */ +gboolean teco_qreg_load(teco_qreg_t *qreg, const gchar *filename, GError **error); +gboolean teco_qreg_save(teco_qreg_t *qreg, const gchar *filename, GError **error); + +/** @memberof teco_qreg_t */ +static inline void +teco_qreg_free(teco_qreg_t *qreg) +{ + teco_doc_clear(&qreg->string); + teco_string_clear(&qreg->head.name); + g_free(qreg); +} + +extern teco_qreg_t *teco_qreg_current; + +/** @extends teco_rb3str_tree_t */ +struct teco_qreg_table_t { + teco_rb3str_tree_t tree; + + /* + * FIXME: Probably even this property can be eliminated. + * The only two tables with undo in the system are + * a) The global register table + * b) The top-level local register table. + */ + gboolean must_undo; +}; + +void teco_qreg_table_init(teco_qreg_table_t *table, gboolean must_undo); + +/** @memberof teco_qreg_table_t */ +static inline teco_qreg_t * +teco_qreg_table_insert(teco_qreg_table_t *table, teco_qreg_t *qreg) +{ + qreg->must_undo = table->must_undo; // FIXME + return (teco_qreg_t *)teco_rb3str_insert(&table->tree, TRUE, &qreg->head); +} + +/** @memberof teco_qreg_table_t */ +static inline teco_qreg_t * +teco_qreg_table_find(teco_qreg_table_t *table, const gchar *name, gsize len) +{ + return (teco_qreg_t *)teco_rb3str_find(&table->tree, TRUE, name, len); +} + +teco_qreg_t *teco_qreg_table_edit_name(teco_qreg_table_t *table, const gchar *name, + gsize len, GError **error); + +/** @memberof teco_qreg_table_t */ +static inline gboolean +teco_qreg_table_edit(teco_qreg_table_t *table, teco_qreg_t *qreg, GError **error) +{ + if (!qreg->vtable->edit(qreg, error)) + return FALSE; + teco_qreg_current = qreg; + return TRUE; +} + +gboolean teco_qreg_table_set_environ(teco_qreg_table_t *table, GError **error); +gchar **teco_qreg_table_get_environ(teco_qreg_table_t *table, GError **error); + +gboolean teco_qreg_table_empty(teco_qreg_table_t *table, GError **error); +void teco_qreg_table_clear(teco_qreg_table_t *table); + +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_qreg_table_t, teco_qreg_table_clear); + +extern teco_qreg_table_t teco_qreg_table_globals; + +gboolean teco_qreg_stack_push(teco_qreg_t *qreg, GError **error); +gboolean teco_qreg_stack_pop(teco_qreg_t *qreg, GError **error); + +typedef enum { + TECO_ED_HOOK_ADD = 1, + TECO_ED_HOOK_EDIT, + TECO_ED_HOOK_CLOSE, + TECO_ED_HOOK_QUIT +} teco_ed_hook_t; + +gboolean teco_ed_hook(teco_ed_hook_t type, GError **error); + +typedef enum { + TECO_MACHINE_QREGSPEC_ERROR = 0, + TECO_MACHINE_QREGSPEC_MORE, + TECO_MACHINE_QREGSPEC_DONE +} teco_machine_qregspec_status_t; + +typedef enum { + /** Register must exist, else fail */ + TECO_QREG_REQUIRED, + /** + * Return NULL if register does not exist. + * You can still call QRegSpecMachine::fail() to require it. + */ + TECO_QREG_OPTIONAL, + /** Initialize register if it does not already exist */ + TECO_QREG_OPTIONAL_INIT +} teco_qreg_type_t; + +typedef struct teco_machine_qregspec_t teco_machine_qregspec_t; + +teco_machine_qregspec_t *teco_machine_qregspec_new(teco_qreg_type_t type, + teco_qreg_table_t *locals, gboolean must_undo); + +void teco_machine_qregspec_reset(teco_machine_qregspec_t *ctx); + +/* + * FIXME: This uses a forward declaration since we must not include parser.h + */ +struct teco_machine_stringbuilding_t *teco_machine_qregspec_get_stringbuilding(teco_machine_qregspec_t *ctx); + +teco_machine_qregspec_status_t teco_machine_qregspec_input(teco_machine_qregspec_t *ctx, gchar chr, + teco_qreg_t **result, + teco_qreg_table_t **result_table, GError **error); + +void teco_machine_qregspec_get_results(teco_machine_qregspec_t *ctx, + teco_qreg_t **result, teco_qreg_table_t **result_table); + +gboolean teco_machine_qregspec_auto_complete(teco_machine_qregspec_t *ctx, teco_string_t *insert); + +void teco_machine_qregspec_free(teco_machine_qregspec_t *ctx); + +/** @memberof teco_machine_qregspec_t */ +void undo__teco_machine_qregspec_free(teco_machine_qregspec_t *); +TECO_DECLARE_UNDO_OBJECT(qregspec, teco_machine_qregspec_t *); + +#define teco_undo_qregspec_own(VAR) \ + (*teco_undo_object_qregspec_push(&(VAR))) diff --git a/src/qregisters.cpp b/src/qregisters.cpp deleted file mode 100644 index c23656c..0000000 --- a/src/qregisters.cpp +++ /dev/null @@ -1,1680 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include - -#include -#include -#include - -#include - -#include "sciteco.h" -#include "string-utils.h" -#include "interface.h" -#include "undo.h" -#include "parser.h" -#include "expressions.h" -#include "document.h" -#include "ring.h" -#include "ioview.h" -#include "eol.h" -#include "error.h" -#include "qregisters.h" - -namespace SciTECO { - -namespace States { - StatePushQReg pushqreg; - StatePopQReg popqreg; - StateEQCommand eqcommand; - StateLoadQReg loadqreg; - StateEPctCommand epctcommand; - StateSaveQReg saveqreg; - StateQueryQReg queryqreg; - StateCtlUCommand ctlucommand; - StateEUCommand eucommand; - StateSetQRegString setqregstring_nobuilding(false); - StateSetQRegString setqregstring_building(true); - StateGetQRegString getqregstring; - StateSetQRegInteger setqreginteger; - StateIncreaseQReg increaseqreg; - StateMacro macro; - StateMacroFile macro_file; - StateCopyToQReg copytoqreg; -} - -namespace QRegisters { - QRegisterTable *locals = NULL; - QRegister *current = NULL; - - static QRegisterStack stack; -} - -static QRegister *register_argument = NULL; - -void -QRegisterData::set_string(const gchar *str, gsize len) -{ - if (QRegisters::current) - QRegisters::current->string.update(QRegisters::view); - - string.reset(); - string.edit(QRegisters::view); - - QRegisters::view.ssm(SCI_BEGINUNDOACTION); - QRegisters::view.ssm(SCI_CLEARALL); - QRegisters::view.ssm(SCI_APPENDTEXT, - len, (sptr_t)(str ? : "")); - QRegisters::view.ssm(SCI_ENDUNDOACTION); - - if (QRegisters::current) - QRegisters::current->string.edit(QRegisters::view); -} - -void -QRegisterData::undo_set_string(void) -{ - if (!must_undo) - return; - - /* - * Necessary, so that upon rubout the - * string's parameters are restored. - */ - string.update(QRegisters::view); - - if (QRegisters::current && QRegisters::current->must_undo) - QRegisters::current->string.undo_edit(QRegisters::view); - - string.undo_reset(); - QRegisters::view.undo_ssm(SCI_UNDO); - - string.undo_edit(QRegisters::view); -} - -void -QRegisterData::append_string(const gchar *str, gsize len) -{ - /* - * NOTE: Will not create undo action - * if string is empty. - * Also, appending preserves the string's - * parameters. - */ - if (!len) - return; - - if (QRegisters::current) - QRegisters::current->string.update(QRegisters::view); - - string.edit(QRegisters::view); - - QRegisters::view.ssm(SCI_BEGINUNDOACTION); - QRegisters::view.ssm(SCI_APPENDTEXT, - len, (sptr_t)str); - QRegisters::view.ssm(SCI_ENDUNDOACTION); - - if (QRegisters::current) - QRegisters::current->string.edit(QRegisters::view); -} - -gchar * -QRegisterData::get_string(void) -{ - gint size; - gchar *str; - - if (!string.is_initialized()) - return g_strdup(""); - - if (QRegisters::current) - QRegisters::current->string.update(QRegisters::view); - - string.edit(QRegisters::view); - - size = QRegisters::view.ssm(SCI_GETLENGTH) + 1; - str = (gchar *)g_malloc(size); - QRegisters::view.ssm(SCI_GETTEXT, size, (sptr_t)str); - - if (QRegisters::current) - QRegisters::current->string.edit(QRegisters::view); - - return str; -} - -gsize -QRegisterData::get_string_size(void) -{ - gsize size; - - if (!string.is_initialized()) - return 0; - - if (QRegisters::current) - QRegisters::current->string.update(QRegisters::view); - - string.edit(QRegisters::view); - - size = QRegisters::view.ssm(SCI_GETLENGTH); - - if (QRegisters::current) - QRegisters::current->string.edit(QRegisters::view); - - return size; -} - -gint -QRegisterData::get_character(gint position) -{ - gint ret = -1; - - if (position < 0) - return -1; - - if (QRegisters::current) - QRegisters::current->string.update(QRegisters::view); - - string.edit(QRegisters::view); - - if (position < QRegisters::view.ssm(SCI_GETLENGTH)) - ret = QRegisters::view.ssm(SCI_GETCHARAT, position); - - if (QRegisters::current) - QRegisters::current->string.edit(QRegisters::view); - - return ret; -} - -void -QRegisterData::undo_exchange_string(QRegisterData ®) -{ - if (must_undo) - string.undo_exchange(); - if (reg.must_undo) - reg.string.undo_exchange(); -} - -void -QRegister::edit(void) -{ - if (QRegisters::current) - QRegisters::current->string.update(QRegisters::view); - - string.edit(QRegisters::view); - interface.show_view(&QRegisters::view); - interface.info_update(this); -} - -void -QRegister::undo_edit(void) -{ - /* - * We might be switching the current document - * to a buffer. - */ - string.update(QRegisters::view); - - if (!must_undo) - return; - - interface.undo_info_update(this); - string.undo_edit(QRegisters::view); - interface.undo_show_view(&QRegisters::view); -} - -void -QRegister::execute(bool locals) -{ - gchar *str = get_string(); - - try { - Execute::macro(str, locals); - } catch (Error &error) { - error.add_frame(new Error::QRegFrame(name)); - - g_free(str); - throw; /* forward */ - } catch (...) { - g_free(str); - throw; /* forward */ - } - - g_free(str); -} - -void -QRegister::undo_set_eol_mode(void) -{ - if (!must_undo) - return; - - /* - * Necessary, so that upon rubout the - * string's parameters are restored. - */ - string.update(QRegisters::view); - - if (QRegisters::current && QRegisters::current->must_undo) - QRegisters::current->string.undo_edit(QRegisters::view); - - QRegisters::view.undo_ssm(SCI_SETEOLMODE, - QRegisters::view.ssm(SCI_GETEOLMODE)); - - string.undo_edit(QRegisters::view); -} - -void -QRegister::set_eol_mode(gint mode) -{ - if (QRegisters::current) - QRegisters::current->string.update(QRegisters::view); - - string.edit(QRegisters::view); - QRegisters::view.ssm(SCI_SETEOLMODE, mode); - - if (QRegisters::current) - QRegisters::current->string.edit(QRegisters::view); -} - -void -QRegister::load(const gchar *filename) -{ - undo_set_string(); - - if (QRegisters::current) - QRegisters::current->string.update(QRegisters::view); - - string.edit(QRegisters::view); - string.reset(); - - /* - * IOView::load() might change the EOL style. - */ - undo_set_eol_mode(); - - /* - * undo_set_string() pushes undo tokens that restore - * the previous document in the view. - * So if loading fails, QRegisters::current will be - * made the current document again. - */ - QRegisters::view.load(filename); - - if (QRegisters::current) - QRegisters::current->string.edit(QRegisters::view); -} - -void -QRegister::save(const gchar *filename) -{ - if (QRegisters::current) - QRegisters::current->string.update(QRegisters::view); - - string.edit(QRegisters::view); - - try { - QRegisters::view.save(filename); - } catch (...) { - if (QRegisters::current) - QRegisters::current->string.edit(QRegisters::view); - throw; /* forward */ - } - - if (QRegisters::current) - QRegisters::current->string.edit(QRegisters::view); -} - -tecoInt -QRegisterBufferInfo::set_integer(tecoInt v) -{ - if (!ring.edit(v)) - throw Error("Invalid buffer id %" TECO_INTEGER_FORMAT, v); - - return v; -} - -void -QRegisterBufferInfo::undo_set_integer(void) -{ - current_doc_undo_edit(); -} - -tecoInt -QRegisterBufferInfo::get_integer(void) -{ - return ring.get_id(); -} - -gchar * -QRegisterBufferInfo::get_string(void) -{ - gchar *str = g_strdup(ring.current->filename ? : ""); - - /* - * On platforms with a default non-forward-slash directory - * separator (i.e. Windows), Buffer::filename will have - * the wrong separator. - * To make the life of macros that evaluate "*" easier, - * the directory separators are normalized to "/" here. - * This does not change the size of the string, so - * get_string_size() still works. - */ - return normalize_path(str); -} - -gsize -QRegisterBufferInfo::get_string_size(void) -{ - return ring.current->filename ? strlen(ring.current->filename) : 0; -} - -gint -QRegisterBufferInfo::get_character(gint position) -{ - if (position < 0 || - position >= (gint)QRegisterBufferInfo::get_string_size()) - return -1; - - return ring.current->filename[position]; -} - -void -QRegisterBufferInfo::edit(void) -{ - gchar *str; - - QRegister::edit(); - - QRegisters::view.ssm(SCI_BEGINUNDOACTION); - str = QRegisterBufferInfo::get_string(); - QRegisters::view.ssm(SCI_SETTEXT, 0, (sptr_t)str); - g_free(str); - QRegisters::view.ssm(SCI_ENDUNDOACTION); - - QRegisters::view.undo_ssm(SCI_UNDO); -} - -void -QRegisterWorkingDir::set_string(const gchar *str, gsize len) -{ - /* str is not null-terminated */ - gchar *dir = g_strndup(str, len); - int ret = g_chdir(dir); - - g_free(dir); - - if (ret) - /* FIXME: Is errno usable on Windows here? */ - throw Error("Cannot change working directory " - "to \"%.*s\"", (int)len, str); -} - -void -QRegisterWorkingDir::undo_set_string(void) -{ - /* pass ownership of current dir string */ - undo.push_own(g_get_current_dir()); -} - -gchar * -QRegisterWorkingDir::get_string(void) -{ - /* - * On platforms with a default non-forward-slash directory - * separator (i.e. Windows), Buffer::filename will have - * the wrong separator. - * To make the life of macros that evaluate "$" easier, - * the directory separators are normalized to "/" here. - * This does not change the size of the string, so - * get_string_size() still works. - */ - return normalize_path(g_get_current_dir()); -} - -gsize -QRegisterWorkingDir::get_string_size(void) -{ - gchar *str = g_get_current_dir(); - gsize len = strlen(str); - - g_free(str); - return len; -} - -gint -QRegisterWorkingDir::get_character(gint position) -{ - gchar *str = QRegisterWorkingDir::get_string(); - gint ret = -1; - - if (position >= 0 && - position < (gint)strlen(str)) - ret = str[position]; - - g_free(str); - return ret; -} - -void -QRegisterWorkingDir::edit(void) -{ - gchar *str; - - QRegister::edit(); - - QRegisters::view.ssm(SCI_BEGINUNDOACTION); - str = QRegisterWorkingDir::get_string(); - QRegisters::view.ssm(SCI_SETTEXT, 0, (sptr_t)str); - g_free(str); - QRegisters::view.ssm(SCI_ENDUNDOACTION); - - QRegisters::view.undo_ssm(SCI_UNDO); -} - -void -QRegisterWorkingDir::exchange_string(QRegisterData ®) -{ - gchar *own_str = QRegisterWorkingDir::get_string(); - gchar *other_str = reg.get_string(); - - QRegisterData::set_string(other_str); - g_free(other_str); - reg.set_string(own_str); - g_free(own_str); -} - -void -QRegisterWorkingDir::undo_exchange_string(QRegisterData ®) -{ - QRegisterWorkingDir::undo_set_string(); - reg.undo_set_string(); -} - -void -QRegisterClipboard::UndoTokenSetClipboard::run(void) -{ - interface.set_clipboard(name, str, str_len); -} - -void -QRegisterClipboard::set_string(const gchar *str, gsize len) -{ - if (Flags::ed & Flags::ED_AUTOEOL) { - GString *str_converted = g_string_sized_new(len); - /* - * This will convert to the Q-Register view's EOL mode. - */ - EOLWriterMem writer(str_converted, - QRegisters::view.ssm(SCI_GETEOLMODE)); - gsize bytes_written; - - /* - * NOTE: Shouldn't throw any error, ever. - */ - bytes_written = writer.convert(str, len); - g_assert(bytes_written == len); - - interface.set_clipboard(get_clipboard_name(), - str_converted->str, str_converted->len); - - g_string_free(str_converted, TRUE); - } else { - /* - * No EOL conversion necessary. The EOLWriter can handle - * this as well, but will result in unnecessary allocations. - */ - interface.set_clipboard(get_clipboard_name(), str, len); - } -} - -void -QRegisterClipboard::undo_set_string(void) -{ - gchar *str; - gsize str_len; - - /* - * Upon rubout, the current contents of the clipboard are - * restored. - * We are checking for undo.enabled instead of relying on - * undo.push_own(), since getting the clipboard - * is an expensive operation that we want to avoid. - */ - if (!undo.enabled) - return; - - /* - * Ownership of str (may be NULL) is passed to - * the undo token. We do not need undo.push_own() since - * we checked for undo.enabled before. - * This avoids any EOL translation as that would be cumbersome - * and could also modify the clipboard in unexpected ways. - */ - str = interface.get_clipboard(get_clipboard_name(), &str_len); - undo.push(get_clipboard_name(), str, str_len); -} - -gchar * -QRegisterClipboard::get_string(gsize *out_len) -{ - if (!(Flags::ed & Flags::ED_AUTOEOL)) { - /* - * No auto-eol conversion - avoid unnecessary copying - * and allocations. - * NOTE: get_clipboard() already returns a null-terminated string. - */ - return interface.get_clipboard(get_clipboard_name(), out_len); - } - - gsize str_len; - gchar *str = interface.get_clipboard(get_clipboard_name(), &str_len); - EOLReaderMem reader(str, str_len); - gchar *str_converted; - - try { - str_converted = reader.convert_all(out_len); - } catch (...) { - g_free(str); - throw; /* forward */ - } - g_free(str); - - return str_converted; -} - -gchar * -QRegisterClipboard::get_string(void) -{ - return QRegisterClipboard::get_string(NULL); -} - -gsize -QRegisterClipboard::get_string_size(void) -{ - gsize str_len; - gchar *str = interface.get_clipboard(get_clipboard_name(), &str_len); - /* - * Using the EOLReader does not hurt much if AutoEOL is disabled - * since we use it only for counting the bytes. - */ - EOLReaderMem reader(str, str_len); - gsize data_len; - gsize converted_len = 0; - - try { - while (reader.convert(data_len)) - converted_len += data_len; - } catch (...) { - g_free(str); - throw; /* forward */ - } - g_free(str); - - return converted_len; -} - -gint -QRegisterClipboard::get_character(gint position) -{ - gsize str_len; - gchar *str = QRegisterClipboard::get_string(&str_len); - gint ret = -1; - - /* - * `str` may be NULL, but only if str_len == 0 as well. - */ - if (position >= 0 && - position < (gint)str_len) - ret = str[position]; - - g_free(str); - return ret; -} - -void -QRegisterClipboard::edit(void) -{ - gchar *str; - gsize str_len; - - QRegister::edit(); - - QRegisters::view.ssm(SCI_BEGINUNDOACTION); - QRegisters::view.ssm(SCI_CLEARALL); - str = QRegisterClipboard::get_string(&str_len); - QRegisters::view.ssm(SCI_APPENDTEXT, str_len, (sptr_t)str); - g_free(str); - QRegisters::view.ssm(SCI_ENDUNDOACTION); - - QRegisters::view.undo_ssm(SCI_UNDO); -} - -void -QRegisterClipboard::exchange_string(QRegisterData ®) -{ - gchar *own_str; - gsize own_str_len; - gchar *other_str = reg.get_string(); - gsize other_str_len = reg.get_string_size(); - - /* - * FIXME: What if `reg` is a clipboard and it changes - * between the two calls? - * QRegister::get_string() should always return the length as well. - */ - QRegisterData::set_string(other_str, other_str_len); - g_free(other_str); - own_str = QRegisterClipboard::get_string(&own_str_len); - reg.set_string(own_str, own_str_len); - g_free(own_str); -} - -void -QRegisterClipboard::undo_exchange_string(QRegisterData ®) -{ - QRegisterClipboard::undo_set_string(); - reg.undo_set_string(); -} - -void -QRegisterTable::UndoTokenRemoveGlobal::run(void) -{ - delete (QRegister *)QRegisters::globals.remove(reg); -} - -void -QRegisterTable::UndoTokenRemoveLocal::run(void) -{ - /* - * NOTE: QRegisters::locals should point - * to the correct table when the token is - * executed. - */ - delete (QRegister *)QRegisters::locals->remove(reg); -} - -void -QRegisterTable::undo_remove(QRegister *reg) -{ - if (!must_undo) - return; - - /* - * NOTE: Could also be solved using a virtual - * method and subclasses... - */ - if (this == &QRegisters::globals) - undo.push(reg); - else - undo.push(reg); -} - -void -QRegisterTable::insert_defaults(void) -{ - /* general purpose registers */ - for (gchar q = 'A'; q <= 'Z'; q++) - insert(q); - for (gchar q = '0'; q <= '9'; q++) - insert(q); -} - -/* - * NOTE: by not making this inline, - * we can access QRegisters::current - */ -void -QRegisterTable::edit(QRegister *reg) -{ - reg->edit(); - QRegisters::current = reg; -} - -/** - * Import process environment into table - * by setting environment registers for every - * environment variable. - * It is assumed that the table does not yet - * contain any environment register. - * - * In general this method is only safe to call - * at startup. - */ -void -QRegisterTable::set_environ(void) -{ - /* - * NOTE: Using g_get_environ() would be more efficient, - * but it appears to be broken, at least on Wine - * and Windows 2000. - */ - gchar **env = g_listenv(); - - for (gchar **key = env; *key; key++) { - gchar name[1 + strlen(*key) + 1]; - QRegister *reg; - - name[0] = '$'; - strcpy(name + 1, *key); - - reg = insert(name); - reg->set_string(g_getenv(*key)); - } - - g_strfreev(env); -} - -/** - * Export environment registers as a list of environment - * variables compatible with `g_get_environ()`. - * - * @return Zero-terminated list of strings in the form - * `NAME=VALUE`. Should be freed with `g_strfreev()`. - */ -gchar ** -QRegisterTable::get_environ(void) -{ - QRegister *first = nfind("$"); - - gint envp_len = 1; - gchar **envp, **p; - - /* - * Iterate over all registers beginning with "$" to - * guess the size required for the environment array. - * This may waste a few bytes because not __every__ - * register beginning with "$" is an environment - * register. - */ - for (QRegister *cur = first; - cur && cur->name[0] == '$'; - cur = (QRegister *)cur->next()) - envp_len++; - - p = envp = (gchar **)g_malloc(sizeof(gchar *)*envp_len); - - for (QRegister *cur = first; - cur && cur->name[0] == '$'; - cur = (QRegister *)cur->next()) { - gchar *value; - - /* - * Ignore the "$" register (not an environment - * variable register) and registers whose - * name contains "=" (not allowed in environment - * variable names). - */ - if (!cur->name[1] || strchr(cur->name+1, '=')) - continue; - - value = cur->get_string(); - /* more efficient than g_environ_setenv() */ - *p++ = g_strconcat(cur->name+1, "=", value, NIL); - g_free(value); - } - - *p = NULL; - - return envp; -} - -/** - * Update process environment with environment registers - * using `g_setenv()`. - * It does not try to unset environment variables that - * are no longer in the Q-Register table. - * - * This method may be dangerous in a multi-threaded environment - * but may be necessary for libraries that access important - * environment variables internally without providing alternative - * APIs. - */ -void -QRegisterTable::update_environ(void) -{ - for (QRegister *cur = nfind("$"); - cur && cur->name[0] == '$'; - cur = (QRegister *)cur->next()) { - gchar *value; - - /* - * Ignore the "$" register (not an environment - * variable register) and registers whose - * name contains "=" (not allowed in environment - * variable names). - */ - if (!cur->name[1] || strchr(cur->name+1, '=')) - continue; - - value = cur->get_string(); - g_setenv(cur->name+1, value, TRUE); - g_free(value); - } -} - -/** - * Free resources associated with table. - * - * This is similar to the destructor but - * has the advantage that we can check whether some - * register is currently edited. - * Since this is not a destructor, we can throw - * errors. - * Therefore this method should be called before - * a (local) QRegisterTable is deleted. - */ -void -QRegisterTable::clear(void) -{ - QRegister *cur; - - while ((cur = (QRegister *)root())) { - if (cur == QRegisters::current) - throw Error("Currently edited Q-Register \"%s\" " - "cannot be discarded", cur->name); - - delete (QRegister *)remove(cur); - } -} - -void -QRegisterStack::UndoTokenPush::run(void) -{ - SLIST_INSERT_HEAD(&stack->head, entry, entries); - entry = NULL; -} - -void -QRegisterStack::UndoTokenPop::run(void) -{ - Entry *entry = SLIST_FIRST(&stack->head); - - SLIST_REMOVE_HEAD(&stack->head, entries); - delete entry; -} - -void -QRegisterStack::push(QRegister ®) -{ - Entry *entry = new Entry(); - - gchar *str = reg.get_string(); - if (*str) - entry->set_string(str); - g_free(str); - entry->string.update(reg.string); - entry->set_integer(reg.get_integer()); - - SLIST_INSERT_HEAD(&head, entry, entries); - undo.push(this); -} - -bool -QRegisterStack::pop(QRegister ®) -{ - Entry *entry = SLIST_FIRST(&head); - - if (!entry) - return false; - - reg.undo_set_integer(); - reg.set_integer(entry->get_integer()); - - /* exchange document ownership between Stack entry and Q-Register */ - reg.undo_exchange_string(*entry); - reg.exchange_string(*entry); - - SLIST_REMOVE_HEAD(&head, entries); - /* Pass entry ownership to undo stack. */ - undo.push_own(this, entry); - - return true; -} - -QRegisterStack::~QRegisterStack() -{ - Entry *entry, *next; - - SLIST_FOREACH_SAFE(entry, &head, entries, next) - delete entry; -} - -void -QRegisters::hook(Hook type) -{ - static const gchar *type2name[] = { - /* [HOOK_ADD-1] = */ "ADD", - /* [HOOK_EDIT-1] = */ "EDIT", - /* [HOOK_CLOSE-1] = */ "CLOSE", - /* [HOOK_QUIT-1] = */ "QUIT", - }; - - QRegister *reg; - - if (!(Flags::ed & Flags::ED_HOOKS)) - return; - - try { - reg = globals["ED"]; - if (!reg) - throw Error("Undefined ED-hook register (\"ED\")"); - - /* - * ED-hook execution should not see any - * integer parameters but the hook type. - * Such parameters could confuse the ED macro - * and macro authors do not expect side effects - * of ED macros on the expression stack. - * Also make sure it does not leave behind - * additional arguments on the stack. - * - * So this effectively executes: - * (typeM[ED]^[) - */ - expressions.brace_open(); - expressions.push(type); - reg->execute(); - expressions.discard_args(); - expressions.brace_close(); - } catch (Error &error) { - const gchar *type_str = type2name[type-1]; - - error.add_frame(new Error::EDHookFrame(type_str)); - throw; /* forward */ - } -} - -void -QRegSpecMachine::reset(void) -{ - MicroStateMachine::reset(); - string_machine.reset(); - undo.push_var(is_local) = false; - undo.push_var(nesting) = 0; - undo.push_str(name); - g_free(name); - name = NULL; -} - -bool -QRegSpecMachine::input(gchar chr, QRegister *&result) -{ - gchar *insert; - -MICROSTATE_START; - switch (chr) { - case '#': - set(&&StateFirstChar); - break; - case '[': - set(&&StateString); - undo.push_var(nesting)++; - break; - case '.': - if (!is_local) { - undo.push_var(is_local) = true; - break; - } - /* fall through */ - default: - undo.push_str(name) = String::chrdup(String::toupper(chr)); - goto done; - } - - return false; - -StateFirstChar: - undo.push_str(name) = (gchar *)g_malloc(3); - name[0] = String::toupper(chr); - name[1] = '\0'; - set(&&StateSecondChar); - return false; - -StateSecondChar: - name[1] = String::toupper(chr); - name[2] = '\0'; - goto done; - -StateString: - switch (chr) { - case '[': - undo.push_var(nesting)++; - break; - case ']': - undo.push_var(nesting)--; - if (!nesting) - goto done; - break; - } - - if (mode > MODE_NORMAL) - return false; - - if (!string_machine.input(chr, insert)) - return false; - - undo.push_str(name); - String::append(name, insert); - g_free(insert); - return false; - -done: - if (mode > MODE_NORMAL) { - /* - * StateExpectQRegs with type != OPTIONAL - * will never see this NULL pointer beyond - * BEGIN_EXEC() - */ - result = NULL; - return true; - } - - QRegisterTable &table = is_local ? *QRegisters::locals - : QRegisters::globals; - - switch (type) { - case QREG_REQUIRED: - result = table[name]; - if (!result) - fail(); - break; - - case QREG_OPTIONAL: - result = table[name]; - break; - - case QREG_OPTIONAL_INIT: - result = table[name]; - if (!result) { - result = table.insert(name); - table.undo_remove(result); - } - break; - } - - return true; -} - -gchar * -QRegSpecMachine::auto_complete(void) -{ - gsize restrict_len = 0; - - if (string_machine.qregspec_machine) - /* nested Q-Reg definition */ - return string_machine.qregspec_machine->auto_complete(); - - if (state == StateStart) - /* single-letter Q-Reg */ - restrict_len = 1; - else if (!nesting) - /* two-letter Q-Reg */ - restrict_len = 2; - - QRegisterTable &table = is_local ? *QRegisters::locals - : QRegisters::globals; - return table.auto_complete(name, nesting == 1 ? ']' : '\0', - restrict_len); -} - -/* - * Command states - */ - -StateExpectQReg::StateExpectQReg(QRegSpecType type) : machine(type) -{ - transitions['\0'] = this; -} - -State * -StateExpectQReg::custom(gchar chr) -{ - QRegister *reg; - - if (!machine.input(chr, reg)) - return this; - - return got_register(reg); -} - -/*$ "[" "[q" push - * [q -- Save Q-Register - * - * Save Q-Register contents on the global Q-Register push-down - * stack. - */ -State * -StatePushQReg::got_register(QRegister *reg) -{ - machine.reset(); - - BEGIN_EXEC(&States::start); - QRegisters::stack.push(*reg); - return &States::start; -} - -/*$ "]" "]q" pop - * ]q -- Restore Q-Register - * - * Restore Q-Register by replacing its contents - * with the contents of the register saved on top of - * the Q-Register push-down stack. - * The stack entry is popped. - * - * In interactive mode, the original contents of - * are not immediately reclaimed but are kept in memory - * to support rubbing out the command. - * Memory is reclaimed on command-line termination. - */ -State * -StatePopQReg::got_register(QRegister *reg) -{ - machine.reset(); - - BEGIN_EXEC(&States::start); - - if (!QRegisters::stack.pop(*reg)) - throw Error("Q-Register stack is empty"); - - return &States::start; -} - -/*$ EQ EQq - * EQq$ -- Edit or load Q-Register - * EQq[file]$ - * - * When specified with an empty string argument, - * EQ makes the currently edited Q-Register. - * Otherwise, when is specified, it is the - * name of a file to read into Q-Register . - * When loading a file, the currently edited - * buffer/register is not changed and the edit position - * of register is reset to 0. - * - * Undefined Q-Registers will be defined. - * The command fails if could not be read. - */ -State * -StateEQCommand::got_register(QRegister *reg) -{ - machine.reset(); - - BEGIN_EXEC(&States::loadqreg); - undo.push_var(register_argument) = reg; - return &States::loadqreg; -} - -State * -StateLoadQReg::got_file(const gchar *filename) -{ - BEGIN_EXEC(&States::start); - - if (*filename) { - /* Load file into Q-Register */ - register_argument->load(filename); - } else { - /* Edit Q-Register */ - current_doc_undo_edit(); - QRegisters::globals.edit(register_argument); - } - - return &States::start; -} - -/*$ E% E%q - * E%q$ -- Save Q-Register string to file - * - * Saves the string contents of Q-Register to - * . - * The must always be specified, as Q-Registers - * have no notion of associated file names. - * - * In interactive mode, the E% command may be rubbed out, - * restoring the previous state of . - * This follows the same rules as with the \fBEW\fP command. - * - * File names may also be tab-completed and string building - * characters are enabled by default. - */ -State * -StateEPctCommand::got_register(QRegister *reg) -{ - machine.reset(); - - BEGIN_EXEC(&States::saveqreg); - undo.push_var(register_argument) = reg; - return &States::saveqreg; -} - -State * -StateSaveQReg::got_file(const gchar *filename) -{ - BEGIN_EXEC(&States::start); - register_argument->save(filename); - return &States::start; -} - -/*$ Q Qq query - * Qq -> n -- Query Q-Register existence, its integer or string characters - * Qq -> character - * :Qq -> -1 | size - * - * Without any arguments, get and return the integer-part of - * Q-Register . - * - * With one argument, return the code at - * from the string-part of Q-Register . - * Positions are handled like buffer positions \(em they - * begin at 0 up to the length of the string minus 1. - * An error is thrown for invalid positions. - * Both non-colon-modified forms of Q require register - * to be defined and fail otherwise. - * - * When colon-modified, Q does not pop any arguments from - * the expression stack and returns the of the string - * in Q-Register if register exists (i.e. is defined). - * Naturally, for empty strings, 0 is returned. - * When colon-modified and Q-Register is undefined, - * -1 is returned instead. - * Therefore checking the return value \fB:Q\fP for values smaller - * 0 allows checking the existence of a register. - * Note that if exists, its string part is not initialized, - * so \fB:Q\fP may be used to handle purely numeric data structures - * without creating Scintilla documents by accident. - * These semantics allow the useful idiom \(lq:Q\fIq\fP">\(rq for - * checking whether a Q-Register exists and has a non-empty string. - * Note also that the return value of \fB:Q\fP may be interpreted - * as a condition boolean that represents the non-existence of . - * If is undefined, it returns \fIsuccess\fP, else a \fIfailure\fP - * boolean. - */ -State * -StateQueryQReg::got_register(QRegister *reg) -{ - /* like BEGIN_EXEC(&States::start), but resets machine */ - if (mode > MODE_NORMAL) - goto reset; - - expressions.eval(); - - if (eval_colon()) { - /* Query Q-Register's existence or string size */ - expressions.push(reg ? reg->get_string_size() - : (tecoInt)-1); - goto reset; - } - - /* - * NOTE: This command is special since the QRegister is required - * without colon and otherwise optional. - * While it may be clearer to model this as two States, - * we cannot currently let parsing depend on the colon-modifier. - * That's why we have to declare the Q-Reg machine as QREG_OPTIONAL - * and care about exception throwing on our own. - */ - if (!reg) - machine.fail(); - - if (expressions.args() > 0) { - /* Query character from Q-Register string */ - gint c = reg->get_character(expressions.pop_num_calc()); - if (c < 0) - throw RangeError('Q'); - expressions.push(c); - } else { - /* Query integer */ - expressions.push(reg->get_integer()); - } - -reset: - machine.reset(); - return &States::start; -} - -/*$ ^Uq - * [c1,c2,...]^Uq[string]$ -- Set or append to Q-Register string without string building - * [c1,c2,...]:^Uq[string]$ - * - * If not colon-modified, it first fills the Q-Register - * with all the values on the expression stack (interpreted as - * codepoints). - * It does so in the order of the arguments, i.e. - * will be the first character in , the second, etc. - * Eventually the argument is appended to the - * register. - * Any existing string value in is overwritten by this operation. - * - * In the colon-modified form ^U does not overwrite existing - * contents of but only appends to it. - * - * If is undefined, it will be defined. - * - * String-building characters are \fBdisabled\fP for ^U - * commands. - * Therefore they are especially well-suited for defining - * \*(ST macros, since string building characters in the - * desired Q-Register contents do not have to be escaped. - * The \fBEU\fP command may be used where string building - * is desired. - */ -State * -StateCtlUCommand::got_register(QRegister *reg) -{ - machine.reset(); - - BEGIN_EXEC(&States::setqregstring_nobuilding); - undo.push_var(register_argument) = reg; - return &States::setqregstring_nobuilding; -} - -/*$ EU EUq - * [c1,c2,...]EUq[string]$ -- Set or append to Q-Register string with string building characters - * [c1,c2,...]:EUq[string]$ - * - * This command sets or appends to the contents of - * Q-Register \fIq\fP. - * It is identical to the \fB^U\fP command, except - * that this form of the command has string building - * characters \fBenabled\fP. - */ -State * -StateEUCommand::got_register(QRegister *reg) -{ - machine.reset(); - - BEGIN_EXEC(&States::setqregstring_building); - undo.push_var(register_argument) = reg; - return &States::setqregstring_building; -} - -void -StateSetQRegString::initial(void) -{ - int args; - - expressions.eval(); - args = expressions.args(); - text_added = args > 0; - if (!args) - return; - - gchar buffer[args+1]; - - buffer[args] = '\0'; - while (args--) - buffer[args] = (gchar)expressions.pop_num_calc(); - - if (eval_colon()) { - /* append to register */ - register_argument->undo_append_string(); - register_argument->append_string(buffer); - } else { - /* set register */ - register_argument->undo_set_string(); - register_argument->set_string(buffer); - } -} - -State * -StateSetQRegString::done(const gchar *str) -{ - BEGIN_EXEC(&States::start); - - if (text_added || eval_colon()) { - /* - * Append to register: - * Note that append_string() does not create an UNDOACTION - * if str == NULL - */ - if (str) { - register_argument->undo_append_string(); - register_argument->append_string(str); - } - } else { - /* set register */ - register_argument->undo_set_string(); - register_argument->set_string(str); - } - - return &States::start; -} - -/*$ G Gq get - * Gq -- Insert Q-Register string - * - * Inserts the string of Q-Register into the buffer - * at its current position. - * Specifying an undefined yields an error. - */ -State * -StateGetQRegString::got_register(QRegister *reg) -{ - gchar *str; - - machine.reset(); - - BEGIN_EXEC(&States::start); - - str = reg->get_string(); - if (*str) { - interface.ssm(SCI_BEGINUNDOACTION); - interface.ssm(SCI_ADDTEXT, strlen(str), (sptr_t)str); - interface.ssm(SCI_SCROLLCARET); - interface.ssm(SCI_ENDUNDOACTION); - ring.dirtify(); - - interface.undo_ssm(SCI_UNDO); - } - g_free(str); - - return &States::start; -} - -/*$ U Uq - * nUq -- Set Q-Register integer - * -Uq - * [n]:Uq -> Success|Failure - * - * Sets the integer-part of Q-Register to . - * \(lq-U\(rq is equivalent to \(lq-1U\(rq, otherwise - * the command fails if is missing. - * - * If the command is colon-modified, it returns a success - * boolean if or \(lq-\(rq is given. - * Otherwise it returns a failure boolean and does not - * modify . - * - * The register is defined if it does not exist. - */ -State * -StateSetQRegInteger::got_register(QRegister *reg) -{ - machine.reset(); - - BEGIN_EXEC(&States::start); - - expressions.eval(); - if (expressions.args() || expressions.num_sign < 0) { - reg->undo_set_integer(); - reg->set_integer(expressions.pop_num_calc()); - - if (eval_colon()) - expressions.push(SUCCESS); - } else if (eval_colon()) { - expressions.push(FAILURE); - } else { - throw ArgExpectedError('U'); - } - - return &States::start; -} - -/*$ % %q increment - * [n]%q -> q+n -- Increase Q-Register integer - * - * Add to the integer part of register , returning - * its new value. - * will be defined if it does not exist. - */ -State * -StateIncreaseQReg::got_register(QRegister *reg) -{ - tecoInt res; - - machine.reset(); - - BEGIN_EXEC(&States::start); - - reg->undo_set_integer(); - res = reg->get_integer() + expressions.pop_num_calc(); - expressions.push(reg->set_integer(res)); - - return &States::start; -} - -/*$ M Mq eval - * Mq -- Execute macro - * :Mq - * - * Execute macro stored in string of Q-Register . - * The command itself does not push or pop and arguments from the stack - * but the macro executed might well do so. - * The new macro invocation level will contain its own go-to label table - * and local Q-Register table. - * Except when the command is colon-modified - in this case, local - * Q-Registers referenced in the macro refer to the parent macro-level's - * local Q-Register table (or whatever level defined one last). - * - * Errors during the macro execution will propagate to the M command. - * In other words if a command in the macro fails, the M command will fail - * and this failure propagates until the top-level macro (e.g. - * the command-line macro). - * - * Note that the string of will be copied upon macro execution, - * so subsequent changes to Q-Register from inside the macro do - * not modify the executed code. - */ -State * -StateMacro::got_register(QRegister *reg) -{ - machine.reset(); - - BEGIN_EXEC(&States::start); - /* don't create new local Q-Registers if colon modifier is given */ - reg->execute(!eval_colon()); - return &States::start; -} - -/*$ EM - * EMfile$ -- Execute macro from file - * :EMfile$ - * - * Read the file with name into memory and execute its contents - * as a macro. - * It is otherwise similar to the \(lqM\(rq command. - * - * If could not be read, the command yields an error. - */ -State * -StateMacroFile::got_file(const gchar *filename) -{ - BEGIN_EXEC(&States::start); - /* don't create new local Q-Registers if colon modifier is given */ - Execute::file(filename, !eval_colon()); - return &States::start; -} - -/*$ X Xq - * [lines]Xq -- Copy into or append to Q-Register - * -Xq - * from,toXq - * [lines]:Xq - * -:Xq - * from,to:Xq - * - * Copy the next or previous number of from the buffer - * into the Q-Register string. - * If is omitted, the sign prefix is implied. - * If two arguments are specified, the characters beginning - * at position up to the character at position - * are copied. - * The semantics of the arguments is analogous to the K - * command's arguments. - * If the command is colon-modified, the characters will be - * appended to the end of register instead. - * - * Register will be created if it is undefined. - */ -State * -StateCopyToQReg::got_register(QRegister *reg) -{ - tecoInt from, len; - Sci_TextRange tr; - - machine.reset(); - - BEGIN_EXEC(&States::start); - expressions.eval(); - - if (expressions.args() <= 1) { - from = interface.ssm(SCI_GETCURRENTPOS); - sptr_t line = interface.ssm(SCI_LINEFROMPOSITION, from) + - expressions.pop_num_calc(); - - if (!Validate::line(line)) - throw RangeError("X"); - - len = interface.ssm(SCI_POSITIONFROMLINE, line) - from; - - if (len < 0) { - from += len; - len *= -1; - } - } else { - tecoInt to = expressions.pop_num(); - from = expressions.pop_num(); - - len = to - from; - - if (len < 0 || !Validate::pos(from) || !Validate::pos(to)) - throw RangeError("X"); - } - - tr.chrg.cpMin = from; - tr.chrg.cpMax = from + len; - tr.lpstrText = (char *)g_malloc(len + 1); - interface.ssm(SCI_GETTEXTRANGE, 0, (sptr_t)&tr); - - if (eval_colon()) { - reg->undo_append_string(); - reg->append_string(tr.lpstrText); - } else { - reg->undo_set_string(); - reg->set_string(tr.lpstrText); - } - g_free(tr.lpstrText); - - return &States::start; -} - -} /* namespace SciTECO */ diff --git a/src/qregisters.h b/src/qregisters.h deleted file mode 100644 index a08bafe..0000000 --- a/src/qregisters.h +++ /dev/null @@ -1,666 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifndef __QREGISTERS_H -#define __QREGISTERS_H - -#include - -#include - -#include -#include - -#include - -#include "sciteco.h" -#include "memory.h" -#include "error.h" -#include "interface.h" -#include "ioview.h" -#include "undo.h" -#include "rbtree.h" -#include "parser.h" -#include "document.h" - -namespace SciTECO { - -namespace QRegisters { - /* initialized after Interface::main() in main() */ - extern IOView view; -} - -/* - * Classes - */ - -class QRegisterData : public Object { -protected: - tecoInt integer; - - class QRegisterString : public Document { - public: - ~QRegisterString() - { - release_document(); - } - - private: - ViewCurrent & - get_create_document_view(void) - { - return QRegisters::view; - } - } string; - -public: - /* - * Whether to generate UndoTokens (unnecessary in macro invocations). - * - * FIXME: Every QRegister has this field, but it only differs - * between local and global QRegisters. This wastes space. - * There must be a more clever way to inherit this property, e.g. - * by setting QRegisters::current_must_undo. - */ - bool must_undo; - - QRegisterData() : integer(0), must_undo(true) {} - virtual ~QRegisterData() {} - - virtual tecoInt - set_integer(tecoInt i) - { - return integer = i; - } - virtual void - undo_set_integer(void) - { - if (must_undo) - undo.push_var(integer); - } - virtual tecoInt - get_integer(void) - { - return integer; - } - - virtual void set_string(const gchar *str, gsize len); - inline void - set_string(const gchar *str) - { - set_string(str, str ? strlen(str) : 0); - } - virtual void undo_set_string(void); - - virtual void append_string(const gchar *str, gsize len); - inline void - append_string(const gchar *str) - { - append_string(str, str ? strlen(str) : 0); - } - virtual inline void - undo_append_string(void) - { - undo_set_string(); - } - virtual gchar *get_string(void); - virtual gsize get_string_size(void); - virtual gint get_character(gint position); - - virtual void - exchange_string(QRegisterData ®) - { - string.exchange(reg.string); - } - virtual void undo_exchange_string(QRegisterData ®); - - /* - * The QRegisterStack must currently still access the - * string fields directly to exchange data efficiently. - */ - friend class QRegisterStack; -}; - -class QRegister : public RBTreeString::RBEntryOwnString, public QRegisterData { -protected: - /** - * The default constructor for subclasses. - * This leaves the name uninitialized. - */ - QRegister() {} - -public: - QRegister(const gchar *name) - : RBTreeString::RBEntryOwnString(name) {} - - virtual ~QRegister() {} - - virtual void edit(void); - virtual void undo_edit(void); - - void execute(bool locals = true); - - void undo_set_eol_mode(void); - void set_eol_mode(gint mode); - - /* - * Load and save already care about undo token - * creation. - */ - void load(const gchar *filename); - void save(const gchar *filename); -}; - -class QRegisterBufferInfo : public QRegister { -public: - QRegisterBufferInfo() : QRegister("*") {} - - /* setting "*" is equivalent to nEB */ - tecoInt set_integer(tecoInt v); - void undo_set_integer(void); - - tecoInt get_integer(void); - - void - set_string(const gchar *str, gsize len) - { - throw QRegOpUnsupportedError(name); - } - void undo_set_string(void) {} - - void - append_string(const gchar *str, gsize len) - { - throw QRegOpUnsupportedError(name); - } - void undo_append_string(void) {} - - gchar *get_string(void); - gsize get_string_size(void); - gint get_character(gint pos); - - void edit(void); -}; - -class QRegisterWorkingDir : public QRegister { -public: - QRegisterWorkingDir() : QRegister("$") {} - - void set_string(const gchar *str, gsize len); - void undo_set_string(void); - - void - append_string(const gchar *str, gsize len) - { - throw QRegOpUnsupportedError(name); - } - void undo_append_string(void) {} - - gchar *get_string(void); - gsize get_string_size(void); - gint get_character(gint pos); - - void edit(void); - - void exchange_string(QRegisterData ®); - void undo_exchange_string(QRegisterData ®); -}; - -class QRegisterClipboard : public QRegister { - class UndoTokenSetClipboard : public UndoToken { - gchar *name; - gchar *str; - gsize str_len; - - public: - /** - * Construct undo token. - * - * This passes ownership of the clipboard content string - * to the undo token object. - */ - UndoTokenSetClipboard(const gchar *_name, gchar *_str, gsize _str_len) - : name(g_strdup(_name)), str(_str), str_len(_str_len) {} - ~UndoTokenSetClipboard() - { - g_free(str); - g_free(name); - } - - void run(void); - }; - - /** - * Gets the clipboard name. - * Can be easily derived from the Q-Register name. - */ - inline const gchar * - get_clipboard_name(void) const - { - return name+1; - } - -public: - QRegisterClipboard(const gchar *_name = NULL) - { - name = g_strconcat("~", _name, NIL); - } - - void set_string(const gchar *str, gsize len); - void undo_set_string(void); - - /* - * FIXME: We could support that. - */ - void - append_string(const gchar *str, gsize len) - { - throw QRegOpUnsupportedError(name); - } - void undo_append_string(void) {} - - gchar *get_string(gsize *out_len); - gchar *get_string(void); - gsize get_string_size(void); - gint get_character(gint pos); - - void edit(void); - - void exchange_string(QRegisterData ®); - void undo_exchange_string(QRegisterData ®); -}; - -class QRegisterTable : private RBTreeString, public Object { - class UndoTokenRemoveGlobal : public UndoToken { - protected: - QRegister *reg; - - public: - UndoTokenRemoveGlobal(QRegister *_reg) - : reg(_reg) {} - - void run(void); - }; - - class UndoTokenRemoveLocal : public UndoTokenRemoveGlobal { - public: - UndoTokenRemoveLocal(QRegister *reg) - : UndoTokenRemoveGlobal(reg) {} - - void run(void); - }; - - bool must_undo; - -public: - QRegisterTable(bool _must_undo = true) - : must_undo(_must_undo) {} - - ~QRegisterTable() - { - QRegister *cur; - - while ((cur = (QRegister *)root())) - delete (QRegister *)remove(cur); - } - - void undo_remove(QRegister *reg); - - inline QRegister * - insert(QRegister *reg) - { - reg->must_undo = must_undo; - RBTreeString::insert(reg); - return reg; - } - inline QRegister * - insert(const gchar *name) - { - return insert(new QRegister(name)); - } - inline QRegister * - insert(gchar name) - { - gchar buf[] = {name, '\0'}; - return insert(buf); - } - - void insert_defaults(void); - - inline QRegister * - find(const gchar *name) - { - return (QRegister *)RBTreeString::find(name); - } - inline QRegister * - operator [](const gchar *name) - { - return find(name); - } - inline QRegister * - operator [](gchar chr) - { - gchar buf[] = {chr, '\0'}; - return find(buf); - } - - inline QRegister * - nfind(const gchar *name) - { - return (QRegister *)RBTreeString::nfind(name); - } - - void edit(QRegister *reg); - inline QRegister * - edit(const gchar *name) - { - QRegister *reg = find(name); - - if (!reg) - return NULL; - edit(reg); - return reg; - } - - void set_environ(void); - gchar **get_environ(void); - void update_environ(void); - - void clear(void); - - inline gchar * - auto_complete(const gchar *name, gchar completed = '\0', gsize max_len = 0) - { - return RBTreeString::auto_complete(name, completed, max_len); - } -}; - -class QRegisterStack : public Object { - class Entry : public QRegisterData { - public: - SLIST_ENTRY(Entry) entries; - - Entry() : QRegisterData() {} - }; - - class UndoTokenPush : public UndoToken { - QRegisterStack *stack; - /* only remaining reference to stack entry */ - Entry *entry; - - public: - UndoTokenPush(QRegisterStack *_stack, Entry *_entry) - : UndoToken(), stack(_stack), entry(_entry) {} - - ~UndoTokenPush() - { - delete entry; - } - - void run(void); - }; - - class UndoTokenPop : public UndoToken { - QRegisterStack *stack; - - public: - UndoTokenPop(QRegisterStack *_stack) - : stack(_stack) {} - - void run(void); - }; - - SLIST_HEAD(Head, Entry) head; - -public: - QRegisterStack() - { - SLIST_INIT(&head); - } - ~QRegisterStack(); - - void push(QRegister ®); - bool pop(QRegister ®); -}; - -enum QRegSpecType { - /** Register must exist, else fail */ - QREG_REQUIRED, - /** - * Return NULL if register does not exist. - * You can still call QRegSpecMachine::fail() to require it. - */ - QREG_OPTIONAL, - /** Initialize register if it does not already exist */ - QREG_OPTIONAL_INIT -}; - -class QRegSpecMachine : public MicroStateMachine { - StringBuildingMachine string_machine; - QRegSpecType type; - - bool is_local; - gint nesting; - gchar *name; - -public: - QRegSpecMachine(QRegSpecType _type = QREG_REQUIRED) - : MicroStateMachine(), - type(_type), - is_local(false), nesting(0), name(NULL) {} - - ~QRegSpecMachine() - { - g_free(name); - } - - void reset(void); - - bool input(gchar chr, QRegister *&result); - - inline void - fail(void) G_GNUC_NORETURN - { - throw InvalidQRegError(name, is_local); - } - - gchar *auto_complete(void); -}; - -/* - * Command states - */ - -/** - * Super class for states accepting Q-Register specifications - */ -class StateExpectQReg : public State { -protected: - QRegSpecMachine machine; - -public: - StateExpectQReg(QRegSpecType type = QREG_REQUIRED); - -private: - State *custom(gchar chr); - -protected: - /** - * Called when a register specification has been - * successfully parsed. - * The QRegSpecMachine is not reset automatically. - * - * @param reg Register of the parsed Q-Reg - * specification. May be NULL in - * parse-only mode or with QREG_OPTIONAL. - * @returns Next parser state. - */ - virtual State *got_register(QRegister *reg) = 0; - - /* in cmdline.cpp */ - void process_edit_cmd(gchar key); -}; - -class StatePushQReg : public StateExpectQReg { -private: - State *got_register(QRegister *reg); -}; - -class StatePopQReg : public StateExpectQReg { -public: - StatePopQReg() : StateExpectQReg(QREG_OPTIONAL_INIT) {} - -private: - State *got_register(QRegister *reg); -}; - -class StateEQCommand : public StateExpectQReg { -public: - StateEQCommand() : StateExpectQReg(QREG_OPTIONAL_INIT) {} - -private: - State *got_register(QRegister *reg); -}; - -class StateLoadQReg : public StateExpectFile { -private: - State *got_file(const gchar *filename); -}; - -class StateEPctCommand : public StateExpectQReg { -private: - State *got_register(QRegister *reg); -}; - -class StateSaveQReg : public StateExpectFile { -private: - State *got_file(const gchar *filename); -}; - -class StateQueryQReg : public StateExpectQReg { -public: - StateQueryQReg() : StateExpectQReg(QREG_OPTIONAL) {} - -private: - State *got_register(QRegister *reg); -}; - -class StateCtlUCommand : public StateExpectQReg { -public: - StateCtlUCommand() : StateExpectQReg(QREG_OPTIONAL_INIT) {} - -private: - State *got_register(QRegister *reg); -}; - -class StateEUCommand : public StateExpectQReg { -public: - StateEUCommand() : StateExpectQReg(QREG_OPTIONAL_INIT) {} - -private: - State *got_register(QRegister *reg); -}; - -class StateSetQRegString : public StateExpectString { - bool text_added; - -public: - StateSetQRegString(bool building) - : StateExpectString(building) {} - -private: - void initial(void); - State *done(const gchar *str); -}; - -class StateGetQRegString : public StateExpectQReg { -private: - State *got_register(QRegister *reg); -}; - -class StateSetQRegInteger : public StateExpectQReg { -public: - StateSetQRegInteger() : StateExpectQReg(QREG_OPTIONAL_INIT) {} - -private: - State *got_register(QRegister *reg); -}; - -class StateIncreaseQReg : public StateExpectQReg { -public: - StateIncreaseQReg() : StateExpectQReg(QREG_OPTIONAL_INIT) {} - -private: - State *got_register(QRegister *reg); -}; - -class StateMacro : public StateExpectQReg { -private: - State *got_register(QRegister *reg); -}; - -class StateMacroFile : public StateExpectFile { -private: - State *got_file(const gchar *filename); -}; - -class StateCopyToQReg : public StateExpectQReg { -public: - StateCopyToQReg() : StateExpectQReg(QREG_OPTIONAL_INIT) {} - -private: - State *got_register(QRegister *reg); -}; - -namespace States { - extern StatePushQReg pushqreg; - extern StatePopQReg popqreg; - extern StateEQCommand eqcommand; - extern StateLoadQReg loadqreg; - extern StateEPctCommand epctcommand; - extern StateSaveQReg saveqreg; - extern StateQueryQReg queryqreg; - extern StateCtlUCommand ctlucommand; - extern StateEUCommand eucommand; - extern StateSetQRegString setqregstring_nobuilding; - extern StateSetQRegString setqregstring_building; - extern StateGetQRegString getqregstring; - extern StateSetQRegInteger setqreginteger; - extern StateIncreaseQReg increaseqreg; - extern StateMacro macro; - extern StateMacroFile macro_file; - extern StateCopyToQReg copytoqreg; -} - -namespace QRegisters { - /* object declared in main.cpp */ - extern QRegisterTable globals; - extern QRegisterTable *locals; - extern QRegister *current; - - enum Hook { - HOOK_ADD = 1, - HOOK_EDIT, - HOOK_CLOSE, - HOOK_QUIT - }; - void hook(Hook type); -} - -} /* namespace SciTECO */ - -#endif diff --git a/src/rb3str.c b/src/rb3str.c new file mode 100644 index 0000000..37dff79 --- /dev/null +++ b/src/rb3str.c @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +/* + * NOTE: Must be included only once. + */ +//#include + +#include "sciteco.h" +#include "interface.h" +#include "string-utils.h" +#include "rb3str.h" + +static gint +teco_rb3str_cmp(const teco_rb3str_head_t *head, const teco_string_t *data) +{ + return teco_string_cmp(&head->key, data->data, data->len); +} + +static gint +teco_rb3str_casecmp(const teco_rb3str_head_t *head, const teco_string_t *data) +{ + return teco_string_casecmp(&head->key, data->data, data->len); +} + +/** @memberof teco_rb3str_tree_t */ +teco_rb3str_head_t * +teco_rb3str_insert(teco_rb3str_tree_t *tree, gboolean case_sensitive, teco_rb3str_head_t *head) +{ + rb3_cmp *cmp = case_sensitive ? (rb3_cmp *)teco_rb3str_cmp : (rb3_cmp *)teco_rb3str_casecmp; + return (teco_rb3str_head_t *)rb3_insert(tree, &head->head, cmp, &head->key); +} + +/** @memberof teco_rb3str_tree_t */ +teco_rb3str_head_t * +teco_rb3str_find(teco_rb3str_tree_t *tree, gboolean case_sensitive, const gchar *str, gsize len) +{ + rb3_cmp *cmp = case_sensitive ? (rb3_cmp *)teco_rb3str_cmp : (rb3_cmp *)teco_rb3str_casecmp; + teco_string_t data = {(gchar *)str, len}; + return (teco_rb3str_head_t *)rb3_find(tree, cmp, &data); +} + +/** @memberof teco_rb3str_tree_t */ +teco_rb3str_head_t * +teco_rb3str_nfind(teco_rb3str_tree_t *tree, gboolean case_sensitive, const gchar *str, gsize len) +{ + rb3_cmp *cmp = case_sensitive ? (rb3_cmp *)teco_rb3str_cmp : (rb3_cmp *)teco_rb3str_casecmp; + teco_string_t data = {(gchar *)str, len}; + + /* + * This is based on rb3_INLINE_find() in rb3ptr.h. + * Alternatively, we might adapt/wrap teco_rb3str_cmp() in order to store + * the last element > data. + */ + struct rb3_head *parent = rb3_get_base(tree); + struct rb3_head *res = NULL; + int dir = RB3_LEFT; + while (rb3_has_child(parent, dir)) { + parent = rb3_get_child(parent, dir); + int r = cmp(parent, &data); + if (r == 0) + return (teco_rb3str_head_t *)parent; + dir = (r < 0) ? RB3_RIGHT : RB3_LEFT; + if (dir == RB3_LEFT) + res = parent; + } + + return (teco_rb3str_head_t *)res; +} + +/** + * Auto-complete string given the entries of a RB tree. + * + * @param tree The RB tree (root). + * @param case_sensitive Whether to match case-sensitive. + * @param str String to complete (not necessarily null-terminated). + * @param str_len Length of characters in `str`. + * @param restrict_len Limit completions to this size. + * @param insert String to set with characters that can be autocompleted. + * @return TRUE if the completion was unambiguous, else FALSE. + * + * @memberof teco_rb3str_tree_t + */ +gboolean +teco_rb3str_auto_complete(teco_rb3str_tree_t *tree, gboolean case_sensitive, + const gchar *str, gsize str_len, gsize restrict_len, teco_string_t *insert) +{ + memset(insert, 0, sizeof(*insert)); + + teco_string_diff_t diff = case_sensitive ? teco_string_diff : teco_string_casediff; + teco_rb3str_head_t *first = NULL; + gsize prefix_len = 0; + guint prefixed_entries = 0; + + for (teco_rb3str_head_t *cur = teco_rb3str_nfind(tree, case_sensitive, str, str_len); + cur && cur->key.len >= str_len && diff(&cur->key, str, str_len) == str_len; + cur = teco_rb3str_get_next(cur)) { + if (restrict_len && cur->key.len != restrict_len) + continue; + + if (G_UNLIKELY(!first)) { + first = cur; + prefix_len = cur->key.len - str_len; + } else { + gsize len = diff(&cur->key, first->key.data, first->key.len) - str_len; + if (len < prefix_len) + prefix_len = len; + } + + prefixed_entries++; + } + + if (prefix_len > 0) { + teco_string_init(insert, first->key.data + str_len, prefix_len); + } else if (prefixed_entries > 1) { + for (teco_rb3str_head_t *cur = first; + cur && cur->key.len >= str_len && diff(&cur->key, str, str_len) == str_len; + cur = teco_rb3str_get_next(cur)) { + if (restrict_len && cur->key.len != restrict_len) + continue; + + teco_interface_popup_add(TECO_POPUP_PLAIN, + cur->key.data, cur->key.len, FALSE); + } + + teco_interface_popup_show(); + } + + return prefixed_entries == 1; +} diff --git a/src/rb3str.h b/src/rb3str.h new file mode 100644 index 0000000..a34362e --- /dev/null +++ b/src/rb3str.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#pragma once + +#include + +#include + +#include "sciteco.h" +#include "string-utils.h" + +/** + * A RB-tree with teco_string_t keys. + * + * NOTE: If the tree's keys do not change and you will never have to free + * an individual node, consider storing the keys in GStringChunk or + * allocating them via teco_string_init_chunk(), as this is faster + * and more memory efficient. + * + * FIXME: Perhaps we should simply directly import and tweak rb3tree.c + * instead of trying to wrap it. + */ +typedef struct rb3_tree teco_rb3str_tree_t; + +/** @extends rb3_head */ +typedef struct { + struct rb3_head head; + /** + * The union exists only to allow a "name" alias for "key". + */ + union { + teco_string_t name; + teco_string_t key; + }; +} teco_rb3str_head_t; + +/** @memberof teco_rb3str_head_t */ +static inline teco_rb3str_head_t * +teco_rb3str_get_next(teco_rb3str_head_t *head) +{ + return (teco_rb3str_head_t *)rb3_get_next(&head->head); +} + +teco_rb3str_head_t *teco_rb3str_insert(teco_rb3str_tree_t *tree, gboolean case_sensitive, + teco_rb3str_head_t *head); + +teco_rb3str_head_t *teco_rb3str_find(teco_rb3str_tree_t *tree, gboolean case_sensitive, + const gchar *str, gsize len); + +teco_rb3str_head_t *teco_rb3str_nfind(teco_rb3str_tree_t *tree, gboolean case_sensitive, + const gchar *str, gsize len); + +gboolean teco_rb3str_auto_complete(teco_rb3str_tree_t *tree, gboolean case_sensitive, + const gchar *str, gsize str_len, gsize restrict_len, + teco_string_t *insert); diff --git a/src/rbtree.cpp b/src/rbtree.cpp deleted file mode 100644 index d2e9450..0000000 --- a/src/rbtree.cpp +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include -#include - -#include "sciteco.h" -#include "rbtree.h" -#include "interface.h" -#include "string-utils.h" - -namespace SciTECO { - -template -gchar * -RBTreeStringT:: -auto_complete(const gchar *key, gchar completed, gsize restrict_len) -{ - gsize key_len; - RBEntryString *first = NULL; - gsize prefix_len = 0; - gint prefixed_entries = 0; - gchar *insert = NULL; - - if (!key) - key = ""; - key_len = strlen(key); - - for (RBEntryString *cur = nfind(key); - cur && !StringNCmp(cur->key, key, key_len); - cur = cur->next()) { - if (restrict_len && strlen(cur->key) != restrict_len) - continue; - - if (!first) - first = cur; - - gsize len = String::diff(first->key + key_len, - cur->key + key_len); - if (!prefix_len || len < prefix_len) - prefix_len = len; - - prefixed_entries++; - } - if (prefix_len > 0) - insert = g_strndup(first->key + key_len, prefix_len); - - if (!insert && prefixed_entries > 1) { - for (RBEntryString *cur = first; - cur && !StringNCmp(cur->key, key, key_len); - cur = cur->next()) { - if (restrict_len && strlen(cur->key) != restrict_len) - continue; - - interface.popup_add(InterfaceCurrent::POPUP_PLAIN, - cur->key); - } - - interface.popup_show(); - } else if (prefixed_entries == 1) { - String::append(insert, completed); - } - - return insert; -} - -template class RBTreeStringT; -template class RBTreeStringT; - -} /* namespace SciTECO */ diff --git a/src/rbtree.h b/src/rbtree.h deleted file mode 100644 index edb3c56..0000000 --- a/src/rbtree.h +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifndef __RBTREE_H -#define __RBTREE_H - -#include - -#include - -#include -#include - -#include "memory.h" - -namespace SciTECO { - -/* - * NOTE: RBTree is not derived from Object, - * so we can derive from RBTree privately. - * Add Object to every RBTree subclass explicitly. - */ -template -class RBTree { -public: - class RBEntry : public Object { - public: - RB_ENTRY(RBEntry) nodes; - - inline RBEntryType * - next(void) - { - return (RBEntryType *)RBTree::Tree_RB_NEXT(this); - } - inline RBEntryType * - prev(void) - { - return (RBEntryType *)RBTree::Tree_RB_PREV(this); - } - }; - -private: - RB_HEAD(Tree, RBEntry) head; - - static inline int - compare_entries(RBEntry *e1, RBEntry *e2) - { - return ((RBEntryType *)e1)->compare(*(RBEntryType *)e2); - } - - /* - * All generated functions are plain-C, so they can be - * static methods. - */ - RB_GENERATE_INTERNAL(Tree, RBEntry, nodes, compare_entries, static); - -public: - RBTree() - { - RB_INIT(&head); - } - ~RBTree() - { - /* - * Keeping the clean up out of this wrapper class - * means we can avoid declaring EBEntry implementations - * virtual. - */ - g_assert(root() == NULL); - } - - inline RBEntryType * - root(void) - { - return (RBEntryType *)RB_ROOT(&head); - } - inline RBEntryType * - insert(RBEntryType *entry) - { - RB_INSERT(Tree, &head, entry); - return entry; - } - inline RBEntryType * - remove(RBEntryType *entry) - { - return (RBEntryType *)RB_REMOVE(Tree, &head, entry); - } - inline RBEntryType * - find(RBEntryType *entry) - { - return (RBEntryType *)RB_FIND(Tree, &head, entry); - } - inline RBEntryType * - operator [](RBEntryType *entry) - { - return find(entry); - } - inline RBEntryType * - nfind(RBEntryType *entry) - { - return (RBEntryType *)RB_NFIND(Tree, &head, entry); - } - inline RBEntryType * - min(void) - { - return (RBEntryType *)RB_MIN(Tree, &head); - } - inline RBEntryType * - max(void) - { - return (RBEntryType *)RB_MAX(Tree, &head); - } -}; - -typedef gint (*StringCmpFunc)(const gchar *str1, const gchar *str2); -typedef gint (*StringNCmpFunc)(const gchar *str1, const gchar *str2, gsize n); - -template -class RBEntryStringT : public RBTree>::RBEntry { -public: - /* - * It is convenient to be able to access the string - * key with various attribute names. - */ - union { - gchar *key; - gchar *name; - }; - - RBEntryStringT(gchar *_key) : key(_key) {} - - inline gint - compare(RBEntryStringT &other) - { - return StringCmp(key, other.key); - } -}; - -template -class RBTreeStringT : public RBTree> { -public: - typedef RBEntryStringT RBEntryString; - - class RBEntryOwnString : public RBEntryString { - public: - RBEntryOwnString(const gchar *key = NULL) - : RBEntryString(key ? g_strdup(key) : NULL) {} - - ~RBEntryOwnString() - { - g_free(RBEntryString::key); - } - }; - - inline RBEntryString * - find(const gchar *str) - { - RBEntryString entry((gchar *)str); - return RBTree::find(&entry); - } - inline RBEntryString * - operator [](const gchar *name) - { - return find(name); - } - - inline RBEntryString * - nfind(const gchar *str) - { - RBEntryString entry((gchar *)str); - return RBTree::nfind(&entry); - } - - gchar *auto_complete(const gchar *key, gchar completed = '\0', - gsize restrict_len = 0); -}; - -typedef RBTreeStringT RBTreeString; -typedef RBTreeStringT RBTreeStringCase; - -/* - * Only these two instantiations of RBTreeStringT are ever used, - * so it is more efficient to explicitly instantiate them. - * NOTE: The insane rules of C++ prevent using the typedefs here... - */ -extern template class RBTreeStringT; -extern template class RBTreeStringT; - -} /* namespace SciTECO */ - -#endif diff --git a/src/ring.c b/src/ring.c new file mode 100644 index 0000000..f9cf41f --- /dev/null +++ b/src/ring.c @@ -0,0 +1,580 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include + +#include "sciteco.h" +#include "file-utils.h" +#include "interface.h" +#include "view.h" +#include "undo.h" +#include "parser.h" +#include "core-commands.h" +#include "expressions.h" +#include "qreg.h" +#include "glob.h" +#include "error.h" +#include "list.h" +#include "ring.h" + +/** @private @static @memberof teco_buffer_t */ +static teco_buffer_t * +teco_buffer_new(void) +{ + teco_buffer_t *ctx = g_new0(teco_buffer_t, 1); + ctx->view = teco_view_new(); + teco_view_setup(ctx->view); + /* only have to do this once: */ + teco_view_set_representations(ctx->view); + return ctx; +} + +/** @private @memberof teco_buffer_t */ +static void +teco_buffer_set_filename(teco_buffer_t *ctx, const gchar *filename) +{ + gchar *resolved = teco_file_get_absolute_path(filename); + g_free(ctx->filename); + ctx->filename = resolved; + teco_interface_info_update(ctx); +} + +/** @memberof teco_buffer_t */ +void +teco_buffer_edit(teco_buffer_t *ctx) +{ + teco_interface_show_view(ctx->view); + teco_interface_info_update(ctx); +} +/** @memberof teco_buffer_t */ +void +teco_buffer_undo_edit(teco_buffer_t *ctx) +{ + undo__teco_interface_info_update_buffer(ctx); + undo__teco_interface_show_view(ctx->view); +} + +/** @private @memberof teco_buffer_t */ +static gboolean +teco_buffer_load(teco_buffer_t *ctx, const gchar *filename, GError **error) +{ + if (!teco_view_load(ctx->view, filename, error)) + return FALSE; + +#if 0 /* NOTE: currently buffer cannot be dirty */ + undo__teco_interface_info_update_buffer(ctx); + teco_undo_gboolean(ctx->dirty) = FALSE; +#endif + + teco_buffer_set_filename(ctx, filename); + return TRUE; +} + +/** @private @memberof teco_buffer_t */ +static gboolean +teco_buffer_save(teco_buffer_t *ctx, const gchar *filename, GError **error) +{ + if (!filename && !ctx->filename) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Cannot save the unnamed file " + "without providing a file name"); + return FALSE; + } + + if (!teco_view_save(ctx->view, filename ? : ctx->filename, error)) + return FALSE; + + /* + * Undirtify + * NOTE: info update is performed by set_filename() + */ + undo__teco_interface_info_update_buffer(ctx); + teco_undo_gboolean(ctx->dirty) = FALSE; + + /* + * FIXME: necessary also if the filename was not specified but the file + * is (was) new, in order to canonicalize the filename. + * May be circumvented by cananonicalizing without requiring the file + * name to exist (like readlink -f) + * NOTE: undo_info_update is already called above + */ + teco_undo_cstring(ctx->filename); + teco_buffer_set_filename(ctx, filename ? : ctx->filename); + + return TRUE; +} + +/** @private @memberof teco_buffer_t */ +static inline void +teco_buffer_free(teco_buffer_t *ctx) +{ + teco_view_free(ctx->view); + g_free(ctx->filename); + g_free(ctx); +} + +TECO_DEFINE_UNDO_CALL(teco_buffer_free, teco_buffer_t *); + +static teco_tailq_entry_t teco_ring_head = TECO_TAILQ_HEAD_INITIALIZER(&teco_ring_head); + +teco_buffer_t *teco_ring_current = NULL; + +teco_buffer_t * +teco_ring_first(void) +{ + return (teco_buffer_t *)teco_ring_head.first; +} + +teco_buffer_t * +teco_ring_last(void) +{ + return (teco_buffer_t *)teco_ring_head.last->prev->next; +} + +static void +teco_undo_ring_edit_action(teco_buffer_t **buffer, gboolean run) +{ + if (run) { + /* + * assumes that buffer still has correct prev/next + * pointers + */ + if (teco_buffer_next(*buffer)) + teco_tailq_insert_before((*buffer)->entry.next, &(*buffer)->entry); + else + teco_tailq_insert_tail(&teco_ring_head, &(*buffer)->entry); + + teco_ring_current = *buffer; + teco_buffer_edit(*buffer); + } else { + teco_buffer_free(*buffer); + } +} + +/* + * Emitted after a buffer close + * The pointer is the only remaining reference to the buffer! + */ +static void +teco_undo_ring_edit(teco_buffer_t *buffer) +{ + teco_buffer_t **ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_ring_edit_action, + sizeof(buffer)); + if (ctx) + *ctx = buffer; + else + teco_buffer_free(buffer); +} + +teco_int_t +teco_ring_get_id(teco_buffer_t *buffer) +{ + teco_int_t ret = 1; + + for (teco_tailq_entry_t *cur = teco_ring_head.first; + cur != &buffer->entry; + cur = cur->next) + ret++; + + return ret; +} + +teco_buffer_t * +teco_ring_find_by_name(const gchar *filename) +{ + g_autofree gchar *resolved = teco_file_get_absolute_path(filename); + + for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) { + teco_buffer_t *buffer = (teco_buffer_t *)cur; + if (!g_strcmp0(buffer->filename, resolved)) + return buffer; + } + + return NULL; +} + +teco_buffer_t * +teco_ring_find_by_id(teco_int_t id) +{ + for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) { + if (!--id) + return (teco_buffer_t *)cur; + } + + return NULL; +} + +void +teco_ring_dirtify(void) +{ + if (teco_qreg_current || teco_ring_current->dirty) + return; + + undo__teco_interface_info_update_buffer(teco_ring_current); + teco_undo_gboolean(teco_ring_current->dirty) = TRUE; + teco_interface_info_update(teco_ring_current); +} + +gboolean +teco_ring_is_any_dirty(void) +{ + for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) { + teco_buffer_t *buffer = (teco_buffer_t *)cur; + if (buffer->dirty) + return TRUE; + } + + return FALSE; +} + +gboolean +teco_ring_save_all_dirty_buffers(GError **error) +{ + for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) { + teco_buffer_t *buffer = (teco_buffer_t *)cur; + /* NOTE: Will fail for a dirty unnamed file */ + if (buffer->dirty && !teco_buffer_save(buffer, NULL, error)) + return FALSE; + } + + return TRUE; +} + +gboolean +teco_ring_edit_by_name(const gchar *filename, GError **error) +{ + teco_buffer_t *buffer = teco_ring_find(filename); + + teco_qreg_current = NULL; + if (buffer) { + teco_ring_current = buffer; + teco_buffer_edit(buffer); + + return teco_ed_hook(TECO_ED_HOOK_EDIT, error); + } + + buffer = teco_buffer_new(); + teco_tailq_insert_tail(&teco_ring_head, &buffer->entry); + + teco_ring_current = buffer; + teco_ring_undo_close(); + + teco_buffer_edit(buffer); + if (filename && g_file_test(filename, G_FILE_TEST_IS_REGULAR)) { + if (!teco_buffer_load(buffer, filename, error)) + return FALSE; + + teco_interface_msg(TECO_MSG_INFO, + "Added file \"%s\" to ring", filename); + } else { + teco_buffer_set_filename(buffer, filename); + + if (filename) + teco_interface_msg(TECO_MSG_INFO, + "Added new file \"%s\" to ring", + filename); + else + teco_interface_msg(TECO_MSG_INFO, + "Added new unnamed file to ring."); + } + + return teco_ed_hook(TECO_ED_HOOK_ADD, error); +} + +gboolean +teco_ring_edit_by_id(teco_int_t id, GError **error) +{ + teco_buffer_t *buffer = teco_ring_find(id); + if (!buffer) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Invalid buffer id %" TECO_INT_FORMAT, id); + return FALSE; + } + + teco_qreg_current = NULL; + teco_ring_current = buffer; + teco_buffer_edit(buffer); + + return teco_ed_hook(TECO_ED_HOOK_EDIT, error); +} + +static void +teco_ring_close_buffer(teco_buffer_t *buffer) +{ + teco_tailq_remove(&teco_ring_head, &buffer->entry); + + if (buffer->filename) + teco_interface_msg(TECO_MSG_INFO, + "Removed file \"%s\" from the ring", + buffer->filename); + else + teco_interface_msg(TECO_MSG_INFO, + "Removed unnamed file from the ring."); +} + +TECO_DEFINE_UNDO_CALL(teco_ring_close_buffer, teco_buffer_t *); + +gboolean +teco_ring_close(GError **error) +{ + teco_buffer_t *buffer = teco_ring_current; + + if (!teco_ed_hook(TECO_ED_HOOK_CLOSE, error)) + return FALSE; + teco_ring_close_buffer(buffer); + teco_ring_current = teco_buffer_next(buffer) ? : teco_buffer_prev(buffer); + /* Transfer responsibility to the undo token object. */ + teco_undo_ring_edit(buffer); + + if (!teco_ring_current) + return teco_ring_edit_by_name(NULL, error); + + teco_buffer_edit(teco_ring_current); + return teco_ed_hook(TECO_ED_HOOK_EDIT, error); +} + +void +teco_ring_undo_close(void) +{ + undo__teco_buffer_free(teco_ring_current); + undo__teco_ring_close_buffer(teco_ring_current); +} + +void +teco_ring_set_scintilla_undo(gboolean state) +{ + for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) { + teco_buffer_t *buffer = (teco_buffer_t *)cur; + teco_view_set_scintilla_undo(buffer->view, state); + } +} + +void +teco_ring_cleanup(void) +{ + teco_tailq_entry_t *next; + + for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = next) { + next = cur->next; + teco_buffer_free((teco_buffer_t *)cur); + } + + teco_ring_head = TECO_TAILQ_HEAD_INITIALIZER(&teco_ring_head); +} + +/* + * Command states + */ + +/* + * FIXME: Should be part of the teco_machine_main_t? + * Unfortunately, we cannot just merge initial() with done(), + * since we want to react immediately to xEB without waiting for $. + */ +static gboolean allow_filename = FALSE; + +static gboolean +teco_state_edit_file_initial(teco_machine_main_t *ctx, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return TRUE; + + teco_int_t id; + if (!teco_expressions_pop_num_calc(&id, -1, error)) + return FALSE; + + allow_filename = TRUE; + + if (id == 0) { + for (teco_buffer_t *cur = teco_ring_first(); cur; cur = teco_buffer_next(cur)) { + const gchar *filename = cur->filename ? : "(Unnamed)"; + teco_interface_popup_add(TECO_POPUP_FILE, filename, + strlen(filename), cur == teco_ring_current); + } + + teco_interface_popup_show(); + } else if (id > 0) { + allow_filename = FALSE; + if (!teco_current_doc_undo_edit(error) || + !teco_ring_edit(id, error)) + return FALSE; + } + + return TRUE; +} + +static teco_state_t * +teco_state_edit_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + if (!allow_filename) { + if (str->len > 0) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "If a buffer is selected by id, the " + "string argument must be empty"); + return NULL; + } + + return &teco_state_start; + } + + if (!teco_current_doc_undo_edit(error)) + return NULL; + + g_autofree gchar *filename = teco_file_expand_path(str->data); + if (teco_globber_is_pattern(filename)) { + g_auto(teco_globber_t) globber; + teco_globber_init(&globber, filename, G_FILE_TEST_IS_REGULAR); + + gchar *globbed_filename; + while ((globbed_filename = teco_globber_next(&globber))) { + gboolean rc = teco_ring_edit(globbed_filename, error); + g_free(globbed_filename); + if (!rc) + return NULL; + } + } else { + if (!teco_ring_edit_by_name(*filename ? filename : NULL, error)) + return NULL; + } + + return &teco_state_start; +} + +/*$ EB edit + * [n]EB[file]$ -- Open or edit file + * nEB$ + * + * Opens or edits the file with name . + * If is not in the buffer ring it is opened, + * added to the ring and set as the currently edited + * buffer. + * If it already exists in the ring, it is merely + * made the current file. + * may be omitted in which case the default + * unnamed buffer is created/edited. + * If an argument is specified as 0, EB will additionally + * display the buffer ring contents in the window's popup + * area. + * Naturally this only has any effect in interactive + * mode. + * + * may also be a glob pattern, in which case + * all regular files matching the pattern are opened/edited. + * Globbing is performed exactly the same as the + * \fBEN\fP command does. + * Also refer to the section called + * .B Glob Patterns + * for more details. + * + * File names of buffers in the ring are normalized + * by making them absolute. + * Any comparison on file names is performed using + * guessed or actual absolute file paths, so that + * one file may be referred to in many different ways + * (paths). + * + * does not have to exist on disk. + * In this case, an empty buffer is created and its + * name is guessed from . + * When the newly created buffer is first saved, + * the file is created on disk and the buffer's name + * will be updated to the absolute path of the file + * on disk. + * + * File names may also be tab-completed and string building + * characters are enabled by default. + * + * If is greater than zero, the string argument + * must be empty. + * Instead selects a buffer from the ring to edit. + * A value of 1 denotes the first buffer, 2 the second, + * ecetera. + */ +TECO_DEFINE_STATE_EXPECTFILE(teco_state_edit_file, + .initial_cb = (teco_state_initial_cb_t)teco_state_edit_file_initial +); + +static teco_state_t * +teco_state_save_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + g_autofree gchar *filename = teco_file_expand_path(str->data); + if (teco_qreg_current) { + if (!teco_qreg_save(teco_qreg_current, filename, error)) + return NULL; + } else { + if (!teco_buffer_save(teco_ring_current, *filename ? filename : NULL, error)) + return NULL; + } + + return &teco_state_start; +} + +/*$ EW write save + * EW$ -- Save current buffer or Q-Register + * EWfile$ + * + * Saves the current buffer to disk. + * If the buffer was dirty, it will be clean afterwards. + * If the string argument is not empty, + * the buffer is saved with the specified file name + * and is renamed in the ring. + * + * The EW command also works if the current document + * is a Q-Register, i.e. a Q-Register is edited. + * In this case, the string contents of the current + * Q-Register are saved to . + * Q-Registers have no notion of associated file names, + * so must be always specified. + * + * In interactive mode, EW is executed immediately and + * may be rubbed out. + * In order to support that, \*(ST creates so called + * save point files. + * It does not merely overwrite existing files when saving + * but moves them to save point files instead. + * Save point files are called \(lq.teco-\fIn\fP-\fIfilename\fP~\(rq, + * where is the name of the saved file and is + * a number that is increased with every save operation. + * Save point files are always created in the same directory + * as the original file to ensure that no copying of the file + * on disk is necessary but only a rename of the file. + * When rubbing out the EW command, \*(ST restores the latest + * save point file by moving (renaming) it back to its + * original path \(em also not requiring any on-disk copying. + * \*(ST is impossible to crash, but just in case it still + * does it may leave behind these save point files which + * must be manually deleted by the user. + * Otherwise save point files are deleted on command line + * termination. + * + * File names may also be tab-completed and string building + * characters are enabled by default. + */ +TECO_DEFINE_STATE_EXPECTFILE(teco_state_save_file); diff --git a/src/ring.cpp b/src/ring.cpp deleted file mode 100644 index ac5faf6..0000000 --- a/src/ring.cpp +++ /dev/null @@ -1,461 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include -#include - -#include - -#include "sciteco.h" -#include "interface.h" -#include "ioview.h" -#include "undo.h" -#include "parser.h" -#include "expressions.h" -#include "qregisters.h" -#include "glob.h" -#include "error.h" -#include "ring.h" - -namespace SciTECO { - -namespace States { - StateEditFile editfile; - StateSaveFile savefile; -} - -void -Buffer::UndoTokenClose::run(void) -{ - ring.close(buffer); - /* NOTE: the buffer is NOT deleted on Token destruction */ - delete buffer; -} - -void -Buffer::save(const gchar *filename) -{ - if (!filename && !Buffer::filename) - throw Error("Cannot save the unnamed file " - "without providing a file name"); - - IOView::save(filename ? : Buffer::filename); - - /* - * Undirtify - * NOTE: info update is performed by set_filename() - */ - interface.undo_info_update(this); - undo.push_var(dirty) = false; - - /* - * FIXME: necessary also if the filename was not specified but the file - * is (was) new, in order to canonicalize the filename. - * May be circumvented by cananonicalizing without requiring the file - * name to exist (like readlink -f) - * NOTE: undo_info_update is already called above - */ - undo.push_str(Buffer::filename); - set_filename(filename ? : Buffer::filename); -} - -void -Ring::UndoTokenEdit::run(void) -{ - /* - * assumes that buffer still has correct prev/next - * pointers - */ - if (buffer->next()) - TAILQ_INSERT_BEFORE(buffer->next(), buffer, buffers); - else - TAILQ_INSERT_TAIL(&ring->head, buffer, buffers); - - ring->current = buffer; - buffer->edit(); - buffer = NULL; -} - -tecoInt -Ring::get_id(Buffer *buffer) -{ - tecoInt ret = 0; - Buffer *cur; - - TAILQ_FOREACH(cur, &head, buffers) { - ret++; - if (cur == buffer) - break; - } - - return ret; -} - -Buffer * -Ring::find(const gchar *filename) -{ - gchar *resolved = get_absolute_path(filename); - Buffer *cur; - - TAILQ_FOREACH(cur, &head, buffers) - if (!g_strcmp0(cur->filename, resolved)) - break; - - g_free(resolved); - return cur; -} - -Buffer * -Ring::find(tecoInt id) -{ - Buffer *cur; - - TAILQ_FOREACH(cur, &head, buffers) - if (!--id) - break; - - return cur; -} - -void -Ring::dirtify(void) -{ - if (QRegisters::current || current->dirty) - return; - - interface.undo_info_update(current); - undo.push_var(current->dirty); - current->dirty = true; - interface.info_update(current); -} - -bool -Ring::is_any_dirty(void) -{ - Buffer *cur; - - TAILQ_FOREACH(cur, &head, buffers) - if (cur->dirty) - return true; - - return false; -} - -void -Ring::save_all_dirty_buffers(void) -{ - Buffer *cur; - - TAILQ_FOREACH(cur, &head, buffers) - if (cur->dirty) - /* NOTE: Will fail for the unnamed file */ - cur->save(); -} - -bool -Ring::edit(tecoInt id) -{ - Buffer *buffer = find(id); - - if (!buffer) - return false; - - QRegisters::current = NULL; - current = buffer; - buffer->edit(); - - QRegisters::hook(QRegisters::HOOK_EDIT); - - return true; -} - -void -Ring::edit(const gchar *filename) -{ - Buffer *buffer = find(filename); - - QRegisters::current = NULL; - if (buffer) { - current = buffer; - buffer->edit(); - - QRegisters::hook(QRegisters::HOOK_EDIT); - } else { - buffer = new Buffer(); - TAILQ_INSERT_TAIL(&head, buffer, buffers); - - current = buffer; - undo_close(); - - if (filename && g_file_test(filename, G_FILE_TEST_IS_REGULAR)) { - buffer->edit(); - buffer->load(filename); - - interface.msg(InterfaceCurrent::MSG_INFO, - "Added file \"%s\" to ring", filename); - } else { - buffer->edit(); - buffer->set_filename(filename); - - if (filename) - interface.msg(InterfaceCurrent::MSG_INFO, - "Added new file \"%s\" to ring", - filename); - else - interface.msg(InterfaceCurrent::MSG_INFO, - "Added new unnamed file to ring."); - } - - QRegisters::hook(QRegisters::HOOK_ADD); - } -} - -void -Ring::close(Buffer *buffer) -{ - TAILQ_REMOVE(&head, buffer, buffers); - - if (buffer->filename) - interface.msg(InterfaceCurrent::MSG_INFO, - "Removed file \"%s\" from the ring", - buffer->filename); - else - interface.msg(InterfaceCurrent::MSG_INFO, - "Removed unnamed file from the ring."); -} - -void -Ring::close(void) -{ - Buffer *buffer = current; - - QRegisters::hook(QRegisters::HOOK_CLOSE); - close(buffer); - current = buffer->next() ? : buffer->prev(); - /* Transfer responsibility to UndoToken object. */ - undo.push_own(this, buffer); - - if (current) { - current->edit(); - QRegisters::hook(QRegisters::HOOK_EDIT); - } else { - edit((const gchar *)NULL); - } -} - -void -Ring::set_scintilla_undo(bool state) -{ - Buffer *cur; - - TAILQ_FOREACH(cur, &head, buffers) - cur->set_scintilla_undo(state); -} - -Ring::~Ring() -{ - Buffer *buffer, *next; - - TAILQ_FOREACH_SAFE(buffer, &head, buffers, next) - delete buffer; -} - -/* - * Command states - */ - -void -StateEditFile::do_edit(const gchar *filename) -{ - current_doc_undo_edit(); - ring.edit(filename); -} - -void -StateEditFile::do_edit(tecoInt id) -{ - current_doc_undo_edit(); - if (!ring.edit(id)) - throw Error("Invalid buffer id %" TECO_INTEGER_FORMAT, id); -} - -/*$ EB edit - * [n]EB[file]$ -- Open or edit file - * nEB$ - * - * Opens or edits the file with name . - * If is not in the buffer ring it is opened, - * added to the ring and set as the currently edited - * buffer. - * If it already exists in the ring, it is merely - * made the current file. - * may be omitted in which case the default - * unnamed buffer is created/edited. - * If an argument is specified as 0, EB will additionally - * display the buffer ring contents in the window's popup - * area. - * Naturally this only has any effect in interactive - * mode. - * - * may also be a glob pattern, in which case - * all regular files matching the pattern are opened/edited. - * Globbing is performed exactly the same as the - * \fBEN\fP command does. - * Also refer to the section called - * .B Glob Patterns - * for more details. - * - * File names of buffers in the ring are normalized - * by making them absolute. - * Any comparison on file names is performed using - * guessed or actual absolute file paths, so that - * one file may be referred to in many different ways - * (paths). - * - * does not have to exist on disk. - * In this case, an empty buffer is created and its - * name is guessed from . - * When the newly created buffer is first saved, - * the file is created on disk and the buffer's name - * will be updated to the absolute path of the file - * on disk. - * - * File names may also be tab-completed and string building - * characters are enabled by default. - * - * If is greater than zero, the string argument - * must be empty. - * Instead selects a buffer from the ring to edit. - * A value of 1 denotes the first buffer, 2 the second, - * ecetera. - */ -void -StateEditFile::initial(void) -{ - tecoInt id = expressions.pop_num_calc(0, -1); - - allowFilename = true; - - if (id == 0) { - for (Buffer *cur = ring.first(); cur; cur = cur->next()) - interface.popup_add(InterfaceCurrent::POPUP_FILE, - cur->filename ? : "(Unnamed)", - cur == ring.current); - - interface.popup_show(); - } else if (id > 0) { - allowFilename = false; - do_edit(id); - } -} - -State * -StateEditFile::got_file(const gchar *filename) -{ - BEGIN_EXEC(&States::start); - - if (!allowFilename) { - if (*filename) - throw Error("If a buffer is selected by id, the " - "string argument must be empty"); - - return &States::start; - } - - if (Globber::is_pattern(filename)) { - Globber globber(filename, G_FILE_TEST_IS_REGULAR); - gchar *globbed_filename; - - while ((globbed_filename = globber.next())) - do_edit(globbed_filename); - } else { - do_edit(*filename ? filename : NULL); - } - - return &States::start; -} - -/*$ EW write save - * EW$ -- Save current buffer or Q-Register - * EWfile$ - * - * Saves the current buffer to disk. - * If the buffer was dirty, it will be clean afterwards. - * If the string argument is not empty, - * the buffer is saved with the specified file name - * and is renamed in the ring. - * - * The EW command also works if the current document - * is a Q-Register, i.e. a Q-Register is edited. - * In this case, the string contents of the current - * Q-Register are saved to . - * Q-Registers have no notion of associated file names, - * so must be always specified. - * - * In interactive mode, EW is executed immediately and - * may be rubbed out. - * In order to support that, \*(ST creates so called - * save point files. - * It does not merely overwrite existing files when saving - * but moves them to save point files instead. - * Save point files are called \(lq.teco-\fIn\fP-\fIfilename\fP~\(rq, - * where is the name of the saved file and is - * a number that is increased with every save operation. - * Save point files are always created in the same directory - * as the original file to ensure that no copying of the file - * on disk is necessary but only a rename of the file. - * When rubbing out the EW command, \*(ST restores the latest - * save point file by moving (renaming) it back to its - * original path \(em also not requiring any on-disk copying. - * \*(ST is impossible to crash, but just in case it still - * does it may leave behind these save point files which - * must be manually deleted by the user. - * Otherwise save point files are deleted on command line - * termination. - * - * File names may also be tab-completed and string building - * characters are enabled by default. - */ -State * -StateSaveFile::got_file(const gchar *filename) -{ - BEGIN_EXEC(&States::start); - - if (QRegisters::current) - QRegisters::current->save(filename); - else - ring.current->save(*filename ? filename : NULL); - - return &States::start; -} - -void -current_doc_undo_edit(void) -{ - if (!QRegisters::current) - ring.undo_edit(); - else - undo.push_var(QRegisters::current)->undo_edit(); -} - -} /* namespace SciTECO */ diff --git a/src/ring.h b/src/ring.h index 79c74f5..d1a45d7 100644 --- a/src/ring.h +++ b/src/ring.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,241 +14,115 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -#ifndef __RING_H -#define __RING_H - -#include +#pragma once #include -#include - #include "sciteco.h" -#include "memory.h" -#include "interface.h" #include "undo.h" -#include "qregisters.h" +#include "qreg.h" +#include "view.h" #include "parser.h" -#include "ioview.h" - -namespace SciTECO { - -/* - * Classes - */ +#include "list.h" -class Buffer : private IOView { - TAILQ_ENTRY(Buffer) buffers; +typedef struct teco_buffer_t { + teco_tailq_entry_t entry; - class UndoTokenClose : public UndoToken { - Buffer *buffer; + teco_view_t *view; - public: - UndoTokenClose(Buffer *_buffer) - : buffer(_buffer) {} - - void run(void); - }; - - inline void - undo_close(void) - { - undo.push(this); - } - -public: gchar *filename; - bool dirty; + gboolean dirty; +} teco_buffer_t; - Buffer() : filename(NULL), dirty(false) - { - initialize(); - /* only have to do this once: */ - set_representations(); - } - - ~Buffer() - { - g_free(filename); - } - - inline Buffer *& - next(void) - { - return TAILQ_NEXT(this, buffers); - } - inline Buffer *& - prev(void) - { - TAILQ_HEAD(Head, Buffer); - - return TAILQ_PREV(this, Head, buffers); - } - - inline void - set_filename(const gchar *filename) - { - gchar *resolved = get_absolute_path(filename); - g_free(Buffer::filename); - Buffer::filename = resolved; - interface.info_update(this); - } - - inline void - edit(void) - { - interface.show_view(this); - interface.info_update(this); - } - inline void - undo_edit(void) - { - interface.undo_info_update(this); - interface.undo_show_view(this); - } - - inline void - load(const gchar *filename) - { - IOView::load(filename); - -#if 0 /* NOTE: currently buffer cannot be dirty */ - interface.undo_info_update(this); - undo.push_var(dirty) = false; -#endif - - set_filename(filename); - } - void save(const gchar *filename = NULL); +/** @memberof teco_buffer_t */ +static inline teco_buffer_t * +teco_buffer_next(teco_buffer_t *ctx) +{ + return (teco_buffer_t *)ctx->entry.next; +} - /* - * Ring manages the buffer list and has privileged - * access. - */ - friend class Ring; -}; +/** @memberof teco_buffer_t */ +static inline teco_buffer_t * +teco_buffer_prev(teco_buffer_t *ctx) +{ + return (teco_buffer_t *)ctx->entry.prev->prev->next; +} -/* object declared in main.cpp */ -extern class Ring : public Object { - /* - * Emitted after a buffer close - * The pointer is the only remaining reference to the buffer! - */ - class UndoTokenEdit : public UndoToken { - Ring *ring; - Buffer *buffer; +void teco_buffer_edit(teco_buffer_t *ctx); +void teco_buffer_undo_edit(teco_buffer_t *ctx); - public: - UndoTokenEdit(Ring *_ring, Buffer *_buffer) - : UndoToken(), ring(_ring), buffer(_buffer) {} - ~UndoTokenEdit() - { - delete buffer; - } +extern teco_buffer_t *teco_ring_current; - void run(void); - }; +teco_buffer_t *teco_ring_first(void); +teco_buffer_t *teco_ring_last(void); - TAILQ_HEAD(Head, Buffer) head; +teco_int_t teco_ring_get_id(teco_buffer_t *buffer); -public: - Buffer *current; +teco_buffer_t *teco_ring_find_by_name(const gchar *filename); +teco_buffer_t *teco_ring_find_by_id(teco_int_t id); - Ring() : current(NULL) - { - TAILQ_INIT(&head); - } - ~Ring(); +#define teco_ring_find(X) \ + (_Generic((X), gchar * : teco_ring_find_by_name, \ + const gchar * : teco_ring_find_by_name, \ + teco_int_t : teco_ring_find_by_id)(X)) - inline Buffer * - first(void) - { - return TAILQ_FIRST(&head); - } - inline Buffer * - last(void) - { - return TAILQ_LAST(&head, Head); - } +void teco_ring_dirtify(void); +gboolean teco_ring_is_any_dirty(void); +gboolean teco_ring_save_all_dirty_buffers(GError **error); - tecoInt get_id(Buffer *buffer); - inline tecoInt - get_id(void) - { - return get_id(current); - } +gboolean teco_ring_edit_by_name(const gchar *filename, GError **error); +gboolean teco_ring_edit_by_id(teco_int_t id, GError **error); - Buffer *find(const gchar *filename); - Buffer *find(tecoInt id); +#define teco_ring_edit(X, ERROR) \ + (_Generic((X), gchar * : teco_ring_edit_by_name, \ + const gchar * : teco_ring_edit_by_name, \ + teco_int_t : teco_ring_edit_by_id)((X), (ERROR))) - void dirtify(void); - bool is_any_dirty(void); - void save_all_dirty_buffers(void); +static inline void +teco_ring_undo_edit(void) +{ + teco_undo_ptr(teco_qreg_current); + teco_undo_ptr(teco_ring_current); + teco_buffer_undo_edit(teco_ring_current); +} - bool edit(tecoInt id); - void edit(const gchar *filename); - inline void - undo_edit(void) - { - undo.push_var(QRegisters::current); - undo.push_var(current)->undo_edit(); - } +gboolean teco_ring_close(GError **error); +void teco_ring_undo_close(void); - void close(Buffer *buffer); - void close(void); - inline void - undo_close(void) - { - current->undo_close(); - } +void teco_ring_set_scintilla_undo(gboolean state); - void set_scintilla_undo(bool state); -} ring; +void teco_ring_cleanup(void); /* * Command states */ -class StateEditFile : public StateExpectFile { -private: - bool allowFilename; - - void do_edit(const gchar *filename); - void do_edit(tecoInt id); - - void initial(void); - State *got_file(const gchar *filename); -}; - -class StateSaveFile : public StateExpectFile { -private: - State *got_file(const gchar *filename); -}; - -namespace States { - extern StateEditFile editfile; - extern StateSaveFile savefile; -} +TECO_DECLARE_STATE(teco_state_edit_file); +TECO_DECLARE_STATE(teco_state_save_file); /* * Helper functions applying to any current * document (whether a buffer or QRegister). * There's currently no better place to put them. */ -void current_doc_undo_edit(void); +static inline gboolean +teco_current_doc_undo_edit(GError **error) +{ + if (!teco_qreg_current) { + teco_ring_undo_edit(); + return TRUE; + } + + teco_undo_ptr(teco_qreg_current); + return teco_qreg_current->vtable->undo_edit(teco_qreg_current, error); +} -static inline bool -current_doc_must_undo(void) +static inline gboolean +teco_current_doc_must_undo(void) { /* * If there's no currently edited Q-Register * we must be editing the current buffer */ - return !QRegisters::current || - QRegisters::current->must_undo; + return !teco_qreg_current || teco_qreg_current->must_undo; } - -} /* namespace SciTECO */ - -#endif diff --git a/src/scintilla.c b/src/scintilla.c new file mode 100644 index 0000000..f58b6f4 --- /dev/null +++ b/src/scintilla.c @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include + +#include "sciteco.h" +#include "string-utils.h" +#include "error.h" +#include "parser.h" +#include "core-commands.h" +#include "undo.h" +#include "expressions.h" +#include "interface.h" +#include "scintilla.h" + +teco_symbol_list_t teco_symbol_list_scintilla = {NULL, 0}; +teco_symbol_list_t teco_symbol_list_scilexer = {NULL, 0}; + +/* + * FIXME: Could be static. + */ +TECO_DEFINE_UNDO_OBJECT_OWN(scintilla_message, teco_machine_scintilla_t, /* don't delete */); + +/** @memberof teco_symbol_list_t */ +void +teco_symbol_list_init(teco_symbol_list_t *ctx, const teco_symbol_entry_t *entries, gint size, + gboolean case_sensitive) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->entries = entries; + ctx->size = size; + ctx->cmp_fnc = case_sensitive ? strncmp : g_ascii_strncasecmp; +} + +/** + * @note Since symbol lists are presorted constant arrays we can do a simple + * binary search. + * This does not use bsearch() since we'd have to prepend `prefix` in front of + * the name. + * + * @memberof teco_symbol_list_t + */ +gint +teco_symbol_list_lookup(teco_symbol_list_t *ctx, const gchar *name, const gchar *prefix) +{ + gsize name_len = strlen(name); + + gsize prefix_skip = strlen(prefix); + if (!ctx->cmp_fnc(name, prefix, prefix_skip)) + prefix_skip = 0; + + gint left = 0; + gint right = ctx->size - 1; + + while (left <= right) { + gint cur = left + (right-left)/2; + gint cmp = ctx->cmp_fnc(ctx->entries[cur].name + prefix_skip, + name, name_len + 1); + + if (!cmp) + return ctx->entries[cur].value; + + if (cmp > 0) + right = cur-1; + else /* cmp < 0 */ + left = cur+1; + } + + return -1; +} + +/** + * Auto-complete a Scintilla symbol. + * + * @param ctx The symbol list. + * @param symbol The symbol to auto-complete or NULL. + * @param insert String to initialize with the completion. + * @return TRUE in case of an unambiguous completion. + * + * @memberof teco_symbol_list_t + */ +gboolean +teco_symbol_list_auto_complete(teco_symbol_list_t *ctx, const gchar *symbol, teco_string_t *insert) +{ + memset(insert, 0, sizeof(*insert)); + + if (!symbol) + symbol = ""; + gsize symbol_len = strlen(symbol); + + if (G_UNLIKELY(!ctx->list)) + for (gint i = ctx->size; i; i--) + ctx->list = g_list_prepend(ctx->list, (gchar *)ctx->entries[i-1].name); + + /* NOTE: element data must not be freed */ + g_autoptr(GList) glist = g_list_copy(ctx->list); + guint glist_len = 0; + + gsize prefix_len = 0; + + for (GList *entry = g_list_first(glist), *next = g_list_next(entry); + entry != NULL; + entry = next, next = entry ? g_list_next(entry) : NULL) { + if (g_ascii_strncasecmp(entry->data, symbol, symbol_len) != 0) { + glist = g_list_delete_link(glist, entry); + continue; + } + + teco_string_t glist_str; + glist_str.data = (gchar *)glist->data + symbol_len; + glist_str.len = strlen(glist_str.data); + + gsize len = teco_string_casediff(&glist_str, (gchar *)entry->data + symbol_len, + strlen(entry->data) - symbol_len); + if (!prefix_len || len < prefix_len) + prefix_len = len; + + glist_len++; + } + + if (prefix_len > 0) { + teco_string_init(insert, (gchar *)glist->data + symbol_len, prefix_len); + } else if (glist_len > 1) { + for (GList *entry = g_list_first(glist); + entry != NULL; + entry = g_list_next(entry)) { + teco_interface_popup_add(TECO_POPUP_PLAIN, entry->data, + strlen(entry->data), FALSE); + } + + teco_interface_popup_show(); + } + + return glist_len == 1; +} + +/* + * Command states + */ + +/* + * FIXME: This state could be static. + */ +TECO_DECLARE_STATE(teco_state_scintilla_lparam); + +static gboolean +teco_scintilla_parse_symbols(teco_machine_scintilla_t *scintilla, const teco_string_t *str, GError **error) +{ + if (teco_string_contains(str, '\0')) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Scintilla symbol names must not contain null-byte"); + return FALSE; + } + + g_auto(GStrv) symbols = g_strsplit(str->data, ",", -1); + + if (!symbols[0]) + return TRUE; + if (*symbols[0]) { + gint v = teco_symbol_list_lookup(&teco_symbol_list_scintilla, symbols[0], "SCI_"); + if (v < 0) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Unknown Scintilla message symbol \"%s\"", + symbols[0]); + return FALSE; + } + scintilla->iMessage = v; + } + + if (!symbols[1]) + return TRUE; + if (*symbols[1]) { + gint v = teco_symbol_list_lookup(&teco_symbol_list_scilexer, symbols[1], ""); + if (v < 0) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Unknown Scintilla Lexer symbol \"%s\"", + symbols[1]); + return FALSE; + } + scintilla->wParam = v; + } + + if (!symbols[2]) + return TRUE; + if (*symbols[2]) { + gint v = teco_symbol_list_lookup(&teco_symbol_list_scilexer, symbols[2], ""); + if (v < 0) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Unknown Scintilla Lexer symbol \"%s\"", + symbols[2]); + return FALSE; + } + scintilla->lParam = v; + } + + return TRUE; +} + +static teco_state_t * +teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_scintilla_lparam; + + /* + * NOTE: This is more memory efficient than pushing the individual + * members of teco_machine_scintilla_t and we won't need to define + * undo methods for the Scintilla types. + */ + if (ctx->parent.must_undo) + teco_undo_object_scintilla_message_push(&ctx->scintilla); + memset(&ctx->scintilla, 0, sizeof(ctx->scintilla)); + + if ((str->len > 0 && !teco_scintilla_parse_symbols(&ctx->scintilla, str, error)) || + !teco_expressions_eval(FALSE, error)) + return NULL; + + teco_int_t value; + + if (!ctx->scintilla.iMessage) { + if (!teco_expressions_args()) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + " command requires at least a message code"); + return NULL; + } + + if (!teco_expressions_pop_num_calc(&value, 0, error)) + return NULL; + ctx->scintilla.iMessage = value; + } + if (!ctx->scintilla.wParam) { + if (!teco_expressions_pop_num_calc(&value, 0, error)) + return NULL; + ctx->scintilla.wParam = value; + } + + return &teco_state_scintilla_lparam; +} + +/* in cmdline.c */ +gboolean teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error); + +/*$ ES scintilla message + * -- Send Scintilla message + * [lParam[,wParam]]ESmessage[,wParam[,lParam]]$[lParam]$ -> result + * + * Send Scintilla message with code specified by symbolic + * name , and . + * may be symbolic when specified as part of the + * first string argument. + * If not it is popped from the stack. + * may be specified as a constant string whose + * pointer is passed to Scintilla if specified as the second + * string argument. + * If the second string argument is empty, is popped + * from the stack instead. + * Parameters popped from the stack may be omitted, in which + * case 0 is implied. + * The message's return value is pushed onto the stack. + * + * All messages defined by Scintilla (as C macros) can be + * used by passing their name as a string to ES + * (e.g. ESSCI_LINESONSCREEN...). + * The \(lqSCI_\(rq prefix may be omitted and message symbols + * are case-insensitive. + * Only the Scintilla lexer symbols (SCLEX_..., SCE_...) + * may be used symbolically with the ES command as , + * other values must be passed as integers on the stack. + * In interactive mode, symbols may be auto-completed by + * pressing Tab. + * String-building characters are by default interpreted + * in the string arguments. + * + * .BR Warning : + * Almost all Scintilla messages may be dispatched using + * this command. + * \*(ST does not keep track of the editor state changes + * performed by these commands and cannot undo them. + * You should never use it to change the editor state + * (position changes, deletions, etc.) or otherwise + * rub out will result in an inconsistent editor state. + * There are however exceptions: + * - In the editor profile and batch mode in general, + * the ES command may be used freely. + * - In the ED hook macro (register \(lqED\(rq), + * when a file is added to the ring, most destructive + * operations can be performed since rubbing out the + * EB command responsible for the hook execution also + * removes the buffer from the ring again. + * - As part of function key macros that immediately + * terminate the command line. + */ +TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_symbols, + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_scintilla_symbols_process_edit_cmd, + .expectstring.last = FALSE +); + +static teco_state_t * +teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + if (teco_string_contains(str, '\0')) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Scintilla lParam string must not contain null-byte."); + return NULL; + } + + if (!ctx->scintilla.lParam) { + if (str->len > 0) { + ctx->scintilla.lParam = (sptr_t)str->data; + } else { + teco_int_t v; + if (!teco_expressions_pop_num_calc(&v, 0, error)) + return NULL; + ctx->scintilla.lParam = v; + } + } + + teco_expressions_push(teco_interface_ssm(ctx->scintilla.iMessage, + ctx->scintilla.wParam, + ctx->scintilla.lParam)); + + return &teco_state_start; +} + +TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_lparam); diff --git a/src/scintilla.h b/src/scintilla.h new file mode 100644 index 0000000..607a3ff --- /dev/null +++ b/src/scintilla.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#pragma once + +#include + +#include "sciteco.h" +#include "string-utils.h" +#include "parser.h" + +typedef struct { + const gchar *name; + gint value; +} teco_symbol_entry_t; + +typedef struct { + const teco_symbol_entry_t *entries; + gint size; + + int (*cmp_fnc)(const char *, const char *, size_t); + + /** + * For auto-completions. + * The list is allocated only ondemand. + */ + GList *list; +} teco_symbol_list_t; + +void teco_symbol_list_init(teco_symbol_list_t *ctx, const teco_symbol_entry_t *entries, gint size, + gboolean case_sensitive); + +gint teco_symbol_list_lookup(teco_symbol_list_t *ctx, const gchar *name, const gchar *prefix); + +gboolean teco_symbol_list_auto_complete(teco_symbol_list_t *ctx, const gchar *symbol, teco_string_t *insert); + +/** @memberof teco_symbol_list_t */ +static inline void +teco_symbol_list_clear(teco_symbol_list_t *ctx) +{ + g_list_free(ctx->list); +} + +extern teco_symbol_list_t teco_symbol_list_scintilla; +extern teco_symbol_list_t teco_symbol_list_scilexer; + +/* + * Command states + */ + +TECO_DECLARE_STATE(teco_state_scintilla_symbols); diff --git a/src/sciteco.h b/src/sciteco.h index 664b69d..dcf5359 100644 --- a/src/sciteco.h +++ b/src/sciteco.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,100 +14,119 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#pragma once -#ifndef __SCITECO_H -#define __SCITECO_H - +#include #include #include -#include "interface.h" - -namespace SciTECO { - #if TECO_INTEGER == 32 -typedef gint32 tecoInt; -#define TECO_INTEGER_FORMAT G_GINT32_FORMAT +typedef gint32 teco_int_t; +#define TECO_INT_FORMAT G_GINT32_FORMAT #elif TECO_INTEGER == 64 -typedef gint64 tecoInt; -#define TECO_INTEGER_FORMAT G_GINT64_FORMAT +typedef gint64 teco_int_t; +#define TECO_INT_FORMAT G_GINT64_FORMAT #else #error Invalid TECO integer storage size #endif -typedef tecoInt tecoBool; - -namespace Flags { - enum { - ED_AUTOCASEFOLD = (1 << 3), - ED_AUTOEOL = (1 << 4), - ED_HOOKS = (1 << 5), - ED_FNKEYS = (1 << 6), - ED_SHELLEMU = (1 << 7), - ED_XTERM_CLIPBOARD = (1 << 8) - }; - - extern tecoInt ed; -} - -extern sig_atomic_t sigint_occurred; /** - * For sentinels: NULL might not be defined as a - * pointer type (LLVM/CLang) + * A TECO boolean - this differs from C booleans. + * See teco_is_success()/teco_is_failure(). */ -#define NIL ((void *)0) +typedef teco_int_t teco_bool_t; + +#define TECO_SUCCESS ((teco_bool_t)-1) +#define TECO_FAILURE ((teco_bool_t)0) + +static inline teco_bool_t +teco_bool(gboolean x) +{ + return x ? TECO_SUCCESS : TECO_FAILURE; +} + +static inline gboolean +teco_is_success(teco_bool_t x) +{ + return x < 0; +} -/** true if C is a control character */ -#define IS_CTL(C) ((C) < ' ') +static inline gboolean +teco_is_failure(teco_bool_t x) +{ + return x >= 0; +} + +/** TRUE if C is a control character */ +#define TECO_IS_CTL(C) ((C) < ' ') /** ASCII character to echo control character C */ -#define CTL_ECHO(C) ((C) | 0x40) +#define TECO_CTL_ECHO(C) ((C) | 0x40) /** * Control character of ASCII C, i.e. * control character corresponding to CTRL+C keypress. */ -#define CTL_KEY(C) ((C) & ~0x40) -/** - * Control character of the escape key. - * Equivalent to CTL_KEY('[') or '\\e', - * but more portable. - */ -#define CTL_KEY_ESC 27 +#define TECO_CTL_KEY(C) ((C) & ~0x40) + /** - * String containing the escape character. - * There is "\e", but it's not really standard C/C++. + * ED flags. + * This is not a bitfield, since it is set from SciTECO. */ -#define CTL_KEY_ESC_STR "\x1B" - -#define SUCCESS ((tecoBool)-1) -#define FAILURE ((tecoBool)0) -#define TECO_BOOL(X) ((X) ? SUCCESS : FAILURE) - -#define IS_SUCCESS(X) ((X) < 0) -#define IS_FAILURE(X) (!IS_SUCCESS(X)) +enum { + TECO_ED_AUTOCASEFOLD = (1 << 3), + TECO_ED_AUTOEOL = (1 << 4), + TECO_ED_HOOKS = (1 << 5), + TECO_ED_FNKEYS = (1 << 6), + TECO_ED_SHELLEMU = (1 << 7), + TECO_ED_XTERM_CLIPBOARD = (1 << 8) +}; -/* in main.cpp */ -void interrupt(void); +/* in main.c */ +extern teco_int_t teco_ed; -/* in main.cpp */ -const gchar *get_eol_seq(gint eol_mode); +/* in main.c */ +extern volatile sig_atomic_t teco_sigint_occurred; -namespace Validate { +/* in main.c */ +void teco_interrupt(void); -static inline bool -pos(gint n) -{ - return n >= 0 && n <= interface.ssm(SCI_GETLENGTH); -} - -static inline bool -line(gint n) -{ - return n >= 0 && n < interface.ssm(SCI_GETLINECOUNT); -} +/* + * Allows automatic cleanup of FILE pointers. + */ +G_DEFINE_AUTOPTR_CLEANUP_FUNC(FILE, fclose); -} /* namespace Validate */ +/* + * BEWARE DRAGONS! + */ +#define __TECO_FE_0(WHAT, WHAT_LAST) +#define __TECO_FE_1(WHAT, WHAT_LAST, X) WHAT_LAST(1,X) +#define __TECO_FE_2(WHAT, WHAT_LAST, X, ...) WHAT(2,X)__TECO_FE_1(WHAT, WHAT_LAST, __VA_ARGS__) +#define __TECO_FE_3(WHAT, WHAT_LAST, X, ...) WHAT(3,X)__TECO_FE_2(WHAT, WHAT_LAST, __VA_ARGS__) +#define __TECO_FE_4(WHAT, WHAT_LAST, X, ...) WHAT(4,X)__TECO_FE_3(WHAT, WHAT_LAST, __VA_ARGS__) +#define __TECO_FE_5(WHAT, WHAT_LAST, X, ...) WHAT(5,X)__TECO_FE_4(WHAT, WHAT_LAST, __VA_ARGS__) +//... repeat as needed -} /* namespace SciTECO */ +#define __TECO_GET_MACRO(_0,_1,_2,_3,_4,_5,NAME,...) NAME -#endif +/** + * Invoke macro `action(ID, ARG)` on every argument + * and `action_last(ID, ARG)` on the very last one. + * Currently works only for 5 arguments, + * but if more are needed you can add __TECO_FE_X macros. + */ +#define TECO_FOR_EACH(action, action_last, ...) \ + __TECO_GET_MACRO(_0,##__VA_ARGS__,__TECO_FE_5,__TECO_FE_4,__TECO_FE_3, \ + __TECO_FE_2,__TECO_FE_1,__TECO_FE_0)(action,action_last,##__VA_ARGS__) + +#define __TECO_GEN_ARG(ID, X) X arg_##ID, +#define __TECO_GEN_ARG_LAST(ID, X) X arg_##ID +#define __TECO_VTABLE_GEN_CALL(ID, X) arg_##ID, +#define __TECO_VTABLE_GEN_CALL_LAST(ID, X) arg_##ID + +#define TECO_DECLARE_VTABLE_METHOD(RET_TYPE, NS, NAME, OBJ_TYPE, ...) \ + static inline RET_TYPE \ + NS##_##NAME(OBJ_TYPE ctx, ##TECO_FOR_EACH(__TECO_GEN_ARG, __TECO_ARG_LAST, ##__VA_ARGS__)) \ + { \ + return ctx->vtable->NAME(ctx, ##TECO_FOR_EACH(__TECO_VTABLE_GEN_CALL, __TECO_VTABLE_GEN_CALL_LAST, ##__VA_ARGS__)); \ + } \ + typedef RET_TYPE (*NS##_##NAME##_t)(OBJ_TYPE, ##__VA_ARGS__) diff --git a/src/search.c b/src/search.c new file mode 100644 index 0000000..e5e4bd8 --- /dev/null +++ b/src/search.c @@ -0,0 +1,1130 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include + +#include "sciteco.h" +#include "string-utils.h" +#include "expressions.h" +#include "interface.h" +#include "undo.h" +#include "qreg.h" +#include "ring.h" +#include "parser.h" +#include "core-commands.h" +#include "error.h" +#include "search.h" + +typedef struct { + /* + * FIXME: Should perhaps all be teco_int_t? + */ + gint dot; + gint from, to; + gint count; + + teco_buffer_t *from_buffer, *to_buffer; +} teco_search_parameters_t; + +TECO_DEFINE_UNDO_OBJECT_OWN(parameters, teco_search_parameters_t, /* don't delete */); + +/* + * FIXME: Global state should be part of teco_machine_main_t + */ +static teco_search_parameters_t teco_search_parameters; + +static teco_machine_qregspec_t *teco_search_qreg_machine = NULL; + +static gboolean +teco_state_search_initial(teco_machine_main_t *ctx, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return TRUE; + + if (G_UNLIKELY(!teco_search_qreg_machine)) + teco_search_qreg_machine = teco_machine_qregspec_new(TECO_QREG_REQUIRED, ctx->qreg_table_locals, + ctx->parent.must_undo); + + teco_undo_object_parameters_push(&teco_search_parameters); + teco_search_parameters.dot = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + + teco_int_t v1, v2; + if (!teco_expressions_pop_num_calc(&v2, teco_num_sign, error)) + return FALSE; + if (teco_expressions_args()) { + /* TODO: optional count argument? */ + if (!teco_expressions_pop_num_calc(&v1, 0, error)) + return FALSE; + if (v1 <= v2) { + teco_search_parameters.count = 1; + teco_search_parameters.from = (gint)v1; + teco_search_parameters.to = (gint)v2; + } else { + teco_search_parameters.count = -1; + teco_search_parameters.from = (gint)v2; + teco_search_parameters.to = (gint)v1; + } + + if (!teco_validate_pos(teco_search_parameters.from) || + !teco_validate_pos(teco_search_parameters.to)) { + /* + * FIXME: In derived classes, the command name will + * no longer be correct. + * Better use a generic error message and prefix the error in the + * derived states. + */ + teco_error_range_set(error, "S"); + return FALSE; + } + } else { + teco_search_parameters.count = (gint)v2; + if (v2 >= 0) { + teco_search_parameters.from = teco_search_parameters.dot; + teco_search_parameters.to = teco_interface_ssm(SCI_GETLENGTH, 0, 0); + } else { + teco_search_parameters.from = 0; + teco_search_parameters.to = teco_search_parameters.dot; + } + } + + teco_search_parameters.from_buffer = teco_qreg_current ? NULL : teco_ring_current; + teco_search_parameters.to_buffer = NULL; + return TRUE; +} + +static const gchar * +teco_regexp_escape_chr(gchar chr) +{ + static gchar escaped[] = {'\\', '\0', '\0', '\0'}; + + if (!chr) { + escaped[1] = 'c'; + escaped[2] = '@'; + return escaped; + } + + escaped[1] = chr; + escaped[2] = '\0'; + return g_ascii_isalnum(chr) ? escaped + 1 : escaped; +} + +typedef enum { + TECO_SEARCH_STATE_START, + TECO_SEARCH_STATE_NOT, + TECO_SEARCH_STATE_CTL_E, + TECO_SEARCH_STATE_ANYQ, + TECO_SEARCH_STATE_MANY, + TECO_SEARCH_STATE_ALT +} teco_search_state_t; + +/** + * Convert a SciTECO pattern character class to a regular + * expression character class. + * It will throw an error for wrong class specs (like invalid + * Q-Registers) but not incomplete ones. + * + * @param state Initial pattern converter state. + * May be modified on return, e.g. when ^E has been + * scanned without a valid class. + * @param pattern The character class definition to convert. + * This may point into a longer pattern. + * The pointer is modified and always left after + * the last character used, so it may point to the + * terminating null byte after the call. + * @param escape_default Whether to treat single characters + * as classes or not. + * @param error A GError. + * @return A regular expression string or NULL in case of error. + * An empty string signals an incomplete class specification. + * When a non-empty string is returned, the state has always + * been reset to TECO_STATE_STATE_START. + * Must be freed with g_free(). + */ +static gchar * +teco_class2regexp(teco_search_state_t *state, teco_string_t *pattern, + gboolean escape_default, GError **error) +{ + while (pattern->len > 0) { + switch (*state) { + case TECO_SEARCH_STATE_START: + switch (*pattern->data) { + case TECO_CTL_KEY('S'): + pattern->data++; + pattern->len--; + return g_strdup("[:^alnum:]"); + case TECO_CTL_KEY('E'): + *state = TECO_SEARCH_STATE_CTL_E; + break; + default: + /* + * Either a single character "class" or + * not a valid class at all. + */ + if (!escape_default) + return g_strdup(""); + pattern->len--; + return g_strdup(teco_regexp_escape_chr(*pattern->data++)); + } + break; + + case TECO_SEARCH_STATE_CTL_E: + switch (teco_ascii_toupper(*pattern->data)) { + case 'A': + pattern->data++; + pattern->len--; + *state = TECO_SEARCH_STATE_START; + return g_strdup("[:alpha:]"); + /* same as */ + case 'B': + pattern->data++; + pattern->len--; + *state = TECO_SEARCH_STATE_START; + return g_strdup("[:^alnum:]"); + case 'C': + pattern->data++; + pattern->len--; + *state = TECO_SEARCH_STATE_START; + return g_strdup("[:alnum:].$"); + case 'D': + pattern->data++; + pattern->len--; + *state = TECO_SEARCH_STATE_START; + return g_strdup("[:digit:]"); + case 'G': + *state = TECO_SEARCH_STATE_ANYQ; + break; + case 'L': + pattern->data++; + pattern->len--; + *state = TECO_SEARCH_STATE_START; + return g_strdup("\r\n\v\f"); + case 'R': + pattern->data++; + pattern->len--; + *state = TECO_SEARCH_STATE_START; + return g_strdup("[:alnum:]"); + case 'V': + pattern->data++; + pattern->len--; + *state = TECO_SEARCH_STATE_START; + return g_strdup("[:lower:]"); + case 'W': + pattern->data++; + pattern->len--; + *state = TECO_SEARCH_STATE_START; + return g_strdup("[:upper:]"); + default: + /* + * Not a valid ^E class, but could still + * be a higher-level ^E pattern. + */ + return g_strdup(""); + } + break; + + case TECO_SEARCH_STATE_ANYQ: { + teco_qreg_t *reg; + + switch (teco_machine_qregspec_input(teco_search_qreg_machine, + *pattern->data, ®, NULL, error)) { + case TECO_MACHINE_QREGSPEC_ERROR: + return NULL; + + case TECO_MACHINE_QREGSPEC_MORE: + /* incomplete, but consume byte */ + break; + + case TECO_MACHINE_QREGSPEC_DONE: + teco_machine_qregspec_reset(teco_search_qreg_machine); + + g_auto(teco_string_t) str = {NULL, 0}; + if (!reg->vtable->get_string(reg, &str.data, &str.len, error)) + return NULL; + + pattern->data++; + pattern->len--; + *state = TECO_SEARCH_STATE_START; + return g_regex_escape_string(str.data, str.len); + } + break; + } + + default: + /* + * Not a class, but could still be any other + * high-level pattern. + */ + return g_strdup(""); + } + + pattern->data++; + pattern->len--; + } + + /* + * End of string. May happen for empty strings but also + * incomplete ^E or ^EG classes. + */ + return g_strdup(""); +} + +/** + * Convert SciTECO pattern to regular expression. + * It will throw an error for definitely wrong pattern constructs + * but not for incomplete patterns (a necessity of search-as-you-type). + * + * @bug Incomplete patterns after a pattern has been closed (i.e. its + * string argument) are currently not reported as errors. + * + * @param pattern The pattern to scan through. + * Modifies the pointer to point after the last + * successfully scanned character, so it can be + * called recursively. It may also point to the + * terminating null byte after the call. + * @param single_expr Whether to scan a single pattern + * expression or an arbitrary sequence. + * @param error A GError. + * @return The regular expression string or NULL in case of GError. + * Must be freed with g_free(). + */ +static gchar * +teco_pattern2regexp(teco_string_t *pattern, gboolean single_expr, GError **error) +{ + teco_search_state_t state = TECO_SEARCH_STATE_START; + g_auto(teco_string_t) re = {NULL, 0}; + + do { + /* + * First check whether it is a class. + * This will not treat individual characters + * as classes, so we do not convert them to regexp + * classes unnecessarily. + */ + g_autofree gchar *temp = teco_class2regexp(&state, pattern, FALSE, error); + if (!temp) + return NULL; + + if (*temp) { + g_assert(state == TECO_SEARCH_STATE_START); + + teco_string_append_c(&re, '['); + teco_string_append(&re, temp, strlen(temp)); + teco_string_append_c(&re, ']'); + + /* teco_class2regexp() already consumed all the bytes */ + continue; + } + + if (!pattern->len) + /* end of pattern */ + break; + + switch (state) { + case TECO_SEARCH_STATE_START: + switch (*pattern->data) { + case TECO_CTL_KEY('X'): teco_string_append_c(&re, '.'); break; + case TECO_CTL_KEY('N'): state = TECO_SEARCH_STATE_NOT; break; + default: { + const gchar *escaped = teco_regexp_escape_chr(*pattern->data); + teco_string_append(&re, escaped, strlen(escaped)); + } + } + break; + + case TECO_SEARCH_STATE_NOT: { + state = TECO_SEARCH_STATE_START; + g_autofree gchar *temp = teco_class2regexp(&state, pattern, TRUE, error); + if (!temp) + return NULL; + if (!*temp) + /* a complete class is strictly required */ + return g_strdup(""); + g_assert(state == TECO_SEARCH_STATE_START); + + teco_string_append(&re, "[^", 2); + teco_string_append(&re, temp, strlen(temp)); + teco_string_append(&re, "]", 1); + + /* class2regexp() already consumed all the bytes */ + continue; + } + + case TECO_SEARCH_STATE_CTL_E: + state = TECO_SEARCH_STATE_START; + switch (teco_ascii_toupper(*pattern->data)) { + case 'M': state = TECO_SEARCH_STATE_MANY; break; + case 'S': teco_string_append(&re, "\\s+", 3); break; + /* same as */ + case 'X': teco_string_append_c(&re, '.'); break; + /* TODO: ASCII octal code!? */ + case '[': + teco_string_append_c(&re, '('); + state = TECO_SEARCH_STATE_ALT; + break; + default: + teco_error_syntax_set(error, *pattern->data); + return NULL; + } + break; + + case TECO_SEARCH_STATE_MANY: { + /* consume exactly one pattern element */ + g_autofree gchar *temp = teco_pattern2regexp(pattern, TRUE, error); + if (!temp) + return NULL; + if (!*temp) + /* a complete expression is strictly required */ + return g_strdup(""); + + teco_string_append(&re, "(", 1); + teco_string_append(&re, temp, strlen(temp)); + teco_string_append(&re, ")+", 2); + state = TECO_SEARCH_STATE_START; + + /* teco_pattern2regexp() already consumed all the bytes */ + continue; + } + + case TECO_SEARCH_STATE_ALT: + switch (*pattern->data) { + case ',': + teco_string_append_c(&re, '|'); + break; + case ']': + teco_string_append_c(&re, ')'); + state = TECO_SEARCH_STATE_START; + break; + default: { + g_autofree gchar *temp = teco_pattern2regexp(pattern, TRUE, error); + if (!temp) + return NULL; + if (!*temp) + /* a complete expression is strictly required */ + return g_strdup(""); + + teco_string_append(&re, temp, strlen(temp)); + + /* pattern2regexp() already consumed all the bytes */ + continue; + } + } + break; + + default: + /* shouldn't happen */ + g_assert_not_reached(); + } + + pattern->data++; + pattern->len--; + } while (!single_expr || state != TECO_SEARCH_STATE_START); + + /* + * Complete open alternative. + * This could be handled like an incomplete expression, + * but closing it automatically improved search-as-you-type. + */ + if (state == TECO_SEARCH_STATE_ALT) + teco_string_append_c(&re, ')'); + + g_assert(!teco_string_contains(&re, '\0')); + return g_steal_pointer(&re.data) ? : g_strdup(""); +} + +static gboolean +teco_do_search(GRegex *re, gint from, gint to, gint *count, GError **error) +{ + g_autoptr(GMatchInfo) info = NULL; + const gchar *buffer = (const gchar *)teco_interface_ssm(SCI_GETCHARACTERPOINTER, 0, 0); + GError *tmp_error = NULL; + + /* + * NOTE: The return boolean does NOT signal whether an error was generated. + */ + g_regex_match_full(re, buffer, (gssize)to, from, 0, &info, &tmp_error); + if (tmp_error) { + g_propagate_error(error, tmp_error); + return FALSE; + } + + gint matched_from = -1, matched_to = -1; + + if (*count >= 0) { + while (g_match_info_matches(info) && --(*count)) { + /* + * NOTE: The return boolean does NOT signal whether an error was generated. + */ + g_match_info_next(info, &tmp_error); + if (tmp_error) { + g_propagate_error(error, tmp_error); + return FALSE; + } + } + + if (!*count) + /* successful */ + g_match_info_fetch_pos(info, 0, + &matched_from, &matched_to); + } else { + /* only keep the last `count' matches, in a circular stack */ + typedef struct { + gint from, to; + } teco_range_t; + + /* + * NOTE: It's theoretically possible that this single allocation + * causes an OOM if (-count) is large enough and memory limiting won't help. + * That's why we exceptionally have to check for allocation success. + */ + g_autofree teco_range_t *matched = g_try_new(teco_range_t, -*count); + if (!matched) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Search count too small (%d)", *count); + return FALSE; + } + + gint matched_total = 0, i = 0; + + while (g_match_info_matches(info)) { + g_match_info_fetch_pos(info, 0, + &matched[i].from, &matched[i].to); + + /* + * NOTE: The return boolean does NOT signal whether an error was generated. + */ + g_match_info_next(info, &tmp_error); + if (tmp_error) { + g_propagate_error(error, tmp_error); + return FALSE; + } + + i = ++matched_total % -(*count); + } + + *count = MIN(*count + matched_total, 0); + if (!*count) { + /* successful -> i points to stack bottom */ + matched_from = matched[i].from; + matched_to = matched[i].to; + } + } + + if (matched_from >= 0 && matched_to >= 0) + /* match success */ + teco_interface_ssm(SCI_SETSEL, matched_from, matched_to); + + return TRUE; +} + +static gboolean +teco_state_search_process(teco_machine_main_t *ctx, const teco_string_t *str, gsize new_chars, GError **error) +{ + static const GRegexCompileFlags flags = G_REGEX_CASELESS | G_REGEX_MULTILINE | + G_REGEX_DOTALL | G_REGEX_RAW; + + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_SETSEL, + teco_interface_ssm(SCI_GETANCHOR, 0, 0), + teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0)); + + teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1); + g_assert(search_reg != NULL); + if (!search_reg->vtable->undo_set_integer(search_reg, error) || + !search_reg->vtable->set_integer(search_reg, TECO_FAILURE, error)) + return FALSE; + + g_autoptr(GRegex) re = NULL; + teco_string_t pattern = *str; + /* NOTE: teco_pattern2regexp() modifies str pointer */ + g_autofree gchar *re_pattern = teco_pattern2regexp(&pattern, FALSE, error); + if (!re_pattern) + return FALSE; + teco_machine_qregspec_reset(teco_search_qreg_machine); +#ifdef DEBUG + g_printf("REGEXP: %s\n", re_pattern); +#endif + if (!*re_pattern) + goto failure; + /* + * FIXME: Should we propagate at least some of the errors? + */ + re = g_regex_new(re_pattern, flags, 0, NULL); + if (!re) + goto failure; + + if (!teco_qreg_current && + teco_ring_current != teco_search_parameters.from_buffer) { + teco_ring_undo_edit(); + teco_buffer_edit(teco_search_parameters.from_buffer); + } + + gint count = teco_search_parameters.count; + + if (!teco_do_search(re, teco_search_parameters.from, teco_search_parameters.to, &count, error)) + return FALSE; + + if (teco_search_parameters.to_buffer && count) { + teco_buffer_t *buffer = teco_search_parameters.from_buffer; + + if (teco_ring_current == buffer) + teco_ring_undo_edit(); + + if (count > 0) { + do { + buffer = teco_buffer_next(buffer) ? : teco_ring_first(); + teco_buffer_edit(buffer); + + if (buffer == teco_search_parameters.to_buffer) { + if (!teco_do_search(re, 0, teco_search_parameters.dot, &count, error)) + return FALSE; + break; + } + + if (!teco_do_search(re, 0, teco_interface_ssm(SCI_GETLENGTH, 0, 0), + &count, error)) + return FALSE; + } while (count); + } else /* count < 0 */ { + do { + buffer = teco_buffer_prev(buffer) ? : teco_ring_last(); + teco_buffer_edit(buffer); + + if (buffer == teco_search_parameters.to_buffer) { + if (!teco_do_search(re, teco_search_parameters.dot, + teco_interface_ssm(SCI_GETLENGTH, 0, 0), + &count, error)) + return FALSE; + break; + } + + if (!teco_do_search(re, 0, teco_interface_ssm(SCI_GETLENGTH, 0, 0), + &count, error)) + return FALSE; + } while (count); + } + + /* + * FIXME: Why is this necessary? + */ + teco_ring_current = buffer; + } + + if (!search_reg->vtable->set_integer(search_reg, teco_bool(!count), error)) + return FALSE; + + if (!count) + return TRUE; + +failure: + teco_interface_ssm(SCI_GOTOPOS, teco_search_parameters.dot, 0); + return TRUE; +} + +static teco_state_t * +teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1); + g_assert(search_reg != NULL); + + if (str->len > 0) { + /* workaround: preserve selection (also on rubout) */ + gint anchor = teco_interface_ssm(SCI_GETANCHOR, 0, 0); + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_SETANCHOR, anchor, 0); + + if (!search_reg->vtable->undo_set_string(search_reg, error) || + !search_reg->vtable->set_string(search_reg, str->data, str->len, error)) + return NULL; + + teco_interface_ssm(SCI_SETANCHOR, anchor, 0); + } else { + g_auto(teco_string_t) search_str = {NULL, 0}; + if (!search_reg->vtable->get_string(search_reg, &search_str.data, &search_str.len, error) || + !teco_state_search_process(ctx, &search_str, search_str.len, error)) + return NULL; + } + + teco_int_t search_state; + if (!search_reg->vtable->get_integer(search_reg, &search_state, error)) + return FALSE; + + if (teco_machine_main_eval_colon(ctx)) + teco_expressions_push(search_state); + else if (teco_is_failure(search_state) && + !teco_loop_stack->len /* not in loop */) + teco_interface_msg(TECO_MSG_ERROR, "Search string not found!"); + + return &teco_state_start; +} + +/** + * @class TECO_DEFINE_STATE_SEARCH + * @implements TECO_DEFINE_STATE_EXPECTSTRING + * @ingroup states + * + * @fixme We need to provide a process_edit_cmd_cb since search patterns + * can also contain Q-Register references. + */ +#define TECO_DEFINE_STATE_SEARCH(NAME, ...) \ + TECO_DEFINE_STATE_EXPECTSTRING(NAME, \ + .initial_cb = (teco_state_initial_cb_t)teco_state_search_initial, \ + .expectstring.process_cb = teco_state_search_process, \ + .expectstring.done_cb = NAME##_done, \ + ##__VA_ARGS__ \ + ) + +/*$ S search pattern + * S[pattern]$ -- Search for pattern + * [n]S[pattern]$ + * -S[pattern]$ + * from,toS[pattern]$ + * :S[pattern]$ -> Success|Failure + * [n]:S[pattern]$ -> Success|Failure + * -:S[pattern]$ -> Success|Failure + * from,to:S[pattern]$ -> Success|Failure + * + * Search for in the current buffer/Q-Register. + * Search order and range depends on the arguments given. + * By default without any arguments, S will search forward + * from dot till file end. + * The optional single argument specifies the occurrence + * to search (1 is the first occurrence, 2 the second, etc.). + * Negative values for perform backward searches. + * If missing, the sign prefix is implied for . + * Therefore \(lq-S\(rq will search for the first occurrence + * of before dot. + * + * If two arguments are specified on the command, + * search will be bounded in the character range up to + * , and only the first occurrence will be searched. + * might be larger than in which case a backward + * search is performed in the selected range. + * + * After performing the search, the search is saved + * in the global search Q-Register \(lq_\(rq. + * A success/failure condition boolean is saved in that + * register's integer part. + * may be omitted in which case the pattern of + * the last search or search and replace command will be + * implied by using the contents of register \(lq_\(rq + * (this could of course also be manually set). + * + * After a successful search, the pointer is positioned after + * the found text in the buffer. + * An unsuccessful search will display an error message but + * not actually yield an error. + * The message displaying is suppressed when executed from loops + * and register \(lq_\(rq is the implied argument for break-commands + * so that a search-break idiom can be implemented as follows: + * .EX + * + * .EE + * Alternatively, S may be colon-modified in which case it returns + * a condition boolean that may be directly evaluated by a + * conditional or break-command. + * + * In interactive mode, searching will be performed immediately + * (\(lqsearch as you type\(rq) highlighting matched text + * on the fly. + * Changing the results in the search being reperformed + * from the beginning. + */ +TECO_DEFINE_STATE_SEARCH(teco_state_search); + +static gboolean +teco_state_search_all_initial(teco_machine_main_t *ctx, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return TRUE; + + teco_undo_object_parameters_push(&teco_search_parameters); + teco_search_parameters.dot = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + + teco_int_t v1, v2; + + if (!teco_expressions_pop_num_calc(&v2, teco_num_sign, error)) + return FALSE; + if (teco_expressions_args()) { + /* TODO: optional count argument? */ + if (!teco_expressions_pop_num_calc(&v1, 0, error)) + return FALSE; + if (v1 <= v2) { + teco_search_parameters.count = 1; + teco_search_parameters.from_buffer = teco_ring_find(v1); + teco_search_parameters.to_buffer = teco_ring_find(v2); + } else { + teco_search_parameters.count = -1; + teco_search_parameters.from_buffer = teco_ring_find(v2); + teco_search_parameters.to_buffer = teco_ring_find(v1); + } + + if (!teco_search_parameters.from_buffer || !teco_search_parameters.to_buffer) { + teco_error_range_set(error, "N"); + return FALSE; + } + } else { + teco_search_parameters.count = (gint)v2; + /* NOTE: on Q-Registers, behave like "S" */ + if (teco_qreg_current) { + teco_search_parameters.from_buffer = NULL; + teco_search_parameters.to_buffer = NULL; + } else { + teco_search_parameters.from_buffer = teco_ring_current; + teco_search_parameters.to_buffer = teco_ring_current; + } + } + + if (teco_search_parameters.count >= 0) { + teco_search_parameters.from = teco_search_parameters.dot; + teco_search_parameters.to = teco_interface_ssm(SCI_GETLENGTH, 0, 0); + } else { + teco_search_parameters.from = 0; + teco_search_parameters.to = teco_search_parameters.dot; + } + + return TRUE; +} + +static teco_state_t * +teco_state_search_all_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode <= TECO_MODE_NORMAL && + (!teco_state_search_done(ctx, str, error) || + !teco_ed_hook(TECO_ED_HOOK_EDIT, error))) + return NULL; + + return &teco_state_start; +} + +/*$ N + * [n]N[pattern]$ -- Search over buffer-boundaries + * -N[pattern]$ + * from,toN[pattern]$ + * [n]:N[pattern]$ -> Success|Failure + * -:N[pattern]$ -> Success|Failure + * from,to:N[pattern]$ -> Success|Failure + * + * Search for over buffer boundaries. + * This command is similar to the regular search command + * (S) but will continue to search for occurrences of + * pattern when the end or beginning of the current buffer + * is reached. + * Occurrences of spanning over buffer boundaries + * will not be found. + * When searching forward N will start in the current buffer + * at dot, continue with the next buffer in the ring searching + * the entire buffer until it reaches the end of the buffer + * ring, continue with the first buffer in the ring until + * reaching the current file again where it searched from the + * beginning of the buffer up to its current dot. + * Searching backwards does the reverse. + * + * N also differs from S in the interpretation of two arguments. + * Using two arguments the search will be bounded between the + * buffer with number , up to the buffer with number + * . + * When specifying buffer ranges, the entire buffers are searched + * from beginning to end. + * may be greater than in which case, searching starts + * at the end of buffer and continues backwards until the + * beginning of buffer has been reached. + * Furthermore as with all buffer numbers, the buffer ring + * is considered a circular structure and it is possible + * to search over buffer ring boundaries by specifying + * buffer numbers greater than the number of buffers in the + * ring. + */ +TECO_DEFINE_STATE_SEARCH(teco_state_search_all, + .initial_cb = (teco_state_initial_cb_t)teco_state_search_all_initial +); + +static teco_state_t * +teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1); + g_assert(search_reg != NULL); + + teco_int_t search_state; + if (!teco_state_search_done(ctx, str, error) || + !search_reg->vtable->get_integer(search_reg, &search_state, error)) + return NULL; + + if (teco_is_failure(search_state)) + return &teco_state_start; + + gint dot = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + if (teco_search_parameters.dot < dot) { + /* kill forwards */ + gint anchor = teco_interface_ssm(SCI_GETANCHOR, 0, 0); + + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_GOTOPOS, dot, 0); + teco_interface_ssm(SCI_GOTOPOS, anchor, 0); + + teco_interface_ssm(SCI_DELETERANGE, teco_search_parameters.dot, + anchor - teco_search_parameters.dot); + } else { + /* kill backwards */ + teco_interface_ssm(SCI_DELETERANGE, dot, teco_search_parameters.dot - dot); + } + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + teco_ring_dirtify(); + + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + + return &teco_state_start; +} + +/*$ FK + * FK[pattern]$ -- Delete up to occurrence of pattern + * [n]FK[pattern]$ + * -FK[pattern]$ + * from,toFK[pattern]$ + * :FK[pattern]$ -> Success|Failure + * [n]:FK[pattern]$ -> Success|Failure + * -:FK[pattern]$ -> Success|Failure + * from,to:FK[pattern]$ -> Success|Failure + * + * FK searches for just like the regular search + * command (S) but when found deletes all text from dot + * up to but not including the found text instance. + * When searching backwards the characters beginning after + * the occurrence of up to dot are deleted. + * + * In interactive mode, deletion is not performed + * as-you-type but only on command termination. + */ +TECO_DEFINE_STATE_SEARCH(teco_state_search_kill); + +static teco_state_t * +teco_state_search_delete_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1); + g_assert(search_reg != NULL); + + teco_int_t search_state; + if (!teco_state_search_done(ctx, str, error) || + !search_reg->vtable->get_integer(search_reg, &search_state, error)) + return NULL; + + if (teco_is_success(search_state)) { + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + teco_interface_ssm(SCI_REPLACESEL, 0, (sptr_t)""); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + teco_ring_dirtify(); + + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + } + + return &teco_state_start; +} + +/*$ FD + * FD[pattern]$ -- Delete occurrence of pattern + * [n]FD[pattern]$ + * -FD[pattern]$ + * from,toFD[pattern]$ + * :FD[pattern]$ -> Success|Failure + * [n]:FD[pattern]$ -> Success|Failure + * -:FD[pattern]$ -> Success|Failure + * from,to:FD[pattern]$ -> Success|Failure + * + * Searches for just like the regular search command + * (S) but when found deletes the entire occurrence. + */ +TECO_DEFINE_STATE_SEARCH(teco_state_search_delete); + +/* + * FIXME: Could be static + */ +TECO_DEFINE_STATE_INSERT(teco_state_replace_insert, + .initial_cb = NULL +); + +static teco_state_t * +teco_state_replace_ignore_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + return &teco_state_start; +} + +/* + * FIXME: Could be static + */ +TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_ignore); + +static teco_state_t * +teco_state_replace_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_replace_ignore; + + teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1); + g_assert(search_reg != NULL); + + teco_int_t search_state; + if (!teco_state_search_delete_done(ctx, str, error) || + !search_reg->vtable->get_integer(search_reg, &search_state, error)) + return NULL; + + return teco_is_success(search_state) ? &teco_state_replace_insert + : &teco_state_replace_ignore; +} + +/*$ FS + * FS[pattern]$[string]$ -- Search and replace + * [n]FS[pattern]$[string]$ + * -FS[pattern]$[string]$ + * from,toFS[pattern]$[string]$ + * :FS[pattern]$[string]$ -> Success|Failure + * [n]:FS[pattern]$[string]$ -> Success|Failure + * -:FS[pattern]$[string]$ -> Success|Failure + * from,to:FS[pattern]$[string]$ -> Success|Failure + * + * Search for just like the regular search command + * (S) does but replace it with if found. + * If is empty, the occurrence will always be + * deleted so \(lqFS[pattern]$$\(rq is similar to + * \(lqFD[pattern]$\(rq. + * The global replace register is \fBnot\fP touched + * by the FS command. + * + * In interactive mode, the replacement will be performed + * immediately and interactively. + */ +TECO_DEFINE_STATE_SEARCH(teco_state_replace, + .expectstring.last = FALSE +); + +/* + * FIXME: TECO_DEFINE_STATE_INSERT() already defines a done_cb(), + * so we had to name this differently. + * Perhaps it simply shouldn't define it. + */ +static teco_state_t * +teco_state_replace_default_insert_done_overwrite(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + teco_qreg_t *replace_reg = teco_qreg_table_find(&teco_qreg_table_globals, "-", 1); + g_assert(replace_reg != NULL); + + if (str->len > 0) { + if (!replace_reg->vtable->undo_set_string(replace_reg, error) || + !replace_reg->vtable->set_string(replace_reg, str->data, str->len, error)) + return NULL; + } else { + g_auto(teco_string_t) replace_str = {NULL, 0}; + if (!replace_reg->vtable->get_string(replace_reg, &replace_str.data, &replace_str.len, error) || + !teco_state_insert_process(ctx, &replace_str, replace_str.len, error)) + return NULL; + } + + return &teco_state_start; +} + +/* + * FIXME: Could be static + */ +TECO_DEFINE_STATE_INSERT(teco_state_replace_default_insert, + .initial_cb = NULL, + .expectstring.done_cb = teco_state_replace_default_insert_done_overwrite +); + +static teco_state_t * +teco_state_replace_default_ignore_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL || + !str->len) + return &teco_state_start; + + teco_qreg_t *replace_reg = teco_qreg_table_find(&teco_qreg_table_globals, "-", 1); + g_assert(replace_reg != NULL); + + if (!replace_reg->vtable->undo_set_string(replace_reg, error) || + !replace_reg->vtable->set_string(replace_reg, str->data, str->len, error)) + return NULL; + + return &teco_state_start; +} + +/* + * FIXME: Could be static + */ +TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_default_ignore); + +static teco_state_t * +teco_state_replace_default_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_replace_default_ignore; + + teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1); + g_assert(search_reg != NULL); + + teco_int_t search_state; + if (!teco_state_search_delete_done(ctx, str, error) || + !search_reg->vtable->get_integer(search_reg, &search_state, error)) + return NULL; + + return teco_is_success(search_state) ? &teco_state_replace_default_insert + : &teco_state_replace_default_ignore; +} + +/*$ FR + * FR[pattern]$[string]$ -- Search and replace with default + * [n]FR[pattern]$[string]$ + * -FR[pattern]$[string]$ + * from,toFR[pattern]$[string]$ + * :FR[pattern]$[string]$ -> Success|Failure + * [n]:FR[pattern]$[string]$ -> Success|Failure + * -:FR[pattern]$[string]$ -> Success|Failure + * from,to:FR[pattern]$[string]$ -> Success|Failure + * + * The FR command is similar to the FS command. + * It searches for just like the regular search + * command (S) and replaces the occurrence with + * similar to what FS does. + * It differs from FS in the fact that the replacement + * string is saved in the global replacement register + * \(lq-\(rq. + * If is empty the string in the global replacement + * register is implied instead. + */ +TECO_DEFINE_STATE_SEARCH(teco_state_replace_default, + .expectstring.last = FALSE +); diff --git a/src/search.cpp b/src/search.cpp deleted file mode 100644 index 63c59e9..0000000 --- a/src/search.cpp +++ /dev/null @@ -1,946 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include -#include - -#include "sciteco.h" -#include "string-utils.h" -#include "expressions.h" -#include "undo.h" -#include "qregisters.h" -#include "ring.h" -#include "parser.h" -#include "search.h" -#include "error.h" - -namespace SciTECO { - -namespace States { - StateSearch search; - StateSearchAll searchall; - StateSearchKill searchkill; - StateSearchDelete searchdelete; - - StateReplace replace; - StateReplace_insert replace_insert; - StateReplace_ignore replace_ignore; - - StateReplaceDefault replacedefault; - StateReplaceDefault_insert replacedefault_insert; - StateReplaceDefault_ignore replacedefault_ignore; -} - -/* - * Command states - */ - -/*$ S search pattern - * S[pattern]$ -- Search for pattern - * [n]S[pattern]$ - * -S[pattern]$ - * from,toS[pattern]$ - * :S[pattern]$ -> Success|Failure - * [n]:S[pattern]$ -> Success|Failure - * -:S[pattern]$ -> Success|Failure - * from,to:S[pattern]$ -> Success|Failure - * - * Search for in the current buffer/Q-Register. - * Search order and range depends on the arguments given. - * By default without any arguments, S will search forward - * from dot till file end. - * The optional single argument specifies the occurrence - * to search (1 is the first occurrence, 2 the second, etc.). - * Negative values for perform backward searches. - * If missing, the sign prefix is implied for . - * Therefore \(lq-S\(rq will search for the first occurrence - * of before dot. - * - * If two arguments are specified on the command, - * search will be bounded in the character range up to - * , and only the first occurrence will be searched. - * might be larger than in which case a backward - * search is performed in the selected range. - * - * After performing the search, the search is saved - * in the global search Q-Register \(lq_\(rq. - * A success/failure condition boolean is saved in that - * register's integer part. - * may be omitted in which case the pattern of - * the last search or search and replace command will be - * implied by using the contents of register \(lq_\(rq - * (this could of course also be manually set). - * - * After a successful search, the pointer is positioned after - * the found text in the buffer. - * An unsuccessful search will display an error message but - * not actually yield an error. - * The message displaying is suppressed when executed from loops - * and register \(lq_\(rq is the implied argument for break-commands - * so that a search-break idiom can be implemented as follows: - * .EX - * - * .EE - * Alternatively, S may be colon-modified in which case it returns - * a condition boolean that may be directly evaluated by a - * conditional or break-command. - * - * In interactive mode, searching will be performed immediately - * (\(lqsearch as you type\(rq) highlighting matched text - * on the fly. - * Changing the results in the search being reperformed - * from the beginning. - */ -void -StateSearch::initial(void) -{ - tecoInt v1, v2; - - undo.push_var(parameters); - - parameters.dot = interface.ssm(SCI_GETCURRENTPOS); - - v2 = expressions.pop_num_calc(); - if (expressions.args()) { - /* TODO: optional count argument? */ - v1 = expressions.pop_num_calc(); - if (v1 <= v2) { - parameters.count = 1; - parameters.from = (gint)v1; - parameters.to = (gint)v2; - } else { - parameters.count = -1; - parameters.from = (gint)v2; - parameters.to = (gint)v1; - } - - if (!Validate::pos(parameters.from) || - !Validate::pos(parameters.to)) - throw RangeError("S"); - } else { - parameters.count = (gint)v2; - if (v2 >= 0) { - parameters.from = parameters.dot; - parameters.to = interface.ssm(SCI_GETLENGTH); - } else { - parameters.from = 0; - parameters.to = parameters.dot; - } - } - - parameters.from_buffer = QRegisters::current ? NULL : ring.current; - parameters.to_buffer = NULL; -} - -static inline const gchar * -regexp_escape_chr(gchar chr) -{ - static gchar escaped[] = {'\\', '\0', '\0'}; - - escaped[1] = chr; - return g_ascii_isalnum(chr) ? escaped + 1 : escaped; -} - -/** - * Convert a SciTECO pattern character class to a regular - * expression character class. - * It will throw an error for wrong class specs (like invalid - * Q-Registers) but not incomplete ones. - * - * @param state Initial pattern converter state. - * May be modified on return, e.g. when ^E has been - * scanned without a valid class. - * @param pattern The character class definition to convert. - * This may point into a longer pattern. - * The pointer is modified and always left after - * the last character used, so it may point to the - * terminating null byte after the call. - * @param escape_default Whether to treat single characters - * as classes or not. - * @return A regular expression string or NULL for incomplete - * class specifications. When a string is returned, the - * state has always been reset to STATE_START. - * Must be freed with g_free(). - */ -gchar * -StateSearch::class2regexp(MatchState &state, const gchar *&pattern, - bool escape_default) -{ - while (*pattern) { - QRegister *reg; - gchar *temp, *temp2; - - switch (state) { - case STATE_START: - switch (*pattern) { - case CTL_KEY('S'): - pattern++; - return g_strdup("[:^alnum:]"); - case CTL_KEY('E'): - state = STATE_CTL_E; - break; - default: - /* - * Either a single character "class" or - * not a valid class at all. - */ - return escape_default ? g_strdup(regexp_escape_chr(*pattern++)) - : NULL; - } - break; - - case STATE_CTL_E: - switch (String::toupper(*pattern)) { - case 'A': - pattern++; - state = STATE_START; - return g_strdup("[:alpha:]"); - /* same as */ - case 'B': - pattern++; - state = STATE_START; - return g_strdup("[:^alnum:]"); - case 'C': - pattern++; - state = STATE_START; - return g_strdup("[:alnum:].$"); - case 'D': - pattern++; - state = STATE_START; - return g_strdup("[:digit:]"); - case 'G': - state = STATE_ANYQ; - break; - case 'L': - pattern++; - state = STATE_START; - return g_strdup("\r\n\v\f"); - case 'R': - pattern++; - state = STATE_START; - return g_strdup("[:alnum:]"); - case 'V': - pattern++; - state = STATE_START; - return g_strdup("[:lower:]"); - case 'W': - pattern++; - state = STATE_START; - return g_strdup("[:upper:]"); - default: - /* - * Not a valid ^E class, but could still - * be a higher-level ^E pattern. - */ - return NULL; - } - break; - - case STATE_ANYQ: - /* will throw an error for invalid specs */ - if (!qreg_machine.input(*pattern, reg)) - /* incomplete, but consume byte */ - break; - qreg_machine.reset(); - - temp = reg->get_string(); - temp2 = g_regex_escape_string(temp, -1); - g_free(temp); - - pattern++; - state = STATE_START; - return temp2; - - default: - /* - * Not a class, but could still be any other - * high-level pattern. - */ - return NULL; - } - - pattern++; - } - - /* - * End of string. May happen for empty strings but also - * incomplete ^E or ^EG classes. - */ - return NULL; -} - -/** - * Convert SciTECO pattern to regular expression. - * It will throw an error for definitely wrong pattern constructs - * but not for incomplete patterns (a necessity of search-as-you-type). - * - * @bug Incomplete patterns after a pattern has been closed (i.e. its - * string argument) are currently not reported as errors. - * - * @param pattern The pattern to scan through. - * Modifies the pointer to point after the last - * successfully scanned character, so it can be - * called recursively. It may also point to the - * terminating null byte after the call. - * @param single_expr Whether to scan a single pattern - * expression or an arbitrary sequence. - * @return The regular expression string or NULL. - * Must be freed with g_free(). - */ -gchar * -StateSearch::pattern2regexp(const gchar *&pattern, - bool single_expr) -{ - MatchState state = STATE_START; - gchar *re = NULL; - - do { - gchar *temp; - - /* - * First check whether it is a class. - * This will not treat individual characters - * as classes, so we do not convert them to regexp - * classes unnecessarily. - * NOTE: This might throw an error. - */ - try { - temp = class2regexp(state, pattern); - } catch (...) { - g_free(re); - throw; - } - if (temp) { - g_assert(state == STATE_START); - - String::append(re, "["); - String::append(re, temp); - String::append(re, "]"); - g_free(temp); - - /* class2regexp() already consumed all the bytes */ - continue; - } - - if (!*pattern) - /* end of pattern */ - break; - - switch (state) { - case STATE_START: - switch (*pattern) { - case CTL_KEY('X'): String::append(re, "."); break; - case CTL_KEY('N'): state = STATE_NOT; break; - default: - String::append(re, regexp_escape_chr(*pattern)); - } - break; - - case STATE_NOT: - state = STATE_START; - try { - temp = class2regexp(state, pattern, true); - } catch (...) { - g_free(re); - throw; - } - if (!temp) - /* a complete class is strictly required */ - goto incomplete; - g_assert(state == STATE_START); - - String::append(re, "[^"); - String::append(re, temp); - String::append(re, "]"); - g_free(temp); - - /* class2regexp() already consumed all the bytes */ - continue; - - case STATE_CTL_E: - state = STATE_START; - switch (String::toupper(*pattern)) { - case 'M': state = STATE_MANY; break; - case 'S': String::append(re, "\\s+"); break; - /* same as */ - case 'X': String::append(re, "."); break; - /* TODO: ASCII octal code!? */ - case '[': - String::append(re, "("); - state = STATE_ALT; - break; - default: - g_free(re); - throw SyntaxError(*pattern); - } - break; - - case STATE_MANY: - /* consume exactly one pattern element */ - try { - temp = pattern2regexp(pattern, true); - } catch (...) { - g_free(re); - throw; - } - if (!temp) - /* a complete expression is strictly required */ - goto incomplete; - - String::append(re, "("); - String::append(re, temp); - String::append(re, ")+"); - g_free(temp); - state = STATE_START; - - /* pattern2regexp() already consumed all the bytes */ - continue; - - case STATE_ALT: - switch (*pattern) { - case ',': - String::append(re, "|"); - break; - case ']': - String::append(re, ")"); - state = STATE_START; - break; - default: - try { - temp = pattern2regexp(pattern, true); - } catch (...) { - g_free(re); - throw; - } - if (!temp) - /* a complete expression is strictly required */ - goto incomplete; - - String::append(re, temp); - g_free(temp); - - /* pattern2regexp() already consumed all the bytes */ - continue; - } - break; - - default: - /* shouldn't happen */ - g_assert_not_reached(); - } - - pattern++; - } while (!single_expr || state != STATE_START); - - /* - * Complete open alternative. - * This could be handled like an incomplete expression, - * but closing it automatically improved search-as-you-type. - */ - if (state == STATE_ALT) - String::append(re, ")"); - - return re; - -incomplete: - g_free(re); - return NULL; -} - -void -StateSearch::do_search(GRegex *re, gint from, gint to, gint &count) -{ - GMatchInfo *info; - const gchar *buffer; - - gint matched_from = -1, matched_to = -1; - - buffer = (const gchar *)interface.ssm(SCI_GETCHARACTERPOINTER); - g_regex_match_full(re, buffer, (gssize)to, from, - (GRegexMatchFlags)0, &info, NULL); - - if (count >= 0) { - while (g_match_info_matches(info) && --count) - g_match_info_next(info, NULL); - - if (!count) - /* successful */ - g_match_info_fetch_pos(info, 0, - &matched_from, &matched_to); - } else { - /* only keep the last `count' matches, in a circular stack */ - struct Range { - gint from, to; - }; - Range *matched = new Range[-count]; - gint matched_total = 0, i = 0; - - while (g_match_info_matches(info)) { - g_match_info_fetch_pos(info, 0, - &matched[i].from, - &matched[i].to); - - g_match_info_next(info, NULL); - i = ++matched_total % -count; - } - - count = MIN(count + matched_total, 0); - if (!count) { - /* successful, i points to stack bottom */ - matched_from = matched[i].from; - matched_to = matched[i].to; - } - - delete[] matched; - } - - g_match_info_free(info); - - if (matched_from >= 0 && matched_to >= 0) - /* match success */ - interface.ssm(SCI_SETSEL, matched_from, matched_to); -} - -void -StateSearch::process(const gchar *str, gint new_chars) -{ - static const gint flags = G_REGEX_CASELESS | G_REGEX_MULTILINE | - G_REGEX_DOTALL | G_REGEX_RAW; - - QRegister *search_reg = QRegisters::globals["_"]; - - gchar *re_pattern; - GRegex *re; - - gint count = parameters.count; - - if (current_doc_must_undo()) - interface.undo_ssm(SCI_SETSEL, - interface.ssm(SCI_GETANCHOR), - interface.ssm(SCI_GETCURRENTPOS)); - - search_reg->undo_set_integer(); - search_reg->set_integer(FAILURE); - - /* - * NOTE: pattern2regexp() modifies str pointer and may throw - */ - re_pattern = pattern2regexp(str); - qreg_machine.reset(); -#ifdef DEBUG - g_printf("REGEXP: %s\n", re_pattern); -#endif - if (!re_pattern) - goto failure; - re = g_regex_new(re_pattern, (GRegexCompileFlags)flags, - (GRegexMatchFlags)0, NULL); - g_free(re_pattern); - if (!re) - goto failure; - - if (!QRegisters::current && - ring.current != parameters.from_buffer) { - ring.undo_edit(); - parameters.from_buffer->edit(); - } - - do_search(re, parameters.from, parameters.to, count); - - if (parameters.to_buffer && count) { - Buffer *buffer = parameters.from_buffer; - - if (ring.current == buffer) - ring.undo_edit(); - - if (count > 0) { - do { - buffer = buffer->next() ? : ring.first(); - buffer->edit(); - - if (buffer == parameters.to_buffer) { - do_search(re, 0, parameters.dot, count); - break; - } - - do_search(re, 0, interface.ssm(SCI_GETLENGTH), - count); - } while (count); - } else /* count < 0 */ { - do { - buffer = buffer->prev() ? : ring.last(); - buffer->edit(); - - if (buffer == parameters.to_buffer) { - do_search(re, parameters.dot, - interface.ssm(SCI_GETLENGTH), - count); - break; - } - - do_search(re, 0, interface.ssm(SCI_GETLENGTH), - count); - } while (count); - } - - ring.current = buffer; - } - - search_reg->set_integer(TECO_BOOL(!count)); - - g_regex_unref(re); - - if (!count) - return; - -failure: - interface.ssm(SCI_GOTOPOS, parameters.dot); -} - -State * -StateSearch::done(const gchar *str) -{ - BEGIN_EXEC(&States::start); - - QRegister *search_reg = QRegisters::globals["_"]; - - if (*str) { - /* workaround: preserve selection (also on rubout) */ - gint anchor = interface.ssm(SCI_GETANCHOR); - if (current_doc_must_undo()) - interface.undo_ssm(SCI_SETANCHOR, anchor); - - search_reg->undo_set_string(); - search_reg->set_string(str); - - interface.ssm(SCI_SETANCHOR, anchor); - } else { - gchar *search_str = search_reg->get_string(); - process(search_str, 0 /* unused */); - g_free(search_str); - } - - if (eval_colon()) - expressions.push(search_reg->get_integer()); - else if (IS_FAILURE(search_reg->get_integer()) && - !loop_stack.items() /* not in loop */) - interface.msg(InterfaceCurrent::MSG_ERROR, "Search string not found!"); - - return &States::start; -} - -/*$ N - * [n]N[pattern]$ -- Search over buffer-boundaries - * -N[pattern]$ - * from,toN[pattern]$ - * [n]:N[pattern]$ -> Success|Failure - * -:N[pattern]$ -> Success|Failure - * from,to:N[pattern]$ -> Success|Failure - * - * Search for over buffer boundaries. - * This command is similar to the regular search command - * (S) but will continue to search for occurrences of - * pattern when the end or beginning of the current buffer - * is reached. - * Occurrences of spanning over buffer boundaries - * will not be found. - * When searching forward N will start in the current buffer - * at dot, continue with the next buffer in the ring searching - * the entire buffer until it reaches the end of the buffer - * ring, continue with the first buffer in the ring until - * reaching the current file again where it searched from the - * beginning of the buffer up to its current dot. - * Searching backwards does the reverse. - * - * N also differs from S in the interpretation of two arguments. - * Using two arguments the search will be bounded between the - * buffer with number , up to the buffer with number - * . - * When specifying buffer ranges, the entire buffers are searched - * from beginning to end. - * may be greater than in which case, searching starts - * at the end of buffer and continues backwards until the - * beginning of buffer has been reached. - * Furthermore as with all buffer numbers, the buffer ring - * is considered a circular structure and it is possible - * to search over buffer ring boundaries by specifying - * buffer numbers greater than the number of buffers in the - * ring. - */ -void -StateSearchAll::initial(void) -{ - tecoInt v1, v2; - - undo.push_var(parameters); - - parameters.dot = interface.ssm(SCI_GETCURRENTPOS); - - v2 = expressions.pop_num_calc(); - if (expressions.args()) { - /* TODO: optional count argument? */ - v1 = expressions.pop_num_calc(); - if (v1 <= v2) { - parameters.count = 1; - parameters.from_buffer = ring.find(v1); - parameters.to_buffer = ring.find(v2); - } else { - parameters.count = -1; - parameters.from_buffer = ring.find(v2); - parameters.to_buffer = ring.find(v1); - } - - if (!parameters.from_buffer || !parameters.to_buffer) - throw RangeError("N"); - } else { - parameters.count = (gint)v2; - /* NOTE: on Q-Registers, behave like "S" */ - if (QRegisters::current) - parameters.from_buffer = parameters.to_buffer = NULL; - else - parameters.from_buffer = parameters.to_buffer = ring.current; - } - - if (parameters.count >= 0) { - parameters.from = parameters.dot; - parameters.to = interface.ssm(SCI_GETLENGTH); - } else { - parameters.from = 0; - parameters.to = parameters.dot; - } -} - -State * -StateSearchAll::done(const gchar *str) -{ - BEGIN_EXEC(&States::start); - - StateSearch::done(str); - QRegisters::hook(QRegisters::HOOK_EDIT); - - return &States::start; -} - -/*$ FK - * FK[pattern]$ -- Delete up to occurrence of pattern - * [n]FK[pattern]$ - * -FK[pattern]$ - * from,toFK[pattern]$ - * :FK[pattern]$ -> Success|Failure - * [n]:FK[pattern]$ -> Success|Failure - * -:FK[pattern]$ -> Success|Failure - * from,to:FK[pattern]$ -> Success|Failure - * - * FK searches for just like the regular search - * command (S) but when found deletes all text from dot - * up to but not including the found text instance. - * When searching backwards the characters beginning after - * the occurrence of up to dot are deleted. - * - * In interactive mode, deletion is not performed - * as-you-type but only on command termination. - */ -State * -StateSearchKill::done(const gchar *str) -{ - gint dot; - - BEGIN_EXEC(&States::start); - - QRegister *search_reg = QRegisters::globals["_"]; - - StateSearch::done(str); - - if (IS_FAILURE(search_reg->get_integer())) - return &States::start; - - dot = interface.ssm(SCI_GETCURRENTPOS); - - interface.ssm(SCI_BEGINUNDOACTION); - if (parameters.dot < dot) { - /* kill forwards */ - gint anchor = interface.ssm(SCI_GETANCHOR); - - if (current_doc_must_undo()) - interface.undo_ssm(SCI_GOTOPOS, dot); - interface.ssm(SCI_GOTOPOS, anchor); - - interface.ssm(SCI_DELETERANGE, - parameters.dot, anchor - parameters.dot); - } else { - /* kill backwards */ - interface.ssm(SCI_DELETERANGE, dot, parameters.dot - dot); - } - interface.ssm(SCI_ENDUNDOACTION); - ring.dirtify(); - - if (current_doc_must_undo()) - interface.undo_ssm(SCI_UNDO); - - return &States::start; -} - -/*$ FD - * FD[pattern]$ -- Delete occurrence of pattern - * [n]FD[pattern]$ - * -FD[pattern]$ - * from,toFD[pattern]$ - * :FD[pattern]$ -> Success|Failure - * [n]:FD[pattern]$ -> Success|Failure - * -:FD[pattern]$ -> Success|Failure - * from,to:FD[pattern]$ -> Success|Failure - * - * Searches for just like the regular search command - * (S) but when found deletes the entire occurrence. - */ -State * -StateSearchDelete::done(const gchar *str) -{ - BEGIN_EXEC(&States::start); - - QRegister *search_reg = QRegisters::globals["_"]; - - StateSearch::done(str); - - if (IS_SUCCESS(search_reg->get_integer())) { - interface.ssm(SCI_BEGINUNDOACTION); - interface.ssm(SCI_REPLACESEL, 0, (sptr_t)""); - interface.ssm(SCI_ENDUNDOACTION); - ring.dirtify(); - - if (current_doc_must_undo()) - interface.undo_ssm(SCI_UNDO); - } - - return &States::start; -} - -/*$ FS - * FS[pattern]$[string]$ -- Search and replace - * [n]FS[pattern]$[string]$ - * -FS[pattern]$[string]$ - * from,toFS[pattern]$[string]$ - * :FS[pattern]$[string]$ -> Success|Failure - * [n]:FS[pattern]$[string]$ -> Success|Failure - * -:FS[pattern]$[string]$ -> Success|Failure - * from,to:FS[pattern]$[string]$ -> Success|Failure - * - * Search for just like the regular search command - * (S) does but replace it with if found. - * If is empty, the occurrence will always be - * deleted so \(lqFS[pattern]$$\(rq is similar to - * \(lqFD[pattern]$\(rq. - * The global replace register is \fBnot\fP touched - * by the FS command. - * - * In interactive mode, the replacement will be performed - * immediately and interactively. - */ -State * -StateReplace::done(const gchar *str) -{ - BEGIN_EXEC(&States::replace_ignore); - - QRegister *search_reg = QRegisters::globals["_"]; - - StateSearchDelete::done(str); - - return IS_SUCCESS(search_reg->get_integer()) - ? (State *)&States::replace_insert - : (State *)&States::replace_ignore; -} - -State * -StateReplace_ignore::done(const gchar *str) -{ - return &States::start; -} - -/*$ FR - * FR[pattern]$[string]$ -- Search and replace with default - * [n]FR[pattern]$[string]$ - * -FR[pattern]$[string]$ - * from,toFR[pattern]$[string]$ - * :FR[pattern]$[string]$ -> Success|Failure - * [n]:FR[pattern]$[string]$ -> Success|Failure - * -:FR[pattern]$[string]$ -> Success|Failure - * from,to:FR[pattern]$[string]$ -> Success|Failure - * - * The FR command is similar to the FS command. - * It searches for just like the regular search - * command (S) and replaces the occurrence with - * similar to what FS does. - * It differs from FS in the fact that the replacement - * string is saved in the global replacement register - * \(lq-\(rq. - * If is empty the string in the global replacement - * register is implied instead. - */ -State * -StateReplaceDefault::done(const gchar *str) -{ - BEGIN_EXEC(&States::replacedefault_ignore); - - QRegister *search_reg = QRegisters::globals["_"]; - - StateSearchDelete::done(str); - - return IS_SUCCESS(search_reg->get_integer()) - ? (State *)&States::replacedefault_insert - : (State *)&States::replacedefault_ignore; -} - -State * -StateReplaceDefault_insert::done(const gchar *str) -{ - BEGIN_EXEC(&States::start); - - QRegister *replace_reg = QRegisters::globals["-"]; - - if (*str) { - replace_reg->undo_set_string(); - replace_reg->set_string(str); - } else { - gchar *replace_str = replace_reg->get_string(); - StateInsert::process(replace_str, strlen(replace_str)); - g_free(replace_str); - } - - return &States::start; -} - -State * -StateReplaceDefault_ignore::done(const gchar *str) -{ - BEGIN_EXEC(&States::start); - - if (*str) { - QRegister *replace_reg = QRegisters::globals["-"]; - - replace_reg->undo_set_string(); - replace_reg->set_string(str); - } - - return &States::start; -} - -} /* namespace SciTECO */ diff --git a/src/search.h b/src/search.h index 0289423..8314f06 100644 --- a/src/search.h +++ b/src/search.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,127 +14,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#pragma once -#ifndef __SEARCH_H -#define __SEARCH_H - -#include - -#include "sciteco.h" #include "parser.h" -#include "ring.h" -#include "qregisters.h" - -namespace SciTECO { - -/* - * "S" command state and base class for all other search/replace commands - */ -class StateSearch : public StateExpectString { -public: - StateSearch(bool last = true) : StateExpectString(true, last) {} - -protected: - struct Parameters { - gint dot; - gint from, to; - gint count; - - Buffer *from_buffer, *to_buffer; - } parameters; - - QRegSpecMachine qreg_machine; - - enum MatchState { - STATE_START, - STATE_NOT, - STATE_CTL_E, - STATE_ANYQ, - STATE_MANY, - STATE_ALT - }; - - gchar *class2regexp(MatchState &state, const gchar *&pattern, - bool escape_default = false); - gchar *pattern2regexp(const gchar *&pattern, bool single_expr = false); - void do_search(GRegex *re, gint from, gint to, gint &count); - - virtual void initial(void); - virtual void process(const gchar *str, gint new_chars); - virtual State *done(const gchar *str); -}; - -class StateSearchAll : public StateSearch { -private: - void initial(void); - State *done(const gchar *str); -}; - -class StateSearchKill : public StateSearch { -private: - State *done(const gchar *str); -}; - -class StateSearchDelete : public StateSearch { -public: - StateSearchDelete(bool last = true) : StateSearch(last) {} - -protected: - State *done(const gchar *str); -}; - -class StateReplace : public StateSearchDelete { -public: - StateReplace() : StateSearchDelete(false) {} - -private: - State *done(const gchar *str); -}; - -class StateReplace_insert : public StateInsert { -private: - void initial(void) {} -}; - -class StateReplace_ignore : public StateExpectString { -private: - State *done(const gchar *str); -}; - -class StateReplaceDefault : public StateSearchDelete { -public: - StateReplaceDefault() : StateSearchDelete(false) {} - -private: - State *done(const gchar *str); -}; - -class StateReplaceDefault_insert : public StateInsert { -private: - void initial(void) {} - State *done(const gchar *str); -}; - -class StateReplaceDefault_ignore : public StateExpectString { -private: - State *done(const gchar *str); -}; - -namespace States { - extern StateSearch search; - extern StateSearchAll searchall; - extern StateSearchKill searchkill; - extern StateSearchDelete searchdelete; - - extern StateReplace replace; - extern StateReplace_insert replace_insert; - extern StateReplace_ignore replace_ignore; - - extern StateReplaceDefault replacedefault; - extern StateReplaceDefault_insert replacedefault_insert; - extern StateReplaceDefault_ignore replacedefault_ignore; -} - -} /* namespace SciTECO */ -#endif +TECO_DECLARE_STATE(teco_state_search); +TECO_DECLARE_STATE(teco_state_search_all); +TECO_DECLARE_STATE(teco_state_search_kill); +TECO_DECLARE_STATE(teco_state_search_delete); +TECO_DECLARE_STATE(teco_state_replace); +TECO_DECLARE_STATE(teco_state_replace_default); diff --git a/src/spawn.c b/src/spawn.c new file mode 100644 index 0000000..1406731 --- /dev/null +++ b/src/spawn.c @@ -0,0 +1,667 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include "sciteco.h" +#include "interface.h" +#include "undo.h" +#include "expressions.h" +#include "qreg.h" +#include "eol.h" +#include "ring.h" +#include "parser.h" +#include "memory.h" +#include "core-commands.h" +#include "qreg-commands.h" +#include "error.h" +#include "spawn.h" + +static void teco_spawn_child_watch_cb(GPid pid, gint status, gpointer data); +static gboolean teco_spawn_stdin_watch_cb(GIOChannel *chan, + GIOCondition condition, gpointer data); +static gboolean teco_spawn_stdout_watch_cb(GIOChannel *chan, + GIOCondition condition, gpointer data); + +/* + * FIXME: Global state should be part of teco_machine_main_t + */ +static struct { + GMainContext *mainctx; + GMainLoop *mainloop; + GSource *child_src; + GSource *stdin_src, *stdout_src; + + teco_int_t from, to; + teco_int_t start; + gboolean text_added; + + teco_eol_writer_t stdin_writer; + teco_eol_reader_t stdout_reader; + + GError *error; + teco_bool_t rc; + + teco_qreg_t *register_argument; +} teco_spawn_ctx; + +static void __attribute__((constructor)) +teco_spawn_init(void) +{ + memset(&teco_spawn_ctx, 0, sizeof(teco_spawn_ctx)); + /* + * Context and loop can be reused between EC invocations. + * However we should not use the default context, since it + * may be used by GTK + */ + teco_spawn_ctx.mainctx = g_main_context_new(); + teco_spawn_ctx.mainloop = g_main_loop_new(teco_spawn_ctx.mainctx, FALSE); +} + +static gchar ** +teco_parse_shell_command_line(const gchar *cmdline, GError **error) +{ + gchar **argv; + +#ifdef G_OS_WIN32 + if (!(teco_ed & TECO_ED_SHELLEMU)) { + teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, "$ComSpec", 8); + g_assert(reg != NULL); + teco_string_t comspec; + if (!reg->vtable->get_string(reg, &comspec.data, &comspec.len, error)) + return NULL; + + argv = g_new(gchar *, 5); + /* + * FIXME: What if $ComSpec contains null-bytes? + */ + argv[0] = comspec.data; + argv[1] = g_strdup("/q"); + argv[2] = g_strdup("/c"); + argv[3] = g_strdup(cmdline); + argv[4] = NULL; + return argv; + } +#elif defined(G_OS_UNIX) + if (!(teco_ed & TECO_ED_SHELLEMU)) { + teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, "$SHELL", 6); + g_assert(reg != NULL); + teco_string_t shell; + if (!reg->vtable->get_string(reg, &shell.data, &shell.len, error)) + return NULL; + + argv = g_new(gchar *, 4); + /* + * FIXME: What if $SHELL contains null-bytes? + */ + argv[0] = shell.data; + argv[1] = g_strdup("-c"); + argv[2] = g_strdup(cmdline); + argv[3] = NULL; + return argv; + } +#endif + + return g_shell_parse_argv(cmdline, NULL, &argv, error) ? argv : NULL; +} + +static gboolean +teco_state_execute_initial(teco_machine_main_t *ctx, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return TRUE; + + if (!teco_expressions_eval(FALSE, error)) + return FALSE; + + teco_bool_t rc = TECO_SUCCESS; + + /* + * By evaluating arguments here, the command may fail + * before the string argument is typed + */ + switch (teco_expressions_args()) { + case 0: + if (teco_num_sign > 0) { + /* pipe nothing, insert at dot */ + teco_spawn_ctx.from = teco_spawn_ctx.to = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + break; + } + /* fall through if prefix sign is "-" */ + + case 1: { + /* pipe and replace line range */ + teco_int_t line; + + teco_spawn_ctx.from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + if (!teco_expressions_pop_num_calc(&line, 0, error)) + return FALSE; + line += teco_interface_ssm(SCI_LINEFROMPOSITION, teco_spawn_ctx.from, 0); + teco_spawn_ctx.to = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0); + rc = teco_bool(teco_validate_line(line)); + + if (teco_spawn_ctx.to < teco_spawn_ctx.from) { + teco_int_t temp = teco_spawn_ctx.from; + teco_spawn_ctx.from = teco_spawn_ctx.to; + teco_spawn_ctx.to = temp; + } + + break; + } + + default: + /* pipe and replace character range */ + if (!teco_expressions_pop_num_calc(&teco_spawn_ctx.to, 0, error) || + !teco_expressions_pop_num_calc(&teco_spawn_ctx.from, 0, error)) + return FALSE; + rc = teco_bool(teco_spawn_ctx.from <= teco_spawn_ctx.to && + teco_validate_pos(teco_spawn_ctx.from) && + teco_validate_pos(teco_spawn_ctx.to)); + break; + } + + if (teco_is_failure(rc)) { + if (!teco_machine_main_eval_colon(ctx)) { + teco_error_range_set(error, "EC"); + return FALSE; + } + + teco_expressions_push(rc); + teco_spawn_ctx.from = teco_spawn_ctx.to = -1; + /* done() will still be called */ + } + + return TRUE; +} + +static teco_state_t * +teco_state_execute_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + static const GSpawnFlags flags = G_SPAWN_DO_NOT_REAP_CHILD | + G_SPAWN_SEARCH_PATH | + G_SPAWN_STDERR_TO_DEV_NULL; + + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + if (teco_spawn_ctx.from < 0) + /* + * teco_state_execute_initial() failed without throwing + * error (colon-modified) + */ + return &teco_state_start; + + teco_spawn_ctx.text_added = FALSE; + teco_spawn_ctx.rc = TECO_FAILURE; + + g_autoptr(GIOChannel) stdin_chan = NULL, stdout_chan = NULL; + g_auto(GStrv) argv = NULL, envp = NULL; + + if (teco_string_contains(str, '\0')) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Command line must not contain null-bytes"); + goto gerror; + } + + argv = teco_parse_shell_command_line(str->data, error); + if (!argv) + goto gerror; + + envp = teco_qreg_table_get_environ(&teco_qreg_table_globals, error); + if (!envp) + goto gerror; + + GPid pid; + gint stdin_fd, stdout_fd; + + if (!g_spawn_async_with_pipes(NULL, argv, envp, flags, NULL, NULL, &pid, + &stdin_fd, &stdout_fd, NULL, error)) + goto gerror; + + teco_spawn_ctx.child_src = g_child_watch_source_new(pid); + g_source_set_callback(teco_spawn_ctx.child_src, (GSourceFunc)teco_spawn_child_watch_cb, + NULL, NULL); + g_source_attach(teco_spawn_ctx.child_src, teco_spawn_ctx.mainctx); + +#ifdef G_OS_WIN32 + stdin_chan = g_io_channel_win32_new_fd(stdin_fd); + stdout_chan = g_io_channel_win32_new_fd(stdout_fd); +#else /* the UNIX constructors should work everywhere else */ + stdin_chan = g_io_channel_unix_new(stdin_fd); + stdout_chan = g_io_channel_unix_new(stdout_fd); +#endif + g_io_channel_set_flags(stdin_chan, G_IO_FLAG_NONBLOCK, NULL); + g_io_channel_set_encoding(stdin_chan, NULL, NULL); + /* + * The EOL writer expects the channel to be buffered + * for performance reasons + */ + g_io_channel_set_buffered(stdin_chan, TRUE); + g_io_channel_set_flags(stdout_chan, G_IO_FLAG_NONBLOCK, NULL); + g_io_channel_set_encoding(stdout_chan, NULL, NULL); + g_io_channel_set_buffered(stdout_chan, FALSE); + + /* + * We always read from the current view, + * so we use its EOL mode. + */ + teco_eol_writer_init_gio(&teco_spawn_ctx.stdin_writer, teco_interface_ssm(SCI_GETEOLMODE, 0, 0), stdin_chan); + teco_eol_reader_init_gio(&teco_spawn_ctx.stdout_reader, stdout_chan); + + teco_spawn_ctx.stdin_src = g_io_create_watch(stdin_chan, + G_IO_OUT | G_IO_ERR | G_IO_HUP); + g_source_set_callback(teco_spawn_ctx.stdin_src, (GSourceFunc)teco_spawn_stdin_watch_cb, + NULL, NULL); + g_source_attach(teco_spawn_ctx.stdin_src, teco_spawn_ctx.mainctx); + + teco_spawn_ctx.stdout_src = g_io_create_watch(stdout_chan, + G_IO_IN | G_IO_ERR | G_IO_HUP); + g_source_set_callback(teco_spawn_ctx.stdout_src, (GSourceFunc)teco_spawn_stdout_watch_cb, + NULL, NULL); + g_source_attach(teco_spawn_ctx.stdout_src, teco_spawn_ctx.mainctx); + + if (!teco_spawn_ctx.register_argument) { + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_GOTOPOS, + teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0); + teco_interface_ssm(SCI_GOTOPOS, teco_spawn_ctx.to, 0); + } + + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + teco_spawn_ctx.start = teco_spawn_ctx.from; + g_main_loop_run(teco_spawn_ctx.mainloop); + if (!teco_spawn_ctx.register_argument) + teco_interface_ssm(SCI_DELETERANGE, teco_spawn_ctx.from, + teco_spawn_ctx.to - teco_spawn_ctx.from); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + + if (teco_spawn_ctx.register_argument) { + if (teco_spawn_ctx.stdout_reader.eol_style >= 0) { + teco_qreg_undo_set_eol_mode(teco_spawn_ctx.register_argument); + teco_qreg_set_eol_mode(teco_spawn_ctx.register_argument, + teco_spawn_ctx.stdout_reader.eol_style); + } + } else if (teco_spawn_ctx.from != teco_spawn_ctx.to || teco_spawn_ctx.text_added) { + /* undo action is only effective if it changed anything */ + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); + teco_ring_dirtify(); + } + + if (!g_source_is_destroyed(teco_spawn_ctx.stdin_src)) + g_io_channel_shutdown(stdin_chan, TRUE, NULL); + teco_eol_reader_clear(&teco_spawn_ctx.stdout_reader); + teco_eol_writer_clear(&teco_spawn_ctx.stdin_writer); + g_source_unref(teco_spawn_ctx.stdin_src); + g_io_channel_shutdown(stdout_chan, TRUE, NULL); + g_source_unref(teco_spawn_ctx.stdout_src); + + g_source_unref(teco_spawn_ctx.child_src); + g_spawn_close_pid(pid); + + if (teco_spawn_ctx.error) { + g_propagate_error(error, teco_spawn_ctx.error); + teco_spawn_ctx.error = NULL; + goto gerror; + } + + if (teco_interface_is_interrupted()) { + teco_error_interrupted_set(error); + return NULL; + } + + if (teco_machine_main_eval_colon(ctx)) + teco_expressions_push(TECO_SUCCESS); + + goto cleanup; + +gerror: + /* `error` has been set */ + if (!teco_machine_main_eval_colon(ctx)) + return NULL; + g_clear_error(error); + + /* May contain the exit status encoded as a teco_bool_t. */ + teco_expressions_push(teco_spawn_ctx.rc); + /* fall through */ + +cleanup: + teco_undo_ptr(teco_spawn_ctx.register_argument) = NULL; + return &teco_state_start; +} + +/* in cmdline.c */ +gboolean teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error); + +/*$ EC pipe filter + * EC[command]$ -- Execute operating system command and filter buffer contents + * linesEC[command]$ + * -EC[command]$ + * from,toEC[command]$ + * :EC[command]$ -> Success|Failure + * lines:EC[command]$ -> Success|Failure + * -:EC[command]$ -> Success|Failure + * from,to:EC[command]$ -> Success|Failure + * + * The EC command allows you to interface with the operating + * system shell and external programs. + * The external program is spawned as a background process + * and its standard input stream is fed with data from the + * current document, i.e. text is piped into the external + * program. + * When automatic EOL translation is enabled, this will + * translate all end of line sequences according to the + * source document's EOL mode (see \fBEL\fP command). + * For instance when piping from a document with DOS + * line breaks, the receiving program will only be sent + * DOS line breaks. + * The process' standard output stream is also redirected + * and inserted into the current document. + * End of line sequences are normalized accordingly + * but the EOL mode guessed from the program's output is + * \fBnot\fP set on the document. + * The process' standard error stream is discarded. + * If data is piped into the external program, its output + * replaces that data in the buffer. + * Dot is always left at the end of the insertion. + * + * If invoked without parameters, no data is piped into + * the process (and no characters are removed) and its + * output is inserted at the current buffer position. + * This is equivalent to invoking \(lq.,.EC\(rq. + * If invoked with one parameter, the next or previous number + * of are piped from the buffer into the program and + * its output replaces these . + * This effectively runs as a filter over . + * \(lq-EC\(rq may be written as a short-cut for \(lq-1EC\(rq. + * When invoked with two parameters, the characters beginning + * at position up to the character at position + * are piped into the program and replaced with its output. + * This effectively runs as a filter over a buffer + * range. + * + * Errors are thrown not only for invalid buffer ranges + * but also for errors during process execution. + * If the external has an unsuccessful exit code, + * the EC command will also fail. + * If the EC command is colon-modified, it will instead return + * a TECO boolean signifying success or failure. + * In case of an unsuccessful exit code, a colon-modified EC + * will return the absolute value of the process exit + * code (which is also a TECO failure boolean) and 0 for all + * other failures. + * This feature may be used to take action depending on a + * specific process exit code. + * + * execution is by default platform-dependent. + * On DOS-like systems like Windows, is passed to + * the command interpreter specified in the \fB$ComSpec\fP + * environment variable with the \(lq/q\(rq and \(lq/c\(rq + * command-line arguments. + * On UNIX-like systems, is passed to the interpreter + * specified by the \fB$SHELL\fP environment variable + * with the \(lq-c\(rq command-line argument. + * Therefore the default shell can be configured using + * the corresponding environment registers. + * The operating system restrictions on the maximum + * length of command-line arguments apply to and + * quoting of parameters within is somewhat platform + * dependent. + * On all other platforms, \*(ST will uniformly parse + * just as an UNIX98 \(lq/bin/sh\(rq would, but without + * performing any expansions. + * The program specified in is searched for in + * standard locations (according to the \fB$PATH\fP environment + * variable). + * This mode of operation can also be enforced on all platforms + * by enabling bit 7 in the ED flag, e.g. by executing + * \(lq0,128ED\(rq, and is recommended when writing cross-platform + * macros using the EC command. + * + * When using an UNIX-compatible shell or the UNIX98 shell emulation, + * you might want to use the \fB^E@\fP string-building character + * to pass Q-Register contents reliably as single arguments to + * the spawned process. + * + * The spawned process inherits both \*(ST's current working + * directory and its environment variables. + * More precisely, \*(ST uses its environment registers + * to construct the spawned process' environment. + * Therefore it is also straight forward to change the working + * directory or some environment variable temporarily + * for a spawned process. + * + * Note that when run interactively and subsequently rubbed + * out, \*(ST can easily undo all changes to the editor + * state. + * It \fBcannot\fP however undo any other side-effects that the + * execution of might have had on your system. + * + * Note also that the EC command blocks indefinitely until + * the completes, which may result in editor hangs. + * You may however interrupt the spawned process by sending + * the \fBSIGINT\fP signal to \*(ST, e.g. by pressing CTRL+C. + * + * In interactive mode, \*(ST performs TAB-completion + * of filenames in the string parameter but + * does not attempt any escaping of shell-relevant + * characters like whitespaces. + */ +TECO_DEFINE_STATE_EXPECTSTRING(teco_state_execute, + .initial_cb = (teco_state_initial_cb_t)teco_state_execute_initial, + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_execute_process_edit_cmd +); + +static teco_state_t * +teco_state_egcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, + teco_qreg_table_t *table, GError **error) +{ + teco_state_expectqreg_reset(ctx); + + if (ctx->mode <= TECO_MODE_NORMAL) + teco_undo_ptr(teco_spawn_ctx.register_argument) = qreg; + return &teco_state_execute; +} + +/*$ EG EGq + * EGq[command]$ -- Set Q-Register to output of operating system command + * linesEGq[command]$ + * -EGq[command]$ + * from,toEGq[command]$ + * :EGq[command]$ -> Success|Failure + * lines:EGq[command]$ -> Success|Failure + * -:EGq[command]$ -> Success|Failure + * from,to:EGq[command]$ -> Success|Failure + * + * Runs an operating system and set Q-Register + * to the data read from its standard output stream. + * Data may be fed to from the current buffer/document. + * The interpretation of the parameters and as well + * as the colon-modification is analoguous to the EC command. + * + * The EG command only differs from EC in not deleting any + * characters from the current buffer, not changing + * the current buffer position and writing process output + * to the Q-Register . + * In other words, the current buffer is not modified by EG. + * Also since EG replaces the string value of , the register's + * EOL mode is set to the mode guessed from the external program's + * output. + * + * The register is defined if it does not already exist. + */ +TECO_DEFINE_STATE_EXPECTQREG(teco_state_egcommand, + .expectqreg.type = TECO_QREG_OPTIONAL_INIT +); + +/* + * Glib callbacks + */ + +static void +teco_spawn_child_watch_cb(GPid pid, gint status, gpointer data) +{ + if (teco_spawn_ctx.error) + /* source has already been dispatched */ + return; + + /* + * There might still be data to read from stdout, + * but we cannot count on the stdout watcher to be ever called again. + */ + teco_spawn_stdout_watch_cb(teco_spawn_ctx.stdout_reader.gio.channel, G_IO_IN, NULL); + + /* + * teco_spawn_stdout_watch_cb() might have set the error. + */ + if (!teco_spawn_ctx.error && !g_spawn_check_exit_status(status, &teco_spawn_ctx.error)) + teco_spawn_ctx.rc = teco_spawn_ctx.error->domain == G_SPAWN_EXIT_ERROR + ? ABS(teco_spawn_ctx.error->code) : TECO_FAILURE; + + g_main_loop_quit(teco_spawn_ctx.mainloop); +} + +static gboolean +teco_spawn_stdin_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data) +{ + if (teco_spawn_ctx.error) + /* source has already been dispatched */ + return G_SOURCE_REMOVE; + + if (!(condition & G_IO_OUT)) + /* stdin might be closed prematurely */ + goto remove; + + /* we always read from the current view */ + sptr_t gap = teco_interface_ssm(SCI_GETGAPPOSITION, 0, 0); + gsize convert_len = teco_spawn_ctx.start < gap && gap < teco_spawn_ctx.to + ? gap - teco_spawn_ctx.start : teco_spawn_ctx.to - teco_spawn_ctx.start; + const gchar *buffer = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, + teco_spawn_ctx.start, convert_len); + + /* + * This cares about automatic EOL conversion and + * returns the number of consumed bytes. + * If it can only write a part of the EOL sequence (ie. CR of CRLF) + * it may return a short byte count (possibly 0) which ensures that + * we do not yet remove the source. + */ + gssize bytes_written = teco_eol_writer_convert(&teco_spawn_ctx.stdin_writer, buffer, + convert_len, &teco_spawn_ctx.error); + if (bytes_written < 0) { + /* GError ocurred */ + g_main_loop_quit(teco_spawn_ctx.mainloop); + return G_SOURCE_REMOVE; + } + + teco_spawn_ctx.start += bytes_written; + + if (teco_spawn_ctx.start == teco_spawn_ctx.to) + /* this will signal EOF to the process */ + goto remove; + + return G_SOURCE_CONTINUE; + +remove: + /* + * Channel is always shut down here (fd is closed), + * so it's always shut down IF the GSource has been + * destroyed. It is not guaranteed to be destroyed + * during the main loop run however since it quits + * as soon as the child was reaped and stdout was read. + */ + g_io_channel_shutdown(chan, TRUE, NULL); + return G_SOURCE_REMOVE; +} + +static gboolean +teco_spawn_stdout_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data) +{ + if (teco_spawn_ctx.error) + /* source has already been dispatched */ + return G_SOURCE_REMOVE; + + for (;;) { + teco_string_t buffer; + + switch (teco_eol_reader_convert(&teco_spawn_ctx.stdout_reader, + &buffer.data, &buffer.len, &teco_spawn_ctx.error)) { + case G_IO_STATUS_ERROR: + goto error; + + case G_IO_STATUS_EOF: + return G_SOURCE_REMOVE; + + default: + break; + } + + if (!buffer.len) + return G_SOURCE_CONTINUE; + + if (teco_spawn_ctx.register_argument) { + if (teco_spawn_ctx.text_added) { + if (!teco_spawn_ctx.register_argument->vtable->undo_append_string(teco_spawn_ctx.register_argument, + &teco_spawn_ctx.error) || + !teco_spawn_ctx.register_argument->vtable->append_string(teco_spawn_ctx.register_argument, + buffer.data, buffer.len, + &teco_spawn_ctx.error)) + goto error; + } else { + if (!teco_spawn_ctx.register_argument->vtable->undo_set_string(teco_spawn_ctx.register_argument, + &teco_spawn_ctx.error) || + !teco_spawn_ctx.register_argument->vtable->set_string(teco_spawn_ctx.register_argument, + buffer.data, buffer.len, + &teco_spawn_ctx.error)) + goto error; + } + } else { + teco_interface_ssm(SCI_ADDTEXT, buffer.len, (sptr_t)buffer.data); + } + teco_spawn_ctx.text_added = TRUE; + + /* + * NOTE: Since this reads from an external process, we could insert + * indefinitely (eg. cat /dev/zero). + */ + if (!teco_memory_check(&teco_spawn_ctx.error)) + goto error; + } + + g_assert_not_reached(); + +error: + g_main_loop_quit(teco_spawn_ctx.mainloop); + return G_SOURCE_REMOVE; +} + +#ifndef NDEBUG +static void __attribute__((destructor)) +teco_spawn_cleanup(void) +{ + g_main_loop_unref(teco_spawn_ctx.mainloop); + g_main_context_unref(teco_spawn_ctx.mainctx); + + if (teco_spawn_ctx.error) + g_error_free(teco_spawn_ctx.error); +} +#endif diff --git a/src/spawn.cpp b/src/spawn.cpp deleted file mode 100644 index b3a8823..0000000 --- a/src/spawn.cpp +++ /dev/null @@ -1,662 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include "sciteco.h" -#include "interface.h" -#include "undo.h" -#include "expressions.h" -#include "qregisters.h" -#include "eol.h" -#include "ring.h" -#include "parser.h" -#include "error.h" -#include "spawn.h" - -/* - * Debian 7 is still at libglib v2.33, so - * for the time being we support this UNIX-only - * implementation of g_spawn_check_exit_status() - * partially emulating libglib v2.34 - */ -#ifndef G_SPAWN_EXIT_ERROR -#ifdef G_OS_UNIX -#warning "libglib v2.34 or later recommended." -#else -#error "libglib v2.34 or later required." -#endif - -#include -#include - -#define G_SPAWN_EXIT_ERROR \ - g_quark_from_static_string("g-spawn-exit-error-quark") - -static gboolean -g_spawn_check_exit_status(gint exit_status, GError **error) -{ - if (!WIFEXITED(exit_status)) { - g_set_error(error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED, - "Abnormal process termination (%d)", - exit_status); - return FALSE; - } - - if (WEXITSTATUS(exit_status) != 0) { - g_set_error(error, G_SPAWN_EXIT_ERROR, WEXITSTATUS(exit_status), - "Unsuccessful exit status %d", - WEXITSTATUS(exit_status)); - return FALSE; - } - - return TRUE; -} - -#endif - -namespace SciTECO { - -namespace States { - StateExecuteCommand executecommand; - StateEGCommand egcommand; -} - -extern "C" { -static void child_watch_cb(GPid pid, gint status, gpointer data); -static gboolean stdin_watch_cb(GIOChannel *chan, - GIOCondition condition, gpointer data); -static gboolean stdout_watch_cb(GIOChannel *chan, - GIOCondition condition, gpointer data); -} - -static QRegister *register_argument = NULL; - -gchar ** -parse_shell_command_line(const gchar *cmdline, GError **error) -{ - gchar **argv; - -#ifdef G_OS_WIN32 - if (!(Flags::ed & Flags::ED_SHELLEMU)) { - QRegister *reg = QRegisters::globals["$COMSPEC"]; - argv = (gchar **)g_malloc(5*sizeof(gchar *)); - argv[0] = reg->get_string(); - argv[1] = g_strdup("/q"); - argv[2] = g_strdup("/c"); - argv[3] = g_strdup(cmdline); - argv[4] = NULL; - return argv; - } -#elif defined(G_OS_UNIX) || defined(G_OS_HAIKU) - if (!(Flags::ed & Flags::ED_SHELLEMU)) { - QRegister *reg = QRegisters::globals["$SHELL"]; - argv = (gchar **)g_malloc(4*sizeof(gchar *)); - argv[0] = reg->get_string(); - argv[1] = g_strdup("-c"); - argv[2] = g_strdup(cmdline); - argv[3] = NULL; - return argv; - } -#endif - - if (!g_shell_parse_argv(cmdline, NULL, &argv, error)) - return NULL; - - return argv; -} - -/*$ EC pipe filter - * EC[command]$ -- Execute operating system command and filter buffer contents - * linesEC[command]$ - * -EC[command]$ - * from,toEC[command]$ - * :EC[command]$ -> Success|Failure - * lines:EC[command]$ -> Success|Failure - * -:EC[command]$ -> Success|Failure - * from,to:EC[command]$ -> Success|Failure - * - * The EC command allows you to interface with the operating - * system shell and external programs. - * The external program is spawned as a background process - * and its standard input stream is fed with data from the - * current document, i.e. text is piped into the external - * program. - * When automatic EOL translation is enabled, this will - * translate all end of line sequences according to the - * source document's EOL mode (see \fBEL\fP command). - * For instance when piping from a document with DOS - * line breaks, the receiving program will only be sent - * DOS line breaks. - * The process' standard output stream is also redirected - * and inserted into the current document. - * End of line sequences are normalized accordingly - * but the EOL mode guessed from the program's output is - * \fBnot\fP set on the document. - * The process' standard error stream is discarded. - * If data is piped into the external program, its output - * replaces that data in the buffer. - * Dot is always left at the end of the insertion. - * - * If invoked without parameters, no data is piped into - * the process (and no characters are removed) and its - * output is inserted at the current buffer position. - * This is equivalent to invoking \(lq.,.EC\(rq. - * If invoked with one parameter, the next or previous number - * of are piped from the buffer into the program and - * its output replaces these . - * This effectively runs as a filter over . - * \(lq-EC\(rq may be written as a short-cut for \(lq-1EC\(rq. - * When invoked with two parameters, the characters beginning - * at position up to the character at position - * are piped into the program and replaced with its output. - * This effectively runs as a filter over a buffer - * range. - * - * Errors are thrown not only for invalid buffer ranges - * but also for errors during process execution. - * If the external has an unsuccessful exit code, - * the EC command will also fail. - * If the EC command is colon-modified, it will instead return - * a TECO boolean signifying success or failure. - * In case of an unsuccessful exit code, a colon-modified EC - * will return the absolute value of the process exit - * code (which is also a TECO failure boolean) and 0 for all - * other failures. - * This feature may be used to take action depending on a - * specific process exit code. - * - * execution is by default platform-dependent. - * On DOS-like systems like Windows, is passed to - * the command interpreter specified in the \fB$COMSPEC\fP - * environment variable with the \(lq/q\(rq and \(lq/c\(rq - * command-line arguments. - * On UNIX-like systems, is passed to the interpreter - * specified by the \fB$SHELL\fP environment variable - * with the \(lq-c\(rq command-line argument. - * Therefore the default shell can be configured using - * the corresponding environment registers. - * The operating system restrictions on the maximum - * length of command-line arguments apply to and - * quoting of parameters within is somewhat platform - * dependent. - * On all other platforms, \*(ST will uniformly parse - * just as an UNIX98 \(lq/bin/sh\(rq would, but without - * performing any expansions. - * The program specified in is searched for in - * standard locations (according to the \fB$PATH\fP environment - * variable). - * This mode of operation can also be enforced on all platforms - * by enabling bit 7 in the ED flag, e.g. by executing - * \(lq0,128ED\(rq, and is recommended when writing cross-platform - * macros using the EC command. - * - * When using an UNIX-compatible shell or the UNIX98 shell emulation, - * you might want to use the \fB^E@\fP string-building character - * to pass Q-Register contents reliably as single arguments to - * the spawned process. - * - * The spawned process inherits both \*(ST's current working - * directory and its environment variables. - * More precisely, \*(ST uses its environment registers - * to construct the spawned process' environment. - * Therefore it is also straight forward to change the working - * directory or some environment variable temporarily - * for a spawned process. - * - * Note that when run interactively and subsequently rubbed - * out, \*(ST can easily undo all changes to the editor - * state. - * It \fBcannot\fP however undo any other side-effects that the - * execution of might have had on your system. - * - * Note also that the EC command blocks indefinitely until - * the completes, which may result in editor hangs. - * You may however interrupt the spawned process by sending - * the \fBSIGINT\fP signal to \*(ST, e.g. by pressing CTRL+C. - * - * In interactive mode, \*(ST performs TAB-completion - * of filenames in the string parameter but - * does not attempt any escaping of shell-relevant - * characters like whitespaces. - */ -StateExecuteCommand::StateExecuteCommand() : StateExpectString() -{ - /* - * Context and loop can be reused between EC invocations. - * However we should not use the default context, since it - * may be used by GTK - */ - ctx.mainctx = g_main_context_new(); - ctx.mainloop = g_main_loop_new(ctx.mainctx, FALSE); -} - -StateExecuteCommand::~StateExecuteCommand() -{ - g_main_loop_unref(ctx.mainloop); -#ifndef G_OS_HAIKU - /* - * Apparently, there's some kind of double-free - * bug in Haiku's glib-2.38. - * It is unknown whether this is has - * already been fixed and affects other platforms - * (but I never observed any segfaults). - */ - g_main_context_unref(ctx.mainctx); -#endif - - delete ctx.error; -} - -void -StateExecuteCommand::initial(void) -{ - tecoBool rc = SUCCESS; - - expressions.eval(); - - /* - * By evaluating arguments here, the command may fail - * before the string argument is typed - */ - switch (expressions.args()) { - case 0: - if (expressions.num_sign > 0) { - /* pipe nothing, insert at dot */ - ctx.from = ctx.to = interface.ssm(SCI_GETCURRENTPOS); - break; - } - /* fall through if prefix sign is "-" */ - - case 1: { - /* pipe and replace line range */ - sptr_t line; - - ctx.from = interface.ssm(SCI_GETCURRENTPOS); - line = interface.ssm(SCI_LINEFROMPOSITION, ctx.from) + - expressions.pop_num_calc(); - ctx.to = interface.ssm(SCI_POSITIONFROMLINE, line); - rc = TECO_BOOL(Validate::line(line)); - - if (ctx.to < ctx.from) { - tecoInt temp = ctx.from; - ctx.from = ctx.to; - ctx.to = temp; - } - - break; - } - - default: - /* pipe and replace character range */ - ctx.to = expressions.pop_num_calc(); - ctx.from = expressions.pop_num_calc(); - rc = TECO_BOOL(ctx.from <= ctx.to && - Validate::pos(ctx.from) && - Validate::pos(ctx.to)); - break; - } - - if (IS_FAILURE(rc)) { - if (eval_colon()) { - expressions.push(rc); - ctx.from = ctx.to = -1; - /* done() will still be called */ - } else { - throw RangeError("EC"); - } - } -} - -State * -StateExecuteCommand::done(const gchar *str) -{ - BEGIN_EXEC(&States::start); - - if (ctx.from < 0) - /* - * initial() failed without throwing - * error (colon-modified) - */ - return &States::start; - - GError *error = NULL; - gchar **argv, **envp; - static const gint flags = G_SPAWN_DO_NOT_REAP_CHILD | - G_SPAWN_SEARCH_PATH | - G_SPAWN_STDERR_TO_DEV_NULL; - - GPid pid; - gint stdin_fd, stdout_fd; - GIOChannel *stdin_chan, *stdout_chan; - - /* - * We always read from the current view, - * so we use its EOL mode. - * - * NOTE: We do not declare the writer/reader objects as part of - * StateExecuteCommand::Context so we do not have to - * reset it. It's only required for the life time of this call - * anyway. - * I do not see a more elegant way out of this. - */ - EOLWriterGIO stdin_writer(interface.ssm(SCI_GETEOLMODE)); - EOLReaderGIO stdout_reader; - - ctx.text_added = false; - - ctx.stdin_writer = &stdin_writer; - ctx.stdout_reader = &stdout_reader; - - delete ctx.error; - ctx.error = NULL; - ctx.rc = FAILURE; - - argv = parse_shell_command_line(str, &error); - if (!argv) - goto gerror; - - envp = QRegisters::globals.get_environ(); - - g_spawn_async_with_pipes(NULL, argv, envp, (GSpawnFlags)flags, - NULL, NULL, &pid, - &stdin_fd, &stdout_fd, NULL, - &error); - - g_strfreev(envp); - g_strfreev(argv); - - if (error) - goto gerror; - - ctx.child_src = g_child_watch_source_new(pid); - g_source_set_callback(ctx.child_src, (GSourceFunc)child_watch_cb, - &ctx, NULL); - g_source_attach(ctx.child_src, ctx.mainctx); - -#ifdef G_OS_WIN32 - stdin_chan = g_io_channel_win32_new_fd(stdin_fd); - stdout_chan = g_io_channel_win32_new_fd(stdout_fd); -#else /* the UNIX constructors should work everywhere else */ - stdin_chan = g_io_channel_unix_new(stdin_fd); - stdout_chan = g_io_channel_unix_new(stdout_fd); -#endif - g_io_channel_set_flags(stdin_chan, G_IO_FLAG_NONBLOCK, NULL); - g_io_channel_set_encoding(stdin_chan, NULL, NULL); - /* - * EOLWriterGIO expects the channel to be buffered - * for performance reasons - */ - g_io_channel_set_buffered(stdin_chan, TRUE); - g_io_channel_set_flags(stdout_chan, G_IO_FLAG_NONBLOCK, NULL); - g_io_channel_set_encoding(stdout_chan, NULL, NULL); - g_io_channel_set_buffered(stdout_chan, FALSE); - - stdin_writer.set_channel(stdin_chan); - stdout_reader.set_channel(stdout_chan); - - ctx.stdin_src = g_io_create_watch(stdin_chan, - (GIOCondition)(G_IO_OUT | G_IO_ERR | G_IO_HUP)); - g_source_set_callback(ctx.stdin_src, (GSourceFunc)stdin_watch_cb, - &ctx, NULL); - g_source_attach(ctx.stdin_src, ctx.mainctx); - - ctx.stdout_src = g_io_create_watch(stdout_chan, - (GIOCondition)(G_IO_IN | G_IO_ERR | G_IO_HUP)); - g_source_set_callback(ctx.stdout_src, (GSourceFunc)stdout_watch_cb, - &ctx, NULL); - g_source_attach(ctx.stdout_src, ctx.mainctx); - - if (!register_argument) { - if (current_doc_must_undo()) - interface.undo_ssm(SCI_GOTOPOS, interface.ssm(SCI_GETCURRENTPOS)); - interface.ssm(SCI_GOTOPOS, ctx.to); - } - - interface.ssm(SCI_BEGINUNDOACTION); - ctx.start = ctx.from; - g_main_loop_run(ctx.mainloop); - if (!register_argument) - interface.ssm(SCI_DELETERANGE, ctx.from, ctx.to - ctx.from); - interface.ssm(SCI_ENDUNDOACTION); - - if (register_argument) { - if (stdout_reader.eol_style >= 0) { - register_argument->undo_set_eol_mode(); - register_argument->set_eol_mode(stdout_reader.eol_style); - } - } else if (ctx.from != ctx.to || ctx.text_added) { - /* undo action is only effective if it changed anything */ - if (current_doc_must_undo()) - interface.undo_ssm(SCI_UNDO); - interface.ssm(SCI_SCROLLCARET); - ring.dirtify(); - } - - if (!g_source_is_destroyed(ctx.stdin_src)) - g_io_channel_shutdown(stdin_chan, TRUE, NULL); - g_io_channel_unref(stdin_chan); - g_source_unref(ctx.stdin_src); - g_io_channel_shutdown(stdout_chan, TRUE, NULL); - g_io_channel_unref(stdout_chan); - g_source_unref(ctx.stdout_src); - - g_source_unref(ctx.child_src); - g_spawn_close_pid(pid); - - if (ctx.error) { - if (!eval_colon()) - throw *ctx.error; - - /* - * This may contain the exit status - * encoded as a tecoBool. - */ - expressions.push(ctx.rc); - goto cleanup; - } - - if (interface.is_interrupted()) - throw Error("Interrupted"); - - if (eval_colon()) - expressions.push(SUCCESS); - - goto cleanup; - -gerror: - if (!eval_colon()) - throw GlibError(error); - g_error_free(error); - - expressions.push(ctx.rc); - -cleanup: - undo.push_var(register_argument) = NULL; - return &States::start; -} - -/*$ EG EGq - * EGq[command]$ -- Set Q-Register to output of operating system command - * linesEGq[command]$ - * -EGq[command]$ - * from,toEGq[command]$ - * :EGq[command]$ -> Success|Failure - * lines:EGq[command]$ -> Success|Failure - * -:EGq[command]$ -> Success|Failure - * from,to:EGq[command]$ -> Success|Failure - * - * Runs an operating system and set Q-Register - * to the data read from its standard output stream. - * Data may be fed to from the current buffer/document. - * The interpretation of the parameters and as well - * as the colon-modification is analoguous to the EC command. - * - * The EG command only differs from EC in not deleting any - * characters from the current buffer, not changing - * the current buffer position and writing process output - * to the Q-Register . - * In other words, the current buffer is not modified by EG. - * Also since EG replaces the string value of , the register's - * EOL mode is set to the mode guessed from the external program's - * output. - * - * The register is defined if it does not already exist. - */ -State * -StateEGCommand::got_register(QRegister *reg) -{ - machine.reset(); - - BEGIN_EXEC(&States::executecommand); - undo.push_var(register_argument) = reg; - return &States::executecommand; -} - -/* - * Glib callbacks - */ - -static void -child_watch_cb(GPid pid, gint status, gpointer data) -{ - StateExecuteCommand::Context &ctx = - *(StateExecuteCommand::Context *)data; - GError *error = NULL; - - /* - * Writing stdin or reading stdout might have already - * failed. We preserve the earliest GError. - */ - if (!ctx.error && !g_spawn_check_exit_status(status, &error)) { - ctx.rc = error->domain == G_SPAWN_EXIT_ERROR - ? ABS(error->code) : FAILURE; - ctx.error = new GlibError(error); - } - - if (g_source_is_destroyed(ctx.stdout_src)) - g_main_loop_quit(ctx.mainloop); -} - -static gboolean -stdin_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data) -{ - StateExecuteCommand::Context &ctx = - *(StateExecuteCommand::Context *)data; - - sptr_t gap; - gsize convert_len; - const gchar *buffer; - gsize bytes_written; - - if (!(condition & G_IO_OUT)) - /* stdin might be closed prematurely */ - goto remove; - - /* we always read from the current view */ - gap = interface.ssm(SCI_GETGAPPOSITION); - convert_len = ctx.start < gap && gap < ctx.to - ? gap - ctx.start : ctx.to - ctx.start; - buffer = (const gchar *)interface.ssm(SCI_GETRANGEPOINTER, - ctx.start, convert_len); - - try { - /* - * This cares about automatic EOL conversion and - * returns the number of consumed bytes. - * If it can only write a part of the EOL sequence (ie. CR of CRLF) - * it may return a short byte count (possibly 0) which ensures that - * we do not yet remove the source. - */ - bytes_written = ctx.stdin_writer->convert(buffer, convert_len); - } catch (Error &e) { - ctx.error = new Error(e); - /* do not yet quit -- we still have to reap the child */ - goto remove; - } - - ctx.start += bytes_written; - - if (ctx.start == ctx.to) - /* this will signal EOF to the process */ - goto remove; - - return G_SOURCE_CONTINUE; - -remove: - /* - * Channel is always shut down here (fd is closed), - * so it's always shut down IF the GSource has been - * destroyed. It is not guaranteed to be destroyed - * during the main loop run however since it quits - * as soon as the child was reaped and stdout was read. - */ - g_io_channel_shutdown(chan, TRUE, NULL); - return G_SOURCE_REMOVE; -} - -static gboolean -stdout_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data) -{ - StateExecuteCommand::Context &ctx = - *(StateExecuteCommand::Context *)data; - - for (;;) { - const gchar *buffer; - gsize data_len; - - try { - buffer = ctx.stdout_reader->convert(data_len); - } catch (Error &e) { - ctx.error = new Error(e); - goto remove; - } - if (!buffer) - /* EOF */ - goto remove; - - if (!data_len) - return G_SOURCE_CONTINUE; - - if (register_argument) { - if (ctx.text_added) { - register_argument->undo_append_string(); - register_argument->append_string(buffer, data_len); - } else { - register_argument->undo_set_string(); - register_argument->set_string(buffer, data_len); - } - } else { - interface.ssm(SCI_ADDTEXT, data_len, (sptr_t)buffer); - } - ctx.text_added = true; - } - - /* not reached */ - return G_SOURCE_CONTINUE; - -remove: - if (g_source_is_destroyed(ctx.child_src)) - g_main_loop_quit(ctx.mainloop); - return G_SOURCE_REMOVE; -} - -} /* namespace SciTECO */ diff --git a/src/spawn.h b/src/spawn.h index 4d7403e..f6dec38 100644 --- a/src/spawn.h +++ b/src/spawn.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,68 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#pragma once -#ifndef __SPAWN_H -#define __SPAWN_H - -#include - -#include "sciteco.h" #include "parser.h" -#include "qregisters.h" -#include "error.h" -#include "eol.h" - -namespace SciTECO { - -gchar **parse_shell_command_line(const gchar *cmdline, GError **error); - -class StateExecuteCommand : public StateExpectString { -public: - StateExecuteCommand(); - ~StateExecuteCommand(); - - struct Context { - GMainContext *mainctx; - GMainLoop *mainloop; - GSource *child_src; - GSource *stdin_src, *stdout_src; - - tecoInt from, to; - tecoInt start; - bool text_added; - - EOLWriterGIO *stdin_writer; - EOLReaderGIO *stdout_reader; - - Error *error; - tecoBool rc; - }; - -private: - Context ctx; - - void initial(void); - State *done(const gchar *str); - -protected: - /* in cmdline.cpp */ - void process_edit_cmd(gchar key); -}; - -class StateEGCommand : public StateExpectQReg { -public: - StateEGCommand() : StateExpectQReg(QREG_OPTIONAL_INIT) {} - -private: - State *got_register(QRegister *reg); -}; - -namespace States { - extern StateExecuteCommand executecommand; - extern StateEGCommand egcommand; -} - -} /* namespace SciTECO */ -#endif +TECO_DECLARE_STATE(teco_state_execute); +TECO_DECLARE_STATE(teco_state_egcommand); diff --git a/src/string-utils.c b/src/string-utils.c new file mode 100644 index 0000000..f15b307 --- /dev/null +++ b/src/string-utils.c @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include "sciteco.h" +#include "undo.h" +#include "string-utils.h" + +/** + * Get echoable (printable) version of a given string. + * + * This converts all control characters to printable + * characters without tabs, line feeds, etc. + * That's also why it can safely return a null-terminated string. + * Useful for displaying Q-Register names and TECO code. + * + * @memberof teco_string_t + */ +gchar * +teco_string_echo(const gchar *str, gsize len) +{ + gchar *ret, *p; + + p = ret = g_malloc(len*2 + 1); + + for (guint i = 0; i < len; i++) { + if (TECO_IS_CTL(str[i])) { + *p++ = '^'; + *p++ = TECO_CTL_ECHO(str[i]); + } else { + *p++ = str[i]; + } + } + *p = '\0'; + + return ret; +} + +/** @memberof teco_string_t */ +void +teco_string_get_coord(const gchar *str, guint pos, guint *line, guint *column) +{ + *line = *column = 1; + + for (guint i = 0; i < pos; i++) { + switch (str[i]) { + case '\r': + if (str[i+1] == '\n') + i++; + /* fall through */ + case '\n': + (*line)++; + (*column) = 1; + break; + default: + (*column)++; + break; + } + } +} + +/** @memberof teco_string_t */ +gsize +teco_string_diff(const teco_string_t *a, const gchar *b, gsize b_len) +{ + gsize len = 0; + + while (len < a->len && len < b_len && + a->data[len] == b[len]) + len++; + + return len; +} + +/** @memberof teco_string_t */ +gsize +teco_string_casediff(const teco_string_t *a, const gchar *b, gsize b_len) +{ + gsize len = 0; + + while (len < a->len && len < b_len && + g_ascii_tolower(a->data[len]) == g_ascii_tolower(b[len])) + len++; + + return len; +} + +/** @memberof teco_string_t */ +gint +teco_string_cmp(const teco_string_t *a, const gchar *b, gsize b_len) +{ + for (guint i = 0; i < a->len; i++) { + if (i == b_len) + /* b is a prefix of a */ + return 1; + gint ret = (gint)a->data[i] - (gint)b[i]; + if (ret != 0) + /* a and b have a common prefix of length i */ + return ret; + } + + return a->len == b_len ? 0 : -1; +} + +/** @memberof teco_string_t */ +gint +teco_string_casecmp(const teco_string_t *a, const gchar *b, gsize b_len) +{ + for (guint i = 0; i < a->len; i++) { + if (i == b_len) + /* b is a prefix of a */ + return 1; + gint ret = (gint)g_ascii_tolower(a->data[i]) - (gint)g_ascii_tolower(b[i]); + if (ret != 0) + /* a and b have a common prefix of length i */ + return ret; + } + + return a->len == b_len ? 0 : -1; +} + +/** + * Find string after the last occurrence of any in a set of characters. + * + * @param str String to search through. + * @param chars Null-terminated set of characters. + * The null-byte itself is always considered part of the set. + * @return A null-terminated suffix of str or NULL. + * + * @memberof teco_string_t + */ +const gchar * +teco_string_last_occurrence(const teco_string_t *str, const gchar *chars) +{ + teco_string_t ret = *str; + + if (!ret.len) + return NULL; + + do { + gint i = teco_string_rindex(&ret, *chars); + if (i >= 0) { + ret.data += i+1; + ret.len -= i+1; + } + } while (*chars++); + + return ret.data; +} + +TECO_DEFINE_UNDO_CALL(teco_string_truncate, teco_string_t *, gsize); + +TECO_DEFINE_UNDO_OBJECT(cstring, gchar *, g_strdup, g_free); + +static inline teco_string_t +teco_string_copy(const teco_string_t str) +{ + teco_string_t ret; + teco_string_init(&ret, str.data, str.len); + return ret; +} + +#define DELETE(X) teco_string_clear(&(X)) +TECO_DEFINE_UNDO_OBJECT(string, teco_string_t, teco_string_copy, DELETE); +TECO_DEFINE_UNDO_OBJECT_OWN(string_own, teco_string_t, DELETE); +#undef DELETE diff --git a/src/string-utils.cpp b/src/string-utils.cpp deleted file mode 100644 index 4c3c53e..0000000 --- a/src/string-utils.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include "sciteco.h" -#include "string-utils.h" - -namespace SciTECO { - -/** - * Canonicalize control characters in str. - * This converts all control characters to printable - * characters without tabs, line feeds, etc. - * Useful for displaying Q-Register names and - * TECO code. - */ -gchar * -String::canonicalize_ctl(const gchar *str) -{ - gsize ret_len = 1; /* for trailing 0 */ - gchar *ret, *p; - - /* - * Instead of approximating size with strlen() - * we can just as well calculate it exactly: - */ - for (const gchar *p = str; *p; p++) - ret_len += IS_CTL(*p) ? 2 : 1; - - p = ret = (gchar *)g_malloc(ret_len); - - while (*str) { - if (IS_CTL(*str)) { - *p++ = '^'; - *p++ = CTL_ECHO(*str++); - } else { - *p++ = *str++; - } - } - *p = '\0'; - - return ret; -} - -void -String::get_coord(const gchar *str, gint pos, - gint &line, gint &column) -{ - line = column = 1; - - for (gint i = 0; i < pos; i++) { - switch (str[i]) { - case '\r': - if (str[i+1] == '\n') - i++; - /* fall through */ - case '\n': - line++; - column = 1; - break; - default: - column++; - break; - } - } -} - -} /* namespace SciTECO */ diff --git a/src/string-utils.h b/src/string-utils.h index 8aeb863..40f1b21 100644 --- a/src/string-utils.h +++ b/src/string-utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,101 +14,173 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -#ifndef __STRING_UTILS_H -#define __STRING_UTILS_H +#pragma once #include #include -namespace SciTECO { - -namespace String { +#include "sciteco.h" +#include "undo.h" /** - * Upper-case ASCII character. + * Upper-case SciTECO command character. * - * There are implementations in glib and libc, - * but defining it here ensures it can be inlined. + * There are implementations in glib (g_ascii_toupper) and libc, + * but this implementation is sufficient for all letters used by SciTECO commands. */ static inline gchar -toupper(gchar chr) +teco_ascii_toupper(gchar chr) { return chr >= 'a' && chr <= 'z' ? chr & ~0x20 : chr; } /** - * Allocate a string containing a single character chr. + * An 8-bit clean null-terminated string. + * + * This is similar to GString, but the container does not need to be allocated + * and the allocation length is not stored. + * Just like GString, teco_string_t are always null-terminated but at the + * same time 8-bit clean (can contain null-characters). + * + * The API is designed such that teco_string_t operations operate on plain + * (null-terminated) C strings, a single character or character array as well as + * on other teco_string_t. + * Input strings will thus usually be specified using a const gchar * and gsize + * and are not necessarily null-terminated. + * A target teco_string_t::data is always null-terminated and thus safe to pass + * to functions expecting traditional null-terminated C strings if you can + * guarantee that it contains no null-character other than the trailing one. */ -static inline gchar * -chrdup(gchar chr) +typedef struct { + /** + * g_malloc() or g_string_chunk_insert()-allocated null-terminated string. + * The pointer is guaranteed to be non-NULL after initialization. + */ + gchar *data; + /** Length of `data` without the trailing null-byte. */ + gsize len; +} teco_string_t; + +/** @memberof teco_string_t */ +static inline void +teco_string_init(teco_string_t *target, const gchar *str, gsize len) { - gchar *ret = (gchar *)g_malloc(2); - - ret[0] = chr; - ret[1] = '\0'; - - return ret; + target->data = g_malloc(len + 1); + memcpy(target->data, str, len); + target->len = len; + target->data[target->len] = '\0'; } /** - * Append null-terminated str2 to non-null-terminated - * str1 of length str1_size. - * The result is not null-terminated. - * This is a very efficient implementation and well - * suited for appending lots of small strings often. + * Allocate a teco_string_t using GStringChunk. + * + * Such strings must not be freed/cleared individually and it is NOT allowed + * to call teco_string_append() and teco_string_truncate() on them. + * On the other hand, they are stored faster and more memory efficient. + * + * @memberof teco_string_t */ static inline void -append(gchar *&str1, gsize str1_size, const gchar *str2) +teco_string_init_chunk(teco_string_t *target, const gchar *str, gssize len, GStringChunk *chunk) { - size_t str2_size = strlen(str2); - str1 = (gchar *)g_realloc(str1, str1_size + str2_size); - if (str1) - memcpy(str1+str1_size, str2, str2_size); + target->data = g_string_chunk_insert_len(chunk, str, len); + target->len = len; } /** - * Append str2 to str1 (both null-terminated). - * This is a very efficient implementation and well - * suited for appending lots of small strings often. + * @note Rounding up the length turned out to bring no benefits, + * at least with glibc's malloc(). + * + * @memberof teco_string_t */ static inline void -append(gchar *&str1, const gchar *str2) +teco_string_append(teco_string_t *target, const gchar *str, gsize len) +{ + target->data = g_realloc(target->data, target->len + len + 1); + memcpy(target->data + target->len, str, len); + target->len += len; + target->data[target->len] = '\0'; +} + +/** @memberof teco_string_t */ +static inline void +teco_string_append_c(teco_string_t *str, gchar chr) { - size_t str1_size = str1 ? strlen(str1) : 0; - str1 = (gchar *)g_realloc(str1, str1_size + strlen(str2) + 1); - strcpy(str1+str1_size, str2); + teco_string_append(str, &chr, sizeof(chr)); } /** - * Append a single character to a null-terminated string. + * @fixme Should this also realloc str->data? + * + * @memberof teco_string_t */ static inline void -append(gchar *&str, gchar chr) +teco_string_truncate(teco_string_t *str, gsize len) { - gchar buf[] = {chr, '\0'}; - append(str, buf); + g_assert(len <= str->len); + if (len) { + str->data[len] = '\0'; + } else { + g_free(str->data); + str->data = NULL; + } + str->len = len; } -gchar *canonicalize_ctl(const gchar *str); +/** @memberof teco_string_t */ +void undo__teco_string_truncate(teco_string_t *, gsize); + +gchar *teco_string_echo(const gchar *str, gsize len); + +void teco_string_get_coord(const gchar *str, guint pos, guint *line, guint *column); + +typedef gsize (*teco_string_diff_t)(const teco_string_t *a, const gchar *b, gsize b_len); +gsize teco_string_diff(const teco_string_t *a, const gchar *b, gsize b_len); +gsize teco_string_casediff(const teco_string_t *a, const gchar *b, gsize b_len); -void get_coord(const gchar *str, gint pos, - gint &line, gint &column); +typedef gint (*teco_string_cmp_t)(const teco_string_t *a, const gchar *b, gsize b_len); +gint teco_string_cmp(const teco_string_t *a, const gchar *b, gsize b_len); +gint teco_string_casecmp(const teco_string_t *a, const gchar *b, gsize b_len); + +/** @memberof teco_string_t */ +static inline gboolean +teco_string_contains(const teco_string_t *str, gchar chr) +{ + return memchr(str->data, chr, str->len) != NULL; +} -static inline gsize -diff(const gchar *a, const gchar *b) +/** @memberof teco_string_t */ +static inline gint +teco_string_rindex(const teco_string_t *str, gchar chr) { - gsize len = 0; + gint i; + for (i = str->len-1; i >= 0 && str->data[i] != chr; i--); + return i; +} - while (*a != '\0' && *a++ == *b++) - len++; +const gchar *teco_string_last_occurrence(const teco_string_t *str, const gchar *chars); - return len; +/** @memberof teco_string_t */ +static inline void +teco_string_clear(teco_string_t *str) +{ + g_free(str->data); } -} /* namespace String */ +TECO_DECLARE_UNDO_OBJECT(cstring, gchar *); + +#define teco_undo_cstring(VAR) \ + (*teco_undo_object_cstring_push(&(VAR))) + +TECO_DECLARE_UNDO_OBJECT(string, teco_string_t); + +#define teco_undo_string(VAR) \ + (*teco_undo_object_string_push(&(VAR))) + +TECO_DECLARE_UNDO_OBJECT(string_own, teco_string_t); -} /* namespace SciTECO */ +#define teco_undo_string_own(VAR) \ + (*teco_undo_object_string_own_push(&(VAR))) -#endif +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_string_t, teco_string_clear); diff --git a/src/symbols-extract.tes b/src/symbols-extract.tes index 7d12ea8..3f71629 100755 --- a/src/symbols-extract.tes +++ b/src/symbols-extract.tes @@ -33,20 +33,28 @@ I/* #include "Q#in" #include "sciteco.h" -#include "symbols.h" +#include "scintilla.h" -namespace SciTECO { - -static const SymbolList::Entry entries[] = {^J +static const teco_symbol_entry_t entries[] = {^J < .,W.Xa 0KK I#ifdef Qa^J^I{"Qa", Qa},^J#endif^J .-Z;> I}; -SymbolList Symbols::Q[getopt.n](entries, G_N_ELEMENTS(entries)); - -} /* namespace SciTECO */^J +static void __attribute__((constructor)) +teco_symbols_init(void) +{ + teco_symbol_list_init(&Q[getopt.n], entries, G_N_ELEMENTS(entries), FALSE); +} + +#ifndef NDEBUG +static void __attribute__((destructor)) +teco_cmdline_cleanup(void) +{ + teco_symbol_list_clear(&Q[getopt.n]); +} +#endif^J !* write output file *! EWQ#ou diff --git a/src/symbols-minimal.cpp b/src/symbols-minimal.cpp deleted file mode 100644 index 1582979..0000000 --- a/src/symbols-minimal.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "sciteco.h" -#include "symbols.h" - -namespace SciTECO { - -namespace Symbols { - SymbolList scintilla; - SymbolList scilexer; -} - -} /* namespace SciTECO */ diff --git a/src/symbols.cpp b/src/symbols.cpp deleted file mode 100644 index 51046d4..0000000 --- a/src/symbols.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include - -#include "sciteco.h" -#include "symbols.h" - -namespace SciTECO { - -/* - * Since symbol lists are presorted constant arrays we can do a simple - * binary search. - */ -gint -SymbolList::lookup(const gchar *name, const gchar *prefix) -{ - gint prefix_skip = strlen(prefix); - gint name_len = strlen(name); - - gint left = 0; - gint right = size - 1; - - if (!cmp_fnc(name, prefix, prefix_skip)) - prefix_skip = 0; - - while (left <= right) { - gint cur = left + (right-left)/2; - gint cmp = cmp_fnc(entries[cur].name + prefix_skip, - name, name_len + 1); - - if (!cmp) - return entries[cur].value; - - if (cmp > 0) - right = cur-1; - else /* cmp < 0 */ - left = cur+1; - } - - return -1; -} - -GList * -SymbolList::get_glist(void) -{ - if (!list) { - for (gint i = size; i; i--) - list = g_list_prepend(list, (gchar *)entries[i-1].name); - } - - return list; -} - -} /* namespace SciTECO */ diff --git a/src/symbols.h b/src/symbols.h deleted file mode 100644 index c7a9c7f..0000000 --- a/src/symbols.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifndef __SYMBOLS_H -#define __SYMBOLS_H - -#include -#include - -#include "memory.h" - -namespace SciTECO { - -class SymbolList : public Object { -public: - struct Entry { - const gchar *name; - gint value; - }; - -private: - const Entry *entries; - gint size; - int (*cmp_fnc)(const char *, const char *, size_t); - - /* for auto-completions */ - GList *list; - -public: - SymbolList(const Entry *_entries = NULL, gint _size = 0, - bool case_sensitive = false) - : entries(_entries), size(_size), list(NULL) - { - cmp_fnc = case_sensitive ? strncmp - : g_ascii_strncasecmp; - } - - ~SymbolList() - { - g_list_free(list); - } - - gint lookup(const gchar *name, const gchar *prefix = ""); - GList *get_glist(void); -}; - -/* objects declared in symbols-minimal.cpp or auto-generated code */ -namespace Symbols { - extern SymbolList scintilla; - extern SymbolList scilexer; -} - -} /* namespace SciTECO */ - -#endif diff --git a/src/undo.c b/src/undo.c new file mode 100644 index 0000000..10e438f --- /dev/null +++ b/src/undo.c @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include + +#include "sciteco.h" +#include "cmdline.h" +#include "undo.h" + +//#define DEBUG + +TECO_DEFINE_UNDO_SCALAR(gchar); +TECO_DEFINE_UNDO_SCALAR(gint); +TECO_DEFINE_UNDO_SCALAR(guint); +TECO_DEFINE_UNDO_SCALAR(gsize); +TECO_DEFINE_UNDO_SCALAR(teco_int_t); +TECO_DEFINE_UNDO_SCALAR(gboolean); +TECO_DEFINE_UNDO_SCALAR(gconstpointer); + +/** + * An undo token. + * + * Undo tokens are generated to revert any + * changes to the editor state, ie. they + * define an action to take upon rubout. + * + * Undo tokens are organized into an undo stack. + */ +typedef struct teco_undo_token_t { + struct teco_undo_token_t *next; + teco_undo_action_t action_cb; + guint8 user_data[]; +} teco_undo_token_t; + +/** + * Stack of teco_undo_token_t lists. + * + * Each stack element represents + * a command line character (the undo tokens + * generated by that character), so it's OK + * to use a data structure that may need + * reallocation but is space efficient. + * This data structure allows us to omit the + * command line program counter from teco_undo_token_t + * but wastes a few bytes for input characters + * that produce no undo tokens (e.g. NOPs like space). + */ +static GPtrArray *teco_undo_heads; + +gboolean teco_undo_enabled = FALSE; + +static void __attribute__((constructor)) +teco_undo_init(void) +{ + teco_undo_heads = g_ptr_array_new(); +} + +/** + * Allocate and push undo token. + * + * This does nothing if undo is disabled and should + * not be used when ownership of some data is to be + * passed to the undo token. + */ +gpointer +teco_undo_push_size(teco_undo_action_t action_cb, gsize size) +{ + if (!teco_undo_enabled) + return NULL; + + teco_undo_token_t *token = g_malloc(sizeof(teco_undo_token_t) + size); + token->action_cb = action_cb; + +#ifdef DEBUG + g_printf("UNDO PUSH %p\n", token); +#endif + + /* + * There can very well be 0 undo tokens + * per input character (e.g. NOPs like space). + */ + while (teco_undo_heads->len <= teco_cmdline.pc) + g_ptr_array_add(teco_undo_heads, NULL); + g_assert(teco_undo_heads->len == teco_cmdline.pc+1); + + token->next = g_ptr_array_index(teco_undo_heads, + teco_undo_heads->len-1); + g_ptr_array_index(teco_undo_heads, teco_undo_heads->len-1) = token; + + return token->user_data; +} + +void +teco_undo_pop(gint pc) +{ + while ((gint)teco_undo_heads->len > pc) { + teco_undo_token_t *top = + g_ptr_array_remove_index(teco_undo_heads, + teco_undo_heads->len-1); + + while (top) { + teco_undo_token_t *next = top->next; + +#ifdef DEBUG + g_printf("UNDO POP %p\n", top); + fflush(stdout); +#endif + top->action_cb(top->user_data, TRUE); + + g_free(top); + top = next; + } + } +} + +void +teco_undo_clear(void) +{ + while (teco_undo_heads->len) { + teco_undo_token_t *top = + g_ptr_array_remove_index(teco_undo_heads, + teco_undo_heads->len-1); + + while (top) { + teco_undo_token_t *next = top->next; + top->action_cb(top->user_data, FALSE); + g_free(top); + top = next; + } + } +} + +/* + * NOTE: This destructor should always be run, even with NDEBUG, + * as there are undo tokens that release more than memory (e.g. files). + */ +static void __attribute__((destructor)) +teco_undo_cleanup(void) +{ + teco_undo_clear(); + g_ptr_array_free(teco_undo_heads, TRUE); +} diff --git a/src/undo.cpp b/src/undo.cpp deleted file mode 100644 index 468c61a..0000000 --- a/src/undo.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2012-2017 Robin Haberkorn - * - * 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 3 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, see . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include - -#include -#include - -#include - -#include "sciteco.h" -#include "cmdline.h" -#include "undo.h" - -namespace SciTECO { - -//#define DEBUG - -UndoStack undo; - -void -UndoStack::push(UndoToken *token) -{ - /* - * All undo token allocations should take place using the - * variadic template version of UndoStack::push(), so we - * don't have to check `enabled` here. - */ - g_assert(enabled == true); - -#ifdef DEBUG - g_printf("UNDO PUSH %p\n", token); -#endif - - /* - * There can very well be 0 undo tokens - * per input character (e.g. NOPs like space). - */ - while (heads->len <= cmdline.pc) - g_ptr_array_add(heads, NULL); - g_assert(heads->len == cmdline.pc+1); - - SLIST_NEXT(token, tokens) = - (UndoToken *)g_ptr_array_index(heads, heads->len-1); - g_ptr_array_index(heads, heads->len-1) = token; -} - -void -UndoStack::pop(gint pc) -{ - while ((gint)heads->len > pc) { - UndoToken *top = - (UndoToken *)g_ptr_array_remove_index(heads, heads->len-1); - - while (top) { - UndoToken *next = SLIST_NEXT(top, tokens); - -#ifdef DEBUG - g_printf("UNDO POP %p\n", top); - fflush(stdout); -#endif - top->run(); - - delete top; - top = next; - } - } -} - -void -UndoStack::clear(void) -{ - while (heads->len) { - UndoToken *top = - (UndoToken *)g_ptr_array_remove_index(heads, heads->len-1); - - while (top) { - UndoToken *next = SLIST_NEXT(top, tokens); - delete top; - top = next; - } - } -} - -} /* namespace SciTECO */ diff --git a/src/undo.h b/src/undo.h index 93f68e5..fc5cccb 100644 --- a/src/undo.h +++ b/src/undo.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Robin Haberkorn + * Copyright (C) 2012-2021 Robin Haberkorn * * 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 @@ -14,227 +14,234 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -#ifndef __UNDO_H -#define __UNDO_H - -#include - -#include +#pragma once #include -#include -#include "memory.h" +#include "sciteco.h" -#ifdef DEBUG -#include "parser.h" -#endif - -namespace SciTECO { +extern gboolean teco_undo_enabled; /** - * Undo tokens are generated to revert any - * changes to the editor state, ie. they - * define an action to take upon rubout. + * A callback to be invoked when an undo token gets executed or cleaned up. + * + * @note Unless you want to cast user_data in every callback implementation, + * you may want to cast your callback type instead to teco_undo_action_t. + * Casting to functions of different signature is theoretically undefined behavior, + * but works on all major platforms including Emscripten, as long as they differ only + * in pointer types. * - * Undo tokens are organized into an undo - * stack. + * @param user_data + * The data allocated by teco_undo_push_size() (usually a context structure). + * You are supposed to free any external resources (heap pointers etc.) referenced + * from it till the end of the callback. + * @param run + * Whether the operation should actually be performed instead of merely freeing + * the associated memory. */ -class UndoToken : public Object { -public: - SLIST_ENTRY(UndoToken) tokens; - - virtual ~UndoToken() {} - - virtual void run(void) = 0; -}; - -template -class UndoTokenVariable : public UndoToken { - Type *ptr; - Type value; - -public: - UndoTokenVariable(Type &variable, Type _value) - : ptr(&variable), value(_value) {} - - void - run(void) - { -#ifdef DEBUG - if ((State **)ptr == &States::current) - g_printf("undo state -> %p\n", (void *)value); -#endif - *ptr = value; - } -}; +typedef void (*teco_undo_action_t)(gpointer user_data, gboolean run); -class UndoTokenString : public UndoToken { - gchar **ptr; - gchar *str; +gpointer teco_undo_push_size(teco_undo_action_t action_cb, gsize size) + G_GNUC_ALLOC_SIZE(2); -public: - UndoTokenString(gchar *&variable, gchar *_str) - : ptr(&variable) - { - str = _str ? g_strdup(_str) : NULL; - } +#define teco_undo_push(NAME) \ + ((NAME##_t *)teco_undo_push_size((teco_undo_action_t)NAME##_action, \ + sizeof(NAME##_t))) - ~UndoTokenString() - { - g_free(str); +/** + * @defgroup undo_objects Undo objects + * + * @note + * The general meta programming approach here is similar to C++ explicit template + * instantiation. + * A macro is expanded for every object type into some compilation unit and a declaration + * into the corresponding header. + * The object's type is mangled into the generated "push"-function's name. + * In case of scalars, C11 Generics and some macro magic is then used to hide the + * type names and for "reference" style passing. + * + * Explicit instantiation could be theoretically avoided using GCC compound expressions + * and nested functions. However, GCC will inevitably generate trampolines which + * are unportable and induce a runtime penalty. + * Furthermore, nested functions are not supported by Clang, where the Blocks extension + * would have to be used instead. + * Another alternative for implicit instantiation would be preprocessing of all source + * files with some custom M4 macros. + */ +/* + * FIXME: Due to the requirements on the variable, we could be tempted to inline + * references to it directly into the action()-function, saving the `ptr` + * in the undo token. This is however often practically not possible. + * We could however add a variant for true global variables. + * + * FIXME: Sometimes, the push-function is used only in a single compilation unit, + * so it should be declared `static` or `static inline`. + * Is it worth complicating our APIs in order to support that? + * + * FIXME: Perhaps better split this into TECO_DEFINE_UNDO_OBJECT() and TECO_DEFINE_UNDO_OBJECT_OWN() + */ +#define __TECO_DEFINE_UNDO_OBJECT(NAME, TYPE, COPY, DELETE, DELETE_IF_DISABLED, DELETE_ON_RUN) \ + typedef struct { \ + TYPE *ptr; \ + TYPE value; \ + } teco_undo_object_##NAME##_t; \ + \ + static void \ + teco_undo_object_##NAME##_action(teco_undo_object_##NAME##_t *ctx, gboolean run) \ + { \ + if (run) { \ + DELETE_ON_RUN(*ctx->ptr); \ + *ctx->ptr = ctx->value; \ + } else { \ + DELETE(ctx->value); \ + } \ + } \ + \ + /** @ingroup undo_objects */ \ + TYPE * \ + teco_undo_object_##NAME##_push(TYPE *ptr) \ + { \ + teco_undo_object_##NAME##_t *ctx = teco_undo_push(teco_undo_object_##NAME); \ + if (ctx) { \ + ctx->ptr = ptr; \ + ctx->value = COPY(*ptr); \ + } else { \ + DELETE_IF_DISABLED(*ptr); \ + } \ + return ptr; \ } - void - run(void) - { - g_free(*ptr); - *ptr = str; - str = NULL; - } -}; +/** + * Defines an undo token push function that when executed restores + * the value/state of a variable of TYPE to the value it had when this + * was called. + * + * This can be used to undo changes to arbitrary variables, either + * requiring explicit memory handling or to scalars. + * + * The lifetime of the variable must be global - a pointer to it must be valid + * until the undo token could be executed. + * This will usually exclude stack-allocated variables or objects. + * + * @param NAME C identifier used for name mangling. + * @param TYPE Type of variable to restore. + * @param COPY A global function/expression to execute in order to copy VAR. + * If left empty, this is an identity operation and ownership + * of the variable is passed to the undo token. + * @param DELETE A global function/expression to execute in order to destruct + * objects of TYPE. Leave empty if destruction is not necessary. + * + * @ingroup undo_objects + */ +#define TECO_DEFINE_UNDO_OBJECT(NAME, TYPE, COPY, DELETE) \ + __TECO_DEFINE_UNDO_OBJECT(NAME, TYPE, COPY, DELETE, /* don't delete if disabled */, DELETE) +/** + * @fixme _OWN variants will invalidate the variable pointer, so perhaps + * it will be clearer to have _SET variants instead. + * + * @ingroup undo_objects + */ +#define TECO_DEFINE_UNDO_OBJECT_OWN(NAME, TYPE, DELETE) \ + __TECO_DEFINE_UNDO_OBJECT(NAME, TYPE, /* pass ownership */, DELETE, DELETE, /* don't delete if run */) -template -class UndoTokenObject : public UndoToken { - Type **ptr; - Type *obj; +/** @ingroup undo_objects */ +#define TECO_DECLARE_UNDO_OBJECT(NAME, TYPE) \ + TYPE *teco_undo_object_##NAME##_push(TYPE *ptr) -public: - UndoTokenObject(Type *&variable, Type *_obj) - : ptr(&variable), obj(_obj) {} +/** @ingroup undo_objects */ +#define TECO_DEFINE_UNDO_SCALAR(TYPE) \ + TECO_DEFINE_UNDO_OBJECT_OWN(TYPE, TYPE, /* don't delete */) - ~UndoTokenObject() - { - delete obj; - } +/** @ingroup undo_objects */ +#define TECO_DECLARE_UNDO_SCALAR(TYPE) \ + TECO_DECLARE_UNDO_OBJECT(TYPE, TYPE) - void - run(void) - { - delete *ptr; - *ptr = obj; - obj = NULL; - } -}; - -extern class UndoStack : public Object { - /** - * Stack of UndoToken lists. - * - * Each stack element represents - * a command line character (the UndoTokens - * generated by that character), so it's OK - * to use a data structure that may need - * reallocation but is space efficient. - * This data structure allows us to omit the - * command line program counter from the UndoTokens - * but wastes a few bytes for input characters - * that produce no UndoToken (e.g. NOPs like space). - */ - GPtrArray *heads; - - void push(UndoToken *token); - -public: - bool enabled; - - UndoStack(bool _enabled = false) - : heads(g_ptr_array_new()), enabled(_enabled) {} - ~UndoStack() - { - clear(); - g_ptr_array_free(heads, TRUE); - } +/* + * FIXME: We had to add -Wno-unused-value to surpress warnings. + * Perhaps it's clearer to sacrifice the lvalue feature. + * + * TODO: Check whether generating an additional check on teco_undo_enabled here + * significantly improves batch-mode performance. + */ +TECO_DECLARE_UNDO_SCALAR(gchar); +#define teco_undo_gchar(VAR) (*teco_undo_object_gchar_push(&(VAR))) - /** - * Allocate and push undo token. - * - * This does nothing if undo is disabled and should - * not be used when ownership of some data is to be - * passed to the undo token. - */ - template - inline void - push(Params && ... params) - { - if (enabled) - push(new TokenType(params...)); - } +TECO_DECLARE_UNDO_SCALAR(gint); +#define teco_undo_gint(VAR) (*teco_undo_object_gint_push(&(VAR))) - /** - * Allocate and push undo token, passing ownership. - * - * This creates and deletes the undo token cheaply - * if undo is disabled, so that data whose ownership - * is passed to the undo token is correctly reclaimed. - * - * @bug We must know which version of push to call - * depending on the token type. This could be hidden - * if UndoTokens had static push methods that take care - * of reclaiming memory. - */ - template - inline void - push_own(Params && ... params) - { - if (enabled) { - push(new TokenType(params...)); - } else { - /* ensures that all memory is reclaimed */ - TokenType dummy(params...); - } - } +TECO_DECLARE_UNDO_SCALAR(guint); +#define teco_undo_guint(VAR) (*teco_undo_object_guint_push(&(VAR))) - template - inline Type & - push_var(Type &variable, Type value) - { - push>(variable, value); - return variable; - } +TECO_DECLARE_UNDO_SCALAR(gsize); +#define teco_undo_gsize(VAR) (*teco_undo_object_gsize_push(&(VAR))) - template - inline Type & - push_var(Type &variable) - { - return push_var(variable, variable); - } +TECO_DECLARE_UNDO_SCALAR(teco_int_t); +#define teco_undo_int(VAR) (*teco_undo_object_teco_int_t_push(&(VAR))) - inline gchar *& - push_str(gchar *&variable, gchar *str) - { - push(variable, str); - return variable; - } - inline gchar *& - push_str(gchar *&variable) - { - return push_str(variable, variable); - } +TECO_DECLARE_UNDO_SCALAR(gboolean); +#define teco_undo_gboolean(VAR) (*teco_undo_object_gboolean_push(&(VAR))) - template - inline Type *& - push_obj(Type *&variable, Type *obj) - { - /* pass ownership of original object */ - push_own>(variable, obj); - return variable; - } +TECO_DECLARE_UNDO_SCALAR(gconstpointer); +#define teco_undo_ptr(VAR) \ + (*(typeof(VAR) *)teco_undo_object_gconstpointer_push((gconstpointer *)&(VAR))) - template - inline Type *& - push_obj(Type *&variable) - { - return push_obj(variable, variable); - } +#define __TECO_GEN_STRUCT(ID, X) X arg_##ID; +//#define __TECO_GEN_ARG(ID, X) X arg_##ID, +//#define __TECO_GEN_ARG_LAST(ID, X) X arg_##ID +#define __TECO_GEN_CALL(ID, X) ctx->arg_##ID, +#define __TECO_GEN_CALL_LAST(ID, X) ctx->arg_##ID +#define __TECO_GEN_INIT(ID, X) ctx->arg_##ID = arg_##ID; - void pop(gint pc); +/** + * @defgroup undo_calls Function calls on rubout. + * @{ + */ - void clear(void); -} undo; +/** + * Create an undo token that calls FNC with arbitrary scalar parameters + * (maximum 5, captured at the time of the call). + * It defines a function undo__FNC() for actually creating the closure. + * + * All arguments must be constants or expressions evaluating to scalars though, + * since no memory management (copying/freeing) is performed. + * + * Tipp: In order to save memory in the undo token structures, it is + * often trivial to define a static inline function that calls FNC and binds + * "constant" parameters. + * + * @param FNC Name of a global function or macro to execute. + * It must be a plain C identifier. + * @param ... The parameter types of FNC (signature). + * Only the types without any variable names must be specified. + * + * @fixme Sometimes, the push-function is used only in a single compilation unit, + * so it should be declared `static` or `static inline`. + * Is it worth complicating our APIs in order to support that? + */ +#define TECO_DEFINE_UNDO_CALL(FNC, ...) \ + typedef struct { \ + TECO_FOR_EACH(__TECO_GEN_STRUCT, __TECO_GEN_STRUCT, ##__VA_ARGS__) \ + } teco_undo_call_##FNC##_t; \ + \ + static void \ + teco_undo_call_##FNC##_action(teco_undo_call_##FNC##_t *ctx, gboolean run) \ + { \ + if (run) \ + FNC(TECO_FOR_EACH(__TECO_GEN_CALL, __TECO_GEN_CALL_LAST, ##__VA_ARGS__)); \ + } \ + \ + /** @ingroup undo_calls */ \ + void \ + undo__##FNC(TECO_FOR_EACH(__TECO_GEN_ARG, __TECO_GEN_ARG_LAST, ##__VA_ARGS__)) \ + { \ + teco_undo_call_##FNC##_t *ctx = teco_undo_push(teco_undo_call_##FNC); \ + if (ctx) { \ + TECO_FOR_EACH(__TECO_GEN_INIT, __TECO_GEN_INIT, ##__VA_ARGS__) \ + } \ + } -} /* namespace SciTECO */ +/** @} */ -#endif +void teco_undo_pop(gint pc); +void teco_undo_clear(void); diff --git a/src/view.c b/src/view.c new file mode 100644 index 0000000..e61c1a6 --- /dev/null +++ b/src/view.c @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_WINDOWS_H +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#include +#include +#include + +#include + +#include "sciteco.h" +#include "file-utils.h" +#include "string-utils.h" +#include "interface.h" +#include "undo.h" +#include "error.h" +#include "qreg.h" +#include "eol.h" +#include "view.h" + +/** @memberof teco_view_t */ +void +teco_view_setup(teco_view_t *ctx) +{ + /* + * Start with or without undo collection, + * depending on teco_undo_enabled. + */ + teco_view_ssm(ctx, SCI_SETUNDOCOLLECTION, teco_undo_enabled, 0); + + teco_view_ssm(ctx, SCI_SETFOCUS, TRUE, 0); + + /* + * Some Scintilla implementations show the horizontal + * scroll bar by default. + * Ensure it is never displayed by default. + */ + teco_view_ssm(ctx, SCI_SETHSCROLLBAR, FALSE, 0); + + /* + * Only margin 1 is given a width by default. + * To provide a minimalist default view, it is disabled. + */ + teco_view_ssm(ctx, SCI_SETMARGINWIDTHN, 1, 0); + + /* + * Set some basic styles in order to provide + * a consistent look across UIs if no profile + * is used. This makes writing UI-agnostic profiles + * and color schemes easier. + * FIXME: Some settings like fonts should probably + * be set per UI (i.e. Scinterm doesn't use it, + * GTK might try to use a system-wide default + * monospaced font). + */ + teco_view_ssm(ctx, SCI_SETCARETSTYLE, CARETSTYLE_BLOCK, 0); + teco_view_ssm(ctx, SCI_SETCARETPERIOD, 0, 0); + teco_view_ssm(ctx, SCI_SETCARETFORE, 0xFFFFFF, 0); + + teco_view_ssm(ctx, SCI_STYLESETFORE, STYLE_DEFAULT, 0xFFFFFF); + teco_view_ssm(ctx, SCI_STYLESETBACK, STYLE_DEFAULT, 0x000000); + teco_view_ssm(ctx, SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)"Courier"); + teco_view_ssm(ctx, SCI_STYLECLEARALL, 0, 0); + + /* + * FIXME: The line number background is apparently not + * affected by SCI_STYLECLEARALL + */ + teco_view_ssm(ctx, SCI_STYLESETBACK, STYLE_LINENUMBER, 0x000000); + + /* + * Use white as the default background color + * for call tips. Necessary since this style is also + * used for popup windows and we need to provide a sane + * default if no color-scheme is applied (and --no-profile). + */ + teco_view_ssm(ctx, SCI_STYLESETFORE, STYLE_CALLTIP, 0x000000); + teco_view_ssm(ctx, SCI_STYLESETBACK, STYLE_CALLTIP, 0xFFFFFF); +} + +TECO_DEFINE_UNDO_CALL(teco_view_ssm, teco_view_t *, unsigned int, uptr_t, sptr_t); + +/** @memberof teco_view_t */ +void +teco_view_set_representations(teco_view_t *ctx) +{ + static const char *reps[] = { + "^@", "^A", "^B", "^C", "^D", "^E", "^F", "^G", + "^H", "TAB" /* ^I */, "LF" /* ^J */, "^K", "^L", "CR" /* ^M */, "^N", "^O", + "^P", "^Q", "^R", "^S", "^T", "^U", "^V", "^W", + "^X", "^Y", "^Z", "$" /* ^[ */, "^\\", "^]", "^^", "^_" + }; + + for (guint cc = 0; cc < G_N_ELEMENTS(reps); cc++) { + gchar buf[] = {(gchar)cc, '\0'}; + teco_view_ssm(ctx, SCI_SETREPRESENTATION, (uptr_t)buf, (sptr_t)reps[cc]); + } +} + +TECO_DEFINE_UNDO_CALL(teco_view_set_representations, teco_view_t *); + +/** + * Loads the view's document by reading all data from + * a GIOChannel. + * The EOL style is guessed from the channel's data + * (if AUTOEOL is enabled). + * This assumes that the channel is blocking. + * Also it tries to guess the size of the file behind + * channel in order to preallocate memory in Scintilla. + * + * Any error reading the GIOChannel is propagated as + * an exception. + * + * @param ctx The view to load. + * @param channel Channel to read from. + * @param error A GError. + * @return FALSE in case of a GError. + * + * @memberof teco_view_t + */ +gboolean +teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **error) +{ + teco_view_ssm(ctx, SCI_BEGINUNDOACTION, 0, 0); + teco_view_ssm(ctx, SCI_CLEARALL, 0, 0); + + /* + * Preallocate memory based on the file size. + * May waste a few bytes if file contains DOS EOLs + * and EOL translation is enabled, but is faster. + * NOTE: g_io_channel_unix_get_fd() should report the correct fd + * on Windows, too. + */ + struct stat stat_buf = {.st_size = 0}; + if (!fstat(g_io_channel_unix_get_fd(channel), &stat_buf) && + stat_buf.st_size > 0) + teco_view_ssm(ctx, SCI_ALLOCATE, stat_buf.st_size, 0); + + g_auto(teco_eol_reader_t) reader; + teco_eol_reader_init_gio(&reader, channel); + + for (;;) { + /* + * NOTE: We don't have to free this data since teco_eol_reader_gio_convert() + * will point it into its internal buffer. + */ + teco_string_t str; + + GIOStatus rc = teco_eol_reader_convert(&reader, &str.data, &str.len, error); + if (rc == G_IO_STATUS_ERROR) { + teco_view_ssm(ctx, SCI_ENDUNDOACTION, 0, 0); + return FALSE; + } + if (rc == G_IO_STATUS_EOF) + break; + + teco_view_ssm(ctx, SCI_APPENDTEXT, str.len, (sptr_t)str.data); + } + + /* + * EOL-style guessed. + * Save it as the buffer's EOL mode, so save() + * can restore the original EOL-style. + * If auto-EOL-translation is disabled, this cannot + * have been guessed and the buffer's EOL mode should + * have a platform default. + * If it is enabled but the stream does not contain any + * EOL characters, the platform default is still assumed. + */ + if (reader.eol_style >= 0) + teco_view_ssm(ctx, SCI_SETEOLMODE, reader.eol_style, 0); + + if (reader.eol_style_inconsistent) + teco_interface_msg(TECO_MSG_WARNING, + "Inconsistent EOL styles normalized"); + + teco_view_ssm(ctx, SCI_ENDUNDOACTION, 0, 0); + return TRUE; +} + +/** + * Load view's document from file. + * + * @memberof teco_view_t + */ +gboolean +teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, GError **error) +{ + g_autoptr(GIOChannel) channel = g_io_channel_new_file(filename, "r", error); + if (!channel) + return FALSE; + + /* + * The file loading algorithm does not need buffered + * streams, so disabling buffering should increase + * performance (slightly). + */ + g_io_channel_set_encoding(channel, NULL, NULL); + g_io_channel_set_buffered(channel, FALSE); + + if (!teco_view_load_from_channel(ctx, channel, error)) { + g_prefix_error(error, "Error reading file \"%s\": ", filename); + return FALSE; + } + + return TRUE; +} + +#if 0 + +/* + * TODO: on UNIX it may be better to open() the current file, unlink() it + * and keep the file descriptor in the undo token. + * When the operation is undone, the file descriptor's contents are written to + * the file (which should be efficient enough because it is written to the same + * filesystem). This way we could avoid messing around with save point files. + */ + +#else + +static gint savepoint_id = 0; + +typedef struct { +#ifdef G_OS_WIN32 + teco_file_attributes_t orig_attrs; +#endif + + gchar *savepoint; + gchar filename[]; +} teco_undo_restore_savepoint_t; + +static void +teco_undo_restore_savepoint_action(teco_undo_restore_savepoint_t *ctx, gboolean run) +{ + if (!run) { + g_unlink(ctx->savepoint); + } else if (!g_rename(ctx->savepoint, ctx->filename)) { +#ifdef G_OS_WIN32 + if (ctx->orig_attrs != TECO_FILE_INVALID_ATTRIBUTES) + teco_file_set_attributes(ctx->filename, ctx->orig_attrs); +#endif + } else { + teco_interface_msg(TECO_MSG_WARNING, + "Unable to restore save point file \"%s\"", + ctx->savepoint); + } + + g_free(ctx->savepoint); + savepoint_id--; +} + +static void +teco_undo_restore_savepoint_push(gchar *savepoint, const gchar *filename) +{ + teco_undo_restore_savepoint_t *ctx; + + ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_restore_savepoint_action, + sizeof(*ctx) + strlen(filename) + 1); + if (ctx) { + ctx->savepoint = savepoint; + strcpy(ctx->filename, filename); + +#ifdef G_OS_WIN32 + ctx->orig_attrs = teco_file_get_attributes(filename); + if (ctx->orig_attrs != TECO_FILE_INVALID_ATTRIBUTES) + teco_file_set_attributes(savepoint, + ctx->orig_attrs | FILE_ATTRIBUTE_HIDDEN); +#endif + } else { + g_unlink(savepoint); + g_free(savepoint); + savepoint_id--; + } +} + +static void +teco_make_savepoint(const gchar *filename) +{ + gchar savepoint_basename[FILENAME_MAX]; + + g_autofree gchar *basename = g_path_get_basename(filename); + g_snprintf(savepoint_basename, sizeof(savepoint_basename), + ".teco-%d-%s~", savepoint_id, basename); + g_autofree gchar *dirname = g_path_get_dirname(filename); + gchar *savepoint = g_build_filename(dirname, savepoint_basename, NULL); + + if (g_rename(filename, savepoint)) { + teco_interface_msg(TECO_MSG_WARNING, + "Unable to create save point file \"%s\"", + savepoint); + g_free(savepoint); + return; + } + savepoint_id++; + + /* + * NOTE: passes ownership of savepoint string to undo token. + */ + teco_undo_restore_savepoint_push(savepoint, filename); +} + +#endif + +/* + * NOTE: Does not simply undo__g_unlink() since `filename` needs to be + * memory managed. + */ +static void +sciteo_undo_remove_file_action(gchar *filename, gboolean run) +{ + if (run) + g_unlink(filename); +} + +static inline void +teco_undo_remove_file_push(const gchar *filename) +{ + gchar *ctx = teco_undo_push_size((teco_undo_action_t)sciteo_undo_remove_file_action, + strlen(filename)+1); + if (ctx) + strcpy(ctx, filename); +} + +gboolean +teco_view_save_to_channel(teco_view_t *ctx, GIOChannel *channel, GError **error) +{ + g_auto(teco_eol_writer_t) writer; + teco_eol_writer_init_gio(&writer, teco_view_ssm(ctx, SCI_GETEOLMODE, 0, 0), channel); + + /* write part of buffer before gap */ + sptr_t gap = teco_view_ssm(ctx, SCI_GETGAPPOSITION, 0, 0); + if (gap > 0) { + const gchar *buffer = (const gchar *)teco_view_ssm(ctx, SCI_GETRANGEPOINTER, 0, gap); + gssize bytes_written = teco_eol_writer_convert(&writer, buffer, gap, error); + if (bytes_written < 0) + return FALSE; + g_assert(bytes_written == (gsize)gap); + } + + /* write part of buffer after gap */ + gsize size = teco_view_ssm(ctx, SCI_GETLENGTH, 0, 0) - gap; + if (size > 0) { + const gchar *buffer = (const gchar *)teco_view_ssm(ctx, SCI_GETRANGEPOINTER, gap, (sptr_t)size); + gssize bytes_written = teco_eol_writer_convert(&writer, buffer, size, error); + if (bytes_written < 0) + return FALSE; + g_assert(bytes_written == size); + } + + return TRUE; +} + +/** @memberof teco_view_t */ +gboolean +teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error) +{ +#ifdef G_OS_UNIX + GStatBuf file_stat; + file_stat.st_uid = -1; + file_stat.st_gid = -1; +#endif + teco_file_attributes_t attributes = TECO_FILE_INVALID_ATTRIBUTES; + + if (teco_undo_enabled) { + if (g_file_test(filename, G_FILE_TEST_IS_REGULAR)) { +#ifdef G_OS_UNIX + g_stat(filename, &file_stat); +#endif + attributes = teco_file_get_attributes(filename); + teco_make_savepoint(filename); + } else { + teco_undo_remove_file_push(filename); + } + } + + /* leaves access mode intact if file still exists */ + g_autoptr(GIOChannel) channel = g_io_channel_new_file(filename, "w", error); + if (!channel) + return FALSE; + + /* + * teco_view_save_to_channel() expects a buffered and blocking channel + */ + g_io_channel_set_encoding(channel, NULL, NULL); + g_io_channel_set_buffered(channel, TRUE); + + if (!teco_view_save_to_channel(ctx, channel, error)) { + g_prefix_error(error, "Error writing file \"%s\": ", filename); + return FALSE; + } + + /* if file existed but has been renamed, restore attributes */ + if (attributes != TECO_FILE_INVALID_ATTRIBUTES) + teco_file_set_attributes(filename, attributes); +#ifdef G_OS_UNIX + /* + * only a good try to inherit owner since process user must have + * CHOWN capability traditionally reserved to root only. + * FIXME: We should probably fall back to another save point + * strategy. + */ + if (fchown(g_io_channel_unix_get_fd(channel), + file_stat.st_uid, file_stat.st_gid)) + teco_interface_msg(TECO_MSG_WARNING, + "Unable to preserve owner of \"%s\": %s", + filename, g_strerror(errno)); +#endif + + return TRUE; +} diff --git a/src/view.h b/src/view.h new file mode 100644 index 0000000..b666d0a --- /dev/null +++ b/src/view.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * 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 3 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, see . + */ +#pragma once + +#include + +#include + +#include "sciteco.h" + +/** + * @interface teco_view_t + * Interface for all SciTECO views. + * + * Methods that must still be implemented in the user-interface + * layer are marked with the \@pure tag. + */ +typedef struct teco_view_t teco_view_t; + +/** @pure @static @memberof teco_view_t */ +teco_view_t *teco_view_new(void); + +void teco_view_setup(teco_view_t *ctx); + +/** @pure @memberof teco_view_t */ +sptr_t teco_view_ssm(teco_view_t *ctx, unsigned int iMessage, uptr_t wParam, sptr_t lParam); + +/** @memberof teco_view_t */ +void undo__teco_view_ssm(teco_view_t *, unsigned int, uptr_t, sptr_t); + +void teco_view_set_representations(teco_view_t *ctx); + +/** @memberof teco_view_t */ +void undo__teco_view_set_representations(teco_view_t *); + +/** @memberof teco_view_t */ +static inline void +teco_view_set_scintilla_undo(teco_view_t *ctx, gboolean state) +{ + teco_view_ssm(ctx, SCI_EMPTYUNDOBUFFER, 0, 0); + teco_view_ssm(ctx, SCI_SETUNDOCOLLECTION, state, 0); +} + +gboolean teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **error); +gboolean teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, GError **error); + +/** @memberof teco_view_t */ +#define teco_view_load(CTX, FROM, ERROR) \ + (_Generic((FROM), GIOChannel * : teco_view_load_from_channel, \ + const gchar * : teco_view_load_from_file)((CTX), (FROM), (ERROR))) + +gboolean teco_view_save_to_channel(teco_view_t *ctx, GIOChannel *channel, GError **error); +gboolean teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error); + +/** @memberof teco_view_t */ +#define teco_view_save(CTX, TO, ERROR) \ + (_Generic((TO), GIOChannel * : teco_view_save_to_channel, \ + const gchar * : teco_view_save_to_file)((CTX), (TO), (ERROR))) + +/** @pure @memberof teco_view_t */ +void teco_view_free(teco_view_t *ctx); diff --git a/tests/atlocal.in b/tests/atlocal.in index a4f30cc..49cb11d 100644 --- a/tests/atlocal.in +++ b/tests/atlocal.in @@ -7,3 +7,7 @@ SCITECO=@abs_top_builddir@/src/sciteco # Make sure that the standard library from the source package # is used. SCITECOPATH=@abs_top_srcdir@/lib + +# Glib debug options +G_SLICE=debug-blocks +G_ENABLE_DIAGNOSTIC=1 diff --git a/tests/testsuite.at b/tests/testsuite.at index 3288900..6c592a0 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -7,6 +7,8 @@ AT_COLOR_TESTS # idiom "(0/0)" to enforce a "Division by zero" error # whenever we want to fail. +AT_BANNER([Features]) + AT_SETUP([Missing left operand]) AT_CHECK([$SCITECO -e '+23='], 1, ignore, ignore) AT_CLEANUP @@ -15,12 +17,38 @@ AT_SETUP([Closing loops at the correct macro level]) AT_CHECK([$SCITECO -e '@^Ua{>} = 128 (at least on Linux). +AT_CHECK([$SCITECO -e "-2147483648@S/foo/"], 1, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Memory limiting during spawning]) +# This might result in an OOM if memory limiting is not working +AT_CHECK([$SCITECO -e "50*1000*1000,2EJ @EC'cat /dev/zero'"], 1, ignore, ignore) +AT_CLEANUP + +AT_BANNER([Known Bugs]) + +AT_SETUP([Pattern matching overflow]) +# Should no longer dump core. +# It could fail because the memory limit is exceeed, +# but not in this case since the match string isn't too large. +AT_CHECK([$SCITECO -e '100000<@I"X">J @S"^EM^X"'], 0, ignore, ignore) +AT_XFAIL_IF([/bin/true]) +AT_CLEANUP + +AT_SETUP([Recursion overflow]) +# Should no longer dump core. +# It could fail because the memory limit is exceeed, +# but not in this case since we limit the recursion. +AT_CHECK([$SCITECO -e "@^Um{U.a Q.a-100000\"<%.aMm'} 0Mm"], 0, ignore, ignore) +AT_XFAIL_IF([/bin/true]) +AT_CLEANUP -- cgit v1.2.3