aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRobin Haberkorn <robin.haberkorn@googlemail.com>2021-05-30 02:38:43 +0200
committerRobin Haberkorn <robin.haberkorn@googlemail.com>2021-05-30 03:12:56 +0200
commit432ad24e382681f1c13b07e8486e91063dd96e2e (patch)
tree51838adac822767bd5884b9383cd4c72f29d3840
parent524bc3960e6a6e5645ce904e20f72479e24e0a23 (diff)
downloadsciteco-432ad24e382681f1c13b07e8486e91063dd96e2e.tar.gz
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.
-rw-r--r--.gitignore9
-rw-r--r--INSTALL21
-rw-r--r--Makefile.am6
-rw-r--r--README12
-rw-r--r--TODO217
-rw-r--r--bootstrap.am6
-rw-r--r--compat/bsd/sys/cdefs.h89
-rw-r--r--compat/bsd/sys/queue.h620
-rw-r--r--compat/bsd/sys/tree.h763
-rw-r--r--configure.ac136
-rw-r--r--contrib/dlmalloc/Makefile.am23
-rw-r--r--contrib/dlmalloc/malloc.c6280
-rw-r--r--contrib/dlmalloc/malloc.h620
-rw-r--r--contrib/rb3ptr/Makefile.am6
-rw-r--r--contrib/rb3ptr/rb3ptr.c505
-rw-r--r--contrib/rb3ptr/rb3ptr.h474
-rw-r--r--debian/copyright44
-rw-r--r--doc/Doxyfile.in25
-rw-r--r--doc/Makefile.am17
-rw-r--r--doc/sciteco.1.in23
-rw-r--r--doc/sciteco.7.template21
-rw-r--r--m4/m4_ax_check_enable_debug.m4124
-rw-r--r--src/Makefile.am100
-rw-r--r--src/cmdline.c1058
-rw-r--r--src/cmdline.cpp1043
-rw-r--r--src/cmdline.h145
-rw-r--r--src/core-commands.c2510
-rw-r--r--src/core-commands.h71
-rw-r--r--src/doc.c209
-rw-r--r--src/doc.h115
-rw-r--r--src/document.cpp122
-rw-r--r--src/document.h141
-rw-r--r--src/eol.c461
-rw-r--r--src/eol.cpp359
-rw-r--r--src/eol.h180
-rw-r--r--src/error.c173
-rw-r--r--src/error.cpp171
-rw-r--r--src/error.h316
-rw-r--r--src/expressions.c384
-rw-r--r--src/expressions.cpp277
-rw-r--r--src/expressions.h399
-rw-r--r--src/file-utils.c343
-rw-r--r--src/file-utils.h (renamed from src/ioview.h)68
-rw-r--r--src/glob.c (renamed from src/glob.cpp)251
-rw-r--r--src/glob.h60
-rw-r--r--src/goto-commands.c171
-rw-r--r--src/goto-commands.h (renamed from src/symbols-minimal.cpp)21
-rw-r--r--src/goto.c182
-rw-r--r--src/goto.cpp193
-rw-r--r--src/goto.h144
-rw-r--r--src/help.c (renamed from src/help.cpp)319
-rw-r--r--src/help.h78
-rw-r--r--src/interface-curses/Makefile.am11
-rw-r--r--src/interface-curses/curses-info-popup.c211
-rw-r--r--src/interface-curses/curses-info-popup.cpp219
-rw-r--r--src/interface-curses/curses-info-popup.h96
-rw-r--r--src/interface-curses/curses-utils.c (renamed from src/interface-curses/curses-utils.cpp)38
-rw-r--r--src/interface-curses/curses-utils.h22
-rw-r--r--src/interface-curses/interface-curses.h199
-rw-r--r--src/interface-curses/interface.c (renamed from src/interface-curses/interface-curses.cpp)1044
-rw-r--r--src/interface-gtk/Makefile.am26
-rw-r--r--src/interface-gtk/fallback.css51
-rw-r--r--src/interface-gtk/gtk-info-popup.gob331
-rw-r--r--src/interface-gtk/gtkflowbox.c4795
-rw-r--r--src/interface-gtk/gtkflowbox.h180
-rw-r--r--src/interface-gtk/interface-gtk.cpp1132
-rw-r--r--src/interface-gtk/interface-gtk.h179
-rw-r--r--src/interface-gtk/interface.c1203
-rw-r--r--src/interface-gtk/teco-gtk-info-popup.gob446
-rw-r--r--src/interface-gtk/teco-gtk-label.gob (renamed from src/interface-gtk/gtk-canonicalized-label.gob)113
-rw-r--r--src/interface.c120
-rw-r--r--src/interface.cpp180
-rw-r--r--src/interface.h446
-rw-r--r--src/ioview.cpp512
-rw-r--r--src/list.h94
-rw-r--r--src/main.c (renamed from src/main.cpp)393
-rw-r--r--src/memory.c672
-rw-r--r--src/memory.cpp350
-rw-r--r--src/memory.h76
-rw-r--r--src/parser.c902
-rw-r--r--src/parser.cpp2883
-rw-r--r--src/parser.h845
-rw-r--r--src/qreg-commands.c760
-rw-r--r--src/qreg-commands.h90
-rw-r--r--src/qreg.c1542
-rw-r--r--src/qreg.h238
-rw-r--r--src/qregisters.cpp1680
-rw-r--r--src/qregisters.h666
-rw-r--r--src/rb3str.c150
-rw-r--r--src/rb3str.h69
-rw-r--r--src/rbtree.cpp90
-rw-r--r--src/rbtree.h205
-rw-r--r--src/ring.c580
-rw-r--r--src/ring.cpp461
-rw-r--r--src/ring.h266
-rw-r--r--src/scintilla.c349
-rw-r--r--src/scintilla.h64
-rw-r--r--src/sciteco.h159
-rw-r--r--src/search.c1130
-rw-r--r--src/search.cpp946
-rw-r--r--src/search.h130
-rw-r--r--src/spawn.c667
-rw-r--r--src/spawn.cpp662
-rw-r--r--src/spawn.h67
-rw-r--r--src/string-utils.c185
-rw-r--r--src/string-utils.cpp87
-rw-r--r--src/string-utils.h178
-rwxr-xr-xsrc/symbols-extract.tes22
-rw-r--r--src/symbols.cpp75
-rw-r--r--src/symbols.h69
-rw-r--r--src/undo.c163
-rw-r--r--src/undo.cpp104
-rw-r--r--src/undo.h399
-rw-r--r--src/view.c439
-rw-r--r--src/view.h75
-rw-r--r--tests/atlocal.in4
-rw-r--r--tests/testsuite.at59
117 files changed, 27099 insertions, 23605 deletions
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
========
<img align="left" alt="SciTECO" src="ico/sciteco-48.png"/>
-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 <LF> 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<LF>^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<description>$ 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<param>$
+ 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 \
<fs@PACKAGE^Q@\e@PACKAGE@\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 <sys/cdefs.h>
-
-#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 <provos@citi.umich.edu>
- * 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 <windows.h>
])
-
- # 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 <malloc.h> 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 <malloc.h>
+ ])
+ ])
+
+ case $host in
+ *-mingw*)
+ AC_CHECK_HEADERS([psapi.h], , [
+ AC_MSG_ERROR([Missing Windows headers!])
+ ], [
+ #include <windows.h>
+ ])
+
+ # 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 <windows.h>
+#include <tchar.h>
+#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 <sys/types.h> /* 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 <stdio.h> /* for printing in malloc_stats */
+#endif /* NO_MALLOC_STATS */
+#ifndef LACKS_ERRNO_H
+#include <errno.h> /* 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 <assert.h>
+#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 <time.h> /* for magic initialization */
+#endif /* WIN32 */
+#ifndef LACKS_STDLIB_H
+#include <stdlib.h> /* for abort() */
+#endif /* LACKS_STDLIB_H */
+#ifndef LACKS_STRING_H
+#include <string.h> /* for memset etc */
+#endif /* LACKS_STRING_H */
+#if USE_BUILTIN_FFS
+#ifndef LACKS_STRINGS_H
+#include <strings.h> /* 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 <sys/mman.h> /* for mmap */
+#undef __USE_GNU
+#else
+#include <sys/mman.h> /* for mmap */
+#endif /* linux */
+#endif /* LACKS_SYS_MMAN_H */
+#ifndef LACKS_FCNTL_H
+#include <fcntl.h>
+#endif /* LACKS_FCNTL_H */
+#endif /* HAVE_MMAP */
+#ifndef LACKS_UNISTD_H
+#include <unistd.h> /* 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 <thread.h>
+#elif !defined(LACKS_SCHED_H)
+#include <sched.h>
+#endif /* solaris or LACKS_SCHED_H */
+#if (defined(USE_RECURSIVE_LOCKS) && USE_RECURSIVE_LOCKS != 0) || !USE_SPIN_LOCKS
+#include <pthread.h>
+#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 <sys/param.h>
+# 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 <Walter@GeNeSys-e.de>.
+ Thanks also to Andreas Mueller <a.mueller at paradatec.de>,
+ 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 <tbennett@nvidia.com> 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 <mcfong at yahoo.com>
+ * 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 <malloc.h>. 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 <malloc.h>. 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 <stddef.h> /* 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 <rb3ptr.h>
+#include <stddef.h> // 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 <stdio.h>
+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 <stdint.h> /* uintptr_t */
+#endif
+
+#ifndef _RB3_ASSERT
+#ifndef assert
+#include <assert.h>
+#endif
+#define _RB3_ASSERT(x) assert(x)
+#endif
+
+#ifndef _RB3_NULL
+#ifndef NULL
+#include <stddef.h>
+#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 <robin.haberkorn@googlemail.com>
+Files: debian/* src/* */Makefile.am lib/*
+Copyright: Copyright 2013-2021 Robin Haberkorn <robin.haberkorn@googlemail.com>
License: GPL-3+
/usr/share/common-licenses/GPL-3
-Files: sciteco/*
-Copyright: Copyright 2012-2017 Robin Haberkorn <robin.haberkorn@googlemail.com>
-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 <neilh@scintilla.org>
@@ -50,11 +56,11 @@ License:
OR PERFORMANCE OF THIS SOFTWARE.
Files: scintilla/scinterm/*
-Copyright: Copyright 2012-2015 Mitchell <mitchell.att.foicica.com>
+Copyright: Copyright 2012-2016 Mitchell <mitchell.att.foicica.com>
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 <rhys.ulerich@gmail.com>
+# Copyright (c) 2014, 2015 Philip Withnall <philip@tecnocode.co.uk>
+#
+# 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+#include <signal.h>
+
+#ifdef HAVE_MALLOC_H
+#include <malloc.h>
+#endif
+#ifdef HAVE_MALLOC_NP_H
+#include <malloc_np.h>
+#endif
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#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, &macro_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, &macro_str.data, &macro_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'):
+ /*
+ * <CTL/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 <TAB> 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 <EQ> 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, <TAB> 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 <q>.
+ */
+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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#ifdef HAVE_MALLOC_H
-#include <malloc.h>
-#endif
-
-#include <string.h>
-#include <signal.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-#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'):
- /*
- * <CTL/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 <TAB> 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, <TAB> 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 <q>.
- */
-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 <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __CMDLINE_H
-#define __CMDLINE_H
+#pragma once
#include <glib.h>
-#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 <http://www.gnu.org/licenses/>.
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#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 <dot>) 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 <n> 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 <bool>
+ * signifies failure (non-negative value).
+ * If colon-modified, breaks from the loop if <bool>
+ * 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 <position>.
+ * If <position> is omitted, 0 is implied and \(lqJ\(rq will
+ * go to the beginning of the buffer.
+ *
+ * If <position> 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 <n> characters
+ * -C
+ * [n]:C -> Success|Failure
+ *
+ * Adds <n> to dot. 1 or -1 is implied if <n> is omitted.
+ * Fails if <n> 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 <n> characters backwards
+ * -R
+ * [n]:R -> Success|Failure
+ *
+ * Subtracts <n> 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 <n> 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 <n> goes to the
+ * beginning of the current line, 1 will go to the
+ * next line, -1 to the previous line etc.
+ * If <n> is omitted, 1 or -1 is implied depending on
+ * the sign prefix.
+ *
+ * If <n> 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 <n> lines backwards
+ * -B
+ * [n]:B -> Success|Failure
+ *
+ * Move dot to the beginning of the line <n>
+ * 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 <n> words forward.
+ * - If <n> is positive, dot is positioned at the beginning
+ * of the word <n> words after the current one.
+ * - If <n> is negative, dot is positioned at the end
+ * of the word <n> words before the current one.
+ * - If <n> 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 <n> words until the end of the
+ * n'th word after the current one.
+ * If <n> is negative, deletes up to end of the
+ * n'th word before the current one.
+ * If <n> 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 <n> words backward.
+ * <n>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
+ * <n>= -- Show value as message
+ *
+ * Shows integer <n> 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 <n> 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 <n> lines after or before the current one.
+ * If <n> is 0, \(lqK\(rq will delete up to the beginning
+ * of the current line.
+ * If <n> 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 <from> and <to> are available, the
+ * command is synonymous to <from>,<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 <n> is positive, the next <n> characters (up to and
+ * character .+<n>) are deleted.
+ * If <n> is negative, the previous <n> characters are
+ * deleted.
+ * If <n> 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 <from> up to <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 <code> of the character
+ * <n> relative to dot from the buffer.
+ * This can be an ASCII <code> or Unicode codepoint
+ * depending on Scintilla's encoding of the current
+ * buffer.
+ * - If <n> is 0, return the <code> of the character
+ * pointed to by dot.
+ * - If <n> is 1, return the <code> of the character
+ * immediately after dot.
+ * - If <n> is -1, return the <code> of the character
+ * immediately preceding dot, ecetera.
+ * - If <n> 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 <A>.
+ */
+ 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:
+ /*
+ * <CTRL/x> 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 <directory> 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 <directory> 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) <n> 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 <radix>.
+ * If <radix> 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 <c>
+ * 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: ^[, <CTRL/[>, <ESC>, $ (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.
+ * <bool> may be a specified to enforce closing dirty
+ * buffers.
+ * If it is a Failure condition boolean (negative),
+ * the buffer will be closed unconditionally.
+ * If <bool> 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.
+ * <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.
+ * <off> is a bitmap of flags to disable (set to 0 in ED
+ * flags) and <on> is a bitmap of flags that is ORed into
+ * the flags variable.
+ * If <off> is omitted, the value 0^_ is implied.
+ * In otherwords, all flags are turned off before turning
+ * on the <on> 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 <EJ>", 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 <EJ>", 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 <EJ>", 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, <EL> 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 <EL>");
+ 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 <EL>",
+ 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 <bool> as a success/truth condition
+ * boolean, EX will not check whether there are modified
+ * buffers and will always succeed.
+ * If <bool> 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, <bool> 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 <n>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.
+ * <c1> is inserted before <c2>, ecetera.
+ * Secondly, the command inserts <text>.
+ * In interactive mode, <text> 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, <CTRL/I>, <TAB>
+ */
+/*$ ^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 <text> 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 <text> 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 <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib.h>
+
+#include <Scintilla.h>
+
+#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 <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <string.h>
+
+#include <glib.h>
+
+#include <Scintilla.h>
+
+#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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <glib.h>
-
-#include <Scintilla.h>
-
-#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 <http://www.gnu.org/licenses/>.
- */
-
-#ifndef __DOCUMENT_H
-#define __DOCUMENT_H
-
-#include <glib.h>
-
-#include <Scintilla.h>
-
-#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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+
+#include <glib.h>
+
+#include <Scintilla.h>
+
+#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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <glib.h>
-
-#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 <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __EOL_H
-#define __EOL_H
-
-#include <string.h>
+#pragma once
#include <glib.h>
#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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+
+#include <glib.h>
+
+#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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <stdarg.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#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 <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __ERROR_H
-#define __ERROR_H
-
-#include <exception>
-#include <typeinfo>
+#pragma once
#include <glib.h>
-#include <glib/gprintf.h>
#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 <http://www.gnu.org/licenses/>.
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib.h>
+
+#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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <glib.h>
-
-#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<numbers>();
- 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<numbers>(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<operators>();
- 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<operators>(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 <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __EXPRESSIONS_H
-#define __EXPRESSIONS_H
+#pragma once
#include <glib.h>
-#include "memory.h"
+#include "sciteco.h"
#include "undo.h"
-#include "error.h"
-
-namespace SciTECO {
-
-template <typename Type>
-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 <ValueStack<Type> &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 <ValueStack<Type> &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 <ValueStack<Type> &stack>
- static inline void
- undo_push(Type value, guint index = 0)
- {
- undo.push<UndoTokenPush<stack>>(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 <ValueStack<Type> &stack>
- static inline void
- undo_pop(guint index = 0)
- {
- undo.push<UndoTokenPop<stack>>(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<tecoInt> NumberStack;
- static NumberStack numbers;
-
- typedef ValueStack<Operator> 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/stat.h>
+
+#ifdef HAVE_WINDOWS_H
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#endif
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#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/ioview.h b/src/file-utils.h
index 2baa21f..496e881 100644
--- a/src/ioview.h
+++ b/src/file-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,27 +14,20 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __IOVIEW_H
-#define __IOVIEW_H
+#pragma once
#include <string.h>
#include <glib.h>
-#include <glib/gstdio.h>
-#include <glib/gprintf.h>
#include "sciteco.h"
-#include "interface.h"
-#include "undo.h"
-
-namespace SciTECO {
+#include "string-utils.h"
-/*
- * Auxiliary functions
- */
+typedef guint32 teco_file_attributes_t;
+#define TECO_FILE_INVALID_ATTRIBUTES G_MAXUINT32
-gchar *expand_path(const gchar *path);
+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.
@@ -49,7 +42,7 @@ gchar *expand_path(const gchar *path);
* @param path Possibly relative path name.
* @return Newly-allocated absolute path name.
*/
-gchar *get_absolute_path(const gchar *path);
+gchar *teco_file_get_absolute_path(const gchar *path);
/**
* Normalize path or file name.
@@ -64,7 +57,7 @@ gchar *get_absolute_path(const gchar *path);
* may be ignored.
*/
static inline gchar *
-normalize_path(gchar *path)
+teco_file_normalize_path(gchar *path)
{
#if G_DIR_SEPARATOR != '/'
return g_strdelimit(path, G_DIR_SEPARATOR_S, '/');
@@ -73,7 +66,9 @@ normalize_path(gchar *path)
#endif
}
-bool file_is_visible(const gchar *path);
+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
@@ -87,7 +82,7 @@ bool file_is_visible(const gchar *path);
* the last used directory separator in the file name.
*/
static inline gsize
-file_get_dirname_len(const gchar *path)
+teco_file_get_dirname_len(const gchar *path)
{
gsize len = 0;
@@ -98,33 +93,14 @@ file_get_dirname_len(const gchar *path)
return len;
}
-class IOView : public ViewCurrent {
- class UndoTokenRemoveFile : public UndoToken {
- gchar *filename;
-
- public:
- UndoTokenRemoveFile(const gchar *_filename)
- : filename(g_strdup(_filename)) {}
- ~UndoTokenRemoveFile()
- {
- g_free(filename);
- }
-
- void
- run(void)
- {
- g_unlink(filename);
- }
- };
-
-public:
- void load(GIOChannel *channel);
- void load(const gchar *filename);
-
- void save(GIOChannel *channel);
- void save(const gchar *filename);
-};
+static inline gboolean
+teco_file_is_dir(const gchar *filename)
+{
+ if (!*filename)
+ return FALSE;
-} /* namespace SciTECO */
+ gchar c = filename[strlen(filename)-1];
+ return G_IS_DIR_SEPARATOR(c);
+}
-#endif
+gboolean teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_t *insert);
diff --git a/src/glob.cpp b/src/glob.c
index e6b5bd4..f6810c2 100644
--- a/src/glob.cpp
+++ b/src/glob.c
@@ -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
@@ -26,25 +26,28 @@
#include <glib/gstdio.h>
#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 "qregisters.h"
+#include "qreg.h"
#include "ring.h"
-#include "ioview.h"
+#include "error.h"
#include "glob.h"
-namespace SciTECO {
-
-namespace States {
- StateGlob_pattern glob_pattern;
- StateGlob_filename glob_filename;
-}
+/*
+ * FIXME: This state could be static.
+ */
+TECO_DECLARE_STATE(teco_state_glob_filename);
-Globber::Globber(const gchar *pattern, GFileTest _test)
- : test(_test)
+/** @memberof teco_globber_t */
+void
+teco_globber_init(teco_globber_t *ctx, const gchar *pattern, GFileTest test)
{
- gsize dirname_len;
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->test = test;
/*
* This finds the directory component including
@@ -55,40 +58,39 @@ Globber::Globber(const gchar *pattern, GFileTest _test)
* 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);
+ gsize dirname_len = teco_file_get_dirname_len(pattern);
+ ctx->dirname = g_strndup(pattern, dirname_len);
- dir = g_dir_open(*dirname ? dirname : ".", 0, NULL);
- /* if dirname does not exist, dir may be NULL */
+ ctx->dir = g_dir_open(*ctx->dirname ? ctx->dirname : ".", 0, NULL);
+ /* if dirname does not exist, the result may be NULL */
- Globber::pattern = compile_pattern(pattern + dirname_len);
+ ctx->pattern = teco_globber_compile_pattern(pattern + dirname_len);
}
+/** @memberof teco_globber_t */
gchar *
-Globber::next(void)
+teco_globber_next(teco_globber_t *ctx)
{
const gchar *basename;
- if (!dir)
+ if (!ctx->dir)
return NULL;
- while ((basename = g_dir_read_name(dir))) {
- gchar *filename;
-
- if (!g_regex_match(pattern, basename, (GRegexMatchFlags)0, 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.
*/
- filename = g_strconcat(dirname, basename, NIL);
+ 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 (test == G_FILE_TEST_EXISTS || g_file_test(filename, test))
+ if (ctx->test == G_FILE_TEST_EXISTS || g_file_test(filename, ctx->test))
return filename;
g_free(filename);
@@ -97,17 +99,20 @@ Globber::next(void)
return NULL;
}
-Globber::~Globber()
+/** @memberof teco_globber_t */
+void
+teco_globber_clear(teco_globber_t *ctx)
{
- if (pattern)
- g_regex_unref(pattern);
- if (dir)
- g_dir_close(dir);
- g_free(dirname);
+ 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 *
-Globber::escape_pattern(const gchar *pattern)
+teco_globber_escape_pattern(const gchar *pattern)
{
gsize escaped_len = 1;
gchar *escaped, *pout;
@@ -129,7 +134,7 @@ Globber::escape_pattern(const gchar *pattern)
break;
}
}
- pout = escaped = (gchar *)g_malloc(escaped_len);
+ pout = escaped = g_malloc(escaped_len);
while (*pattern) {
switch (*pattern) {
@@ -163,13 +168,12 @@ Globber::escape_pattern(const gchar *pattern)
* @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 *
-Globber::compile_pattern(const gchar *pattern)
+teco_globber_compile_pattern(const gchar *pattern)
{
- gchar *pattern_regex, *pout;
- GRegex *pattern_compiled;
-
enum {
STATE_WILDCARD,
STATE_CLASS_START,
@@ -187,7 +191,8 @@ Globber::compile_pattern(const gchar *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);
+ g_autofree gchar *pattern_regex = g_malloc(strlen(pattern)*2 + 1 + 1);
+ gchar *pout = pattern_regex;
while (*pattern) {
if (state == STATE_WILDCARD) {
@@ -277,16 +282,14 @@ Globber::compile_pattern(const gchar *pattern)
*pout++ = '$';
*pout = '\0';
- pattern_compiled = g_regex_new(pattern_regex,
- (GRegexCompileFlags)(G_REGEX_DOTALL | G_REGEX_ANCHORED),
- (GRegexMatchFlags)0, NULL);
+ 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);
- g_free(pattern_regex);
return pattern_compiled;
}
@@ -294,6 +297,25 @@ Globber::compile_pattern(const gchar *pattern)
* 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
@@ -347,7 +369,7 @@ Globber::compile_pattern(const gchar *pattern)
* 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.
+ * 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.
@@ -420,37 +442,26 @@ Globber::compile_pattern(const gchar *pattern)
* 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);
- }
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_glob_pattern,
+ .expectstring.last = FALSE
+);
- return &States::glob_filename;
-}
-
-State *
-StateGlob_filename::got_file(const gchar *filename)
+static teco_state_t *
+teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
{
- BEGIN_EXEC(&States::start);
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
- tecoInt teco_test_mode;
GFileTest file_flags = G_FILE_TEST_EXISTS;
- bool matching = false;
- bool colon_modified = eval_colon();
+ gboolean matching = FALSE;
+ gboolean colon_modified = teco_machine_main_eval_colon(ctx);
- QRegister *glob_reg = QRegisters::globals["_"];
- gchar *pattern_str;
+ teco_int_t teco_test_mode;
- expressions.eval();
- teco_test_mode = expressions.pop_num_calc(0, 0);
+ 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.
@@ -464,91 +475,99 @@ StateGlob_filename::got_file(const gchar *filename)
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 <EN>", teco_test_mode);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Invalid file test %" TECO_INT_FORMAT " for <EN>",
+ teco_test_mode);
+ return NULL;
}
- pattern_str = glob_reg->get_string();
+ 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 (*filename) {
+ if (str->len > 0) {
/*
* Match pattern against provided file name
*/
- GRegex *pattern = Globber::compile_pattern(pattern_str);
+ 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, (GRegexMatchFlags)0, NULL) &&
- (!teco_test_mode || g_file_test(filename, file_flags))) {
+ if (g_regex_match(pattern, filename, 0, NULL) &&
+ (teco_test_mode == 0 || 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);
+ /*
+ * 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;
+ matching = TRUE;
}
-
- g_regex_unref(pattern);
} else if (colon_modified) {
/*
* Match pattern against directory contents (globbing),
- * returning SUCCESS if at least one file matches
+ * returning TECO_SUCCESS if at least one file matches
*/
- Globber globber(pattern_str, file_flags);
- gchar *globbed_filename = globber.next();
+ g_auto(teco_globber_t) globber;
- matching = globbed_filename != NULL;
+ teco_globber_init(&globber, pattern_str.data, file_flags);
+ g_autofree gchar *globbed_filename = teco_globber_next(&globber);
- g_free(globbed_filename);
+ matching = globbed_filename != NULL;
} else {
/*
* Match pattern against directory contents (globbing),
- * inserting all matching file names (linefeed-terminated)
+ * inserting all matching file names (null-byte-terminated)
*/
- Globber globber(pattern_str, file_flags);
-
- gchar *globbed_filename;
+ g_auto(teco_globber_t) globber;
+ teco_globber_init(&globber, pattern_str.data, file_flags);
- interface.ssm(SCI_BEGINUNDOACTION);
-
- while ((globbed_filename = globber.next())) {
- size_t len = strlen(globbed_filename);
- /* overwrite trailing null */
- globbed_filename[len] = '\n';
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ gchar *globbed_filename;
+ while ((globbed_filename = teco_globber_next(&globber))) {
/*
- * FIXME: Once we're 8-bit clean, we should
- * add the filenames null-terminated
- * (there may be linebreaks in filename).
+ * FIXME: Filenames may contain linefeeds.
+ * But if we add them null-terminated, they will be relatively hard to parse.
*/
- interface.ssm(SCI_ADDTEXT, len+1,
- (sptr_t)globbed_filename);
+ 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;
+ matching = TRUE;
}
- interface.ssm(SCI_SCROLLCARET);
- interface.ssm(SCI_ENDUNDOACTION);
+ teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
}
- g_free(pattern_str);
-
if (colon_modified) {
- expressions.push(TECO_BOOL(matching));
+ teco_expressions_push(teco_bool(matching));
} else if (matching) {
/* text has been inserted */
- ring.dirtify();
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
+ teco_ring_dirtify();
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
}
- glob_reg->undo_set_integer();
- glob_reg->set_integer(TECO_BOOL(matching));
+ if (!glob_reg->vtable->undo_set_integer(glob_reg, error) ||
+ !glob_reg->vtable->set_integer(glob_reg, teco_bool(matching), error))
+ return NULL;
- return &States::start;
+ return &teco_state_start;
}
-} /* namespace SciTECO */
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_glob_filename);
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 <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __GLOB_H
-#define __GLOB_H
+#pragma once
#include <string.h>
#include <glib.h>
-#include <glib/gstdio.h>
#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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+
+#include <glib.h>
+
+#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 <O> 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 <label>.
+ * The simple go-to command is a special case of the
+ * computed go-to command.
+ * A comma-separated list of labels may be specified
+ * in the string argument.
+ * The label to jump to is selected by <n> (1 is <label1>,
+ * 2 is <label2>, etc.).
+ * If <n> is omitted, 1 is implied.
+ *
+ * If the label selected by <n> is does not exist in the
+ * list of labels, the command does nothing.
+ * Label definitions are cached in a table, so that
+ * if the label to go to has already been defined, the
+ * go-to command will jump immediately.
+ * Otherwise, parsing continues until the <label>
+ * is defined.
+ * The command will yield an error if a label has
+ * not been defined when the macro or command-line
+ * is terminated.
+ * In the latter case, the user will not be able to
+ * terminate the command-line.
+ */
+TECO_DEFINE_STATE_EXPECTSTRING(teco_state_goto,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_goto_process_edit_cmd
+);
diff --git a/src/symbols-minimal.cpp b/src/goto-commands.h
index 1582979..792a4e4 100644
--- a/src/symbols-minimal.cpp
+++ b/src/goto-commands.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,19 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+#pragma once
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
+#include "parser.h"
+#include "string-utils.h"
-#include "sciteco.h"
-#include "symbols.h"
+extern teco_string_t teco_goto_skip_label;
-namespace SciTECO {
-
-namespace Symbols {
- SymbolList scintilla;
- SymbolList scilexer;
-}
-
-} /* namespace SciTECO */
+TECO_DECLARE_STATE(teco_state_label);
+TECO_DECLARE_STATE(teco_state_goto);
diff --git a/src/goto.c b/src/goto.c
new file mode 100644
index 0000000..38717f3
--- /dev/null
+++ b/src/goto.c
@@ -0,0 +1,182 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "undo.h"
+#include "rb3str.h"
+#include "goto.h"
+
+//#define DEBUG
+
+/** @extends teco_rb3str_head_t */
+typedef struct {
+ teco_rb3str_head_t head;
+ gint pc;
+} teco_goto_label_t;
+
+/** @private @static @memberof teco_goto_label_t */
+static teco_goto_label_t *
+teco_goto_label_new(const gchar *name, gsize len, gint pc)
+{
+ teco_goto_label_t *label = g_new0(teco_goto_label_t, 1);
+ teco_string_init(&label->head.name, name, len);
+ label->pc = pc;
+ return label;
+}
+
+/** @private @memberof teco_goto_label_t */
+static inline void
+teco_goto_label_free(teco_goto_label_t *label)
+{
+ teco_string_clear(&label->head.name);
+ g_free(label);
+}
+
+/*
+ * FIXME: Most of these methods could be static since
+ * they are only called from goto.c.
+ */
+
+#ifdef DEBUG
+static void
+teco_goto_table_dump(teco_goto_table_t *ctx)
+{
+ for (rb3_head *cur = rb3_get_min(&ctx->tree);
+ cur != NULL;
+ cur = rb3_get_next(cur)) {
+ teco_goto_label_t *label = (teco_goto_label_t *)cur;
+ g_autofree *label_printable;
+ label_printable = teco_string_echo(cur->head.key.data, cur->head.key.len);
+
+ g_printf("table[\"%s\"] = %d\n", label_printable, label->pc);
+ }
+ g_printf("---END---\n");
+}
+#endif
+
+/** @memberof teco_goto_table_t */
+gint
+teco_goto_table_remove(teco_goto_table_t *ctx, const gchar *name, gsize len)
+{
+ gint existing_pc = -1;
+
+ teco_goto_label_t *label = (teco_goto_label_t *)teco_rb3str_find(&ctx->tree, TRUE, name, len);
+ if (label) {
+ existing_pc = label->pc;
+ rb3_unlink_and_rebalance(&label->head.head);
+ teco_goto_label_free(label);
+ }
+
+ return existing_pc;
+}
+
+/** @memberof teco_goto_table_t */
+gint
+teco_goto_table_find(teco_goto_table_t *ctx, const gchar *name, gsize len)
+{
+ teco_goto_label_t *label = (teco_goto_label_t *)teco_rb3str_find(&ctx->tree, TRUE, name, len);
+ return label ? label->pc : -1;
+}
+
+/** @memberof teco_goto_table_t */
+gint
+teco_goto_table_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gint pc)
+{
+ if (pc < 0)
+ return teco_goto_table_remove(ctx, name, len);
+
+ gint existing_pc = -1;
+
+ teco_goto_label_t *label = (teco_goto_label_t *)teco_rb3str_find(&ctx->tree, TRUE, name, len);
+ if (label) {
+ existing_pc = label->pc;
+ label->pc = pc;
+ } else {
+ label = teco_goto_label_new(name, len, pc);
+ teco_rb3str_insert(&ctx->tree, TRUE, &label->head);
+ }
+
+#ifdef DEBUG
+ teco_goto_table_dump(ctx);
+#endif
+
+ return existing_pc;
+}
+
+/*
+ * NOTE: We don't simply TECO_DEFINE_UNDO_CALL(), so we can store `name`
+ * as part of the undo token.
+ * If it would be a temporary pointer, TECO_DEFINE_UNDO_CALL() wouldn't
+ * do anyway.
+ */
+typedef struct {
+ teco_goto_table_t *table;
+ gint pc;
+ gsize len;
+ gchar name[];
+} teco_goto_table_undo_set_t;
+
+static void
+teco_goto_table_undo_set_action(teco_goto_table_undo_set_t *ctx, gboolean run)
+{
+ if (run) {
+ teco_goto_table_set(ctx->table, ctx->name, ctx->len, ctx->pc);
+#ifdef DEBUG
+ teco_goto_table_dump(ctx->table);
+#endif
+ }
+}
+
+/** @memberof teco_goto_table_t */
+void
+teco_goto_table_undo_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gint pc)
+{
+ if (!ctx->must_undo)
+ return;
+
+ teco_goto_table_undo_set_t *token;
+ token = teco_undo_push_size((teco_undo_action_t)teco_goto_table_undo_set_action,
+ sizeof(*token) + len);
+ if (token) {
+ token->table = ctx;
+ token->pc = pc;
+ token->len = len;
+ memcpy(token->name, name, len);
+ }
+}
+
+/** @memberof teco_goto_table_t */
+void
+teco_goto_table_clear(teco_goto_table_t *ctx)
+{
+ struct rb3_head *cur;
+
+ while ((cur = rb3_get_root(&ctx->tree))) {
+ rb3_unlink_and_rebalance(cur);
+ teco_goto_label_free((teco_goto_label_t *)cur);
+ }
+}
diff --git a/src/goto.cpp b/src/goto.cpp
deleted file mode 100644
index 14a4655..0000000
--- a/src/goto.cpp
+++ /dev/null
@@ -1,193 +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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#include "sciteco.h"
-#include "string-utils.h"
-#include "expressions.h"
-#include "parser.h"
-#include "undo.h"
-#include "goto.h"
-
-namespace SciTECO {
-
-namespace States {
- StateLabel label;
- StateGotoCmd gotocmd;
-}
-
-namespace Goto {
- GotoTable *table = NULL;
- gchar *skip_label = NULL;
-}
-
-gint
-GotoTable::remove(const gchar *name)
-{
- gint existing_pc = -1;
-
- Label *existing = (Label *)RBTreeString::find(name);
-
- if (existing) {
- existing_pc = existing->pc;
- RBTreeString::remove(existing);
- delete existing;
- }
-
- return existing_pc;
-}
-
-gint
-GotoTable::find(const gchar *name)
-{
- Label *existing = (Label *)RBTreeString::find(name);
-
- return existing ? existing->pc : -1;
-}
-
-gint
-GotoTable::set(const gchar *name, gint pc)
-{
- if (pc < 0)
- return remove(name);
-
- gint existing_pc = -1;
- Label *existing = (Label *)RBTreeString::find(name);
-
- if (existing) {
- existing_pc = existing->pc;
- g_free(existing->name);
- existing->name = g_strdup(name);
- existing->pc = pc;
- } else {
- RBTree::insert(new Label(name, pc));
- }
-
-#ifdef DEBUG
- dump();
-#endif
-
- return existing_pc;
-}
-
-#ifdef DEBUG
-void
-GotoTable::dump(void)
-{
- for (Label *cur = (Label *)min();
- cur != NULL;
- cur = (Label *)cur->next())
- g_printf("table[\"%s\"] = %d\n", cur->name, cur->pc);
- g_printf("---END---\n");
-}
-#endif
-
-/*
- * Command states
- */
-
-StateLabel::StateLabel() : State()
-{
- transitions['\0'] = this;
-}
-
-State *
-StateLabel::custom(gchar chr)
-{
- if (chr == '!') {
- Goto::table->undo_set(strings[0],
- Goto::table->set(strings[0], macro_pc));
-
- if (!g_strcmp0(strings[0], Goto::skip_label)) {
- g_free(undo.push_str(Goto::skip_label));
- Goto::skip_label = NULL;
-
- undo.push_var(mode) = MODE_NORMAL;
- }
-
- g_free(undo.push_str(strings[0]));
- strings[0] = NULL;
-
- return &States::start;
- }
-
- String::append(undo.push_str(strings[0]), chr);
- return this;
-}
-
-/*$ O
- * Olabel$ -- Go to label
- * [n]Olabel1[,label2,...]$
- *
- * Go to <label>.
- * The simple go-to command is a special case of the
- * computed go-to command.
- * A comma-separated list of labels may be specified
- * in the string argument.
- * The label to jump to is selected by <n> (1 is <label1>,
- * 2 is <label2>, etc.).
- * If <n> is omitted, the sign prefix is implied.
- *
- * If the label selected by <n> is does not exist in the
- * list of labels, the command does nothing.
- * Label definitions are cached in a table, so that
- * if the label to go to has already been defined, the
- * go-to command will jump immediately.
- * Otherwise, parsing continues until the <label>
- * is defined.
- * The command will yield an error if a label has
- * not been defined when the macro or command-line
- * is terminated.
- * In the latter case, the user will not be able to
- * terminate the command-line.
- */
-State *
-StateGotoCmd::done(const gchar *str)
-{
- tecoInt value;
- gchar **labels;
-
- BEGIN_EXEC(&States::start);
-
- value = expressions.pop_num_calc();
- labels = g_strsplit(str, ",", -1);
-
- if (value > 0 && value <= (tecoInt)g_strv_length(labels) &&
- *labels[value-1]) {
- gint pc = Goto::table->find(labels[value-1]);
-
- if (pc >= 0) {
- macro_pc = pc;
- } else {
- /* skip till label is defined */
- undo.push_str(Goto::skip_label);
- Goto::skip_label = g_strdup(labels[value-1]);
- undo.push_var(mode) = MODE_PARSE_ONLY_GOTO;
- }
- }
-
- g_strfreev(labels);
- return &States::start;
-}
-
-} /* namespace SciTECO */
diff --git a/src/goto.h b/src/goto.h
index c45ac27..4c54847 100644
--- a/src/goto.h
+++ b/src/goto.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,131 +14,45 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __GOTO_H
-#define __GOTO_H
-
-#include <string.h>
+#pragma once
#include <glib.h>
-#include <glib/gprintf.h>
#include "sciteco.h"
-#include "memory.h"
-#include "parser.h"
-#include "undo.h"
-#include "rbtree.h"
-
-namespace SciTECO {
-
-class GotoTable : private RBTreeString, public Object {
- class UndoTokenSet : public UndoToken {
- GotoTable *table;
-
- gchar *name;
- gint pc;
-
- public:
- UndoTokenSet(GotoTable *_table, const gchar *_name, gint _pc = -1)
- : table(_table), name(g_strdup(_name)), pc(_pc) {}
- ~UndoTokenSet()
- {
- g_free(name);
- }
+#include "string-utils.h"
+#include "rb3str.h"
- void
- run(void)
- {
- table->set(name, pc);
-#ifdef DEBUG
- table->dump();
-#endif
- }
- };
+/** @extends teco_rb3str_tree_t */
+typedef struct {
+ teco_rb3str_tree_t tree;
- class Label : public RBTreeString::RBEntryOwnString {
- public:
- gint pc;
-
- Label(const gchar *name, gint _pc = -1)
- : RBEntryOwnString(name), pc(_pc) {}
- };
-
- /*
- * whether to generate UndoTokens (unnecessary in macro invocations)
+ /**
+ * Whether to generate undo tokens (unnecessary in macro invocations)
*/
- bool must_undo;
-
-public:
- GotoTable(bool _undo = true) : must_undo(_undo) {}
-
- ~GotoTable()
- {
- clear();
- }
-
- gint remove(const gchar *name);
- gint find(const gchar *name);
-
- gint set(const gchar *name, gint pc);
- inline void
- undo_set(const gchar *name, gint pc = -1)
- {
- if (must_undo)
- undo.push<UndoTokenSet>(this, name, pc);
- }
-
- inline void
- clear(void)
- {
- Label *cur;
-
- while ((cur = (Label *)root()))
- delete (Label *)RBTreeString::remove(cur);
- }
-
- inline gchar *
- auto_complete(const gchar *name, gchar completed = ',')
- {
- return RBTreeString::auto_complete(name, completed);
- }
-
-#ifdef DEBUG
- void dump(void);
-#endif
-};
-
-namespace Goto {
- extern GotoTable *table;
- extern gchar *skip_label;
+ gboolean must_undo;
+} teco_goto_table_t;
+
+/** @memberof teco_goto_table_t */
+static inline void
+teco_goto_table_init(teco_goto_table_t *ctx, gboolean must_undo)
+{
+ rb3_reset_tree(&ctx->tree);
+ ctx->must_undo = must_undo;
}
-/*
- * Command states
- */
-
-class StateLabel : public State {
-public:
- StateLabel();
+gint teco_goto_table_remove(teco_goto_table_t *ctx, const gchar *name, gsize len);
-private:
- State *custom(gchar chr);
-};
+gint teco_goto_table_find(teco_goto_table_t *ctx, const gchar *name, gsize len);
-class StateGotoCmd : public StateExpectString {
-private:
- State *done(const gchar *str);
+gint teco_goto_table_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gint pc);
+void teco_goto_table_undo_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gint pc);
-protected:
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
-};
-
-namespace States {
- extern StateLabel label;
- extern StateGotoCmd gotocmd;
+/** @memberof teco_goto_table_t */
+static inline gboolean
+teco_goto_table_auto_complete(teco_goto_table_t *ctx, const gchar *str, gsize len,
+ teco_string_t *insert)
+{
+ return teco_rb3str_auto_complete(&ctx->tree, TRUE, str, len, 0, insert);
}
-} /* namespace SciTECO */
-
-#endif
+void teco_goto_table_clear(teco_goto_table_t *ctx);
diff --git a/src/help.cpp b/src/help.c
index e40e85e..fe6df1d 100644
--- a/src/help.cpp
+++ b/src/help.c
@@ -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
@@ -21,7 +21,7 @@
#include <stdio.h>
#include <stdlib.h>
-#include <errno.h>
+#include <string.h>
#include <glib.h>
#include <glib/gstdio.h>
@@ -29,48 +29,89 @@
#include "sciteco.h"
#include "string-utils.h"
+#include "error.h"
#include "parser.h"
-#include "qregisters.h"
+#include "core-commands.h"
+#include "qreg.h"
#include "ring.h"
#include "interface.h"
+#include "rb3str.h"
#include "help.h"
-namespace SciTECO {
+static void teco_help_set(const gchar *topic_name, const gchar *filename, teco_int_t pos);
-HelpIndex help_index;
+static GStringChunk *teco_help_chunk = NULL;
-namespace States {
- StateGetHelp gethelp;
+/** @extends teco_rb3str_head_t */
+typedef struct {
+ teco_rb3str_head_t head;
+
+ teco_int_t pos;
+ gchar filename[];
+} teco_help_topic_t;
+
+/** @static @memberof teco_help_topic_t */
+static teco_help_topic_t *
+teco_help_topic_new(const gchar *topic_name, const gchar *filename, teco_int_t pos)
+{
+ /*
+ * Topics are inserted only once into the RB tree, so we can store
+ * the strings in a GStringChunk.
+ *
+ * FIXME: The same should be true for teco_help_topic_t object itself.
+ * It could be allocated via a stack allocator.
+ */
+ teco_help_topic_t *topic = g_malloc0(sizeof(teco_help_topic_t) + strlen(filename) + 1);
+ teco_string_init_chunk(&topic->head.name, topic_name, strlen(topic_name), teco_help_chunk);
+ topic->pos = pos;
+ strcpy(topic->filename, filename);
+ return topic;
}
-void
-HelpIndex::load(void)
+/** @memberof teco_help_topic_t */
+static inline void
+teco_help_topic_free(teco_help_topic_t *ctx)
{
- gchar *lib_path;
- gchar *women_path;
- GDir *women_dir;
- const gchar *basename;
+ /*
+ * NOTE: The topic name is allocated via GStringChunk and can only be
+ * be deallocated together.
+ */
+ g_free(ctx);
+}
+
+static teco_rb3str_tree_t teco_help_tree;
- if (G_LIKELY(min() != NULL))
+static gboolean
+teco_help_init(GError **error)
+{
+ if (G_LIKELY(teco_help_chunk != NULL))
/* already loaded */
- return;
+ return TRUE;
- lib_path = QRegisters::globals["$SCITECOPATH"]->get_string();
- women_path = g_build_filename(lib_path, "women", NIL);
- g_free(lib_path);
+ teco_help_chunk = g_string_chunk_new(32);
+ rb3_reset_tree(&teco_help_tree);
- women_dir = g_dir_open(women_path, 0, NULL);
- if (!women_dir) {
- g_free(women_path);
- return;
- }
+ teco_qreg_t *lib_reg = teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECOPATH", 12);
+ g_assert(lib_reg != NULL);
+ g_auto(teco_string_t) lib_path = {NULL, 0};
+ if (!lib_reg->vtable->get_string(lib_reg, &lib_path.data, &lib_path.len, error))
+ return FALSE;
+ /*
+ * FIXME: lib_path may contain null-bytes.
+ * It's not clear how to deal with this.
+ */
+ g_autofree gchar *women_path = g_build_filename(lib_path.data, "women", NULL);
- while ((basename = g_dir_read_name(women_dir))) {
- gchar *filename, *filename_tec;
- FILE *file;
- gchar buffer[1024];
- gchar *topic;
+ /*
+ * FIXME: We might want to gracefully handle only the G_FILE_ERROR_NOENT
+ * error and propagate all other errors?
+ */
+ g_autoptr(GDir) women_dir = g_dir_open(women_path, 0, NULL);
+ if (!women_dir)
+ return TRUE;
+ const gchar *basename;
+ while ((basename = g_dir_read_name(women_dir))) {
if (!g_str_has_suffix(basename, ".woman"))
continue;
@@ -78,10 +119,9 @@ HelpIndex::load(void)
* Open the corresponding SciTECO macro to read
* its first line.
*/
- filename = g_build_filename(women_path, basename, NIL);
- filename_tec = g_strconcat(filename, ".tec", NIL);
- file = g_fopen(filename_tec, "r");
- g_free(filename_tec);
+ g_autofree gchar *filename = g_build_filename(women_path, basename, NULL);
+ g_autofree gchar *filename_tec = g_strconcat(filename, ".tec", NULL);
+ g_autoptr(FILE) file = g_fopen(filename_tec, "r");
if (!file) {
/*
* There might simply be no support script for
@@ -89,10 +129,8 @@ HelpIndex::load(void)
* In this case we create a topic using the filename
* without an extension.
*/
- topic = g_strndup(basename, strlen(basename)-6);
- set(topic, filename);
- g_free(topic);
- g_free(filename);
+ g_autofree gchar *topic = g_strndup(basename, strlen(basename)-6);
+ teco_help_set(topic, filename, 0);
continue;
}
@@ -101,25 +139,25 @@ HelpIndex::load(void)
* header containing the position to topic index.
* Every topic will be on its own line and they are unlikely
* to be very long, so we can use fgets() here.
+ *
* NOTE: Since we haven't opened with the "b" flag,
* fgets() will translate linebreaks to LF even on
* MSVCRT (Windows).
*/
+ gchar buffer[1024];
if (!fgets(buffer, sizeof(buffer), file) ||
!g_str_has_prefix(buffer, "!*")) {
- interface.msg(InterfaceCurrent::MSG_WARNING,
- "Missing or invalid topic line in womanpage script \"%s\"",
- filename);
- g_free(filename);
+ teco_interface_msg(TECO_MSG_WARNING,
+ "Missing or invalid topic line in womanpage script \"%s\"",
+ filename);
continue;
}
/* skip opening comment */
- topic = buffer+2;
+ gchar *topic = buffer+2;
do {
gchar *endptr;
- tecoInt pos = strtoul(topic, &endptr, 10);
- gsize len;
+ teco_int_t pos = strtoul(topic, &endptr, 10);
/*
* This also breaks at the last line of the
@@ -131,26 +169,20 @@ HelpIndex::load(void)
/*
* Strip the likely LF at the end of the line.
*/
- len = strlen(endptr)-1;
+ gsize len = strlen(endptr)-1;
if (G_LIKELY(endptr[len] == '\n'))
endptr[len] = '\0';
- set(endptr+1, filename, pos);
+ teco_help_set(endptr+1, filename, pos);
} while ((topic = fgets(buffer, sizeof(buffer), file)));
-
- fclose(file);
- g_free(filename);
}
- g_dir_close(women_dir);
- g_free(women_path);
+ return TRUE;
}
-HelpIndex::Topic *
-HelpIndex::find(const gchar *name)
+static inline teco_help_topic_t *
+teco_help_find(const gchar *topic_name)
{
- Topic *ret;
-
/*
* The topic index contains printable characters
* only (to avoid having to perform string building
@@ -159,24 +191,16 @@ HelpIndex::find(const gchar *name)
* Therefore, we expand control characters in the
* look-up string to their printable forms.
*/
- gchar *term = String::canonicalize_ctl(name);
-
- ret = (Topic *)RBTreeStringCase::find(term);
-
- g_free(term);
- return ret;
+ g_autofree gchar *term = teco_string_echo(topic_name, strlen(topic_name));
+ return (teco_help_topic_t *)teco_rb3str_find(&teco_help_tree, FALSE, term, strlen(term));
}
-void
-HelpIndex::set(const gchar *name, const gchar *filename, tecoInt pos)
+static void
+teco_help_set(const gchar *topic_name, const gchar *filename, teco_int_t pos)
{
- Topic *topic = new Topic(name, filename, pos);
- Topic *existing;
-
- existing = (Topic *)RBTree<RBEntryString>::find(topic);
+ teco_help_topic_t *topic;
+ teco_help_topic_t *existing = teco_help_find(topic_name);
if (existing) {
- gchar *basename;
-
if (!strcmp(existing->filename, filename)) {
/*
* A topic with the same name already exists
@@ -186,28 +210,113 @@ HelpIndex::set(const gchar *name, const gchar *filename, tecoInt pos)
* FIXME: Perhaps make it unique again!?
*/
existing->pos = pos;
- delete topic;
return;
}
/* in another file -> make name unique */
- interface.msg(InterfaceCurrent::MSG_WARNING,
- "Topic collision: \"%s\" defined in \"%s\" and \"%s\"",
- name, existing->filename, filename);
-
- String::append(topic->name, ":");
- basename = g_path_get_basename(filename);
- String::append(topic->name, basename);
- g_free(basename);
+ teco_interface_msg(TECO_MSG_WARNING,
+ "Topic collision: \"%s\" defined in \"%s\" and \"%s\"",
+ topic_name, existing->filename, filename);
+
+ g_autofree gchar *basename = g_path_get_basename(filename);
+ g_autofree gchar *unique_name = g_strconcat(topic_name, ":", basename, NULL);
+ topic = teco_help_topic_new(unique_name, filename, pos);
+ } else {
+ topic = teco_help_topic_new(topic_name, filename, pos);
}
- RBTree::insert(topic);
+ teco_rb3str_insert(&teco_help_tree, FALSE, &topic->head);
+}
+
+gboolean
+teco_help_auto_complete(const gchar *topic_name, teco_string_t *insert)
+{
+ return teco_rb3str_auto_complete(&teco_help_tree, FALSE, topic_name,
+ topic_name ? strlen(topic_name) : 0, 0, insert);
+}
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_help_cleanup(void)
+{
+ if (!teco_help_chunk)
+ /* not initialized */
+ return;
+ g_string_chunk_free(teco_help_chunk);
+
+ struct rb3_head *cur;
+
+ while ((cur = rb3_get_root(&teco_help_tree))) {
+ rb3_unlink_and_rebalance(cur);
+ teco_help_topic_free((teco_help_topic_t *)cur);
+ }
}
+#endif
/*
* Command states
*/
+static gboolean
+teco_state_help_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ /*
+ * The help-index is populated on demand,
+ * so we start up quicker and batch mode does
+ * not depend on the availability of the standard
+ * library.
+ */
+ return teco_help_init(error);
+}
+
+static teco_state_t *
+teco_state_help_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,
+ "Help topic must not contain null-byte");
+ return NULL;
+ }
+ teco_help_topic_t *topic = teco_help_find(str->data);
+ if (!topic) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Topic \"%s\" not found", str->data);
+ return NULL;
+ }
+
+ teco_ring_undo_edit();
+ /*
+ * ED hooks with the default lexer framework
+ * will usually load the styling SciTECO script
+ * when editing the buffer for the first time.
+ */
+ if (!teco_ring_edit(topic->filename, error))
+ return NULL;
+
+ /*
+ * Make sure the topic is visible.
+ * We do need undo tokens for this (even though
+ * the buffer is removed on rubout if the woman
+ * page is viewed first) since we might browse
+ * multiple topics in the same buffer without
+ * closing it first.
+ */
+ undo__teco_interface_ssm(SCI_GOTOPOS,
+ teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0);
+ teco_interface_ssm(SCI_GOTOPOS, topic->pos, 0);
+
+ return &teco_state_start;
+}
+
+/* in cmdline.c */
+gboolean teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar chr, GError **error);
+
/*$ "?" help
* ?[topic]$ -- Get help for topic
*
@@ -263,7 +372,7 @@ HelpIndex::set(const gchar *name, const gchar *filename, tecoInt pos)
* \fIgrosciteco\fP formatter and the \fIsciteco.tmac\fP
* GNU troff macros.
* When using womanpages generated by \fIgrosciteco\fP,
- * help topics can be defined using the \fBSCITECO_TOPIC\fP
+ * help topics can be defined using the \fBTECO_TOPIC\fP
* Troff macro.
* This flexible system allows \*(ST to access internal
* and third-party help files written in plain-text or
@@ -273,50 +382,8 @@ HelpIndex::set(const gchar *name, const gchar *filename, tecoInt pos)
*
* The \fB?\fP command does not have string building enabled.
*/
-void
-StateGetHelp::initial(void)
-{
- /*
- * The help-index is populated on demand,
- * so we start up quicker and batch mode does
- * not depend on the availability of the standard
- * library.
- */
- help_index.load();
-}
-
-State *
-StateGetHelp::done(const gchar *str)
-{
- HelpIndex::Topic *topic;
-
- BEGIN_EXEC(&States::start);
-
- topic = help_index.find(str);
- if (!topic)
- throw Error("Topic \"%s\" not found", str);
-
- ring.undo_edit();
- /*
- * ED hooks with the default lexer framework
- * will usually load the styling SciTECO script
- * when editing the buffer for the first time.
- */
- ring.edit(topic->filename);
-
- /*
- * Make sure the topic is visible.
- * We do need undo tokens for this (even though
- * the buffer is removed on rubout if the woman
- * page is viewed first) since we might browse
- * multiple topics in the same buffer without
- * closing it first.
- */
- interface.undo_ssm(SCI_GOTOPOS,
- interface.ssm(SCI_GETCURRENTPOS));
- interface.ssm(SCI_GOTOPOS, topic->pos);
-
- return &States::start;
-}
-
-} /* namespace SciTECO */
+TECO_DEFINE_STATE_EXPECTSTRING(teco_state_help,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_help_initial,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_help_process_edit_cmd,
+ .expectstring.string_building = FALSE
+);
diff --git a/src/help.h b/src/help.h
index 6287f4d..2859a03 100644
--- a/src/help.h
+++ b/src/help.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,85 +14,17 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __HELP_H
-#define __HELP_H
-
-#include <string.h>
+#pragma once
#include <glib.h>
-#include <glib/gprintf.h>
-#include "sciteco.h"
-#include "memory.h"
+#include "string-utils.h"
#include "parser.h"
-#include "undo.h"
-#include "rbtree.h"
-
-namespace SciTECO {
-
-class HelpIndex : private RBTreeStringCase, public Object {
-public:
- class Topic : public RBTreeStringCase::RBEntryOwnString {
- public:
- gchar *filename;
- tecoInt pos;
-
- Topic(const gchar *name, const gchar *_filename = NULL, tecoInt _pos = 0)
- : RBEntryOwnString(name),
- filename(_filename ? g_strdup(_filename) : NULL),
- pos(_pos) {}
- ~Topic()
- {
- g_free(filename);
- }
- };
-
- ~HelpIndex()
- {
- Topic *cur;
-
- while ((cur = (Topic *)root()))
- delete (Topic *)remove(cur);
- }
-
- void load(void);
- Topic *find(const gchar *name);
-
- void set(const gchar *name, const gchar *filename,
- tecoInt pos = 0);
-
- inline gchar *
- auto_complete(const gchar *name, gchar completed = '\0')
- {
- return RBTreeStringCase::auto_complete(name, completed);
- }
-};
-
-extern HelpIndex help_index;
+gboolean teco_help_auto_complete(const gchar *topic_name, teco_string_t *insert);
/*
* Command states
*/
-class StateGetHelp : public StateExpectString {
-public:
- StateGetHelp() : StateExpectString(false) {}
-
-private:
- void initial(void);
- State *done(const gchar *str);
-
-protected:
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
-};
-
-namespace States {
- extern StateGetHelp gethelp;
-}
-
-} /* namespace SciTECO */
-
-#endif
+TECO_DECLARE_STATE(teco_state_help);
diff --git a/src/interface-curses/Makefile.am b/src/interface-curses/Makefile.am
index 01e68ae..14fc920 100644
--- a/src/interface-curses/Makefile.am
+++ b/src/interface-curses/Makefile.am
@@ -1,8 +1,9 @@
-AM_CPPFLAGS += -I$(top_srcdir)/src
+AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr \
+ -I$(top_srcdir)/src
-AM_CXXFLAGS = -Wall -Wno-char-subscripts
+AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value
noinst_LTLIBRARIES = libsciteco-interface.la
-libsciteco_interface_la_SOURCES = interface-curses.cpp interface-curses.h \
- curses-utils.cpp curses-utils.h \
- curses-info-popup.cpp curses-info-popup.h
+libsciteco_interface_la_SOURCES = interface.c \
+ curses-utils.c curses-utils.h \
+ curses-info-popup.c curses-info-popup.h
diff --git a/src/interface-curses/curses-info-popup.c b/src/interface-curses/curses-info-popup.c
new file mode 100644
index 0000000..7d661a2
--- /dev/null
+++ b/src/interface-curses/curses-info-popup.c
@@ -0,0 +1,211 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib.h>
+
+#include <curses.h>
+
+#include "list.h"
+#include "string-utils.h"
+#include "interface.h"
+#include "curses-utils.h"
+#include "curses-info-popup.h"
+
+/*
+ * FIXME: This is redundant with teco-gtk-info-popup.gob.
+ */
+typedef struct {
+ teco_stailq_entry_t entry;
+
+ teco_popup_entry_type_t type;
+ teco_string_t name;
+ gboolean highlight;
+} teco_popup_entry_t;
+
+void
+teco_curses_info_popup_add(teco_curses_info_popup_t *ctx, teco_popup_entry_type_t type,
+ const gchar *name, gsize name_len, gboolean highlight)
+{
+ if (G_UNLIKELY(!ctx->chunk))
+ ctx->chunk = g_string_chunk_new(32);
+
+ /*
+ * FIXME: Test with g_slice_new()...
+ * It could however cause problems upon command-line termination
+ * and may not be measurably faster.
+ */
+ teco_popup_entry_t *entry = g_new(teco_popup_entry_t, 1);
+ entry->type = type;
+ /*
+ * Popup entries aren't removed individually, so we can
+ * more efficiently store them via GStringChunk.
+ */
+ teco_string_init_chunk(&entry->name, name, name_len, ctx->chunk);
+ entry->highlight = highlight;
+
+ teco_stailq_insert_tail(&ctx->list, &entry->entry);
+
+ ctx->longest = MAX(ctx->longest, (gint)name_len);
+ ctx->length++;
+}
+
+static void
+teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, attr_t attr)
+{
+ int cols = getmaxx(stdscr); /**! screen width */
+ int pad_lines; /**! pad height */
+ gint pad_cols; /**! entry columns */
+ gint pad_colwidth; /**! width per entry column */
+
+ /* reserve 2 spaces between columns */
+ pad_colwidth = MIN(ctx->longest + 2, cols - 2);
+
+ /* pad_cols = floor((cols - 2) / pad_colwidth) */
+ pad_cols = (cols - 2) / pad_colwidth;
+ /* pad_lines = ceil(length / pad_cols) */
+ pad_lines = (ctx->length+pad_cols-1) / pad_cols;
+
+ /*
+ * Render the entire autocompletion list into a pad
+ * which can be higher than the physical screen.
+ * The pad uses two columns less than the screen since
+ * it will be drawn into the popup window which has left
+ * and right borders.
+ */
+ ctx->pad = newpad(pad_lines, cols - 2);
+
+ wbkgd(ctx->pad, ' ' | attr);
+
+ /*
+ * cur_col is the row currently written.
+ * It does not wrap but grows indefinitely.
+ * Therefore the real current row is (cur_col % popup_cols)
+ */
+ gint cur_col = 0;
+ for (teco_stailq_entry_t *cur = ctx->list.first; cur != NULL; cur = cur->next) {
+ teco_popup_entry_t *entry = (teco_popup_entry_t *)cur;
+ gint cur_line = cur_col/pad_cols + 1;
+
+ wmove(ctx->pad, cur_line-1,
+ (cur_col % pad_cols)*pad_colwidth);
+
+ wattrset(ctx->pad, entry->highlight ? A_BOLD : A_NORMAL);
+
+ switch (entry->type) {
+ case TECO_POPUP_FILE:
+ case TECO_POPUP_DIRECTORY:
+ g_assert(!teco_string_contains(&entry->name, '\0'));
+ teco_curses_format_filename(ctx->pad, entry->name.data, -1);
+ break;
+ default:
+ teco_curses_format_str(ctx->pad, entry->name.data, entry->name.len, -1);
+ break;
+ }
+
+ cur_col++;
+ }
+}
+
+void
+teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr)
+{
+ if (!ctx->length)
+ /* nothing to display */
+ return;
+
+ int lines, cols; /* screen dimensions */
+ getmaxyx(stdscr, lines, cols);
+
+ if (ctx->window)
+ delwin(ctx->window);
+
+ if (!ctx->pad)
+ teco_curses_info_popup_init_pad(ctx, attr);
+ gint pad_lines = getmaxy(ctx->pad);
+
+ /*
+ * Popup window can cover all but one screen row.
+ * Another row is reserved for the top border.
+ */
+ gint popup_lines = MIN(pad_lines + 1, lines - 1);
+
+ /* window covers message, scintilla and info windows */
+ ctx->window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0);
+
+ wbkgdset(ctx->window, ' ' | attr);
+
+ wborder(ctx->window,
+ ACS_VLINE,
+ ACS_VLINE, /* may be overwritten with scrollbar */
+ ACS_HLINE,
+ ' ', /* no bottom line */
+ ACS_ULCORNER, ACS_URCORNER,
+ ACS_VLINE, ACS_VLINE);
+
+ copywin(ctx->pad, ctx->window,
+ ctx->pad_first_line, 0,
+ 1, 1, popup_lines - 1, cols - 2, FALSE);
+
+ if (pad_lines <= popup_lines - 1)
+ /* no need for scrollbar */
+ return;
+
+ /* bar_height = ceil((popup_lines-1)/pad_lines * (popup_lines-2)) */
+ gint bar_height = ((popup_lines-1)*(popup_lines-2) + pad_lines-1) /
+ pad_lines;
+ /* bar_y = floor(pad_first_line/pad_lines * (popup_lines-2)) + 1 */
+ gint bar_y = ctx->pad_first_line*(popup_lines-2) / pad_lines + 1;
+
+ mvwvline(ctx->window, 1, cols-1, ACS_CKBOARD, popup_lines-2);
+ /*
+ * We do not use ACS_BLOCK here since it will not
+ * always be drawn as a solid block (e.g. xterm).
+ * Instead, simply draw reverse blanks.
+ */
+ wmove(ctx->window, bar_y, cols-1);
+ wattron(ctx->window, A_REVERSE);
+ wvline(ctx->window, ' ', bar_height);
+
+ /* progress scroll position */
+ ctx->pad_first_line += popup_lines - 1;
+ /* wrap on last shown page */
+ ctx->pad_first_line %= pad_lines;
+ if (pad_lines - ctx->pad_first_line < popup_lines - 1)
+ /* show last page */
+ ctx->pad_first_line = pad_lines - (popup_lines - 1);
+}
+
+void
+teco_curses_info_popup_clear(teco_curses_info_popup_t *ctx)
+{
+ if (ctx->window)
+ delwin(ctx->window);
+ if (ctx->pad)
+ delwin(ctx->pad);
+ if (ctx->chunk)
+ g_string_chunk_free(ctx->chunk);
+
+ teco_stailq_entry_t *entry;
+ while ((entry = teco_stailq_remove_head(&ctx->list)))
+ g_free(entry);
+
+ teco_curses_info_popup_init(ctx);
+}
diff --git a/src/interface-curses/curses-info-popup.cpp b/src/interface-curses/curses-info-popup.cpp
deleted file mode 100644
index 487f1b7..0000000
--- a/src/interface-curses/curses-info-popup.cpp
+++ /dev/null
@@ -1,219 +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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <string.h>
-
-#include <glib.h>
-
-#include <curses.h>
-
-#include "curses-utils.h"
-#include "curses-info-popup.h"
-
-namespace SciTECO {
-
-void
-CursesInfoPopup::add(PopupEntryType type,
- const gchar *name, bool highlight)
-{
- size_t name_len = strlen(name);
- Entry *entry = (Entry *)g_malloc(sizeof(Entry) + name_len + 1);
-
- entry->type = type;
- entry->highlight = highlight;
- strcpy(entry->name, name);
-
- longest = MAX(longest, (gint)name_len);
- length++;
-
- /*
- * Entries are added in reverse (constant time for GSList),
- * so they will later have to be reversed.
- */
- list = g_slist_prepend(list, entry);
-}
-
-void
-CursesInfoPopup::init_pad(attr_t attr)
-{
- int cols = getmaxx(stdscr); /* screen width */
- int pad_lines; /* pad height */
- gint pad_cols; /* entry columns */
- gint pad_colwidth; /* width per entry column */
-
- gint cur_col;
-
- /* reserve 2 spaces between columns */
- pad_colwidth = MIN(longest + 2, cols - 2);
-
- /* pad_cols = floor((cols - 2) / pad_colwidth) */
- pad_cols = (cols - 2) / pad_colwidth;
- /* pad_lines = ceil(length / pad_cols) */
- pad_lines = (length+pad_cols-1) / pad_cols;
-
- /*
- * Render the entire autocompletion list into a pad
- * which can be higher than the physical screen.
- * The pad uses two columns less than the screen since
- * it will be drawn into the popup window which has left
- * and right borders.
- */
- pad = newpad(pad_lines, cols - 2);
-
- wbkgd(pad, ' ' | attr);
-
- /*
- * cur_col is the row currently written.
- * It does not wrap but grows indefinitely.
- * Therefore the real current row is (cur_col % popup_cols)
- */
- cur_col = 0;
- for (GSList *cur = list; cur != NULL; cur = g_slist_next(cur)) {
- Entry *entry = (Entry *)cur->data;
- gint cur_line = cur_col/pad_cols + 1;
-
- wmove(pad, cur_line-1,
- (cur_col % pad_cols)*pad_colwidth);
-
- wattrset(pad, entry->highlight ? A_BOLD : A_NORMAL);
-
- switch (entry->type) {
- case POPUP_FILE:
- case POPUP_DIRECTORY:
- Curses::format_filename(pad, entry->name);
- break;
- default:
- Curses::format_str(pad, entry->name);
- break;
- }
-
- cur_col++;
- }
-}
-
-void
-CursesInfoPopup::show(attr_t attr)
-{
- int lines, cols; /* screen dimensions */
- gint pad_lines;
- gint popup_lines;
- gint bar_height, bar_y;
-
- if (!length)
- /* nothing to display */
- return;
-
- getmaxyx(stdscr, lines, cols);
-
- if (window)
- delwin(window);
- else
- /* reverse list only once */
- list = g_slist_reverse(list);
-
- if (!pad)
- init_pad(attr);
- pad_lines = getmaxy(pad);
-
- /*
- * Popup window can cover all but one screen row.
- * Another row is reserved for the top border.
- */
- popup_lines = MIN(pad_lines + 1, lines - 1);
-
- /* window covers message, scintilla and info windows */
- window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0);
-
- wbkgdset(window, ' ' | attr);
-
- wborder(window,
- ACS_VLINE,
- ACS_VLINE, /* may be overwritten with scrollbar */
- ACS_HLINE,
- ' ', /* no bottom line */
- ACS_ULCORNER, ACS_URCORNER,
- ACS_VLINE, ACS_VLINE);
-
- copywin(pad, window,
- pad_first_line, 0,
- 1, 1, popup_lines - 1, cols - 2, FALSE);
-
- if (pad_lines <= popup_lines - 1)
- /* no need for scrollbar */
- return;
-
- /* bar_height = ceil((popup_lines-1)/pad_lines * (popup_lines-2)) */
- bar_height = ((popup_lines-1)*(popup_lines-2) + pad_lines-1) /
- pad_lines;
- /* bar_y = floor(pad_first_line/pad_lines * (popup_lines-2)) + 1 */
- bar_y = pad_first_line*(popup_lines-2) / pad_lines + 1;
-
- mvwvline(window, 1, cols-1, ACS_CKBOARD, popup_lines-2);
- /*
- * We do not use ACS_BLOCK here since it will not
- * always be drawn as a solid block (e.g. xterm).
- * Instead, simply draw reverse blanks.
- */
- wmove(window, bar_y, cols-1);
- wattron(window, A_REVERSE);
- wvline(window, ' ', bar_height);
-
- /* progress scroll position */
- pad_first_line += popup_lines - 1;
- /* wrap on last shown page */
- pad_first_line %= pad_lines;
- if (pad_lines - pad_first_line < popup_lines - 1)
- /* show last page */
- pad_first_line = pad_lines - (popup_lines - 1);
-}
-
-void
-CursesInfoPopup::clear(void)
-{
- g_slist_free_full(list, g_free);
- list = NULL;
- length = 0;
- longest = 0;
-
- pad_first_line = 0;
-
- if (window) {
- delwin(window);
- window = NULL;
- }
-
- if (pad) {
- delwin(pad);
- pad = NULL;
- }
-}
-
-CursesInfoPopup::~CursesInfoPopup()
-{
- if (window)
- delwin(window);
- if (pad)
- delwin(pad);
- if (list)
- g_slist_free_full(list, g_free);
-}
-
-} /* namespace SciTECO */
diff --git a/src/interface-curses/curses-info-popup.h b/src/interface-curses/curses-info-popup.h
index af09cb4..d911182 100644
--- a/src/interface-curses/curses-info-popup.h
+++ b/src/interface-curses/curses-info-popup.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,78 +14,52 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+#pragma once
-#ifndef __CURSES_INFO_POPUP_H
-#define __CURSES_INFO_POPUP_H
+#include <string.h>
#include <glib.h>
#include <curses.h>
-#include "memory.h"
+#include "list.h"
+#include "interface.h"
-namespace SciTECO {
+typedef struct {
+ WINDOW *window; /**! window showing part of pad */
+ WINDOW *pad; /**! full-height entry list */
-class CursesInfoPopup : public Object {
-public:
- /**
- * @bug This is identical to the type defined in
- * interface.h. But for the sake of abstraction
- * we cannot access it here (or in gtk-info-popup
- * for that matter).
- */
- enum PopupEntryType {
- POPUP_PLAIN,
- POPUP_FILE,
- POPUP_DIRECTORY
- };
+ teco_stailq_head_t list; /**! list of popup entries */
+ gint longest; /**! size of longest entry */
+ gint length; /**! total number of popup entries */
-private:
- WINDOW *window; /**! window showing part of pad */
- WINDOW *pad; /**! full-height entry list */
+ gint pad_first_line; /**! first line in pad to show */
- struct Entry {
- PopupEntryType type;
- bool highlight;
- gchar name[];
- };
+ GStringChunk *chunk; /**! string chunk for all popup entry names */
+} teco_curses_info_popup_t;
- GSList *list; /**! list of popup entries */
- gint longest; /**! size of longest entry */
- gint length; /**! total number of popup entries */
+static inline void
+teco_curses_info_popup_init(teco_curses_info_popup_t *ctx)
+{
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->list = TECO_STAILQ_HEAD_INITIALIZER(&ctx->list);
+}
- gint pad_first_line; /**! first line in pad to show */
+void teco_curses_info_popup_add(teco_curses_info_popup_t *ctx, teco_popup_entry_type_t type,
+ const gchar *name, gsize name_len, gboolean highlight);
-public:
- CursesInfoPopup() : window(NULL), pad(NULL),
- list(NULL), longest(0), length(0),
- pad_first_line(0) {}
+void teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr);
+static inline bool
+teco_curses_info_popup_is_shown(teco_curses_info_popup_t *ctx)
+{
+ return ctx->window != NULL;
+}
- void add(PopupEntryType type,
- const gchar *name, bool highlight = false);
+static inline void
+teco_curses_info_popup_noutrefresh(teco_curses_info_popup_t *ctx)
+{
+ if (ctx->window)
+ wnoutrefresh(ctx->window);
+}
- void show(attr_t attr);
- inline bool
- is_shown(void)
- {
- return window != NULL;
- }
-
- void clear(void);
-
- inline void
- noutrefresh(void)
- {
- if (window)
- wnoutrefresh(window);
- }
-
- ~CursesInfoPopup();
-
-private:
- void init_pad(attr_t attr);
-};
-
-} /* namespace SciTECO */
-
-#endif
+void teco_curses_info_popup_clear(teco_curses_info_popup_t *ctx);
diff --git a/src/interface-curses/curses-utils.cpp b/src/interface-curses/curses-utils.c
index f5d5c8c..ace5795 100644
--- a/src/interface-curses/curses-utils.cpp
+++ b/src/interface-curses/curses-utils.c
@@ -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
@@ -29,19 +29,14 @@
#include "string-utils.h"
#include "curses-utils.h"
-namespace SciTECO {
-
gsize
-Curses::format_str(WINDOW *win, const gchar *str,
- gssize len, gint max_width)
+teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width)
{
int old_x, old_y;
gint chars_added = 0;
getyx(win, old_y, old_x);
- if (len < 0)
- len = strlen(str);
if (max_width < 0)
max_width = getmaxx(win) - old_x;
@@ -51,7 +46,7 @@ Curses::format_str(WINDOW *win, const gchar *str,
* View::set_representations()
*/
switch (*str) {
- case CTL_KEY_ESC:
+ case '\e':
chars_added++;
if (chars_added > max_width)
goto truncate;
@@ -80,12 +75,12 @@ Curses::format_str(WINDOW *win, const gchar *str,
waddch(win, 'B' | A_REVERSE);
break;
default:
- if (IS_CTL(*str)) {
+ if (TECO_IS_CTL(*str)) {
chars_added += 2;
if (chars_added > max_width)
goto truncate;
waddch(win, '^' | A_REVERSE);
- waddch(win, CTL_ECHO(*str) | A_REVERSE);
+ waddch(win, TECO_CTL_ECHO(*str) | A_REVERSE);
} else {
chars_added++;
if (chars_added > max_width)
@@ -114,29 +109,29 @@ truncate:
}
gsize
-Curses::format_filename(WINDOW *win, const gchar *filename,
- gint max_width)
+teco_curses_format_filename(WINDOW *win, const gchar *filename,
+ gint max_width)
{
int old_x = getcurx(win);
- gchar *filename_canon = String::canonicalize_ctl(filename);
- size_t filename_len = strlen(filename_canon);
+ g_autofree gchar *filename_printable = teco_string_echo(filename, strlen(filename));
+ size_t filename_len = strlen(filename_printable);
if (max_width < 0)
max_width = getmaxx(win) - old_x;
if (filename_len <= (size_t)max_width) {
- waddstr(win, filename_canon);
+ waddstr(win, filename_printable);
} else {
- const gchar *keep_post = filename_canon + filename_len -
+ const gchar *keep_post = filename_printable + filename_len -
max_width + 3;
#ifdef G_OS_WIN32
- const gchar *keep_pre = g_path_skip_root(filename_canon);
+ const gchar *keep_pre = g_path_skip_root(filename_printable);
if (keep_pre) {
- waddnstr(win, filename_canon,
- keep_pre - filename_canon);
- keep_post += keep_pre - filename_canon;
+ waddnstr(win, filename_printable,
+ keep_pre - filename_printable);
+ keep_post += keep_pre - filename_printable;
}
#endif
wattron(win, A_UNDERLINE | A_BOLD);
@@ -145,8 +140,5 @@ Curses::format_filename(WINDOW *win, const gchar *filename,
waddstr(win, keep_post);
}
- g_free(filename_canon);
return getcurx(win) - old_x;
}
-
-} /* namespace SciTECO */
diff --git a/src/interface-curses/curses-utils.h b/src/interface-curses/curses-utils.h
index 778f39e..3a681a4 100644
--- a/src/interface-curses/curses-utils.h
+++ b/src/interface-curses/curses-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,26 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __CURSES_UTILS_H
-#define __CURSES_UTILS_H
+#pragma once
#include <glib.h>
#include <curses.h>
-namespace SciTECO {
-
-namespace Curses {
-
-gsize format_str(WINDOW *win, const gchar *str,
- gssize len = -1, gint max_width = -1);
-
-gsize format_filename(WINDOW *win, const gchar *filename,
- gint max_width = -1);
-
-} /* namespace Curses */
-
-} /* namespace SciTECO */
+gsize teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width);
-#endif
+gsize teco_curses_format_filename(WINDOW *win, const gchar *filename, gint max_width);
diff --git a/src/interface-curses/interface-curses.h b/src/interface-curses/interface-curses.h
deleted file mode 100644
index 32fff1d..0000000
--- a/src/interface-curses/interface-curses.h
+++ /dev/null
@@ -1,199 +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 <http://www.gnu.org/licenses/>.
- */
-
-#ifndef __INTERFACE_CURSES_H
-#define __INTERFACE_CURSES_H
-
-#include <stdarg.h>
-
-#include <glib.h>
-
-#include <curses.h>
-
-#include <Scintilla.h>
-#include <ScintillaTerm.h>
-
-#include "interface.h"
-#include "curses-info-popup.h"
-
-namespace SciTECO {
-
-typedef class ViewCurses : public View<ViewCurses> {
- Scintilla *sci;
-
-public:
- ViewCurses() : sci(NULL) {}
-
- /* implementation of View::initialize() */
- void initialize_impl(void);
-
- inline ~ViewCurses()
- {
- /*
- * NOTE: This deletes/frees the view's
- * curses WINDOW, despite of what old versions
- * of the Scinterm documentation claim.
- */
- if (sci)
- scintilla_delete(sci);
- }
-
- inline void
- noutrefresh(void)
- {
- scintilla_noutrefresh(sci);
- }
-
- inline void
- refresh(void)
- {
- scintilla_refresh(sci);
- }
-
- inline WINDOW *
- get_window(void)
- {
- return scintilla_get_window(sci);
- }
-
- /* implementation of View::ssm() */
- inline sptr_t
- ssm_impl(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0)
- {
- return scintilla_send_message(sci, iMessage, wParam, lParam);
- }
-} ViewCurrent;
-
-typedef class InterfaceCurses : public Interface<InterfaceCurses, ViewCurses> {
- /**
- * Mapping of the first 16 curses color codes (that may or may not
- * correspond with the standard terminal color codes) to
- * Scintilla-compatible RGB values (red is LSB) to initialize after
- * Curses startup.
- * Negative values mean no color redefinition (keep the original
- * palette entry).
- */
- gint32 color_table[16];
-
- /**
- * Mapping of the first 16 curses color codes to their
- * original values for restoring them on shutdown.
- * Unfortunately, this may not be supported on all
- * curses ports, so this array may be unused.
- */
- struct {
- short r, g, b;
- } orig_color_table[16];
-
- int stdout_orig, stderr_orig;
- SCREEN *screen;
- FILE *screen_tty;
-
- WINDOW *info_window;
- enum {
- INFO_TYPE_BUFFER = 0,
- INFO_TYPE_QREGISTER
- } info_type;
- gchar *info_current;
-
- WINDOW *msg_window;
-
- WINDOW *cmdline_window, *cmdline_pad;
- gsize cmdline_len, cmdline_rubout_len;
-
- CursesInfoPopup popup;
-
-public:
- InterfaceCurses();
- ~InterfaceCurses();
-
- /* override of Interface::init() */
- void init(void);
-
- /* override of Interface::init_color() */
- void init_color(guint color, guint32 rgb);
-
- /* implementation of Interface::vmsg() */
- void vmsg_impl(MessageType type, const gchar *fmt, va_list ap);
- /* override of Interface::msg_clear() */
- void msg_clear(void);
-
- /* implementation of Interface::show_view() */
- void show_view_impl(ViewCurses *view);
-
- /* implementation of Interface::info_update() */
- void info_update_impl(const QRegister *reg);
- void info_update_impl(const Buffer *buffer);
-
- /* implementation of Interface::cmdline_update() */
- void cmdline_update_impl(const Cmdline *cmdline);
-
- /* override of Interface::set_clipboard() */
- void set_clipboard(const gchar *name,
- const gchar *str = NULL, gssize str_len = -1);
- /* override of Interface::get_clipboard() */
- gchar *get_clipboard(const gchar *name, gsize *str_len = NULL);
-
- /* implementation of Interface::popup_add() */
- inline void
- popup_add_impl(PopupEntryType type,
- const gchar *name, bool highlight = false)
- {
- /* FIXME: The enum casting is dangerous */
- if (cmdline_window)
- /* interactive mode */
- popup.add((CursesInfoPopup::PopupEntryType)type,
- name, highlight);
- }
-
- /* implementation of Interface::popup_show() */
- void popup_show_impl(void);
- /* implementation of Interface::popup_is_shown() */
- inline bool
- popup_is_shown_impl(void)
- {
- return popup.is_shown();
- }
-
- /* implementation of Interface::popup_clear() */
- void popup_clear_impl(void);
-
- /* main entry point (implementation) */
- void event_loop_impl(void);
-
-private:
- void init_color_safe(guint color, guint32 rgb);
- void restore_colors(void);
-
- void init_screen(void);
- void init_interactive(void);
- void restore_batch(void);
-
- void init_clipboard(void);
-
- void resize_all_windows(void);
-
- void set_window_title(const gchar *title);
- void draw_info(void);
- void draw_cmdline(void);
-
- friend void event_loop_iter();
-} InterfaceCurrent;
-
-} /* namespace SciTECO */
-
-#endif
diff --git a/src/interface-curses/interface-curses.cpp b/src/interface-curses/interface.c
index a06fe30..821581b 100644
--- a/src/interface-curses/interface-curses.cpp
+++ b/src/interface-curses/interface.c
@@ -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
@@ -27,6 +27,15 @@
#include <locale.h>
#include <errno.h>
+#ifdef HAVE_WINDOWS_H
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#endif
+
+#ifdef EMSCRIPTEN
+#include <emscripten.h>
+#endif
+
#include <glib.h>
#include <glib/gprintf.h>
#include <glib/gstdio.h>
@@ -45,26 +54,17 @@
#include <Scintilla.h>
#include <ScintillaTerm.h>
-#ifdef EMSCRIPTEN
-#include <emscripten.h>
-#endif
-
#include "sciteco.h"
#include "string-utils.h"
#include "cmdline.h"
-#include "qregisters.h"
+#include "qreg.h"
#include "ring.h"
#include "error.h"
-#include "interface.h"
-#include "interface-curses.h"
#include "curses-utils.h"
#include "curses-info-popup.h"
-
-#ifdef HAVE_WINDOWS_H
-/* here it shouldn't cause conflicts with other headers */
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#endif
+#include "view.h"
+#include "memory.h"
+#include "interface.h"
/**
* Whether we have PDCurses-only routines:
@@ -99,18 +99,11 @@
#endif
#ifdef NCURSES_VERSION
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
-/**
- * Whether we're on ncurses/UNIX.
- * Haiku has a UNIX-like terminal and is largely
- * POSIX compliant, so we can handle it like a
- * UNIX ncurses.
- */
+#ifdef G_OS_UNIX
+/** Whether we're on ncurses/UNIX. */
#define NCURSES_UNIX
#elif defined(G_OS_WIN32)
-/**
- * Whether we're on ncurses/win32 console
- */
+/** Whether we're on ncurses/win32 console */
#define NCURSES_WIN32
#endif
#endif
@@ -123,10 +116,6 @@
#define CURSES_TTY
#endif
-namespace SciTECO {
-
-extern "C" {
-
/*
* PDCurses/win32a by default assigns functions to certain
* keys like CTRL+V, CTRL++, CTRL+- and CTRL+=.
@@ -147,9 +136,6 @@ int PDC_set_function_key(const unsigned function, const int new_key);
#define FUNCTION_KEY_CHOOSE_FONT 4
#endif
-static void scintilla_notify(Scintilla *sci, int idFrom,
- void *notify, void *user_data);
-
#if defined(PDCURSES_WIN32) || defined(NCURSES_WIN32)
/**
@@ -161,11 +147,11 @@ static void scintilla_notify(Scintilla *sci, int idFrom,
* separate thread.
*/
static BOOL WINAPI
-console_ctrl_handler(DWORD type)
+teco_console_ctrl_handler(DWORD type)
{
switch (type) {
case CTRL_C_EVENT:
- sigint_occurred = TRUE;
+ teco_sigint_occurred = TRUE;
return TRUE;
}
@@ -174,9 +160,7 @@ console_ctrl_handler(DWORD type)
#endif
-} /* extern "C" */
-
-static gint xterm_version(void) G_GNUC_UNUSED;
+static gint teco_xterm_version(void) G_GNUC_UNUSED;
#define UNNAMED_FILE "(Unnamed)"
@@ -223,12 +207,12 @@ static gint xterm_version(void) G_GNUC_UNUSED;
* for each component).
*/
static inline void
-rgb2curses(guint32 rgb, short &r, short &g, short &b)
+teco_rgb2curses_triple(guint32 rgb, gshort *r, gshort *g, gshort *b)
{
/* NOTE: We could also use 200/51 */
- r = ((rgb & 0x0000FF) >> 0)*1000/0xFF;
- g = ((rgb & 0x00FF00) >> 8)*1000/0xFF;
- b = ((rgb & 0xFF0000) >> 16)*1000/0xFF;
+ *r = ((rgb & 0x0000FF) >> 0)*1000/0xFF;
+ *g = ((rgb & 0x00FF00) >> 8)*1000/0xFF;
+ *b = ((rgb & 0xFF0000) >> 16)*1000/0xFF;
}
/**
@@ -240,8 +224,8 @@ rgb2curses(guint32 rgb, short &r, short &g, short &b)
* It is equivalent to Scinterm's internal `term_color`
* function.
*/
-static short
-rgb2curses(guint32 rgb)
+static gshort
+teco_rgb2curses(guint32 rgb)
{
switch (rgb) {
case 0x000000: return COLOR_BLACK;
@@ -266,20 +250,19 @@ rgb2curses(guint32 rgb)
}
static gint
-xterm_version(void)
+teco_xterm_version(void)
{
static gint xterm_patch = -2;
- const gchar *term = g_getenv("TERM");
- const gchar *xterm_version;
-
/*
* The XTerm patch level (version) is cached.
*/
- if (xterm_patch != -2)
+ if (G_LIKELY(xterm_patch != -2))
return xterm_patch;
xterm_patch = -1;
+ const gchar *term = g_getenv("TERM");
+
if (!term || !g_str_has_prefix(term, "xterm"))
/* no XTerm */
return -1;
@@ -290,7 +273,7 @@ xterm_version(void)
* XTERM_VERSION however should be sufficient to tell
* whether we are running under a real XTerm.
*/
- xterm_version = g_getenv("XTERM_VERSION");
+ const gchar *xterm_version = g_getenv("XTERM_VERSION");
if (!xterm_version)
/* no XTerm */
return -1;
@@ -305,32 +288,121 @@ xterm_version(void)
return xterm_patch;
}
-void
-ViewCurses::initialize_impl(void)
+/*
+ * NOTE: The teco_view_t pointer is reused to directly
+ * point to the Scintilla object.
+ * This saves one heap object per view.
+ */
+
+static void
+teco_view_scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data)
+{
+ teco_interface_process_notify(notify);
+}
+
+teco_view_t *
+teco_view_new(void)
+{
+ return (teco_view_t *)scintilla_new(teco_view_scintilla_notify);
+}
+
+static inline void
+teco_view_noutrefresh(teco_view_t *ctx)
+{
+ scintilla_noutrefresh((Scintilla *)ctx);
+}
+
+static inline WINDOW *
+teco_view_get_window(teco_view_t *ctx)
+{
+ return scintilla_get_window((Scintilla *)ctx);
+}
+
+sptr_t
+teco_view_ssm(teco_view_t *ctx, unsigned int iMessage, uptr_t wParam, sptr_t lParam)
{
- sci = scintilla_new(scintilla_notify);
- setup();
+ return scintilla_send_message((Scintilla *)ctx, iMessage, wParam, lParam);
}
-InterfaceCurses::InterfaceCurses() : stdout_orig(-1), stderr_orig(-1),
- screen(NULL),
- screen_tty(NULL),
- info_window(NULL),
- info_type(INFO_TYPE_BUFFER),
- info_current(NULL),
- msg_window(NULL),
- cmdline_window(NULL), cmdline_pad(NULL),
- cmdline_len(0), cmdline_rubout_len(0)
+void
+teco_view_free(teco_view_t *ctx)
{
- for (guint i = 0; i < G_N_ELEMENTS(color_table); i++)
- color_table[i] = -1;
- for (guint i = 0; i < G_N_ELEMENTS(orig_color_table); i++)
- orig_color_table[i].r = -1;
+ scintilla_delete((Scintilla *)ctx);
}
+static struct {
+ /**
+ * Mapping of the first 16 curses color codes (that may or may not
+ * correspond with the standard terminal color codes) to
+ * Scintilla-compatible RGB values (red is LSB) to initialize after
+ * Curses startup.
+ * Negative values mean no color redefinition (keep the original
+ * palette entry).
+ */
+ gint32 color_table[16];
+
+ /**
+ * Mapping of the first 16 curses color codes to their
+ * original values for restoring them on shutdown.
+ * Unfortunately, this may not be supported on all
+ * curses ports, so this array may be unused.
+ */
+ struct {
+ gshort r, g, b;
+ } orig_color_table[16];
+
+ int stdout_orig, stderr_orig;
+ SCREEN *screen;
+ FILE *screen_tty;
+
+ WINDOW *info_window;
+ enum {
+ TECO_INFO_TYPE_BUFFER = 0,
+ TECO_INFO_TYPE_QREG
+ } info_type;
+ teco_string_t info_current;
+
+ WINDOW *msg_window;
+
+ WINDOW *cmdline_window, *cmdline_pad;
+ gsize cmdline_len, cmdline_rubout_len;
+
+ teco_curses_info_popup_t popup;
+
+ /**
+ * GError "thrown" by teco_interface_event_loop_iter().
+ * Having this in a variable avoids problems with EMScripten.
+ */
+ GError *event_loop_error;
+} teco_interface;
+
+static void teco_interface_init_color_safe(guint color, guint32 rgb);
+static void teco_interface_restore_colors(void);
+
+static void teco_interface_init_screen(void);
+static gboolean teco_interface_init_interactive(GError **error);
+static void teco_interface_restore_batch(void);
+
+static void teco_interface_init_clipboard(void);
+
+static void teco_interface_resize_all_windows(void);
+
+static void teco_interface_set_window_title(const gchar *title);
+static void teco_interface_draw_info(void);
+static void teco_interface_draw_cmdline(void);
+
void
-InterfaceCurses::init(void)
+teco_interface_init(void)
{
+ for (guint i = 0; i < G_N_ELEMENTS(teco_interface.color_table); i++)
+ teco_interface.color_table[i] = -1;
+ for (guint i = 0; i < G_N_ELEMENTS(teco_interface.orig_color_table); i++)
+ teco_interface.orig_color_table[i].r = -1;
+
+ teco_interface.stdout_orig = teco_interface.stderr_orig = -1;
+
+ teco_curses_info_popup_init(&teco_interface.popup);
+
/*
* We must register this handler to handle
* asynchronous interruptions via CTRL+C
@@ -338,40 +410,45 @@ InterfaceCurses::init(void)
* have won't do.
*/
#if defined(PDCURSES_WIN32) || defined(NCURSES_WIN32)
- SetConsoleCtrlHandler(console_ctrl_handler, TRUE);
+ SetConsoleCtrlHandler(teco_console_ctrl_handler, TRUE);
#endif
/*
* Make sure we have a string for the info line
- * even if info_update() is never called.
+ * even if teco_interface_info_update() is never called.
*/
- info_current = g_strdup(PACKAGE_NAME);
+ teco_string_init(&teco_interface.info_current, PACKAGE_NAME, strlen(PACKAGE_NAME));
/*
* On all platforms except Curses/XTerm, it's
* safe to initialize the clipboards now.
*/
#ifndef CURSES_TTY
- init_clipboard();
+ teco_interface_init_clipboard();
#endif
}
-void
-InterfaceCurses::init_color_safe(guint color, guint32 rgb)
+GOptionGroup *
+teco_interface_get_options(void)
{
- short r, g, b;
+ return NULL;
+}
+static void
+teco_interface_init_color_safe(guint color, guint32 rgb)
+{
#ifdef PDCURSES_WIN32
- if (orig_color_table[color].r < 0) {
+ if (teco_interface.orig_color_table[color].r < 0) {
color_content((short)color,
- &orig_color_table[color].r,
- &orig_color_table[color].g,
- &orig_color_table[color].b);
+ &teco_interface.orig_color_table[color].r,
+ &teco_interface.orig_color_table[color].g,
+ &teco_interface.orig_color_table[color].b);
}
#endif
- rgb2curses(rgb, r, g, b);
- ::init_color((short)color, r, g, b);
+ gshort r, g, b;
+ teco_rgb2curses_triple(rgb, &r, &g, &b);
+ init_color((short)color, r, g, b);
}
#ifdef PDCURSES_WIN32
@@ -381,29 +458,29 @@ InterfaceCurses::init_color_safe(guint color, guint32 rgb)
* the real console color palette - or at least the default
* palette when the console started.
*/
-void
-InterfaceCurses::restore_colors(void)
+static void
+teco_interface_restore_colors(void)
{
if (!can_change_color())
return;
- for (guint i = 0; i < G_N_ELEMENTS(orig_color_table); i++) {
- if (orig_color_table[i].r < 0)
+ for (guint i = 0; i < G_N_ELEMENTS(teco_interface.orig_color_table); i++) {
+ if (teco_interface.orig_color_table[i].r < 0)
continue;
- ::init_color((short)i,
- orig_color_table[i].r,
- orig_color_table[i].g,
- orig_color_table[i].b);
+ init_color((short)i,
+ teco_interface.orig_color_table[i].r,
+ teco_interface.orig_color_table[i].g,
+ teco_interface.orig_color_table[i].b);
}
}
#elif defined(CURSES_TTY)
/*
- * FIXME: On UNIX/ncurses init_color_safe() __may__ change the
- * terminal's palette permanently and there does not appear to be
- * any portable way of restoring the original one.
+ * FIXME: On UNIX/ncurses teco_interface_init_color_safe() __may__
+ * change the terminal's palette permanently and there does not
+ * appear to be any portable way of restoring the original one.
* Curses has color_content(), but there is actually no terminal
* that allows querying the current palette and so color_content()
* will return bogus "default" values and only for the first 8 colors.
@@ -424,23 +501,23 @@ InterfaceCurses::restore_colors(void)
* already properly restored on endwin().
* Welcome in Curses hell.
*/
-void
-InterfaceCurses::restore_colors(void)
+static void
+teco_interface_restore_colors(void)
{
- if (xterm_version() < 0)
+ if (teco_xterm_version() < 0)
return;
/*
* Looks like a real XTerm
*/
- fputs(CTL_KEY_ESC_STR "]104\a", screen_tty);
- fflush(screen_tty);
+ fputs("\e]104\a", teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
}
#else /* !PDCURSES_WIN32 && !CURSES_TTY */
-void
-InterfaceCurses::restore_colors(void)
+static void
+teco_interface_restore_colors(void)
{
/*
* No way to restore the palette, or it's
@@ -451,9 +528,9 @@ InterfaceCurses::restore_colors(void)
#endif
void
-InterfaceCurses::init_color(guint color, guint32 rgb)
+teco_interface_init_color(guint color, guint32 rgb)
{
- if (color >= G_N_ELEMENTS(color_table))
+ if (color >= G_N_ELEMENTS(teco_interface.color_table))
return;
#if defined(__PDCURSES__) && !defined(PDC_RGB)
@@ -469,12 +546,12 @@ InterfaceCurses::init_color(guint color, guint32 rgb)
((color & 0x1) << 2) | ((color & 0x4) >> 2);
#endif
- if (cmdline_window) {
+ if (teco_interface.cmdline_window) {
/* interactive mode */
if (!can_change_color())
return;
- init_color_safe(color, rgb);
+ teco_interface_init_color_safe(color, rgb);
} else {
/*
* batch mode: store colors,
@@ -482,21 +559,21 @@ InterfaceCurses::init_color(guint color, guint32 rgb)
* which is called by Scinterm when interactive
* mode is initialized
*/
- color_table[color] = (gint32)rgb;
+ teco_interface.color_table[color] = (gint32)rgb;
}
}
#ifdef CURSES_TTY
-void
-InterfaceCurses::init_screen(void)
+static void
+teco_interface_init_screen(void)
{
- screen_tty = g_fopen("/dev/tty", "r+");
+ teco_interface.screen_tty = g_fopen("/dev/tty", "r+");
/* should never fail */
- g_assert(screen_tty != NULL);
+ g_assert(teco_interface.screen_tty != NULL);
- screen = newterm(NULL, screen_tty, screen_tty);
- if (!screen) {
+ teco_interface.screen = newterm(NULL, teco_interface.screen_tty, teco_interface.screen_tty);
+ if (!teco_interface.screen) {
g_fprintf(stderr, "Error initializing interactive mode. "
"$TERM may be incorrect.\n");
exit(EXIT_FAILURE);
@@ -509,25 +586,23 @@ InterfaceCurses::init_screen(void)
* interrupt terminal interaction.
*/
if (isatty(1)) {
- FILE *stdout_new;
- stdout_orig = dup(1);
- g_assert(stdout_orig >= 0);
- stdout_new = g_freopen("/dev/null", "a+", stdout);
+ teco_interface.stdout_orig = dup(1);
+ g_assert(teco_interface.stdout_orig >= 0);
+ FILE *stdout_new = g_freopen("/dev/null", "a+", stdout);
g_assert(stdout_new != NULL);
}
if (isatty(2)) {
- FILE *stderr_new;
- stderr_orig = dup(2);
- g_assert(stderr_orig >= 0);
- stderr_new = g_freopen("/dev/null", "a+", stderr);
+ teco_interface.stderr_orig = dup(2);
+ g_assert(teco_interface.stderr_orig >= 0);
+ FILE *stderr_new = g_freopen("/dev/null", "a+", stderr);
g_assert(stderr_new != NULL);
}
}
#elif defined(XCURSES)
-void
-InterfaceCurses::init_screen(void)
+static void
+teco_interface_init_screen(void)
{
const char *argv[] = {PACKAGE_NAME, NULL};
@@ -551,16 +626,16 @@ InterfaceCurses::init_screen(void)
#else
-void
-InterfaceCurses::init_screen(void)
+static void
+teco_interface_init_screen(void)
{
initscr();
}
#endif
-void
-InterfaceCurses::init_interactive(void)
+static gboolean
+teco_interface_init_interactive(GError **error)
{
/*
* Curses accesses many environment variables
@@ -569,7 +644,8 @@ InterfaceCurses::init_interactive(void)
* environment before initscr()/newterm().
* This is safe to do here since there are no threads.
*/
- QRegisters::globals.update_environ();
+ if (!teco_qreg_table_set_environ(&teco_qreg_table_globals, error))
+ return FALSE;
/*
* On UNIX terminals, the escape key is usually
@@ -625,41 +701,41 @@ InterfaceCurses::init_interactive(void)
/* for displaying UTF-8 characters properly */
setlocale(LC_CTYPE, "");
- init_screen();
+ teco_interface_init_screen();
cbreak();
noecho();
/* Scintilla draws its own cursor */
curs_set(0);
- info_window = newwin(1, 0, 0, 0);
+ teco_interface.info_window = newwin(1, 0, 0, 0);
- msg_window = newwin(1, 0, LINES - 2, 0);
+ teco_interface.msg_window = newwin(1, 0, LINES - 2, 0);
- cmdline_window = newwin(0, 0, LINES - 1, 0);
- keypad(cmdline_window, TRUE);
+ teco_interface.cmdline_window = newwin(0, 0, LINES - 1, 0);
+ keypad(teco_interface.cmdline_window, TRUE);
#ifdef EMCURSES
- nodelay(cmdline_window, TRUE);
+ nodelay(teco_interface.cmdline_window, TRUE);
#endif
/*
* Will also initialize Scinterm, Curses color pairs
* and resizes the current view.
*/
- if (current_view)
- show_view(current_view);
+ if (teco_interface_current_view)
+ teco_interface_show_view(teco_interface_current_view);
/*
* Only now it's safe to redefine the 16 default colors.
*/
if (can_change_color()) {
- for (guint i = 0; i < G_N_ELEMENTS(color_table); i++) {
+ for (guint i = 0; i < G_N_ELEMENTS(teco_interface.color_table); i++) {
/*
* init_color() may still fail if COLORS < 16
*/
- if (color_table[i] >= 0)
- init_color_safe(i, (guint32)color_table[i]);
+ if (teco_interface.color_table[i] >= 0)
+ teco_interface_init_color_safe(i, (guint32)teco_interface.color_table[i]);
}
}
@@ -670,12 +746,14 @@ InterfaceCurses::init_interactive(void)
* with stdout.
*/
#ifdef CURSES_TTY
- init_clipboard();
+ teco_interface_init_clipboard();
#endif
+
+ return TRUE;
}
-void
-InterfaceCurses::restore_batch(void)
+static void
+teco_interface_restore_batch(void)
{
/*
* Set window title to a reasonable default,
@@ -685,7 +763,7 @@ InterfaceCurses::restore_batch(void)
* is necessary.
*/
#if defined(CURSES_TTY) && defined(HAVE_TIGETSTR)
- set_window_title(g_getenv("TERM") ? : "");
+ teco_interface_set_window_title(g_getenv("TERM") ? : "");
#endif
/*
@@ -693,61 +771,59 @@ InterfaceCurses::restore_batch(void)
* (i.e. return to batch mode)
*/
endwin();
- restore_colors();
+ teco_interface_restore_colors();
/*
* Restore stdout and stderr, so output goes to
* the terminal again in case we "muted" them.
*/
#ifdef CURSES_TTY
- if (stdout_orig >= 0) {
- int fd = dup2(stdout_orig, 1);
+ if (teco_interface.stdout_orig >= 0) {
+ int fd = dup2(teco_interface.stdout_orig, 1);
g_assert(fd == 1);
}
- if (stderr_orig >= 0) {
- int fd = dup2(stderr_orig, 2);
+ if (teco_interface.stderr_orig >= 0) {
+ int fd = dup2(teco_interface.stderr_orig, 2);
g_assert(fd == 2);
}
#endif
/*
- * See vmsg_impl(): It looks at msg_win to determine
+ * See teco_interface_vmsg(): It looks at msg_window to determine
* whether we're in batch mode.
*/
- if (msg_window) {
- delwin(msg_window);
- msg_window = NULL;
+ if (teco_interface.msg_window) {
+ delwin(teco_interface.msg_window);
+ teco_interface.msg_window = NULL;
}
}
-void
-InterfaceCurses::resize_all_windows(void)
+static void
+teco_interface_resize_all_windows(void)
{
int lines, cols; /* screen dimensions */
getmaxyx(stdscr, lines, cols);
- wresize(info_window, 1, cols);
- wresize(current_view->get_window(),
+ wresize(teco_interface.info_window, 1, cols);
+ wresize(teco_view_get_window(teco_interface_current_view),
lines - 3, cols);
- wresize(msg_window, 1, cols);
- mvwin(msg_window, lines - 2, 0);
- wresize(cmdline_window, 1, cols);
- mvwin(cmdline_window, lines - 1, 0);
-
- draw_info();
- msg_clear(); /* FIXME: use saved message */
- popup_clear();
- draw_cmdline();
+ wresize(teco_interface.msg_window, 1, cols);
+ mvwin(teco_interface.msg_window, lines - 2, 0);
+ wresize(teco_interface.cmdline_window, 1, cols);
+ mvwin(teco_interface.cmdline_window, lines - 1, 0);
+
+ teco_interface_draw_info();
+ teco_interface_msg_clear(); /* FIXME: use saved message */
+ teco_interface_popup_clear();
+ teco_interface_draw_cmdline();
}
void
-InterfaceCurses::vmsg_impl(MessageType type, const gchar *fmt, va_list ap)
+teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
{
- short fg, bg;
-
- if (!msg_window) { /* batch mode */
- stdio_vmsg(type, fmt, ap);
+ if (!teco_interface.msg_window) { /* batch mode */
+ teco_interface_stdio_vmsg(type, fmt, ap);
return;
}
@@ -759,67 +835,65 @@ InterfaceCurses::vmsg_impl(MessageType type, const gchar *fmt, va_list ap)
defined(CURSES_TTY) || defined(NCURSES_WIN32)
va_list aq;
va_copy(aq, ap);
- stdio_vmsg(type, fmt, aq);
+ teco_interface_stdio_vmsg(type, fmt, aq);
va_end(aq);
#endif
- fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
+ short fg, bg;
+
+ fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
switch (type) {
default:
- case MSG_USER:
- bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
+ case TECO_MSG_USER:
+ bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
break;
- case MSG_INFO:
+ case TECO_MSG_INFO:
bg = COLOR_GREEN;
break;
- case MSG_WARNING:
+ case TECO_MSG_WARNING:
bg = COLOR_YELLOW;
break;
- case MSG_ERROR:
+ case TECO_MSG_ERROR:
bg = COLOR_RED;
beep();
break;
}
- wmove(msg_window, 0, 0);
- wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
- vw_printw(msg_window, fmt, ap);
- wclrtoeol(msg_window);
+ wmove(teco_interface.msg_window, 0, 0);
+ wbkgdset(teco_interface.msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+ vw_printw(teco_interface.msg_window, fmt, ap);
+ wclrtoeol(teco_interface.msg_window);
}
void
-InterfaceCurses::msg_clear(void)
+teco_interface_msg_clear(void)
{
- short fg, bg;
-
- if (!msg_window) /* batch mode */
+ if (!teco_interface.msg_window) /* batch mode */
return;
- fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
- bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
- wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
- werase(msg_window);
+ wbkgdset(teco_interface.msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+ werase(teco_interface.msg_window);
}
void
-InterfaceCurses::show_view_impl(ViewCurses *view)
+teco_interface_show_view(teco_view_t *view)
{
- int lines, cols; /* screen dimensions */
- WINDOW *current_view_win;
-
- current_view = view;
+ teco_interface_current_view = view;
- if (!cmdline_window) /* batch mode */
+ if (!teco_interface.cmdline_window) /* batch mode */
return;
- current_view_win = current_view->get_window();
+ WINDOW *current_view_win = teco_view_get_window(teco_interface_current_view);
/*
* screen size might have changed since
* this view's WINDOW was last active
*/
+ int lines, cols; /* screen dimensions */
getmaxyx(stdscr, lines, cols);
wresize(current_view_win, lines - 3, cols);
/* Set up window position: never changes */
@@ -828,8 +902,8 @@ InterfaceCurses::show_view_impl(ViewCurses *view)
#if PDCURSES
-void
-InterfaceCurses::set_window_title(const gchar *title)
+static void
+teco_interface_set_window_title(const gchar *title)
{
static gchar *last_title = NULL;
@@ -851,8 +925,8 @@ InterfaceCurses::set_window_title(const gchar *title)
#elif defined(CURSES_TTY) && defined(HAVE_TIGETSTR)
-void
-InterfaceCurses::set_window_title(const gchar *title)
+static void
+teco_interface_set_window_title(const gchar *title)
{
if (!has_status_line || !to_status_line || !from_status_line)
return;
@@ -882,30 +956,26 @@ InterfaceCurses::set_window_title(const gchar *title)
* we do not let curses write to stdout.
* NOTE: This leaves the title set after we quit.
*/
- fputs(to_status_line, screen_tty);
- fputs(title, screen_tty);
- fputs(from_status_line, screen_tty);
- fflush(screen_tty);
+ fputs(to_status_line, teco_interface.screen_tty);
+ fputs(title, teco_interface.screen_tty);
+ fputs(from_status_line, teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
}
#else
-void
-InterfaceCurses::set_window_title(const gchar *title)
+static void
+teco_interface_set_window_title(const gchar *title)
{
/* no way to set window title */
}
#endif
-void
-InterfaceCurses::draw_info(void)
+static void
+teco_interface_draw_info(void)
{
- short fg, bg;
- const gchar *info_type_str;
- gchar *info_current_canon, *title;
-
- if (!info_window) /* batch mode */
+ if (!teco_interface.info_window) /* batch mode */
return;
/*
@@ -913,69 +983,75 @@ InterfaceCurses::draw_info(void)
* the current buffer's STYLE_DEFAULT.
* The same style is used for MSG_USER messages.
*/
- fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
- bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
+
+ wmove(teco_interface.info_window, 0, 0);
+ wbkgdset(teco_interface.info_window, ' ' | SCI_COLOR_ATTR(fg, bg));
- wmove(info_window, 0, 0);
- wbkgdset(info_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+ const gchar *info_type_str;
- switch (info_type) {
- case INFO_TYPE_QREGISTER:
+ switch (teco_interface.info_type) {
+ case TECO_INFO_TYPE_QREG:
info_type_str = PACKAGE_NAME " - <QRegister> ";
- waddstr(info_window, info_type_str);
+ waddstr(teco_interface.info_window, info_type_str);
/* same formatting as in command lines */
- Curses::format_str(info_window, info_current);
+ teco_curses_format_str(teco_interface.info_window,
+ teco_interface.info_current.data,
+ teco_interface.info_current.len, -1);
break;
- case INFO_TYPE_BUFFER:
+ case TECO_INFO_TYPE_BUFFER:
info_type_str = PACKAGE_NAME " - <Buffer> ";
- waddstr(info_window, info_type_str);
- Curses::format_filename(info_window, info_current);
+ waddstr(teco_interface.info_window, info_type_str);
+ g_assert(!teco_string_contains(&teco_interface.info_current, '\0'));
+ teco_curses_format_filename(teco_interface.info_window,
+ teco_interface.info_current.data, -1);
break;
default:
g_assert_not_reached();
}
- wclrtoeol(info_window);
+ wclrtoeol(teco_interface.info_window);
/*
* Make sure the title will consist only of printable
* characters
*/
- info_current_canon = String::canonicalize_ctl(info_current);
- title = g_strconcat(info_type_str, info_current_canon, NIL);
- g_free(info_current_canon);
- set_window_title(title);
- g_free(title);
+ g_autofree gchar *info_current_printable;
+ info_current_printable = teco_string_echo(teco_interface.info_current.data,
+ teco_interface.info_current.len);
+ g_autofree gchar *title = g_strconcat(info_type_str, info_current_printable, NULL);
+ teco_interface_set_window_title(title);
}
void
-InterfaceCurses::info_update_impl(const QRegister *reg)
+teco_interface_info_update_qreg(const teco_qreg_t *reg)
{
- g_free(info_current);
- /* NOTE: will contain control characters */
- info_type = INFO_TYPE_QREGISTER;
- info_current = g_strdup(reg->name);
- /* NOTE: drawn in event_loop_iter() */
+ teco_string_clear(&teco_interface.info_current);
+ teco_string_init(&teco_interface.info_current,
+ reg->head.name.data, reg->head.name.len);
+ teco_interface.info_type = TECO_INFO_TYPE_QREG;
+ /* NOTE: drawn in teco_interface_event_loop_iter() */
}
void
-InterfaceCurses::info_update_impl(const Buffer *buffer)
+teco_interface_info_update_buffer(const teco_buffer_t *buffer)
{
- g_free(info_current);
- info_type = INFO_TYPE_BUFFER;
- info_current = g_strconcat(buffer->filename ? : UNNAMED_FILE,
- buffer->dirty ? "*" : " ", NIL);
- /* NOTE: drawn in event_loop_iter() */
+ const gchar *filename = buffer->filename ? : UNNAMED_FILE;
+
+ teco_string_clear(&teco_interface.info_current);
+ teco_string_init(&teco_interface.info_current, filename, strlen(filename));
+ teco_string_append_c(&teco_interface.info_current,
+ buffer->dirty ? '*' : ' ');
+ teco_interface.info_type = TECO_INFO_TYPE_BUFFER;
+ /* NOTE: drawn in teco_interface_event_loop_iter() */
}
void
-InterfaceCurses::cmdline_update_impl(const Cmdline *cmdline)
+teco_interface_cmdline_update(const teco_cmdline_t *cmdline)
{
- short fg, bg;
- int max_cols = 1;
-
/*
* Replace entire pre-formatted command-line.
* We don't know if it is similar to the last one,
@@ -983,18 +1059,22 @@ InterfaceCurses::cmdline_update_impl(const Cmdline *cmdline)
* We approximate the size of the new formatted command-line,
* wasting a few bytes for control characters.
*/
- if (cmdline_pad)
- delwin(cmdline_pad);
- for (guint i = 0; i < cmdline->len+cmdline->rubout_len; i++)
- max_cols += IS_CTL((*cmdline)[i]) ? 3 : 1;
- cmdline_pad = newpad(1, max_cols);
+ if (teco_interface.cmdline_pad)
+ delwin(teco_interface.cmdline_pad);
- fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
- bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
- wcolor_set(cmdline_pad, SCI_COLOR_PAIR(fg, bg), NULL);
+ int max_cols = 1;
+ for (guint i = 0; i < cmdline->str.len; i++)
+ max_cols += TECO_IS_CTL(cmdline->str.data[i]) ? 3 : 1;
+ teco_interface.cmdline_pad = newpad(1, max_cols);
+
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
+ wcolor_set(teco_interface.cmdline_pad, SCI_COLOR_PAIR(fg, bg), NULL);
/* format effective command line */
- cmdline_len = Curses::format_str(cmdline_pad, cmdline->str, cmdline->len);
+ teco_interface.cmdline_len =
+ teco_curses_format_str(teco_interface.cmdline_pad,
+ cmdline->str.data, cmdline->effective_len, -1);
/*
* A_BOLD should result in either a bold font or a brighter
@@ -1006,61 +1086,61 @@ InterfaceCurses::cmdline_update_impl(const Cmdline *cmdline)
* for rubbed out parts of the command line which will
* be user-configurable.
*/
- wattron(cmdline_pad, A_UNDERLINE | A_BOLD);
+ wattron(teco_interface.cmdline_pad, A_UNDERLINE | A_BOLD);
/*
* Format rubbed-out command line.
* NOTE: This formatting will never be truncated since we're
* writing into the pad which is large enough.
*/
- cmdline_rubout_len = Curses::format_str(cmdline_pad, cmdline->str + cmdline->len,
- cmdline->rubout_len);
+ teco_interface.cmdline_rubout_len =
+ teco_curses_format_str(teco_interface.cmdline_pad, cmdline->str.data + cmdline->effective_len,
+ cmdline->str.len - cmdline->effective_len, -1);
/* highlight cursor after effective command line */
- if (cmdline_rubout_len) {
- attr_t attr;
- short pair;
+ if (teco_interface.cmdline_rubout_len) {
+ attr_t attr = 0;
+ short pair = 0;
- wmove(cmdline_pad, 0, cmdline_len);
- wattr_get(cmdline_pad, &attr, &pair, NULL);
- wchgat(cmdline_pad, 1,
+ wmove(teco_interface.cmdline_pad, 0, teco_interface.cmdline_len);
+ wattr_get(teco_interface.cmdline_pad, &attr, &pair, NULL);
+ wchgat(teco_interface.cmdline_pad, 1,
(attr & A_UNDERLINE) | A_REVERSE, pair, NULL);
} else {
- cmdline_len++;
- wattroff(cmdline_pad, A_UNDERLINE | A_BOLD);
- waddch(cmdline_pad, ' ' | A_REVERSE);
+ teco_interface.cmdline_len++;
+ wattroff(teco_interface.cmdline_pad, A_UNDERLINE | A_BOLD);
+ waddch(teco_interface.cmdline_pad, ' ' | A_REVERSE);
}
- draw_cmdline();
+ teco_interface_draw_cmdline();
}
-void
-InterfaceCurses::draw_cmdline(void)
+static void
+teco_interface_draw_cmdline(void)
{
- short fg, bg;
/* total width available for command line */
- guint total_width = getmaxx(cmdline_window) - 1;
- /* beginning of command line to show */
- guint disp_offset;
- /* length of command line to show */
- guint disp_len;
+ guint total_width = getmaxx(teco_interface.cmdline_window) - 1;
- disp_offset = cmdline_len -
- MIN(cmdline_len,
- total_width/2 + cmdline_len % MAX(total_width/2, 1));
+ /* beginning of command line to show */
+ guint disp_offset = teco_interface.cmdline_len -
+ MIN(teco_interface.cmdline_len,
+ total_width/2 + teco_interface.cmdline_len % MAX(total_width/2, 1));
/*
+ * length of command line to show
+ *
* NOTE: we do not use getmaxx(cmdline_pad) here since it may be
* larger than the text the pad contains.
*/
- disp_len = MIN(total_width, cmdline_len+cmdline_rubout_len - disp_offset);
+ guint disp_len = MIN(total_width, teco_interface.cmdline_len +
+ teco_interface.cmdline_rubout_len - disp_offset);
- fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
- bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
- wbkgdset(cmdline_window, ' ' | SCI_COLOR_ATTR(fg, bg));
- werase(cmdline_window);
- mvwaddch(cmdline_window, 0, 0, '*' | A_BOLD);
- copywin(cmdline_pad, cmdline_window,
+ wbkgdset(teco_interface.cmdline_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+ werase(teco_interface.cmdline_window);
+ mvwaddch(teco_interface.cmdline_window, 0, 0, '*' | A_BOLD);
+ copywin(teco_interface.cmdline_pad, teco_interface.cmdline_window,
0, disp_offset, 0, 1, 0, disp_len, FALSE);
}
@@ -1073,12 +1153,11 @@ InterfaceCurses::draw_cmdline(void)
* it corresponds to the X11 PRIMARY, SECONDARY or
* CLIPBOARD selections.
*/
-void
-InterfaceCurses::init_clipboard(void)
+static void
+teco_interface_init_clipboard(void)
{
char *contents;
long length;
- int rc;
/*
* Even on PDCurses, while the clipboard functions are
@@ -1089,71 +1168,68 @@ InterfaceCurses::init_clipboard(void)
* This could be done at compile time, but this way is more
* generic (albeit inefficient).
*/
- rc = PDC_getclipboard(&contents, &length);
+ int rc = PDC_getclipboard(&contents, &length);
if (rc == PDC_CLIP_ACCESS_ERROR)
return;
if (rc == PDC_CLIP_SUCCESS)
PDC_freeclipboard(contents);
- QRegisters::globals.insert(new QRegisterClipboard());
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
}
-void
-InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len)
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, GError **error)
{
- int rc;
-
- if (str) {
- if (str_len < 0)
- str_len = strlen(str);
-
- rc = PDC_setclipboard(str, str_len);
- } else {
- rc = PDC_clearclipboard();
+ int rc = str ? PDC_setclipboard(str, str_len) : PDC_clearclipboard();
+ if (rc != PDC_CLIP_SUCCESS) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Error %d copying to clipboard", rc);
+ return FALSE;
}
- if (rc != PDC_CLIP_SUCCESS)
- throw Error("Error %d copying to clipboard", rc);
+ return TRUE;
}
-gchar *
-InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len)
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
{
char *contents;
long length = 0;
- int rc;
- gchar *str;
/*
* NOTE: It is undefined whether we can pass in NULL for length.
*/
- rc = PDC_getclipboard(&contents, &length);
- if (str_len)
- *str_len = length;
+ int rc = PDC_getclipboard(&contents, &length);
+ *len = length;
if (rc == PDC_CLIP_EMPTY)
- return NULL;
- if (rc != PDC_CLIP_SUCCESS)
- throw Error("Error %d retrieving clipboard", rc);
+ return TRUE;
+ if (rc != PDC_CLIP_SUCCESS) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Error %d retrieving clipboard", rc);
+ return FALSE;
+ }
/*
* PDCurses defines its own free function and there is no
* way to find out which allocator was used.
* We must therefore copy the memory to be on the safe side.
- * At least we can null-terminate the return string in the
- * process (PDCurses does not guarantee that either).
+ * At least, the result is guaranteed to be null-terminated
+ * and thus teco_string_t-compatible
+ * (PDCurses does not guarantee that either).
*/
- str = (gchar *)g_malloc(length + 1);
- memcpy(str, contents, length);
- str[length] = '\0';
+ if (str) {
+ *str = memcpy(g_malloc(length + 1), contents, length);
+ (*str)[length] = '\0';
+ }
PDC_freeclipboard(contents);
- return str;
+ return TRUE;
}
#elif defined(CURSES_TTY)
-void
-InterfaceCurses::init_clipboard(void)
+static void
+teco_interface_init_clipboard(void)
{
/*
* At least on XTerm, there are escape sequences
@@ -1167,13 +1243,13 @@ InterfaceCurses::init_clipboard(void)
* not register the clipboard registers if they aren't.
* Therefore, a special XTerm clipboard ED flag an be set by the user.
*/
- if (!(Flags::ed & Flags::ED_XTERM_CLIPBOARD) || xterm_version() < 203)
+ if (!(teco_ed & TECO_ED_XTERM_CLIPBOARD) || teco_xterm_version() < 203)
return;
- QRegisters::globals.insert(new QRegisterClipboard());
- QRegisters::globals.insert(new QRegisterClipboard("P"));
- QRegisters::globals.insert(new QRegisterClipboard("S"));
- QRegisters::globals.insert(new QRegisterClipboard("C"));
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("P"));
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("S"));
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("C"));
}
static inline gchar
@@ -1189,27 +1265,21 @@ get_selection_by_name(const gchar *name)
return g_ascii_tolower(*name) ? : 'c';
}
-void
-InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len)
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len,
+ GError **error)
{
+ fputs("\e]52;", teco_interface.screen_tty);
+ fputc(get_selection_by_name(name), teco_interface.screen_tty);
+ fputc(';', teco_interface.screen_tty);
+
/*
* Enough space for 1024 Base64-encoded bytes.
*/
gchar buffer[(1024 / 3) * 4 + 4];
gsize out_len;
-
/* g_base64_encode_step() state: */
- gint state = 0;
- gint save = 0;
-
- fputs(CTL_KEY_ESC_STR "]52;", screen_tty);
- fputc(get_selection_by_name(name), screen_tty);
- fputc(';', screen_tty);
-
- if (!str)
- str_len = 0;
- else if (str_len < 0)
- str_len = strlen(str);
+ gint state = 0, save = 0;
while (str_len > 0) {
gsize step_len = MIN(1024, str_len);
@@ -1221,41 +1291,32 @@ InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_l
out_len = g_base64_encode_step((const guchar *)str,
step_len, FALSE,
buffer, &state, &save);
- fwrite(buffer, 1, out_len, screen_tty);
+ fwrite(buffer, 1, out_len, teco_interface.screen_tty);
str_len -= step_len;
str += step_len;
}
out_len = g_base64_encode_close(FALSE, buffer, &state, &save);
- fwrite(buffer, 1, out_len, screen_tty);
+ fwrite(buffer, 1, out_len, teco_interface.screen_tty);
- fputc('\a', screen_tty);
- fflush(screen_tty);
+ fputc('\a', teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
+
+ return TRUE;
}
-gchar *
-InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len)
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
{
/*
- * Space for storing one group of decoded Base64 characters
- * and the OSC-52 response.
- */
- gchar buffer[MAX(3, 7)];
- GString *str_base64;
-
- /* g_base64_decode_step() state: */
- gint state = 0;
- guint save = 0;
-
- /*
* Query the clipboard -- XTerm will reply with the
* OSC-52 command that would set the current selection.
*/
- fputs(CTL_KEY_ESC_STR "]52;", screen_tty);
- fputc(get_selection_by_name(name), screen_tty);
- fputs(";?\a", screen_tty);
- fflush(screen_tty);
+ fputs("\e]52;", teco_interface.screen_tty);
+ fputc(get_selection_by_name(name), teco_interface.screen_tty);
+ fputs(";?\a", teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
/*
* It is very well possible that the XTerm clipboard
@@ -1277,22 +1338,32 @@ InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len)
if (getch() == ERR) {
/* timeout */
cbreak();
- throw Error("Timed out reading XTerm clipboard");
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Timed out reading XTerm clipboard");
+ return FALSE;
}
}
- str_base64 = g_string_new("");
+ GString *str_base64 = g_string_new("");
+ /* g_base64_decode_step() state: */
+ gint state = 0;
+ guint save = 0;
for (;;) {
- gchar c;
- gsize out_len;
+ /*
+ * Space for storing one group of decoded Base64 characters
+ * and the OSC-52 response.
+ */
+ gchar buffer[MAX(3, 7)];
- c = (gchar)getch();
+ gchar c = (gchar)getch();
if (c == ERR) {
/* timeout */
cbreak();
g_string_free(str_base64, TRUE);
- throw Error("Timed out reading XTerm clipboard");
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Timed out reading XTerm clipboard");
+ return FALSE;
}
if (c == '\a')
break;
@@ -1301,30 +1372,29 @@ InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len)
* This could be simplified using sscanf() and
* g_base64_decode(), but we avoid one allocation
* to get the entire Base64 string.
- * (Also to allow for timeouts, we must should
+ * (Also to allow for timeouts, we should
* read character-wise using getch() anyway.)
*/
- out_len = g_base64_decode_step(&c, sizeof(c),
- (guchar *)buffer,
- &state, &save);
+ gsize out_len = g_base64_decode_step(&c, sizeof(c),
+ (guchar *)buffer,
+ &state, &save);
g_string_append_len(str_base64, buffer, out_len);
}
cbreak();
- if (str_len)
- *str_len = str_base64->len;
+ if (str)
+ *str = str_base64->str;
+ *len = str_base64->len;
- /*
- * If the clipboard answer is empty, return NULL.
- */
- return g_string_free(str_base64, str_base64->len == 0);
+ g_string_free(str_base64, !str);
+ return TRUE;
}
#else
-void
-InterfaceCurses::init_clipboard(void)
+static void
+teco_interface_init_clipboard(void)
{
/*
* No native clipboard support, so no clipboard Q-Regs are
@@ -1332,37 +1402,55 @@ InterfaceCurses::init_clipboard(void)
*/
}
-void
-InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len)
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len,
+ GError **error)
{
- throw Error("Setting clipboard unsupported");
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Setting clipboard unsupported");
+ return FALSE;
}
-gchar *
-InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len)
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
{
- throw Error("Getting clipboard unsupported");
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Getting clipboard unsupported");
+ return FALSE;
}
#endif /* !__PDCURSES__ && !CURSES_TTY */
void
-InterfaceCurses::popup_show_impl(void)
+teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len,
+ gboolean highlight)
{
- short fg, bg;
+ if (teco_interface.cmdline_window)
+ /* interactive mode */
+ teco_curses_info_popup_add(&teco_interface.popup, type, name, name_len, highlight);
+}
- if (!cmdline_window)
+void
+teco_interface_popup_show(void)
+{
+ if (!teco_interface.cmdline_window)
/* batch mode */
return;
- fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_CALLTIP));
- bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_CALLTIP));
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_CALLTIP, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_CALLTIP, 0));
- popup.show(SCI_COLOR_ATTR(fg, bg));
+ teco_curses_info_popup_show(&teco_interface.popup, SCI_COLOR_ATTR(fg, bg));
+}
+
+gboolean
+teco_interface_popup_is_shown(void)
+{
+ return teco_curses_info_popup_is_shown(&teco_interface.popup);
}
void
-InterfaceCurses::popup_clear_impl(void)
+teco_interface_popup_clear(void)
{
#ifdef __PDCURSES__
/*
@@ -1374,31 +1462,35 @@ InterfaceCurses::popup_clear_impl(void)
* Actually we would expect this to be necessary on any curses,
* but ncurses doesn't require this.
*/
- if (popup.is_shown()) {
- touchwin(info_window);
- touchwin(msg_window);
+ if (teco_curses_info_popup_is_shown(&teco_interface.popup)) {
+ touchwin(teco_interface.info_window);
+ touchwin(teco_interface.msg_window);
}
#endif
- popup.clear();
+ teco_curses_info_popup_clear(&teco_interface.popup);
+ teco_curses_info_popup_init(&teco_interface.popup);
+}
+
+gboolean
+teco_interface_is_interrupted(void)
+{
+ return teco_sigint_occurred != FALSE;
}
/**
* One iteration of the event loop.
*
- * This is a global function, so it may
- * be used as an Emscripten callback.
+ * This is a global function, so it may be used as an asynchronous Emscripten callback.
+ * While this function cannot directly throw GErrors,
+ * it can set teco_interface.event_loop_error.
*
- * @bug
- * Can probably be defined as a static method,
- * so we can avoid declaring it a fried function of
- * InterfaceCurses.
+ * @fixme Thrown errors should be somehow caught when building for EMScripten as well.
+ * Perhaps in a goto-block.
*/
void
-event_loop_iter()
+teco_interface_event_loop_iter(void)
{
- int key;
-
/*
* On PDCurses/win32, raw() and cbreak() does
* not disable and enable CTRL+C handling properly.
@@ -1426,7 +1518,7 @@ event_loop_iter()
* escape sequences.
*/
#ifdef NCURSES_UNIX
- keypad(interface.cmdline_window, Flags::ed & Flags::ED_FNKEYS);
+ keypad(teco_interface.cmdline_window, teco_ed & TECO_ED_FNKEYS);
#endif
/* no special <CTRL/C> handling */
@@ -1434,9 +1526,15 @@ event_loop_iter()
#ifdef PDCURSES_WIN32
SetConsoleMode(console_hnd, console_mode & ~ENABLE_PROCESSED_INPUT);
#endif
- key = wgetch(interface.cmdline_window);
+ /*
+ * Memory limiting is stopped temporarily, since it might otherwise
+ * constantly place 100% load on the CPU.
+ */
+ teco_memory_stop_limiting();
+ int key = wgetch(teco_interface.cmdline_window);
+ teco_memory_start_limiting();
/* allow asynchronous interruptions on <CTRL/C> */
- sigint_occurred = FALSE;
+ teco_sigint_occurred = FALSE;
noraw(); /* FIXME: necessary because of NCURSES_WIN32 bug */
cbreak();
#ifdef PDCURSES_WIN32
@@ -1451,10 +1549,10 @@ event_loop_iter()
#if PDCURSES
resize_term(0, 0);
#endif
- interface.resize_all_windows();
+ teco_interface_resize_all_windows();
break;
#endif
- case CTL_KEY('H'):
+ case TECO_CTL_KEY('H'):
case 0x7F: /* ^? */
case KEY_BACKSPACE:
/*
@@ -1465,18 +1563,25 @@ event_loop_iter()
* backspace.
* In SciTECO backspace is normalized to ^H.
*/
- cmdline.keypress(CTL_KEY('H'));
+ if (!teco_cmdline_keypress_c(TECO_CTL_KEY('H'),
+ &teco_interface.event_loop_error))
+ return;
break;
case KEY_ENTER:
case '\r':
case '\n':
- cmdline.keypress('\n');
+ if (!teco_cmdline_keypress_c('\n', &teco_interface.event_loop_error))
+ return;
break;
/*
* Function key macros
*/
-#define FN(KEY) case KEY_##KEY: cmdline.fnmacro(#KEY); break
+#define FN(KEY) \
+ case KEY_##KEY: \
+ if (!teco_cmdline_fnmacro(#KEY, &teco_interface.event_loop_error)) \
+ return; \
+ break
#define FNS(KEY) FN(KEY); FN(S##KEY)
FN(DOWN); FN(UP); FNS(LEFT); FNS(RIGHT);
FNS(HOME);
@@ -1485,7 +1590,9 @@ event_loop_iter()
g_snprintf(macro_name, sizeof(macro_name),
"F%d", key - KEY_F0);
- cmdline.fnmacro(macro_name);
+ if (!teco_cmdline_fnmacro(macro_name,
+ &teco_interface.event_loop_error))
+ return;
break;
}
FNS(DC);
@@ -1503,8 +1610,9 @@ event_loop_iter()
* Control keys and keys with printable representation
*/
default:
- if (key <= 0xFF)
- cmdline.keypress((gchar)key);
+ if (key <= 0xFF &&
+ !teco_cmdline_keypress_c(key, &teco_interface.event_loop_error))
+ return;
}
/*
@@ -1513,37 +1621,38 @@ event_loop_iter()
* so we redraw it here, where the overhead does
* not matter much.
*/
- interface.draw_info();
- wnoutrefresh(interface.info_window);
- interface.current_view->noutrefresh();
- wnoutrefresh(interface.msg_window);
- wnoutrefresh(interface.cmdline_window);
- interface.popup.noutrefresh();
+ teco_interface_draw_info();
+ wnoutrefresh(teco_interface.info_window);
+ teco_view_noutrefresh(teco_interface_current_view);
+ wnoutrefresh(teco_interface.msg_window);
+ wnoutrefresh(teco_interface.cmdline_window);
+ teco_curses_info_popup_noutrefresh(&teco_interface.popup);
doupdate();
}
-void
-InterfaceCurses::event_loop_impl(void)
+gboolean
+teco_interface_event_loop(GError **error)
{
- static const Cmdline empty_cmdline;
+ static const teco_cmdline_t empty_cmdline; // FIXME
/*
* Initialize Curses for interactive mode
*/
- init_interactive();
+ if (!teco_interface_init_interactive(error))
+ return FALSE;
/* initial refresh */
- draw_info();
- wnoutrefresh(info_window);
- current_view->noutrefresh();
- msg_clear();
- wnoutrefresh(msg_window);
- cmdline_update(&empty_cmdline);
- wnoutrefresh(cmdline_window);
+ teco_interface_draw_info();
+ wnoutrefresh(teco_interface.info_window);
+ teco_view_noutrefresh(teco_interface_current_view);
+ teco_interface_msg_clear();
+ wnoutrefresh(teco_interface.msg_window);
+ teco_interface_cmdline_update(&empty_cmdline);
+ wnoutrefresh(teco_interface.cmdline_window);
doupdate();
#ifdef EMCURSES
- PDC_emscripten_set_handler(event_loop_iter, TRUE);
+ PDC_emscripten_set_handler(teco_interface_event_loop_iter, TRUE);
/*
* We must not block emscripten's main loop,
* instead event_loop_iter() is called asynchronously.
@@ -1556,28 +1665,41 @@ InterfaceCurses::event_loop_impl(void)
*/
emscripten_exit_with_live_runtime();
#else
- try {
- for (;;)
- event_loop_iter();
- } catch (Quit) {
- /* SciTECO termination (e.g. EX$$) */
+ while (!teco_interface.event_loop_error)
+ teco_interface_event_loop_iter();
+
+ /*
+ * The error needs to be propagated only if this is
+ * NOT a SciTECO termination (e.g. EX$$)
+ */
+ if (!g_error_matches(teco_interface.event_loop_error,
+ TECO_ERROR, TECO_ERROR_QUIT)) {
+ g_propagate_error(error, g_steal_pointer(&teco_interface.event_loop_error));
+ return FALSE;
}
+ g_clear_error(&teco_interface.event_loop_error);
- restore_batch();
+ teco_interface_restore_batch();
#endif
+
+ return TRUE;
}
-InterfaceCurses::~InterfaceCurses()
+void
+teco_interface_cleanup(void)
{
- if (info_window)
- delwin(info_window);
- g_free(info_current);
- if (cmdline_window)
- delwin(cmdline_window);
- if (cmdline_pad)
- delwin(cmdline_pad);
- if (msg_window)
- delwin(msg_window);
+ if (teco_interface.event_loop_error)
+ g_error_free(teco_interface.event_loop_error);
+
+ if (teco_interface.info_window)
+ delwin(teco_interface.info_window);
+ teco_string_clear(&teco_interface.info_current);
+ if (teco_interface.cmdline_window)
+ delwin(teco_interface.cmdline_window);
+ if (teco_interface.cmdline_pad)
+ delwin(teco_interface.cmdline_pad);
+ if (teco_interface.msg_window)
+ delwin(teco_interface.msg_window);
/*
* PDCurses (win32) crashes if initscr() wasn't called.
@@ -1586,28 +1708,16 @@ InterfaceCurses::~InterfaceCurses()
* instead.
*/
#ifndef XCURSES
- if (info_window && !isendwin())
+ if (teco_interface.info_window && !isendwin())
endwin();
#endif
- if (screen)
- delscreen(screen);
- if (screen_tty)
- fclose(screen_tty);
- if (stderr_orig >= 0)
- close(stderr_orig);
- if (stdout_orig >= 0)
- close(stdout_orig);
-}
-
-/*
- * Callbacks
- */
-
-static void
-scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data)
-{
- interface.process_notify((SCNotification *)notify);
+ if (teco_interface.screen)
+ delscreen(teco_interface.screen);
+ if (teco_interface.screen_tty)
+ fclose(teco_interface.screen_tty);
+ if (teco_interface.stderr_orig >= 0)
+ close(teco_interface.stderr_orig);
+ if (teco_interface.stdout_orig >= 0)
+ close(teco_interface.stdout_orig);
}
-
-} /* namespace SciTECO */
diff --git a/src/interface-gtk/Makefile.am b/src/interface-gtk/Makefile.am
index 825c5d3..af26519 100644
--- a/src/interface-gtk/Makefile.am
+++ b/src/interface-gtk/Makefile.am
@@ -1,22 +1,18 @@
-AM_CPPFLAGS += -I$(top_srcdir)/src
+AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr \
+ -I$(top_srcdir)/src
-AM_CFLAGS = -Wall
-AM_CXXFLAGS = -Wall -Wno-char-subscripts
+AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value
-EXTRA_DIST = gtk-info-popup.gob \
- gtk-canonicalized-label.gob
-BUILT_SOURCES = gtk-info-popup.c \
- gtk-info-popup.h gtk-info-popup-private.h \
- gtk-canonicalized-label.c \
- gtk-canonicalized-label.h
+EXTRA_DIST = teco-gtk-info-popup.gob \
+ teco-gtk-label.gob
+BUILT_SOURCES = teco-gtk-info-popup.c teco-gtk-info-popup.h \
+ teco-gtk-info-popup-private.h \
+ teco-gtk-label.c teco-gtk-label.h
noinst_LTLIBRARIES = libsciteco-interface.la
-libsciteco_interface_la_SOURCES = interface-gtk.cpp interface-gtk.h
-if GTK_FLOW_BOX_FALLBACK
-libsciteco_interface_la_SOURCES += gtkflowbox.c gtkflowbox.h
-endif
-nodist_libsciteco_interface_la_SOURCES = gtk-info-popup.c \
- gtk-canonicalized-label.c
+libsciteco_interface_la_SOURCES = interface.c
+nodist_libsciteco_interface_la_SOURCES = teco-gtk-info-popup.c \
+ teco-gtk-label.c
dist_pkgdata_DATA = fallback.css
diff --git a/src/interface-gtk/fallback.css b/src/interface-gtk/fallback.css
index c8f5431..90ad4ed 100644
--- a/src/interface-gtk/fallback.css
+++ b/src/interface-gtk/fallback.css
@@ -6,6 +6,13 @@
* This may cause problems with your current Gtk theme.
* You can copy this file to $SCITECOCONFIG/.teco_css
* to fix it up or add other style customizations.
+ * You could of course also import it using
+ * @import "/usr/share/sciteco/fallback.css";
+ *
+ * NOTE: Avoid using CSS element names like GtkLabel
+ * since Gtk switched from type names to custom names
+ * in Gtk+ v3.20 and it is impossible/cumbersome to
+ * write a CSS compatible with both.
*/
/*
@@ -17,14 +24,12 @@
* - type-label: The label showing the current document type
* - name-label: THe label showing the current document name
*/
-.info-qregister,
-.info-buffer {
+.info-buffer, .info-qregister {
background-color: @sciteco_default_fg_color;
background-image: none;
}
-.info-qregister GtkLabel,
-.info-buffer GtkLabel {
+.info-buffer *, .info-qregister * {
color: @sciteco_default_bg_color;
text-shadow: none;
}
@@ -39,11 +44,6 @@
}
/*
- * Scintilla views
- */
-ScintillaObject {}
-
-/*
* The message bar (#sciteco-message-bar).
*
* The "question" class refers to G_MESSAGE_QUESTION.
@@ -51,25 +51,32 @@ ScintillaObject {}
* reason that there is no class for G_MESSAGE_OTHER that
* we could use for styling.
*/
-#sciteco-message-bar.question {
- background-color: @sciteco_default_fg_color;
+#sciteco-message-bar .label {
+ color: @sciteco_default_bg_color;
+ text-shadow: none;
+}
+
+#sciteco-message-bar {
background-image: none;
}
-#sciteco-message-bar.question GtkLabel {
- color: @sciteco_default_bg_color;
- text-shadow: none;
+#sciteco-message-bar.question {
+ background-color: @sciteco_default_fg_color;
+}
+#sciteco-message-bar.info {
+ background-color: green;
+}
+#sciteco-message-bar.error {
+ background-color: yellow;
+}
+#sciteco-message-bar.error {
+ background-color: red;
}
/*
* The command line area (#sciteco-cmdline)
*/
-#sciteco-cmdline {
- color: @sciteco_default_fg_color;
- text-shadow: none;
- background-color: @sciteco_default_bg_color;
- background-image: none;
-}
+#sciteco-cmdline {}
/*
* The autocompletion popup (#sciteco-info-popup).
@@ -81,11 +88,11 @@ ScintillaObject {}
background-image: none;
}
-#sciteco-info-popup GtkLabel {
+#sciteco-info-popup .label {
color: @sciteco_calltip_fg_color;
text-shadow: none;
}
-#sciteco-info-popup .highlight GtkLabel {
+#sciteco-info-popup .highlight .label {
font-weight: bold;
}
diff --git a/src/interface-gtk/gtk-info-popup.gob b/src/interface-gtk/gtk-info-popup.gob
deleted file mode 100644
index edc6612..0000000
--- a/src/interface-gtk/gtk-info-popup.gob
+++ /dev/null
@@ -1,331 +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 <http://www.gnu.org/licenses/>.
- */
-
-requires 2.0.20
-
-%ctop{
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <math.h>
-
-#include <glib/gprintf.h>
-
-#ifndef HAVE_GTK_FLOW_BOX_NEW
-#include "gtkflowbox.h"
-#endif
-
-#include "gtk-canonicalized-label.h"
-%}
-
-%h{
-#include <gtk/gtk.h>
-#include <gdk/gdk.h>
-
-#include <gio/gio.h>
-%}
-
-enum GTK_INFO_POPUP {
- PLAIN,
- FILE,
- DIRECTORY
-} Gtk:Info:Popup:Entry:Type;
-
-/*
- * NOTE: Deriving from GtkEventBox ensures that we can
- * set a background on the entire popup widget.
- */
-class Gtk:Info:Popup from Gtk:Event:Box {
- public GtkAdjustment *hadjustment;
- public GtkAdjustment *vadjustment;
-
- private GtkWidget *flow_box;
-
- init(self)
- {
- GtkWidget *box, *viewport;
-
- /*
- * A box containing a viewport and scrollbar will
- * "emulate" a scrolled window.
- * We cannot use a scrolled window since it ignores
- * the preferred height of its viewport which breaks
- * height-for-width management.
- */
- box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
-
- self->hadjustment = gtk_adjustment_new(0, 0, 0, 0, 0, 0);
- self->vadjustment = gtk_adjustment_new(0, 0, 0, 0, 0, 0);
-
- GtkWidget *scrollbar = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL,
- self->vadjustment);
- /* show/hide the scrollbar dynamically */
- g_signal_connect(self->vadjustment, "changed",
- G_CALLBACK(self_vadjustment_changed), scrollbar);
-
- self->_priv->flow_box = gtk_flow_box_new();
- /* take as little height as necessary */
- gtk_orientable_set_orientation(GTK_ORIENTABLE(self->_priv->flow_box),
- GTK_ORIENTATION_HORIZONTAL);
- //gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(self->_priv->flow_box), TRUE);
- /* this for focus handling only, not for scrolling */
- gtk_flow_box_set_hadjustment(GTK_FLOW_BOX(self->_priv->flow_box),
- self->hadjustment);
- gtk_flow_box_set_vadjustment(GTK_FLOW_BOX(self->_priv->flow_box),
- self->vadjustment);
-
- viewport = gtk_viewport_new(self->hadjustment, self->vadjustment);
- gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
- gtk_container_add(GTK_CONTAINER(viewport), self->_priv->flow_box);
-
- gtk_box_pack_start(GTK_BOX(box), viewport, TRUE, TRUE, 0);
- gtk_box_pack_start(GTK_BOX(box), scrollbar, FALSE, FALSE, 0);
- gtk_widget_show_all(box);
-
- /*
- * NOTE: Everything shown except the top-level container.
- * Therefore a gtk_widget_show() is enough to show our popup.
- */
- gtk_container_add(GTK_CONTAINER(self), box);
- }
-
- /**
- * Allocate position in an overlay.
- *
- * This function can be used as the "get-child-position" signal
- * handler of a GtkOverlay in order to position the popup at the
- * bottom of the overlay's main child, spanning the entire width.
- * In contrast to the GtkOverlay's default allocation schemes,
- * this makes sure that the widget will not be larger than the
- * main child, so the popup properly scrolls when becoming too large
- * in height.
- *
- * @param user_data unused by this callback
- */
- public gboolean
- get_position_in_overlay(Gtk:Overlay *overlay, Gtk:Widget *widget,
- Gdk:Rectangle *allocation, gpointer user_data)
- {
- GtkWidget *main_child = gtk_bin_get_child(GTK_BIN(overlay));
- GtkAllocation main_child_alloc;
- gint natural_height;
-
- gtk_widget_get_allocation(main_child, &main_child_alloc);
- gtk_widget_get_preferred_height_for_width(widget,
- main_child_alloc.width,
- NULL, &natural_height);
-
- /*
- * FIXME: Probably due to some bug in the height-for-width
- * calculation of Gtk (at least in 3.10 or in the GtkFlowBox
- * fallback included with SciTECO), the natural height
- * is a bit too small to accommodate the entire GtkFlowBox,
- * resulting in the GtkViewport always scrolling.
- * This hack fixes it up in a NONPORTABLE manner.
- */
- natural_height += 5;
-
- allocation->width = main_child_alloc.width;
- allocation->height = MIN(natural_height, main_child_alloc.height);
- allocation->x = 0;
- allocation->y = main_child_alloc.height - allocation->height;
-
- return TRUE;
- }
-
- /*
- * Adapted from GtkScrolledWindow's gtk_scrolled_window_scroll_event()
- * since the viewport does not react to scroll events.
- * This is registered for our container widget instead of only for
- * GtkViewport since this is what GtkScrolledWindow does.
- * FIXME: May need to handle non-delta scrolling, i.e. GDK_SCROLL_UP
- * and GDK_SCROLL_DOWN.
- */
- override (Gtk:Widget) gboolean
- scroll_event(Gtk:Widget *widget, Gdk:Event:Scroll *event)
- {
- Self *self = SELF(widget);
- gdouble delta_x, delta_y;
-
- if (gdk_event_get_scroll_deltas((GdkEvent *)event,
- &delta_x, &delta_y)) {
- GtkAdjustment *adj = self->vadjustment;
- gdouble page_size = gtk_adjustment_get_page_size(adj);
- gdouble scroll_unit = pow(page_size, 2.0 / 3.0);
- gdouble new_value;
-
- new_value = CLAMP(gtk_adjustment_get_value(adj) + delta_y * scroll_unit,
- gtk_adjustment_get_lower(adj),
- gtk_adjustment_get_upper(adj) -
- gtk_adjustment_get_page_size(adj));
-
- gtk_adjustment_set_value(adj, new_value);
-
- return TRUE;
- }
-
- return FALSE;
- }
-
- private void
- vadjustment_changed(Gtk:Adjustment *vadjustment, gpointer user_data)
- {
- GtkWidget *scrollbar = GTK_WIDGET(user_data);
-
- /*
- * This shows/hides the widget using opacity instead of using
- * gtk_widget_set_visibility() since the latter would influence
- * size allocations. A widget with opacity 0 keeps its size.
- */
- gtk_widget_set_opacity(scrollbar,
- gtk_adjustment_get_upper(vadjustment) -
- gtk_adjustment_get_lower(vadjustment) >
- gtk_adjustment_get_page_size(vadjustment) ? 1 : 0);
- }
-
- public GtkWidget *
- new(void)
- {
- Self *widget = GET_NEW;
- return GTK_WIDGET(widget);
- }
-
- public GIcon *
- get_icon_for_path(const gchar *path, const gchar *fallback_name)
- {
- GFile *file;
- GFileInfo *info;
- GIcon *icon = NULL;
-
- file = g_file_new_for_path(path);
- info = g_file_query_info(file, "standard::icon", 0, NULL, NULL);
- if (info) {
- icon = g_file_info_get_icon(info);
- g_object_ref(icon);
- g_object_unref(info);
- } else {
- /* fall back to standard icon, but this can still return NULL! */
- icon = g_icon_new_for_string(fallback_name, NULL);
- }
- g_object_unref(file);
-
- return icon;
- }
-
- public void
- add(self, Gtk:Info:Popup:Entry:Type type,
- const gchar *name, gboolean highlight)
- {
- GtkWidget *hbox;
- GtkWidget *label;
-
- hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
- if (highlight)
- gtk_style_context_add_class(gtk_widget_get_style_context(hbox),
- "highlight");
-
- if (type == GTK_INFO_POPUP_FILE || type == GTK_INFO_POPUP_DIRECTORY) {
- const gchar *fallback = type == GTK_INFO_POPUP_FILE ? "text-x-generic"
- : "folder";
- GIcon *icon;
-
- icon = self_get_icon_for_path(name, fallback);
- if (icon) {
- GtkWidget *image;
-
- image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU);
- g_object_unref(icon);
- gtk_box_pack_start(GTK_BOX(hbox), image,
- FALSE, FALSE, 0);
- }
- }
-
- label = gtk_canonicalized_label_new(name);
- gtk_widget_set_halign(label, GTK_ALIGN_START);
- gtk_widget_set_valign(label, GTK_ALIGN_CENTER);
-
- /*
- * FIXME: This makes little sense once we've got mouse support.
- * But for the time being, it's a useful setting.
- */
- gtk_label_set_selectable(GTK_LABEL(label), TRUE);
-
- switch (type) {
- case GTK_INFO_POPUP_PLAIN:
- gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_START);
- break;
- case GTK_INFO_POPUP_FILE:
- case GTK_INFO_POPUP_DIRECTORY:
- gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_MIDDLE);
- break;
- }
-
- gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0);
-
- gtk_widget_show_all(hbox);
- gtk_container_add(GTK_CONTAINER(self->_priv->flow_box), hbox);
- }
-
- public void
- scroll_page(self)
- {
- GtkAdjustment *adj = self->vadjustment;
- gdouble new_value;
-
- if (gtk_adjustment_get_value(adj) + gtk_adjustment_get_page_size(adj) ==
- gtk_adjustment_get_upper(adj)) {
- /* wrap and scroll back to the top */
- new_value = gtk_adjustment_get_lower(adj);
- } else {
- /* scroll one page */
- GList *child_list;
-
- new_value = gtk_adjustment_get_value(adj) +
- gtk_adjustment_get_page_size(adj);
-
- /*
- * Adjust this so only complete entries are shown.
- * Effectively, this rounds down to the line height.
- */
- child_list = gtk_container_get_children(GTK_CONTAINER(self->_priv->flow_box));
- if (child_list) {
- new_value -= (gint)new_value %
- gtk_widget_get_allocated_height(GTK_WIDGET(child_list->data));
- g_list_free(child_list);
- }
-
- /* clip to the maximum possible value */
- new_value = MIN(new_value, gtk_adjustment_get_upper(adj));
- }
-
- gtk_adjustment_set_value(adj, new_value);
- }
-
- public void
- clear(self)
- {
- GList *children;
-
- children = gtk_container_get_children(GTK_CONTAINER(self->_priv->flow_box));
- for (GList *cur = g_list_first(children);
- cur != NULL;
- cur = g_list_next(cur))
- gtk_widget_destroy(GTK_WIDGET(cur->data));
- g_list_free(children);
- }
-}
diff --git a/src/interface-gtk/gtkflowbox.c b/src/interface-gtk/gtkflowbox.c
deleted file mode 100644
index 1a5c2e9..0000000
--- a/src/interface-gtk/gtkflowbox.c
+++ /dev/null
@@ -1,4795 +0,0 @@
-/*
- * Copyright (C) 2007-2010 Openismus GmbH
- * Copyright (C) 2013 Red Hat, Inc.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Library General Public
- * License as published by the Free Software Foundation; either
- * version 2 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Library General Public License for more details.
- *
- * You should have received a copy of the GNU Library General Public
- * License along with this library; if not, see <http://www.gnu.org/licenses/>.
- *
- * Authors:
- * Tristan Van Berkom <tristanvb@openismus.com>
- * Matthias Clasen <mclasen@redhat.com>
- * William Jon McCann <jmccann@redhat.com>
- */
-
-/* Preamble {{{1 */
-
-/**
- * SECTION:gtkflowbox
- * @Short_Description: A container that allows reflowing its children
- * @Title: GtkFlowBox
- *
- * A GtkFlowBox positions child widgets in sequence according to its
- * orientation.
- *
- * For instance, with the horizontal orientation, the widgets will be
- * arranged from left to right, starting a new row under the previous
- * row when necessary. Reducing the width in this case will require more
- * rows, so a larger height will be requested.
- *
- * Likewise, with the vertical orientation, the widgets will be arranged
- * from top to bottom, starting a new column to the right when necessary.
- * Reducing the height will require more columns, so a larger width will
- * be requested.
- *
- * The children of a GtkFlowBox can be dynamically sorted and filtered.
- *
- * Although a GtkFlowBox must have only #GtkFlowBoxChild children,
- * you can add any kind of widget to it via gtk_container_add(), and
- * a GtkFlowBoxChild widget will automatically be inserted between
- * the box and the widget.
- *
- * Also see #GtkListBox.
- *
- * GtkFlowBox was added in GTK+ 3.12.
- */
-#ifdef HAVE_CONFIG_H
-#include <config.h>
-#endif
-
-#include <gtk/gtk.h>
-
-#include "gtkflowbox.h"
-
-/* Forward declarations and utilities {{{1 */
-
-static void gtk_flow_box_update_cursor (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-static void gtk_flow_box_select_and_activate (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-static void gtk_flow_box_update_selection (GtkFlowBox *box,
- GtkFlowBoxChild *child,
- gboolean modify,
- gboolean extend);
-static void gtk_flow_box_apply_filter (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-static void gtk_flow_box_apply_sort (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-static gint gtk_flow_box_sort (GtkFlowBoxChild *a,
- GtkFlowBoxChild *b,
- GtkFlowBox *box);
-
-static void
-get_current_selection_modifiers (GtkWidget *widget,
- gboolean *modify,
- gboolean *extend)
-{
- GdkModifierType state = 0;
- GdkModifierType mask;
-
- *modify = FALSE;
- *extend = FALSE;
-
- if (gtk_get_current_event_state (&state))
- {
- mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_MODIFY_SELECTION);
- if ((state & mask) == mask)
- *modify = TRUE;
- mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_EXTEND_SELECTION);
- if ((state & mask) == mask)
- *extend = TRUE;
- }
-}
-
-static void
-path_from_horizontal_line_rects (cairo_t *cr,
- GdkRectangle *lines,
- gint n_lines)
-{
- gint start_line, end_line;
- GdkRectangle *r;
- gint i;
-
- /* Join rows vertically by extending to the middle */
- for (i = 0; i < n_lines - 1; i++)
- {
- GdkRectangle *r1 = &lines[i];
- GdkRectangle *r2 = &lines[i+1];
- gint gap, old;
-
- gap = r2->y - (r1->y + r1->height);
- r1->height += gap / 2;
- old = r2->y;
- r2->y = r1->y + r1->height;
- r2->height += old - r2->y;
- }
-
- cairo_new_path (cr);
- start_line = 0;
-
- do
- {
- for (i = start_line; i < n_lines; i++)
- {
- r = &lines[i];
- if (i == start_line)
- cairo_move_to (cr, r->x + r->width, r->y);
- else
- cairo_line_to (cr, r->x + r->width, r->y);
- cairo_line_to (cr, r->x + r->width, r->y + r->height);
-
- if (i < n_lines - 1 &&
- (r->x + r->width < lines[i+1].x ||
- r->x > lines[i+1].x + lines[i+1].width))
- {
- i++;
- break;
- }
- }
- end_line = i;
- for (i = end_line - 1; i >= start_line; i--)
- {
- r = &lines[i];
- cairo_line_to (cr, r->x, r->y + r->height);
- cairo_line_to (cr, r->x, r->y);
- }
- cairo_close_path (cr);
- start_line = end_line;
- }
- while (end_line < n_lines);
-}
-
-static void
-path_from_vertical_line_rects (cairo_t *cr,
- GdkRectangle *lines,
- gint n_lines)
-{
- gint start_line, end_line;
- GdkRectangle *r;
- gint i;
-
- /* Join rows horizontally by extending to the middle */
- for (i = 0; i < n_lines - 1; i++)
- {
- GdkRectangle *r1 = &lines[i];
- GdkRectangle *r2 = &lines[i+1];
- gint gap, old;
-
- gap = r2->x - (r1->x + r1->width);
- r1->width += gap / 2;
- old = r2->x;
- r2->x = r1->x + r1->width;
- r2->width += old - r2->x;
- }
-
- cairo_new_path (cr);
- start_line = 0;
-
- do
- {
- for (i = start_line; i < n_lines; i++)
- {
- r = &lines[i];
- if (i == start_line)
- cairo_move_to (cr, r->x, r->y + r->height);
- else
- cairo_line_to (cr, r->x, r->y + r->height);
- cairo_line_to (cr, r->x + r->width, r->y + r->height);
-
- if (i < n_lines - 1 &&
- (r->y + r->height < lines[i+1].y ||
- r->y > lines[i+1].y + lines[i+1].height))
- {
- i++;
- break;
- }
- }
- end_line = i;
- for (i = end_line - 1; i >= start_line; i--)
- {
- r = &lines[i];
- cairo_line_to (cr, r->x + r->width, r->y);
- cairo_line_to (cr, r->x, r->y);
- }
- cairo_close_path (cr);
- start_line = end_line;
- }
- while (end_line < n_lines);
-}
-
-/* GtkFlowBoxChild {{{1 */
-
-/* GObject boilerplate {{{2 */
-
-enum {
- CHILD_ACTIVATE,
- CHILD_LAST_SIGNAL
-};
-
-static guint child_signals[CHILD_LAST_SIGNAL] = { 0 };
-
-typedef struct _GtkFlowBoxChildPrivate GtkFlowBoxChildPrivate;
-struct _GtkFlowBoxChildPrivate
-{
- GSequenceIter *iter;
- gboolean selected;
-};
-
-#define CHILD_PRIV(child) ((GtkFlowBoxChildPrivate*)gtk_flow_box_child_get_instance_private ((GtkFlowBoxChild*)(child)))
-
-G_DEFINE_TYPE_WITH_PRIVATE (GtkFlowBoxChild, gtk_flow_box_child, GTK_TYPE_BIN)
-
-/* Internal API {{{2 */
-
-static GtkFlowBox *
-gtk_flow_box_child_get_box (GtkFlowBoxChild *child)
-{
- GtkWidget *parent;
-
- parent = gtk_widget_get_parent (GTK_WIDGET (child));
- if (parent && GTK_IS_FLOW_BOX (parent))
- return GTK_FLOW_BOX (parent);
-
- return NULL;
-}
-
-static void
-gtk_flow_box_child_set_focus (GtkFlowBoxChild *child)
-{
- GtkFlowBox *box = gtk_flow_box_child_get_box (child);
- gboolean modify;
- gboolean extend;
-
- get_current_selection_modifiers (GTK_WIDGET (box), &modify, &extend);
-
- if (modify)
- gtk_flow_box_update_cursor (box, child);
- else
- gtk_flow_box_update_selection (box, child, FALSE, FALSE);
-}
-
-/* GtkWidget implementation {{{2 */
-
-static gboolean
-gtk_flow_box_child_focus (GtkWidget *widget,
- GtkDirectionType direction)
-{
- gboolean had_focus = FALSE;
- GtkWidget *child;
-
- child = gtk_bin_get_child (GTK_BIN (widget));
-
- g_object_get (widget, "has-focus", &had_focus, NULL);
- if (had_focus)
- {
- /* If on row, going right, enter into possible container */
- if (child &&
- (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD))
- {
- if (gtk_widget_child_focus (GTK_WIDGET (child), direction))
- return TRUE;
- }
-
- return FALSE;
- }
- else if (gtk_container_get_focus_child (GTK_CONTAINER (widget)) != NULL)
- {
- /* Child has focus, always navigate inside it first */
- if (gtk_widget_child_focus (child, direction))
- return TRUE;
-
- /* If exiting child container to the left, select child */
- if (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD)
- {
- gtk_flow_box_child_set_focus (GTK_FLOW_BOX_CHILD (widget));
- return TRUE;
- }
-
- return FALSE;
- }
- else
- {
- /* If coming from the left, enter into possible container */
- if (child &&
- (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD))
- {
- if (gtk_widget_child_focus (child, direction))
- return TRUE;
- }
-
- gtk_flow_box_child_set_focus (GTK_FLOW_BOX_CHILD (widget));
- return TRUE;
- }
-}
-
-static void
-gtk_flow_box_child_activate (GtkFlowBoxChild *child)
-{
- GtkFlowBox *box;
-
- box = gtk_flow_box_child_get_box (child);
- if (box)
- gtk_flow_box_select_and_activate (box, child);
-}
-
-static gboolean
-gtk_flow_box_child_draw (GtkWidget *widget,
- cairo_t *cr)
-{
- GtkAllocation allocation = {0};
- GtkStyleContext* context;
- GtkStateFlags state;
- GtkBorder border;
- gint focus_pad;
-
- gtk_widget_get_allocation (widget, &allocation);
- context = gtk_widget_get_style_context (widget);
- state = gtk_widget_get_state_flags (widget);
-
- gtk_render_background (context, cr, 0, 0, allocation.width, allocation.height);
- gtk_render_frame (context, cr, 0, 0, allocation.width, allocation.height);
-
- if (gtk_widget_has_visible_focus (widget))
- {
- gtk_style_context_get_border (context, state, &border);
-
- gtk_style_context_get_style (context,
- "focus-padding", &focus_pad,
- NULL);
- gtk_render_focus (context, cr, border.left + focus_pad, border.top + focus_pad,
- allocation.width - 2 * focus_pad - border.left - border.right,
- allocation.height - 2 * focus_pad - border.top - border.bottom);
- }
-
- GTK_WIDGET_CLASS (gtk_flow_box_child_parent_class)->draw (widget, cr);
-
- return TRUE;
-}
-
-/* Size allocation {{{3 */
-
-static void
-gtk_flow_box_child_get_full_border (GtkFlowBoxChild *child,
- GtkBorder *full_border)
-{
- GtkWidget *widget = GTK_WIDGET (child);
- GtkStyleContext *context;
- GtkStateFlags state;
- GtkBorder padding, border;
- int focus_width, focus_pad;
-
- context = gtk_widget_get_style_context (widget);
- state = gtk_style_context_get_state (context);
-
- gtk_style_context_get_padding (context, state, &padding);
- gtk_style_context_get_border (context, state, &border);
- gtk_style_context_get_style (context,
- "focus-line-width", &focus_width,
- "focus-padding", &focus_pad,
- NULL);
-
- full_border->left = padding.left + border.left + focus_width + focus_pad;
- full_border->right = padding.right + border.right + focus_width + focus_pad;
- full_border->top = padding.top + border.top + focus_width + focus_pad;
- full_border->bottom = padding.bottom + border.bottom + focus_width + focus_pad;
-}
-
-static GtkSizeRequestMode
-gtk_flow_box_child_get_request_mode (GtkWidget *widget)
-{
- GtkFlowBox *box;
-
- box = gtk_flow_box_child_get_box (GTK_FLOW_BOX_CHILD (widget));
- if (box)
- return gtk_widget_get_request_mode (GTK_WIDGET (box));
- else
- return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
-}
-
-static void gtk_flow_box_child_get_preferred_width_for_height (GtkWidget *widget,
- gint height,
- gint *minimum_width,
- gint *natural_width);
-static void gtk_flow_box_child_get_preferred_height (GtkWidget *widget,
- gint *minimum_height,
- gint *natural_height);
-
-static void
-gtk_flow_box_child_get_preferred_height_for_width (GtkWidget *widget,
- gint width,
- gint *minimum_height,
- gint *natural_height)
-{
- if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
- {
- GtkWidget *child;
- gint child_min = 0, child_natural = 0;
- GtkBorder full_border = { 0, };
-
- gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border);
- child = gtk_bin_get_child (GTK_BIN (widget));
- if (child && gtk_widget_get_visible (child))
- gtk_widget_get_preferred_height_for_width (child, width - full_border.left - full_border.right,
- &child_min, &child_natural);
-
- if (minimum_height)
- *minimum_height = full_border.top + child_min + full_border.bottom;
- if (natural_height)
- *natural_height = full_border.top + child_natural + full_border.bottom;
- }
- else
- {
- gtk_flow_box_child_get_preferred_height (widget, minimum_height, natural_height);
- }
-}
-
-static void
-gtk_flow_box_child_get_preferred_width (GtkWidget *widget,
- gint *minimum_width,
- gint *natural_width)
-{
- if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
- {
- GtkWidget *child;
- gint child_min = 0, child_natural = 0;
- GtkBorder full_border = { 0, };
-
- gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border);
- child = gtk_bin_get_child (GTK_BIN (widget));
- if (child && gtk_widget_get_visible (child))
- gtk_widget_get_preferred_width (child, &child_min, &child_natural);
-
- if (minimum_width)
- *minimum_width = full_border.left + child_min + full_border.right;
- if (natural_width)
- *natural_width = full_border.left + child_natural + full_border.right;
- }
- else
- {
- gint natural_height;
-
- gtk_flow_box_child_get_preferred_height (widget, NULL, &natural_height);
- gtk_flow_box_child_get_preferred_width_for_height (widget, natural_height,
- minimum_width, natural_width);
- }
-}
-
-static void
-gtk_flow_box_child_get_preferred_width_for_height (GtkWidget *widget,
- gint height,
- gint *minimum_width,
- gint *natural_width)
-{
- if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
- {
- gtk_flow_box_child_get_preferred_width (widget, minimum_width, natural_width);
- }
- else
- {
- GtkWidget *child;
- gint child_min = 0, child_natural = 0;
- GtkBorder full_border = { 0, };
-
- gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border);
- child = gtk_bin_get_child (GTK_BIN (widget));
- if (child && gtk_widget_get_visible (child))
- gtk_widget_get_preferred_width_for_height (child, height - full_border.top - full_border.bottom,
- &child_min, &child_natural);
-
- if (minimum_width)
- *minimum_width = full_border.left + child_min + full_border.right;
- if (natural_width)
- *natural_width = full_border.left + child_natural + full_border.right;
- }
-}
-
-static void
-gtk_flow_box_child_get_preferred_height (GtkWidget *widget,
- gint *minimum_height,
- gint *natural_height)
-{
- if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
- {
- gint natural_width;
-
- gtk_flow_box_child_get_preferred_width (widget, NULL, &natural_width);
- gtk_flow_box_child_get_preferred_height_for_width (widget, natural_width,
- minimum_height, natural_height);
- }
- else
- {
- GtkWidget *child;
- gint child_min = 0, child_natural = 0;
- GtkBorder full_border = { 0, };
-
- gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border);
- child = gtk_bin_get_child (GTK_BIN (widget));
- if (child && gtk_widget_get_visible (child))
- gtk_widget_get_preferred_height (child, &child_min, &child_natural);
-
- if (minimum_height)
- *minimum_height = full_border.top + child_min + full_border.bottom;
- if (natural_height)
- *natural_height = full_border.top + child_natural + full_border.bottom;
- }
-}
-
-static void
-gtk_flow_box_child_size_allocate (GtkWidget *widget,
- GtkAllocation *allocation)
-{
- GtkWidget *child;
-
- gtk_widget_set_allocation (widget, allocation);
-
- child = gtk_bin_get_child (GTK_BIN (widget));
- if (child && gtk_widget_get_visible (child))
- {
- GtkAllocation child_allocation;
- GtkBorder border = { 0, };
-
- gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &border);
-
- child_allocation.x = allocation->x + border.left;
- child_allocation.y = allocation->y + border.top;
- child_allocation.width = allocation->width - border.left - border.right;
- child_allocation.height = allocation->height - border.top - border.bottom;
-
- child_allocation.width = MAX (1, child_allocation.width);
- child_allocation.height = MAX (1, child_allocation.height);
-
- gtk_widget_size_allocate (child, &child_allocation);
- }
-}
-
-/* GObject implementation {{{2 */
-
-static void
-gtk_flow_box_child_class_init (GtkFlowBoxChildClass *class)
-{
- GObjectClass *object_class = G_OBJECT_CLASS (class);
- GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
-
- widget_class->draw = gtk_flow_box_child_draw;
- widget_class->get_request_mode = gtk_flow_box_child_get_request_mode;
- widget_class->get_preferred_height = gtk_flow_box_child_get_preferred_height;
- widget_class->get_preferred_height_for_width = gtk_flow_box_child_get_preferred_height_for_width;
- widget_class->get_preferred_width = gtk_flow_box_child_get_preferred_width;
- widget_class->get_preferred_width_for_height = gtk_flow_box_child_get_preferred_width_for_height;
- widget_class->size_allocate = gtk_flow_box_child_size_allocate;
- widget_class->focus = gtk_flow_box_child_focus;
-
- class->activate = gtk_flow_box_child_activate;
-
- /**
- * GtkFlowBoxChild::activate:
- * @child: The child on which the signal is emitted
- *
- * The ::activate signal is emitted when the user activates
- * a child widget in a #GtkFlowBox, either by clicking or
- * double-clicking, or by using the Space or Enter key.
- *
- * While this signal is used as a
- * [keybinding signal][GtkBindingSignal],
- * it can be used by applications for their own purposes.
- */
- child_signals[CHILD_ACTIVATE] =
- g_signal_new ("activate",
- G_OBJECT_CLASS_TYPE (object_class),
- G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxChildClass, activate),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
- widget_class->activate_signal = child_signals[CHILD_ACTIVATE];
-
- gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_LIST_ITEM);
-}
-
-static void
-gtk_flow_box_child_init (GtkFlowBoxChild *child)
-{
- GtkStyleContext *context;
-
- gtk_widget_set_can_focus (GTK_WIDGET (child), TRUE);
- gtk_widget_set_redraw_on_allocate (GTK_WIDGET (child), TRUE);
-
- context = gtk_widget_get_style_context (GTK_WIDGET (child));
- gtk_style_context_add_class (context, "grid-child");
-}
-
-/* Public API {{{2 */
-
-/**
- * gtk_flow_box_child_new:
- *
- * Creates a new #GtkFlowBoxChild, to be used as a child
- * of a #GtkFlowBox.
- *
- * Returns: a new #GtkFlowBoxChild
- *
- * Since: 3.12
- */
-GtkWidget *
-gtk_flow_box_child_new (void)
-{
- return g_object_new (GTK_TYPE_FLOW_BOX_CHILD, NULL);
-}
-
-/**
- * gtk_flow_box_child_get_index:
- * @child: a #GtkFlowBoxChild
- *
- * Gets the current index of the @child in its #GtkFlowBox container.
- *
- * Returns: the index of the @child, or -1 if the @child is not
- * in a flow box.
- *
- * Since: 3.12
- */
-gint
-gtk_flow_box_child_get_index (GtkFlowBoxChild *child)
-{
- GtkFlowBoxChildPrivate *priv;
-
- g_return_val_if_fail (GTK_IS_FLOW_BOX_CHILD (child), -1);
-
- priv = CHILD_PRIV (child);
-
- if (priv->iter != NULL)
- return g_sequence_iter_get_position (priv->iter);
-
- return -1;
-}
-
-/**
- * gtk_flow_box_child_is_selected:
- * @child: a #GtkFlowBoxChild
- *
- * Returns whether the @child is currently selected in its
- * #GtkFlowBox container.
- *
- * Returns: %TRUE if @child is selected
- *
- * Since: 3.12
- */
-gboolean
-gtk_flow_box_child_is_selected (GtkFlowBoxChild *child)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX_CHILD (child), FALSE);
-
- return CHILD_PRIV (child)->selected;
-}
-
-/**
- * gtk_flow_box_child_changed:
- * @child: a #GtkFlowBoxChild
- *
- * Marks @child as changed, causing any state that depends on this
- * to be updated. This affects sorting and filtering.
- *
- * Note that calls to this method must be in sync with the data
- * used for the sorting and filtering functions. For instance, if
- * the list is mirroring some external data set, and *two* children
- * changed in the external data set when you call
- * gtk_flow_box_child_changed() on the first child, the sort function
- * must only read the new data for the first of the two changed
- * children, otherwise the resorting of the children will be wrong.
- *
- * This generally means that if you don’t fully control the data
- * model, you have to duplicate the data that affects the sorting
- * and filtering functions into the widgets themselves. Another
- * alternative is to call gtk_flow_box_invalidate_sort() on any
- * model change, but that is more expensive.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_child_changed (GtkFlowBoxChild *child)
-{
- GtkFlowBox *box;
-
- g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child));
-
- box = gtk_flow_box_child_get_box (child);
-
- if (box == NULL)
- return;
-
- gtk_flow_box_apply_sort (box, child);
- gtk_flow_box_apply_filter (box, child);
-}
-
-/* GtkFlowBox {{{1 */
-
-/* Constants {{{2 */
-
-#define DEFAULT_MAX_CHILDREN_PER_LINE 7
-#define RUBBERBAND_START_DISTANCE 32
-#define AUTOSCROLL_FAST_DISTANCE 32
-#define AUTOSCROLL_FACTOR 20
-#define AUTOSCROLL_FACTOR_FAST 10
-
-/* GObject boilerplate {{{2 */
-
-enum {
- CHILD_ACTIVATED,
- SELECTED_CHILDREN_CHANGED,
- ACTIVATE_CURSOR_CHILD,
- TOGGLE_CURSOR_CHILD,
- MOVE_CURSOR,
- SELECT_ALL,
- UNSELECT_ALL,
- LAST_SIGNAL
-};
-
-static guint signals[LAST_SIGNAL] = { 0 };
-
-enum {
- PROP_0,
- PROP_ORIENTATION,
- PROP_HOMOGENEOUS,
- PROP_HALIGN_POLICY,
- PROP_VALIGN_POLICY,
- PROP_COLUMN_SPACING,
- PROP_ROW_SPACING,
- PROP_MIN_CHILDREN_PER_LINE,
- PROP_MAX_CHILDREN_PER_LINE,
- PROP_SELECTION_MODE,
- PROP_ACTIVATE_ON_SINGLE_CLICK
-};
-
-typedef struct _GtkFlowBoxPrivate GtkFlowBoxPrivate;
-struct _GtkFlowBoxPrivate {
- GtkOrientation orientation;
- gboolean homogeneous;
-
- guint row_spacing;
- guint column_spacing;
-
- GtkFlowBoxChild *prelight_child;
- GtkFlowBoxChild *cursor_child;
- GtkFlowBoxChild *selected_child;
-
- gboolean active_child_active;
- GtkFlowBoxChild *active_child;
-
- GtkSelectionMode selection_mode;
-
- GtkAdjustment *hadjustment;
- GtkAdjustment *vadjustment;
- gboolean activate_on_single_click;
-
- guint16 min_children_per_line;
- guint16 max_children_per_line;
- guint16 cur_children_per_line;
-
- GSequence *children;
-
- GtkFlowBoxFilterFunc filter_func;
- gpointer filter_data;
- GDestroyNotify filter_destroy;
-
- GtkFlowBoxSortFunc sort_func;
- gpointer sort_data;
- GDestroyNotify sort_destroy;
-
- gboolean track_motion;
- gboolean rubberband_select;
- GtkFlowBoxChild *rubberband_first;
- GtkFlowBoxChild *rubberband_last;
- gint button_down_x;
- gint button_down_y;
- gboolean rubberband_modify;
- gboolean rubberband_extend;
- GdkDevice *rubberband_device;
-
- GtkScrollType autoscroll_mode;
- guint autoscroll_id;
-};
-
-#define BOX_PRIV(box) ((GtkFlowBoxPrivate*)gtk_flow_box_get_instance_private ((GtkFlowBox*)(box)))
-
-G_DEFINE_TYPE_WITH_CODE (GtkFlowBox, gtk_flow_box, GTK_TYPE_CONTAINER,
- G_ADD_PRIVATE (GtkFlowBox)
- G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
-
-/* Internal API, utilities {{{2 */
-
-#define ORIENTATION_ALIGN(box) \
- (BOX_PRIV(box)->orientation == GTK_ORIENTATION_HORIZONTAL \
- ? gtk_widget_get_halign (GTK_WIDGET (box)) \
- : gtk_widget_get_valign (GTK_WIDGET (box)))
-
-#define OPPOSING_ORIENTATION_ALIGN(box) \
- (BOX_PRIV(box)->orientation == GTK_ORIENTATION_HORIZONTAL \
- ? gtk_widget_get_valign (GTK_WIDGET (box)) \
- : gtk_widget_get_halign (GTK_WIDGET (box)))
-
-/* Children are visible if they are shown by the app (visible)
- * and not filtered out (child_visible) by the box
- */
-static inline gboolean
-child_is_visible (GtkWidget *child)
-{
- return gtk_widget_get_visible (child) &&
- gtk_widget_get_child_visible (child);
-}
-
-static gint
-get_visible_children (GtkFlowBox *box)
-{
- GSequenceIter *iter;
- gint i = 0;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
-
- child = g_sequence_get (iter);
- if (child_is_visible (child))
- i++;
- }
-
- return i;
-}
-
-static GtkFlowBoxChild *
-gtk_flow_box_find_child_at_pos (GtkFlowBox *box,
- gint x,
- gint y)
-{
- GtkWidget *child;
- GSequenceIter *iter;
- GtkAllocation allocation;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- child = g_sequence_get (iter);
- if (!child_is_visible (child))
- continue;
- gtk_widget_get_allocation (child, &allocation);
- if (x >= allocation.x && x < (allocation.x + allocation.width) &&
- y >= allocation.y && y < (allocation.y + allocation.height))
- return GTK_FLOW_BOX_CHILD (child);
- }
-
- return NULL;
-}
-
-static void
-gtk_flow_box_update_prelight (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (child != priv->prelight_child)
- {
- priv->prelight_child = child;
- gtk_widget_queue_draw (GTK_WIDGET (box));
- }
-}
-
-static void
-gtk_flow_box_update_active (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gboolean val;
-
- val = priv->active_child == child;
- if (priv->active_child != NULL &&
- val != priv->active_child_active)
- {
- priv->active_child_active = val;
- gtk_widget_queue_draw (GTK_WIDGET (box));
- }
-}
-
-static void
-gtk_flow_box_apply_filter (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gboolean do_show;
-
- do_show = TRUE;
- if (priv->filter_func != NULL)
- do_show = priv->filter_func (child, priv->filter_data);
-
- gtk_widget_set_child_visible (GTK_WIDGET (child), do_show);
-}
-
-static void
-gtk_flow_box_apply_filter_all (GtkFlowBox *box)
-{
- GSequenceIter *iter;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkFlowBoxChild *child;
-
- child = g_sequence_get (iter);
- gtk_flow_box_apply_filter (box, child);
- }
- gtk_widget_queue_resize (GTK_WIDGET (box));
-}
-
-static void
-gtk_flow_box_apply_sort (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- if (BOX_PRIV (box)->sort_func != NULL)
- {
- g_sequence_sort_changed (CHILD_PRIV (child)->iter,
- (GCompareDataFunc)gtk_flow_box_sort, box);
- gtk_widget_queue_resize (GTK_WIDGET (box));
- }
-}
-
-/* Selection utilities {{{3 */
-
-static gboolean
-gtk_flow_box_child_set_selected (GtkFlowBoxChild *child,
- gboolean selected)
-{
- if (CHILD_PRIV (child)->selected != selected)
- {
- CHILD_PRIV (child)->selected = selected;
- if (selected)
- gtk_widget_set_state_flags (GTK_WIDGET (child),
- GTK_STATE_FLAG_SELECTED, FALSE);
- else
- gtk_widget_unset_state_flags (GTK_WIDGET (child),
- GTK_STATE_FLAG_SELECTED);
-
- gtk_widget_queue_draw (GTK_WIDGET (child));
-
- return TRUE;
- }
-
- return FALSE;
-}
-
-static gboolean
-gtk_flow_box_unselect_all_internal (GtkFlowBox *box)
-{
- GtkFlowBoxChild *child;
- GSequenceIter *iter;
- gboolean dirty = FALSE;
-
- if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE)
- return FALSE;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- child = g_sequence_get (iter);
- dirty |= gtk_flow_box_child_set_selected (child, FALSE);
- }
-
- return dirty;
-}
-
-static void
-gtk_flow_box_unselect_child_internal (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- if (!CHILD_PRIV (child)->selected)
- return;
-
- if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE)
- return;
- else if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE)
- gtk_flow_box_unselect_all_internal (box);
- else
- gtk_flow_box_child_set_selected (child, FALSE);
-
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-static void
-gtk_flow_box_update_cursor (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- BOX_PRIV (box)->cursor_child = child;
- gtk_widget_grab_focus (GTK_WIDGET (child));
- gtk_widget_queue_draw (GTK_WIDGET (child));
-}
-
-static void
-gtk_flow_box_select_child_internal (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- if (CHILD_PRIV (child)->selected)
- return;
-
- if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE)
- return;
- if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE)
- gtk_flow_box_unselect_all_internal (box);
-
- gtk_flow_box_child_set_selected (child, TRUE);
- BOX_PRIV (box)->selected_child = child;
-
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-static void
-gtk_flow_box_select_all_between (GtkFlowBox *box,
- GtkFlowBoxChild *child1,
- GtkFlowBoxChild *child2,
- gboolean modify)
-{
- GSequenceIter *iter, *iter1, *iter2;
-
- if (child1)
- iter1 = CHILD_PRIV (child1)->iter;
- else
- iter1 = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
-
- if (child2)
- iter2 = CHILD_PRIV (child2)->iter;
- else
- iter2 = g_sequence_get_end_iter (BOX_PRIV (box)->children);
-
- if (g_sequence_iter_compare (iter2, iter1) < 0)
- {
- iter = iter1;
- iter1 = iter2;
- iter2 = iter;
- }
-
- for (iter = iter1;
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
-
- child = g_sequence_get (iter);
- if (child_is_visible (child))
- {
- if (modify)
- gtk_flow_box_child_set_selected (GTK_FLOW_BOX_CHILD (child), !CHILD_PRIV (child)->selected);
- else
- gtk_flow_box_child_set_selected (GTK_FLOW_BOX_CHILD (child), TRUE);
- }
-
- if (g_sequence_iter_compare (iter, iter2) == 0)
- break;
- }
-}
-
-static void
-gtk_flow_box_update_selection (GtkFlowBox *box,
- GtkFlowBoxChild *child,
- gboolean modify,
- gboolean extend)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- gtk_flow_box_update_cursor (box, child);
-
- if (priv->selection_mode == GTK_SELECTION_NONE)
- return;
-
- if (priv->selection_mode == GTK_SELECTION_BROWSE)
- {
- gtk_flow_box_unselect_all_internal (box);
- gtk_flow_box_child_set_selected (child, TRUE);
- priv->selected_child = child;
- }
- else if (priv->selection_mode == GTK_SELECTION_SINGLE)
- {
- gboolean was_selected;
-
- was_selected = CHILD_PRIV (child)->selected;
- gtk_flow_box_unselect_all_internal (box);
- gtk_flow_box_child_set_selected (child, modify ? !was_selected : TRUE);
- priv->selected_child = CHILD_PRIV (child)->selected ? child : NULL;
- }
- else /* GTK_SELECTION_MULTIPLE */
- {
- if (extend)
- {
- gtk_flow_box_unselect_all_internal (box);
- if (priv->selected_child == NULL)
- {
- gtk_flow_box_child_set_selected (child, TRUE);
- priv->selected_child = child;
- }
- else
- gtk_flow_box_select_all_between (box, priv->selected_child, child, FALSE);
- }
- else
- {
- if (modify)
- {
- gtk_flow_box_child_set_selected (child, !CHILD_PRIV (child)->selected);
- }
- else
- {
- gtk_flow_box_unselect_all_internal (box);
- gtk_flow_box_child_set_selected (child, !CHILD_PRIV (child)->selected);
- priv->selected_child = child;
- }
- }
- }
-
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-static void
-gtk_flow_box_select_and_activate (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- if (child != NULL)
- {
- gtk_flow_box_select_child_internal (box, child);
- gtk_flow_box_update_cursor (box, child);
- g_signal_emit (box, signals[CHILD_ACTIVATED], 0, child);
- }
-}
-
-/* Focus utilities {{{3 */
-
-static GSequenceIter *
-gtk_flow_box_get_previous_focusable (GtkFlowBox *box,
- GSequenceIter *iter)
-{
- GtkFlowBoxChild *child;
-
- while (!g_sequence_iter_is_begin (iter))
- {
- iter = g_sequence_iter_prev (iter);
- child = g_sequence_get (iter);
- if (child_is_visible (GTK_WIDGET (child)) &&
- gtk_widget_is_sensitive (GTK_WIDGET (child)))
- return iter;
- }
-
- return NULL;
-}
-
-static GSequenceIter *
-gtk_flow_box_get_next_focusable (GtkFlowBox *box,
- GSequenceIter *iter)
-{
- GtkFlowBoxChild *child;
-
- while (TRUE)
- {
- iter = g_sequence_iter_next (iter);
- if (g_sequence_iter_is_end (iter))
- return NULL;
- child = g_sequence_get (iter);
- if (child_is_visible (GTK_WIDGET (child)) &&
- gtk_widget_is_sensitive (GTK_WIDGET (child)))
- return iter;
- }
-
- return NULL;
-}
-
-static GSequenceIter *
-gtk_flow_box_get_first_focusable (GtkFlowBox *box)
-{
- GSequenceIter *iter;
- GtkFlowBoxChild *child;
-
- iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- if (g_sequence_iter_is_end (iter))
- return NULL;
-
- child = g_sequence_get (iter);
- if (child_is_visible (GTK_WIDGET (child)) &&
- gtk_widget_is_sensitive (GTK_WIDGET (child)))
- return iter;
-
- return gtk_flow_box_get_next_focusable (box, iter);
-}
-
-static GSequenceIter *
-gtk_flow_box_get_last_focusable (GtkFlowBox *box)
-{
- GSequenceIter *iter;
-
- iter = g_sequence_get_end_iter (BOX_PRIV (box)->children);
- return gtk_flow_box_get_previous_focusable (box, iter);
-}
-
-
-static GSequenceIter *
-gtk_flow_box_get_above_focusable (GtkFlowBox *box,
- GSequenceIter *iter)
-{
- GtkFlowBoxChild *child = NULL;
- gint i;
-
- while (TRUE)
- {
- i = 0;
- while (i < BOX_PRIV (box)->cur_children_per_line)
- {
- if (g_sequence_iter_is_begin (iter))
- return NULL;
- iter = g_sequence_iter_prev (iter);
- child = g_sequence_get (iter);
- if (child_is_visible (GTK_WIDGET (child)))
- i++;
- }
- if (child && gtk_widget_get_sensitive (GTK_WIDGET (child)))
- return iter;
- }
-
- return NULL;
-}
-
-static GSequenceIter *
-gtk_flow_box_get_below_focusable (GtkFlowBox *box,
- GSequenceIter *iter)
-{
- GtkFlowBoxChild *child = NULL;
- gint i;
-
- while (TRUE)
- {
- i = 0;
- while (i < BOX_PRIV (box)->cur_children_per_line)
- {
- iter = g_sequence_iter_next (iter);
- if (g_sequence_iter_is_end (iter))
- return NULL;
- child = g_sequence_get (iter);
- if (child_is_visible (GTK_WIDGET (child)))
- i++;
- }
- if (child && gtk_widget_get_sensitive (GTK_WIDGET (child)))
- return iter;
- }
-
- return NULL;
-}
-
-/* GtkWidget implementation {{{2 */
-
-/* Size allocation {{{3 */
-
-/* Used in columned modes where all items share at least their
- * equal widths or heights
- */
-static void
-get_max_item_size (GtkFlowBox *box,
- GtkOrientation orientation,
- gint *min_size,
- gint *nat_size)
-{
- GSequenceIter *iter;
- gint max_min_size = 0;
- gint max_nat_size = 0;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- gint child_min, child_nat;
-
- child = g_sequence_get (iter);
-
- if (!child_is_visible (child))
- continue;
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- gtk_widget_get_preferred_width (child, &child_min, &child_nat);
- else
- gtk_widget_get_preferred_height (child, &child_min, &child_nat);
-
- max_min_size = MAX (max_min_size, child_min);
- max_nat_size = MAX (max_nat_size, child_nat);
- }
-
- if (min_size)
- *min_size = max_min_size;
-
- if (nat_size)
- *nat_size = max_nat_size;
-}
-
-
-/* Gets the largest minimum/natural size for a given size (used to get
- * the largest item heights for a fixed item width and the opposite)
- */
-static void
-get_largest_size_for_opposing_orientation (GtkFlowBox *box,
- GtkOrientation orientation,
- gint item_size,
- gint *min_item_size,
- gint *nat_item_size)
-{
- GSequenceIter *iter;
- gint max_min_size = 0;
- gint max_nat_size = 0;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- gint child_min, child_nat;
-
- child = g_sequence_get (iter);
-
- if (!child_is_visible (child))
- continue;
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- gtk_widget_get_preferred_height_for_width (child,
- item_size,
- &child_min, &child_nat);
- else
- gtk_widget_get_preferred_width_for_height (child,
- item_size,
- &child_min, &child_nat);
-
- max_min_size = MAX (max_min_size, child_min);
- max_nat_size = MAX (max_nat_size, child_nat);
- }
-
- if (min_item_size)
- *min_item_size = max_min_size;
-
- if (nat_item_size)
- *nat_item_size = max_nat_size;
-}
-
-/* Gets the largest minimum/natural size on a single line for a given size
- * (used to get the largest line heights for a fixed item width and the opposite
- * while iterating over a list of children, note the new index is returned)
- */
-static GSequenceIter *
-get_largest_size_for_line_in_opposing_orientation (GtkFlowBox *box,
- GtkOrientation orientation,
- GSequenceIter *cursor,
- gint line_length,
- GtkRequestedSize *item_sizes,
- gint extra_pixels,
- gint *min_item_size,
- gint *nat_item_size)
-{
- GSequenceIter *iter;
- gint max_min_size = 0;
- gint max_nat_size = 0;
- gint i;
-
- i = 0;
- for (iter = cursor;
- !g_sequence_iter_is_end (iter) && i < line_length;
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- gint child_min, child_nat, this_item_size;
-
- child = g_sequence_get (iter);
-
- if (!child_is_visible (child))
- continue;
-
- /* Distribute the extra pixels to the first children in the line
- * (could be fancier and spread them out more evenly) */
- this_item_size = item_sizes[i].minimum_size;
- if (extra_pixels > 0 && ORIENTATION_ALIGN (box) == GTK_ALIGN_FILL)
- {
- this_item_size++;
- extra_pixels--;
- }
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- gtk_widget_get_preferred_height_for_width (child,
- this_item_size,
- &child_min, &child_nat);
- else
- gtk_widget_get_preferred_width_for_height (child,
- this_item_size,
- &child_min, &child_nat);
-
- max_min_size = MAX (max_min_size, child_min);
- max_nat_size = MAX (max_nat_size, child_nat);
-
- i++;
- }
-
- if (min_item_size)
- *min_item_size = max_min_size;
-
- if (nat_item_size)
- *nat_item_size = max_nat_size;
-
- /* Return next item in the list */
- return iter;
-}
-
-/* fit_aligned_item_requests() helper */
-static gint
-gather_aligned_item_requests (GtkFlowBox *box,
- GtkOrientation orientation,
- gint line_length,
- gint item_spacing,
- gint n_children,
- GtkRequestedSize *item_sizes)
-{
- GSequenceIter *iter;
- gint i;
- gint extra_items, natural_line_size = 0;
-
- extra_items = n_children % line_length;
-
- i = 0;
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- GtkAlign item_align;
- gint child_min, child_nat;
- gint position;
-
- child = g_sequence_get (iter);
-
- if (!child_is_visible (child))
- continue;
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- gtk_widget_get_preferred_width (child,
- &child_min, &child_nat);
- else
- gtk_widget_get_preferred_height (child,
- &child_min, &child_nat);
-
- /* Get the index and push it over for the last line when spreading to the end */
- position = i % line_length;
-
- item_align = ORIENTATION_ALIGN (box);
- if (item_align == GTK_ALIGN_END && i >= n_children - extra_items)
- position += line_length - extra_items;
-
- /* Round up the size of every column/row */
- item_sizes[position].minimum_size = MAX (item_sizes[position].minimum_size, child_min);
- item_sizes[position].natural_size = MAX (item_sizes[position].natural_size, child_nat);
-
- i++;
- }
-
- for (i = 0; i < line_length; i++)
- natural_line_size += item_sizes[i].natural_size;
-
- natural_line_size += (line_length - 1) * item_spacing;
-
- return natural_line_size;
-}
-
-static GtkRequestedSize *
-fit_aligned_item_requests (GtkFlowBox *box,
- GtkOrientation orientation,
- gint avail_size,
- gint item_spacing,
- gint *line_length, /* in-out */
- gint items_per_line,
- gint n_children)
-{
- GtkRequestedSize *sizes, *try_sizes;
- gint try_line_size, try_length;
-
- sizes = g_new0 (GtkRequestedSize, *line_length);
-
- /* get the sizes for the initial guess */
- try_line_size = gather_aligned_item_requests (box,
- orientation,
- *line_length,
- item_spacing,
- n_children,
- sizes);
-
- /* Try columnizing the whole thing and adding an item to the end of
- * the line; try to fit as many columns into the available size as
- * possible
- */
- for (try_length = *line_length + 1; try_line_size < avail_size; try_length++)
- {
- try_sizes = g_new0 (GtkRequestedSize, try_length);
- try_line_size = gather_aligned_item_requests (box,
- orientation,
- try_length,
- item_spacing,
- n_children,
- try_sizes);
-
- if (try_line_size <= avail_size &&
- items_per_line >= try_length)
- {
- *line_length = try_length;
-
- g_free (sizes);
- sizes = try_sizes;
- }
- else
- {
- /* oops, this one failed; stick to the last size that fit and then return */
- g_free (try_sizes);
- break;
- }
- }
-
- return sizes;
-}
-
-typedef struct {
- GArray *requested;
- gint extra_pixels;
-} AllocatedLine;
-
-static gint
-get_offset_pixels (GtkAlign align,
- gint pixels)
-{
- gint offset;
-
- switch (align) {
- case GTK_ALIGN_START:
- case GTK_ALIGN_FILL:
- offset = 0;
- break;
- case GTK_ALIGN_CENTER:
- offset = pixels / 2;
- break;
- case GTK_ALIGN_END:
- offset = pixels;
- break;
- default:
- g_assert_not_reached ();
- break;
- }
-
- return offset;
-}
-
-static void
-gtk_flow_box_size_allocate (GtkWidget *widget,
- GtkAllocation *allocation)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkAllocation child_allocation;
- gint avail_size, avail_other_size, min_items, item_spacing, line_spacing;
- GtkAlign item_align;
- GtkAlign line_align;
- GdkWindow *window;
- GtkRequestedSize *line_sizes = NULL;
- GtkRequestedSize *item_sizes = NULL;
- gint min_item_size, nat_item_size;
- gint line_length;
- gint item_size = 0;
- gint line_size = 0, min_fixed_line_size = 0, nat_fixed_line_size = 0;
- gint line_offset, item_offset, n_children, n_lines, line_count;
- gint extra_pixels = 0, extra_per_item = 0, extra_extra = 0;
- gint extra_line_pixels = 0, extra_per_line = 0, extra_line_extra = 0;
- gint i, this_line_size;
- GSequenceIter *iter;
-
- child_allocation.x = 0;
- child_allocation.y = 0;
- child_allocation.width = 0;
- child_allocation.height = 0;
-
- gtk_widget_set_allocation (widget, allocation);
- window = gtk_widget_get_window (widget);
- if (window != NULL)
- gdk_window_move_resize (window,
- allocation->x, allocation->y,
- allocation->width, allocation->height);
-
- child_allocation.x = 0;
- child_allocation.y = 0;
- child_allocation.width = allocation->width;
-
- min_items = MAX (1, priv->min_children_per_line);
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- avail_size = allocation->width;
- avail_other_size = allocation->height;
- item_spacing = priv->column_spacing; line_spacing = priv->row_spacing;
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- avail_size = allocation->height;
- avail_other_size = allocation->width;
- item_spacing = priv->row_spacing;
- line_spacing = priv->column_spacing;
- }
-
- item_align = ORIENTATION_ALIGN (box);
- line_align = OPPOSING_ORIENTATION_ALIGN (box);
-
- /* Get how many lines we'll be needing to flow */
- n_children = get_visible_children (box);
- if (n_children <= 0)
- return;
-
- /*
- * Deal with ALIGNED/HOMOGENEOUS modes first, start with
- * initial guesses at item/line sizes
- */
- get_max_item_size (box, priv->orientation, &min_item_size, &nat_item_size);
- if (nat_item_size <= 0)
- return;
-
- /* By default flow at the natural item width */
- line_length = avail_size / (nat_item_size + item_spacing);
-
- /* After the above aproximation, check if we cant fit one more on the line */
- if (line_length * item_spacing + (line_length + 1) * nat_item_size <= avail_size)
- line_length++;
-
- /* Its possible we were allocated just less than the natural width of the
- * minimum item flow length */
- line_length = MAX (min_items, line_length);
- line_length = MIN (line_length, priv->max_children_per_line);
-
- /* Here we just use the largest height-for-width and use that for the height
- * of all lines */
- if (priv->homogeneous)
- {
- n_lines = n_children / line_length;
- if ((n_children % line_length) > 0)
- n_lines++;
-
- n_lines = MAX (n_lines, 1);
-
- /* Now we need the real item allocation size */
- item_size = (avail_size - (line_length - 1) * item_spacing) / line_length;
-
- /* Cut out the expand space if we're not distributing any */
- if (item_align != GTK_ALIGN_FILL)
- item_size = MIN (item_size, nat_item_size);
-
- get_largest_size_for_opposing_orientation (box,
- priv->orientation,
- item_size,
- &min_fixed_line_size,
- &nat_fixed_line_size);
-
- /* resolve a fixed 'line_size' */
- line_size = (avail_other_size - (n_lines - 1) * line_spacing) / n_lines;
-
- if (line_align != GTK_ALIGN_FILL)
- line_size = MIN (line_size, nat_fixed_line_size);
-
- /* Get the real extra pixels incase of GTK_ALIGN_START lines */
- extra_pixels = avail_size - (line_length - 1) * item_spacing - item_size * line_length;
- extra_line_pixels = avail_other_size - (n_lines - 1) * line_spacing - line_size * n_lines;
- }
- else
- {
- gboolean first_line = TRUE;
-
- /* Find the amount of columns that can fit aligned into the available space
- * and collect their requests.
- */
- item_sizes = fit_aligned_item_requests (box,
- priv->orientation,
- avail_size,
- item_spacing,
- &line_length,
- priv->max_children_per_line,
- n_children);
-
- /* Calculate the number of lines after determining the final line_length */
- n_lines = n_children / line_length;
- if ((n_children % line_length) > 0)
- n_lines++;
-
- n_lines = MAX (n_lines, 1);
- line_sizes = g_new0 (GtkRequestedSize, n_lines);
-
- /* Get the available remaining size */
- avail_size -= (line_length - 1) * item_spacing;
- for (i = 0; i < line_length; i++)
- avail_size -= item_sizes[i].minimum_size;
-
- /* Perform a natural allocation on the columnized items and get the remaining pixels */
- if (avail_size > 0)
- extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes);
-
- /* Now that we have the size of each column of items find the size of each individual
- * line based on the aligned item sizes.
- */
-
- for (i = 0, iter = g_sequence_get_begin_iter (priv->children);
- !g_sequence_iter_is_end (iter) && i < n_lines;
- i++)
- {
- iter = get_largest_size_for_line_in_opposing_orientation (box,
- priv->orientation,
- iter,
- line_length,
- item_sizes,
- extra_pixels,
- &line_sizes[i].minimum_size,
- &line_sizes[i].natural_size);
-
-
- /* Its possible a line is made of completely invisible children */
- if (line_sizes[i].natural_size > 0)
- {
- if (first_line)
- first_line = FALSE;
- else
- avail_other_size -= line_spacing;
-
- avail_other_size -= line_sizes[i].minimum_size;
-
- line_sizes[i].data = GINT_TO_POINTER (i);
- }
- }
-
- /* Distribute space among lines naturally */
- if (avail_other_size > 0)
- extra_line_pixels = gtk_distribute_natural_allocation (avail_other_size, n_lines, line_sizes);
- }
-
- /*
- * Initial sizes of items/lines guessed at this point,
- * go on to distribute expand space if needed.
- */
-
- priv->cur_children_per_line = line_length;
-
- /* FIXME: This portion needs to consider which columns
- * and rows asked for expand space and distribute those
- * accordingly for the case of ALIGNED allocation.
- *
- * If at least one child in a column/row asked for expand;
- * we should make that row/column expand entirely.
- */
-
- /* Calculate expand space per item */
- if (item_align == GTK_ALIGN_FILL)
- {
- extra_per_item = extra_pixels / line_length;
- extra_extra = extra_pixels % line_length;
- }
-
- /* Calculate expand space per line */
- if (line_align == GTK_ALIGN_FILL)
- {
- extra_per_line = extra_line_pixels / n_lines;
- extra_line_extra = extra_line_pixels % n_lines;
- }
-
- /*
- * Prepare item/line initial offsets and jump into the
- * real allocation loop.
- */
- line_offset = item_offset = 0;
-
- /* prepend extra space to item_offset/line_offset for SPREAD_END */
- item_offset += get_offset_pixels (item_align, extra_pixels);
- line_offset += get_offset_pixels (line_align, extra_line_pixels);
-
- /* Get the allocation size for the first line */
- if (priv->homogeneous)
- this_line_size = line_size;
- else
- {
- this_line_size = line_sizes[0].minimum_size;
-
- if (line_align == GTK_ALIGN_FILL)
- {
- this_line_size += extra_per_line;
-
- if (extra_line_extra > 0)
- this_line_size++;
- }
- }
-
- i = 0;
- line_count = 0;
- for (iter = g_sequence_get_begin_iter (priv->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- gint position;
- gint this_item_size;
-
- child = g_sequence_get (iter);
-
- if (!child_is_visible (child))
- continue;
-
- /* Get item position */
- position = i % line_length;
-
- /* adjust the line_offset/count at the beginning of each new line */
- if (i > 0 && position == 0)
- {
- /* Push the line_offset */
- line_offset += this_line_size + line_spacing;
-
- line_count++;
-
- /* Get the new line size */
- if (priv->homogeneous)
- this_line_size = line_size;
- else
- {
- this_line_size = line_sizes[line_count].minimum_size;
-
- if (line_align == GTK_ALIGN_FILL)
- {
- this_line_size += extra_per_line;
-
- if (line_count < extra_line_extra)
- this_line_size++;
- }
- }
-
- item_offset = 0;
-
- if (item_align == GTK_ALIGN_CENTER)
- {
- item_offset += get_offset_pixels (item_align, extra_pixels);
- }
- else if (item_align == GTK_ALIGN_END)
- {
- item_offset += get_offset_pixels (item_align, extra_pixels);
-
- /* If we're on the last line, prepend the space for
- * any leading items */
- if (line_count == n_lines -1)
- {
- gint extra_items = n_children % line_length;
-
- if (priv->homogeneous)
- {
- item_offset += item_size * (line_length - extra_items);
- item_offset += item_spacing * (line_length - extra_items);
- }
- else
- {
- gint j;
-
- for (j = 0; j < (line_length - extra_items); j++)
- {
- item_offset += item_sizes[j].minimum_size;
- item_offset += item_spacing;
- }
- }
- }
- }
- }
-
- /* Push the index along for the last line when spreading to the end */
- if (item_align == GTK_ALIGN_END && line_count == n_lines -1)
- {
- gint extra_items = n_children % line_length;
-
- position += line_length - extra_items;
- }
-
- if (priv->homogeneous)
- this_item_size = item_size;
- else
- this_item_size = item_sizes[position].minimum_size;
-
- if (item_align == GTK_ALIGN_FILL)
- {
- this_item_size += extra_per_item;
-
- if (position < extra_extra)
- this_item_size++;
- }
-
- /* Do the actual allocation */
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- child_allocation.x = item_offset;
- child_allocation.y = line_offset;
- child_allocation.width = this_item_size;
- child_allocation.height = this_line_size;
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- child_allocation.x = line_offset;
- child_allocation.y = item_offset;
- child_allocation.width = this_line_size;
- child_allocation.height = this_item_size;
- }
-
- if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
- child_allocation.x = allocation->x + allocation->width - (child_allocation.x - allocation->x) - child_allocation.width;
- gtk_widget_size_allocate (child, &child_allocation);
-
- item_offset += this_item_size;
- item_offset += item_spacing;
-
- i++;
- }
-
- g_free (item_sizes);
- g_free (line_sizes);
-}
-
-static GtkSizeRequestMode
-gtk_flow_box_get_request_mode (GtkWidget *widget)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
-
- return (BOX_PRIV (box)->orientation == GTK_ORIENTATION_HORIZONTAL) ?
- GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH : GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT;
-}
-
-/* Gets the largest minimum and natural length of
- * 'line_length' consecutive items when aligned into rows/columns */
-static void
-get_largest_aligned_line_length (GtkFlowBox *box,
- GtkOrientation orientation,
- gint line_length,
- gint *min_size,
- gint *nat_size)
-{
- GSequenceIter *iter;
- gint max_min_size = 0;
- gint max_nat_size = 0;
- gint spacing, i;
- GtkRequestedSize *aligned_item_sizes;
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- spacing = BOX_PRIV (box)->column_spacing;
- else
- spacing = BOX_PRIV (box)->row_spacing;
-
- aligned_item_sizes = g_new0 (GtkRequestedSize, line_length);
-
- /* Get the largest sizes of each index in the line.
- */
- i = 0;
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- gint child_min, child_nat;
-
- child = g_sequence_get (iter);
- if (!child_is_visible (child))
- continue;
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- gtk_widget_get_preferred_width (child,
- &child_min, &child_nat);
- else /* GTK_ORIENTATION_VERTICAL */
- gtk_widget_get_preferred_height (child,
- &child_min, &child_nat);
-
- aligned_item_sizes[i % line_length].minimum_size =
- MAX (aligned_item_sizes[i % line_length].minimum_size, child_min);
-
- aligned_item_sizes[i % line_length].natural_size =
- MAX (aligned_item_sizes[i % line_length].natural_size, child_nat);
-
- i++;
- }
-
- /* Add up the largest indexes */
- for (i = 0; i < line_length; i++)
- {
- max_min_size += aligned_item_sizes[i].minimum_size;
- max_nat_size += aligned_item_sizes[i].natural_size;
- }
-
- g_free (aligned_item_sizes);
-
- max_min_size += (line_length - 1) * spacing;
- max_nat_size += (line_length - 1) * spacing;
-
- if (min_size)
- *min_size = max_min_size;
-
- if (nat_size)
- *nat_size = max_nat_size;
-}
-
-
-static void
-gtk_flow_box_get_preferred_width (GtkWidget *widget,
- gint *minimum_size,
- gint *natural_size)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gint min_item_width, nat_item_width;
- gint min_items, nat_items;
- gint min_width, nat_width;
-
- min_items = MAX (1, priv->min_children_per_line);
- nat_items = MAX (min_items, priv->max_children_per_line);
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- min_width = nat_width = 0;
-
- if (!priv->homogeneous)
- {
- /* When not homogeneous; horizontally oriented boxes
- * need enough width for the widest row */
- if (min_items == 1)
- {
- get_max_item_size (box,
- GTK_ORIENTATION_HORIZONTAL,
- &min_item_width,
- &nat_item_width);
-
- min_width += min_item_width;
- nat_width += nat_item_width;
- }
- else
- {
- gint min_line_length, nat_line_length;
-
- get_largest_aligned_line_length (box,
- GTK_ORIENTATION_HORIZONTAL,
- min_items,
- &min_line_length,
- &nat_line_length);
-
- if (nat_items > min_items)
- get_largest_aligned_line_length (box,
- GTK_ORIENTATION_HORIZONTAL,
- nat_items,
- NULL,
- &nat_line_length);
-
- min_width += min_line_length;
- nat_width += nat_line_length;
- }
- }
- else /* In homogeneous mode; horizontally oriented boxs
- * give the same width to all children */
- {
- get_max_item_size (box, GTK_ORIENTATION_HORIZONTAL,
- &min_item_width, &nat_item_width);
-
- min_width += min_item_width * min_items;
- min_width += (min_items -1) * priv->column_spacing;
-
- nat_width += nat_item_width * nat_items;
- nat_width += (nat_items -1) * priv->column_spacing;
- }
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- /* Return the width for the minimum height */
- gint min_height;
-
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, NULL);
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_width_for_height (widget,
- min_height,
- &min_width,
- &nat_width);
-
- }
-
- if (minimum_size)
- *minimum_size = min_width;
-
- if (natural_size)
- *natural_size = nat_width;
-}
-
-static void
-gtk_flow_box_get_preferred_height (GtkWidget *widget,
- gint *minimum_size,
- gint *natural_size)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gint min_item_height, nat_item_height;
- gint min_items, nat_items;
- gint min_height, nat_height;
-
- min_items = MAX (1, priv->min_children_per_line);
- nat_items = MAX (min_items, priv->max_children_per_line);
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- /* Return the height for the minimum width */
- gint min_width;
-
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, NULL);
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_height_for_width (widget,
- min_width,
- &min_height,
- &nat_height);
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- min_height = nat_height = 0;
-
- if (! priv->homogeneous)
- {
- /* When not homogeneous; vertically oriented boxes
- * need enough height for the tallest column */
- if (min_items == 1)
- {
- get_max_item_size (box, GTK_ORIENTATION_VERTICAL,
- &min_item_height, &nat_item_height);
-
- min_height += min_item_height;
- nat_height += nat_item_height;
- }
- else
- {
- gint min_line_length, nat_line_length;
-
- get_largest_aligned_line_length (box,
- GTK_ORIENTATION_VERTICAL,
- min_items,
- &min_line_length,
- &nat_line_length);
-
- if (nat_items > min_items)
- get_largest_aligned_line_length (box,
- GTK_ORIENTATION_VERTICAL,
- nat_items,
- NULL,
- &nat_line_length);
-
- min_height += min_line_length;
- nat_height += nat_line_length;
- }
-
- }
- else
- {
- /* In homogeneous mode; vertically oriented boxes
- * give the same height to all children
- */
- get_max_item_size (box,
- GTK_ORIENTATION_VERTICAL,
- &min_item_height,
- &nat_item_height);
-
- min_height += min_item_height * min_items;
- min_height += (min_items -1) * priv->row_spacing;
-
- nat_height += nat_item_height * nat_items;
- nat_height += (nat_items -1) * priv->row_spacing;
- }
- }
-
- if (minimum_size)
- *minimum_size = min_height;
-
- if (natural_size)
- *natural_size = nat_height;
-}
-
-static void
-gtk_flow_box_get_preferred_height_for_width (GtkWidget *widget,
- gint width,
- gint *minimum_height,
- gint *natural_height)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gint min_item_width, nat_item_width;
- gint min_items;
- gint min_height, nat_height;
- gint avail_size, n_children;
-
- min_items = MAX (1, priv->min_children_per_line);
-
- min_height = 0;
- nat_height = 0;
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- gint min_width;
- gint line_length;
- gint item_size, extra_pixels;
-
- n_children = get_visible_children (box);
- if (n_children <= 0)
- goto out;
-
- /* Make sure its no smaller than the minimum */
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, NULL);
-
- avail_size = MAX (width, min_width);
- if (avail_size <= 0)
- goto out;
-
- get_max_item_size (box, GTK_ORIENTATION_HORIZONTAL, &min_item_width, &nat_item_width);
- if (nat_item_width <= 0)
- goto out;
-
- /* By default flow at the natural item width */
- line_length = avail_size / (nat_item_width + priv->column_spacing);
-
- /* After the above aproximation, check if we cant fit one more on the line */
- if (line_length * priv->column_spacing + (line_length + 1) * nat_item_width <= avail_size)
- line_length++;
-
- /* Its possible we were allocated just less than the natural width of the
- * minimum item flow length
- */
- line_length = MAX (min_items, line_length);
- line_length = MIN (line_length, priv->max_children_per_line);
-
- /* Now we need the real item allocation size */
- item_size = (avail_size - (line_length - 1) * priv->column_spacing) / line_length;
-
- /* Cut out the expand space if we're not distributing any */
- if (gtk_widget_get_halign (widget) != GTK_ALIGN_FILL)
- {
- item_size = MIN (item_size, nat_item_width);
- extra_pixels = 0;
- }
- else
- /* Collect the extra pixels for expand children */
- extra_pixels = (avail_size - (line_length - 1) * priv->column_spacing) % line_length;
-
- if (priv->homogeneous)
- {
- gint min_item_height, nat_item_height;
- gint lines;
-
- /* Here we just use the largest height-for-width and
- * add up the size accordingly
- */
- get_largest_size_for_opposing_orientation (box,
- GTK_ORIENTATION_HORIZONTAL,
- item_size,
- &min_item_height,
- &nat_item_height);
-
- /* Round up how many lines we need to allocate for */
- lines = n_children / line_length;
- if ((n_children % line_length) > 0)
- lines++;
-
- min_height = min_item_height * lines;
- nat_height = nat_item_height * lines;
-
- min_height += (lines - 1) * priv->row_spacing;
- nat_height += (lines - 1) * priv->row_spacing;
- }
- else
- {
- gint min_line_height, nat_line_height, i;
- gboolean first_line = TRUE;
- GtkRequestedSize *item_sizes;
- GSequenceIter *iter;
-
- /* First get the size each set of items take to span the line
- * when aligning the items above and below after flowping.
- */
- item_sizes = fit_aligned_item_requests (box,
- priv->orientation,
- avail_size,
- priv->column_spacing,
- &line_length,
- priv->max_children_per_line,
- n_children);
-
- /* Get the available remaining size */
- avail_size -= (line_length - 1) * priv->column_spacing;
- for (i = 0; i < line_length; i++)
- avail_size -= item_sizes[i].minimum_size;
-
- if (avail_size > 0)
- extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes);
-
- for (iter = g_sequence_get_begin_iter (priv->children);
- !g_sequence_iter_is_end (iter);)
- {
- iter = get_largest_size_for_line_in_opposing_orientation (box,
- GTK_ORIENTATION_HORIZONTAL,
- iter,
- line_length,
- item_sizes,
- extra_pixels,
- &min_line_height,
- &nat_line_height);
- /* Its possible the line only had invisible widgets */
- if (nat_line_height > 0)
- {
- if (first_line)
- first_line = FALSE;
- else
- {
- min_height += priv->row_spacing;
- nat_height += priv->row_spacing;
- }
-
- min_height += min_line_height;
- nat_height += nat_line_height;
- }
- }
-
- g_free (item_sizes);
- }
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- /* Return the minimum height */
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, &nat_height);
- }
-
- out:
-
- if (minimum_height)
- *minimum_height = min_height;
-
- if (natural_height)
- *natural_height = nat_height;
-}
-
-static void
-gtk_flow_box_get_preferred_width_for_height (GtkWidget *widget,
- gint height,
- gint *minimum_width,
- gint *natural_width)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gint min_item_height, nat_item_height;
- gint min_items;
- gint min_width, nat_width;
- gint avail_size, n_children;
-
- min_items = MAX (1, priv->min_children_per_line);
-
- min_width = 0;
- nat_width = 0;
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- /* Return the minimum width */
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, &nat_width);
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- gint min_height;
- gint line_length;
- gint item_size, extra_pixels;
-
- n_children = get_visible_children (box);
- if (n_children <= 0)
- goto out;
-
- /* Make sure its no smaller than the minimum */
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, NULL);
-
- avail_size = MAX (height, min_height);
- if (avail_size <= 0)
- goto out;
-
- get_max_item_size (box, GTK_ORIENTATION_VERTICAL, &min_item_height, &nat_item_height);
-
- /* By default flow at the natural item width */
- line_length = avail_size / (nat_item_height + priv->row_spacing);
-
- /* After the above aproximation, check if we cant fit one more on the line */
- if (line_length * priv->row_spacing + (line_length + 1) * nat_item_height <= avail_size)
- line_length++;
-
- /* Its possible we were allocated just less than the natural width of the
- * minimum item flow length
- */
- line_length = MAX (min_items, line_length);
- line_length = MIN (line_length, priv->max_children_per_line);
-
- /* Now we need the real item allocation size */
- item_size = (avail_size - (line_length - 1) * priv->row_spacing) / line_length;
-
- /* Cut out the expand space if we're not distributing any */
- if (gtk_widget_get_valign (widget) != GTK_ALIGN_FILL)
- {
- item_size = MIN (item_size, nat_item_height);
- extra_pixels = 0;
- }
- else
- /* Collect the extra pixels for expand children */
- extra_pixels = (avail_size - (line_length - 1) * priv->row_spacing) % line_length;
-
- if (priv->homogeneous)
- {
- gint min_item_width, nat_item_width;
- gint lines;
-
- /* Here we just use the largest height-for-width and
- * add up the size accordingly
- */
- get_largest_size_for_opposing_orientation (box,
- GTK_ORIENTATION_VERTICAL,
- item_size,
- &min_item_width,
- &nat_item_width);
-
- /* Round up how many lines we need to allocate for */
- n_children = get_visible_children (box);
- lines = n_children / line_length;
- if ((n_children % line_length) > 0)
- lines++;
-
- min_width = min_item_width * lines;
- nat_width = nat_item_width * lines;
-
- min_width += (lines - 1) * priv->column_spacing;
- nat_width += (lines - 1) * priv->column_spacing;
- }
- else
- {
- gint min_line_width, nat_line_width, i;
- gboolean first_line = TRUE;
- GtkRequestedSize *item_sizes;
- GSequenceIter *iter;
-
- /* First get the size each set of items take to span the line
- * when aligning the items above and below after flowping.
- */
- item_sizes = fit_aligned_item_requests (box,
- priv->orientation,
- avail_size,
- priv->row_spacing,
- &line_length,
- priv->max_children_per_line,
- n_children);
-
- /* Get the available remaining size */
- avail_size -= (line_length - 1) * priv->column_spacing;
- for (i = 0; i < line_length; i++)
- avail_size -= item_sizes[i].minimum_size;
-
- if (avail_size > 0)
- extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes);
-
- for (iter = g_sequence_get_begin_iter (priv->children);
- !g_sequence_iter_is_end (iter);)
- {
- iter = get_largest_size_for_line_in_opposing_orientation (box,
- GTK_ORIENTATION_VERTICAL,
- iter,
- line_length,
- item_sizes,
- extra_pixels,
- &min_line_width,
- &nat_line_width);
-
- /* Its possible the last line only had invisible widgets */
- if (nat_line_width > 0)
- {
- if (first_line)
- first_line = FALSE;
- else
- {
- min_width += priv->column_spacing;
- nat_width += priv->column_spacing;
- }
-
- min_width += min_line_width;
- nat_width += nat_line_width;
- }
- }
- g_free (item_sizes);
- }
- }
-
- out:
- if (minimum_width)
- *minimum_width = min_width;
-
- if (natural_width)
- *natural_width = nat_width;
-}
-
-/* Drawing {{{3 */
-
-static gboolean
-gtk_flow_box_draw (GtkWidget *widget,
- cairo_t *cr)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkAllocation allocation = { 0, };
- GtkStyleContext* context;
-
- gtk_widget_get_allocation (GTK_WIDGET (box), &allocation);
- context = gtk_widget_get_style_context (GTK_WIDGET (box));
- gtk_render_background (context, cr, 0, 0, allocation.width, allocation.height);
-
- GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->draw (widget, cr);
-
- if (priv->rubberband_first && priv->rubberband_last)
- {
- GSequenceIter *iter, *iter1, *iter2;
- GdkRectangle line_rect, rect;
- GArray *lines;
- gboolean vertical;
-
- vertical = priv->orientation == GTK_ORIENTATION_VERTICAL;
-
- cairo_save (cr);
-
- context = gtk_widget_get_style_context (widget);
- gtk_style_context_save (context);
- gtk_style_context_add_class (context, GTK_STYLE_CLASS_RUBBERBAND);
-
- iter1 = CHILD_PRIV (priv->rubberband_first)->iter;
- iter2 = CHILD_PRIV (priv->rubberband_last)->iter;
-
- if (g_sequence_iter_compare (iter2, iter1) < 0)
- {
- iter = iter1;
- iter1 = iter2;
- iter2 = iter;
- }
-
- line_rect.width = 0;
- lines = g_array_new (FALSE, FALSE, sizeof (GdkRectangle));
-
- for (iter = iter1;
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
-
- child = g_sequence_get (iter);
- gtk_widget_get_allocation (GTK_WIDGET (child), &rect);
- if (line_rect.width == 0)
- line_rect = rect;
- else
- {
- if ((vertical && rect.x == line_rect.x) ||
- (!vertical && rect.y == line_rect.y))
- gdk_rectangle_union (&rect, &line_rect, &line_rect);
- else
- {
- g_array_append_val (lines, line_rect);
- line_rect = rect;
- }
- }
-
- if (g_sequence_iter_compare (iter, iter2) == 0)
- break;
- }
-
- if (line_rect.width != 0)
- g_array_append_val (lines, line_rect);
-
- if (lines->len > 0)
- {
- GtkStateFlags state;
- cairo_path_t *path;
- GtkBorder border;
- GdkRGBA border_color;
-
- if (vertical)
- path_from_vertical_line_rects (cr, (GdkRectangle *)lines->data, lines->len);
- else
- path_from_horizontal_line_rects (cr, (GdkRectangle *)lines->data, lines->len);
-
- /* For some reason we need to copy and reapply the path,
- * or it gets eaten by gtk_render_background()
- */
- path = cairo_copy_path (cr);
-
- cairo_save (cr);
- cairo_clip (cr);
- gtk_widget_get_allocation (widget, &allocation);
- gtk_render_background (context, cr,
- 0, 0,
- allocation.width, allocation.height);
- cairo_restore (cr);
-
- cairo_append_path (cr, path);
- cairo_path_destroy (path);
-
- state = gtk_widget_get_state_flags (widget);
- gtk_style_context_get_border_color (context, state, &border_color);
- gtk_style_context_get_border (context, state, &border);
-
- cairo_set_line_width (cr, border.left);
- gdk_cairo_set_source_rgba (cr, &border_color);
- cairo_stroke (cr);
- }
- g_array_free (lines, TRUE);
-
- gtk_style_context_restore (context);
- cairo_restore (cr);
- }
-
- return TRUE;
-}
-
-/* Autoscrolling {{{3 */
-
-static void
-remove_autoscroll (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (priv->autoscroll_id)
- {
- gtk_widget_remove_tick_callback (GTK_WIDGET (box), priv->autoscroll_id);
- priv->autoscroll_id = 0;
- }
-
- priv->autoscroll_mode = GTK_SCROLL_NONE;
-}
-
-static gboolean
-autoscroll_cb (GtkWidget *widget,
- GdkFrameClock *frame_clock,
- gpointer data)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (data);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkAdjustment *adjustment;
- gdouble factor;
- gdouble increment;
- gdouble value;
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- adjustment = priv->vadjustment;
- else
- adjustment = priv->hadjustment;
-
- switch (priv->autoscroll_mode)
- {
- case GTK_SCROLL_STEP_FORWARD:
- factor = AUTOSCROLL_FACTOR;
- break;
- case GTK_SCROLL_STEP_BACKWARD:
- factor = - AUTOSCROLL_FACTOR;
- break;
- case GTK_SCROLL_PAGE_FORWARD:
- factor = AUTOSCROLL_FACTOR_FAST;
- break;
- case GTK_SCROLL_PAGE_BACKWARD:
- factor = - AUTOSCROLL_FACTOR_FAST;
- break;
- default:
- g_assert_not_reached ();
- }
-
- increment = gtk_adjustment_get_step_increment (adjustment) / factor;
-
- value = gtk_adjustment_get_value (adjustment);
- value += increment;
- gtk_adjustment_set_value (adjustment, value);
-
- if (priv->rubberband_select)
- {
- gint x, y;
- GtkFlowBoxChild *child;
-
- gdk_window_get_device_position (gtk_widget_get_window (widget),
- priv->rubberband_device,
- &x, &y, NULL);
-
- child = gtk_flow_box_find_child_at_pos (box, x, y);
-
- gtk_flow_box_update_prelight (box, child);
- gtk_flow_box_update_active (box, child);
-
- if (child != NULL)
- priv->rubberband_last = child;
- }
-
- return G_SOURCE_CONTINUE;
-}
-
-static void
-add_autoscroll (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (priv->autoscroll_id != 0 ||
- priv->autoscroll_mode == GTK_SCROLL_NONE)
- return;
-
- priv->autoscroll_id = gtk_widget_add_tick_callback (GTK_WIDGET (box),
- (GtkTickCallback)autoscroll_cb,
- box,
- NULL);
-}
-
-static gboolean
-get_view_rect (GtkFlowBox *box,
- GdkRectangle *rect)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkWidget *parent;
- GdkWindow *view;
-
- parent = gtk_widget_get_parent (GTK_WIDGET (box));
- if (GTK_IS_VIEWPORT (parent))
- {
- view = gtk_viewport_get_view_window (GTK_VIEWPORT (parent));
- rect->x = gtk_adjustment_get_value (priv->hadjustment);
- rect->y = gtk_adjustment_get_value (priv->vadjustment);
- rect->width = gdk_window_get_width (view);
- rect->height = gdk_window_get_height (view);
- return TRUE;
- }
-
- return FALSE;
-}
-
-static void
-update_autoscroll_mode (GtkFlowBox *box,
- gint x,
- gint y)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkScrollType mode = GTK_SCROLL_NONE;
- GdkRectangle rect;
- gint size, pos;
-
- if (priv->rubberband_select && get_view_rect (box, &rect))
- {
- if (priv->orientation == GTK_ORIENTATION_VERTICAL)
- {
- size = rect.width;
- pos = x - rect.x;
- }
- else
- {
- size = rect.height;
- pos = y - rect.y;
- }
-
- if (pos < 0 - AUTOSCROLL_FAST_DISTANCE)
- mode = GTK_SCROLL_PAGE_BACKWARD;
- else if (pos > size + AUTOSCROLL_FAST_DISTANCE)
- mode = GTK_SCROLL_PAGE_FORWARD;
- else if (pos < 0)
- mode = GTK_SCROLL_STEP_BACKWARD;
- else if (pos > size)
- mode = GTK_SCROLL_STEP_FORWARD;
- }
-
- if (mode != priv->autoscroll_mode)
- {
- remove_autoscroll (box);
- priv->autoscroll_mode = mode;
- add_autoscroll (box);
- }
-}
-
-/* Event handling {{{3 */
-
-static gboolean
-gtk_flow_box_enter_notify_event (GtkWidget *widget,
- GdkEventCrossing *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxChild *child;
-
- if (event->window != gtk_widget_get_window (GTK_WIDGET (box)))
- return FALSE;
-
- child = gtk_flow_box_find_child_at_pos (box, event->x, event->y);
- gtk_flow_box_update_prelight (box, child);
- gtk_flow_box_update_active (box, child);
-
- return FALSE;
-}
-
-static gboolean
-gtk_flow_box_leave_notify_event (GtkWidget *widget,
- GdkEventCrossing *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxChild *child = NULL;
-
- if (event->window != gtk_widget_get_window (GTK_WIDGET (box)))
- return FALSE;
-
- if (event->detail != GDK_NOTIFY_INFERIOR)
- child = NULL;
- else
- child = gtk_flow_box_find_child_at_pos (box, event->x, event->y);
-
- gtk_flow_box_update_prelight (box, child);
- gtk_flow_box_update_active (box, child);
-
- return FALSE;
-}
-
-static gboolean
-gtk_flow_box_motion_notify_event (GtkWidget *widget,
- GdkEventMotion *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkFlowBoxChild *child;
- GdkWindow *window;
- GdkWindow *event_window;
- gint relative_x;
- gint relative_y;
- gdouble parent_x;
- gdouble parent_y;
-
- window = gtk_widget_get_window (GTK_WIDGET (box));
- event_window = event->window;
- relative_x = event->x;
- relative_y = event->y;
-
- while ((event_window != NULL) && (event_window != window))
- {
- gdk_window_coords_to_parent (event_window,
- relative_x, relative_y,
- &parent_x, &parent_y);
- relative_x = parent_x;
- relative_y = parent_y;
- event_window = gdk_window_get_effective_parent (event_window);
- }
-
- child = gtk_flow_box_find_child_at_pos (box, relative_x, relative_y);
- gtk_flow_box_update_prelight (box, child);
- gtk_flow_box_update_active (box, child);
-
- if (priv->track_motion)
- {
- if (!priv->rubberband_select &&
- (event->x - priv->button_down_x) * (event->x - priv->button_down_x) +
- (event->y - priv->button_down_y) * (event->y - priv->button_down_y) > RUBBERBAND_START_DISTANCE * RUBBERBAND_START_DISTANCE)
- {
- priv->rubberband_select = TRUE;
- priv->rubberband_first = gtk_flow_box_find_child_at_pos (box, priv->button_down_x, priv->button_down_y);
-
- /* Grab focus here, so Escape-to-stop-rubberband works */
- gtk_flow_box_update_cursor (box, priv->rubberband_first);
- }
-
- if (priv->rubberband_select)
- {
- if (priv->rubberband_first == NULL)
- priv->rubberband_first = child;
- if (child != NULL)
- priv->rubberband_last = child;
-
- update_autoscroll_mode (box, event->x, event->y);
- }
- }
-
- return FALSE;
-}
-
-static gboolean
-gtk_flow_box_button_press_event (GtkWidget *widget,
- GdkEventButton *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkFlowBoxChild *child;
-
- if (event->button == GDK_BUTTON_PRIMARY)
- {
- child = gtk_flow_box_find_child_at_pos (box, event->x, event->y);
- if (child != NULL)
- {
- priv->active_child = child;
- priv->active_child_active = TRUE;
- gtk_widget_queue_draw (GTK_WIDGET (box));
- if (event->type == GDK_2BUTTON_PRESS &&
- !priv->activate_on_single_click)
- {
- g_signal_emit (box, signals[CHILD_ACTIVATED], 0, child);
- return TRUE;
- }
- }
-
- if (priv->selection_mode == GTK_SELECTION_MULTIPLE)
- {
- priv->track_motion = TRUE;
- priv->rubberband_select = FALSE;
- priv->rubberband_first = NULL;
- priv->rubberband_last = NULL;
- priv->button_down_x = event->x;
- priv->button_down_y = event->y;
- priv->rubberband_device = gdk_event_get_device ((GdkEvent*)event);
- get_current_selection_modifiers (widget, &priv->rubberband_modify, &priv->rubberband_extend);
- }
- }
-
- return FALSE;
-}
-
-static void
-gtk_flow_box_stop_rubberband (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- priv->rubberband_select = FALSE;
- priv->rubberband_first = NULL;
- priv->rubberband_last = NULL;
- priv->rubberband_device = NULL;
-
- remove_autoscroll (box);
-
- gtk_widget_queue_draw (GTK_WIDGET (box));
-}
-
-static gboolean
-gtk_flow_box_button_release_event (GtkWidget *widget,
- GdkEventButton *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (event->button == GDK_BUTTON_PRIMARY)
- {
- if (priv->active_child != NULL && priv->active_child_active)
- {
- if (priv->activate_on_single_click)
- gtk_flow_box_select_and_activate (box, priv->active_child);
- else
- {
- gboolean modify;
- gboolean extend;
- GdkDevice *device;
-
- get_current_selection_modifiers (widget, &modify, &extend);
-
- /* With touch, we default to modifying the selection.
- * You can still clear the selection and start over
- * by holding Ctrl.
- */
- device = gdk_event_get_source_device ((GdkEvent *)event);
- if (gdk_device_get_source (device) == GDK_SOURCE_TOUCHSCREEN)
- modify = !modify;
-
- gtk_flow_box_update_selection (box, priv->active_child, modify, extend);
- }
- }
-
- priv->active_child = NULL;
- priv->active_child_active = FALSE;
- gtk_widget_queue_draw (GTK_WIDGET (box));
- }
-
- priv->track_motion = FALSE;
- if (priv->rubberband_select)
- {
- if (!priv->rubberband_extend && !priv->rubberband_modify)
- gtk_flow_box_unselect_all_internal (box);
- gtk_flow_box_select_all_between (box, priv->rubberband_first, priv->rubberband_last, priv->rubberband_modify);
-
- gtk_flow_box_stop_rubberband (box);
-
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-
- }
-
- return FALSE;
-}
-
-static gboolean
-gtk_flow_box_key_press_event (GtkWidget *widget,
- GdkEventKey *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (priv->rubberband_select)
- {
- if (event->keyval == GDK_KEY_Escape)
- {
- gtk_flow_box_stop_rubberband (box);
- return TRUE;
- }
- }
-
- return GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->key_press_event (widget, event);
-}
-
-static void
-gtk_flow_box_grab_notify (GtkWidget *widget,
- gboolean was_grabbed)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (!was_grabbed)
- {
- if (priv->rubberband_select)
- gtk_flow_box_stop_rubberband (box);
- }
-}
-
-/* Realize and map {{{3 */
-
-static void
-gtk_flow_box_realize (GtkWidget *widget)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkAllocation allocation;
- GdkWindowAttr attributes = {0};
- GdkWindow *window;
-
- gtk_widget_get_allocation (GTK_WIDGET (box), &allocation);
- gtk_widget_set_realized (GTK_WIDGET (box), TRUE);
-
- attributes.x = allocation.x;
- attributes.y = allocation.y;
- attributes.width = allocation.width;
- attributes.height = allocation.height;
- attributes.window_type = GDK_WINDOW_CHILD;
- attributes.event_mask = gtk_widget_get_events (GTK_WIDGET (box))
- | GDK_ENTER_NOTIFY_MASK
- | GDK_LEAVE_NOTIFY_MASK
- | GDK_POINTER_MOTION_MASK
- | GDK_EXPOSURE_MASK
- | GDK_KEY_PRESS_MASK
- | GDK_BUTTON_PRESS_MASK
- | GDK_BUTTON_RELEASE_MASK;
- attributes.wclass = GDK_INPUT_OUTPUT;
-
- window = gdk_window_new (gtk_widget_get_parent_window (GTK_WIDGET (box)),
- &attributes, GDK_WA_X | GDK_WA_Y);
- gtk_widget_register_window (GTK_WIDGET (box), window);
- gtk_widget_set_window (GTK_WIDGET (box), window);
- gtk_style_context_set_background (gtk_widget_get_style_context (GTK_WIDGET (box)), window);
-}
-
-static void
-gtk_flow_box_unmap (GtkWidget *widget)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
-
- remove_autoscroll (box);
-
- GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->unmap (widget);
-}
-
-/* GtkContainer implementation {{{2 */
-
-static void
-gtk_flow_box_add (GtkContainer *container,
- GtkWidget *child)
-{
- gtk_flow_box_insert (GTK_FLOW_BOX (container), child, -1);
-}
-
-static void
-gtk_flow_box_remove (GtkContainer *container,
- GtkWidget *widget)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (container);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gboolean was_visible;
- gboolean was_selected;
- GtkFlowBoxChild *child;
-
- if (GTK_IS_FLOW_BOX_CHILD (widget))
- child = GTK_FLOW_BOX_CHILD (widget);
- else
- {
- child = (GtkFlowBoxChild*)gtk_widget_get_parent (widget);
- if (!GTK_IS_FLOW_BOX_CHILD (child))
- {
- g_warning ("Tried to remove non-child %p\n", widget);
- return;
- }
- }
-
- was_visible = child_is_visible (GTK_WIDGET (child));
- was_selected = CHILD_PRIV (child)->selected;
-
- if (child == priv->prelight_child)
- priv->prelight_child = NULL;
- if (child == priv->active_child)
- priv->active_child = NULL;
- if (child == priv->selected_child)
- priv->selected_child = NULL;
-
- gtk_widget_unparent (GTK_WIDGET (child));
- g_sequence_remove (CHILD_PRIV (child)->iter);
-
- if (was_visible && gtk_widget_get_visible (GTK_WIDGET (box)))
- gtk_widget_queue_resize (GTK_WIDGET (box));
-
- if (was_selected)
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-static void
-gtk_flow_box_forall (GtkContainer *container,
- gboolean include_internals,
- GtkCallback callback,
- gpointer callback_target)
-{
- GSequenceIter *iter;
- GtkWidget *child;
-
- iter = g_sequence_get_begin_iter (BOX_PRIV (container)->children);
- while (!g_sequence_iter_is_end (iter))
- {
- child = g_sequence_get (iter);
- iter = g_sequence_iter_next (iter);
- callback (child, callback_target);
- }
-}
-
-static GType
-gtk_flow_box_child_type (GtkContainer *container)
-{
- return GTK_TYPE_FLOW_BOX_CHILD;
-}
-
-/* Keynav {{{2 */
-
-static gboolean
-gtk_flow_box_focus (GtkWidget *widget,
- GtkDirectionType direction)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkWidget *focus_child;
- GSequenceIter *iter;
- GtkFlowBoxChild *next_focus_child;
-
- focus_child = gtk_container_get_focus_child (GTK_CONTAINER (box));
- next_focus_child = NULL;
-
- if (focus_child != NULL)
- {
- if (gtk_widget_child_focus (focus_child, direction))
- return TRUE;
-
- iter = CHILD_PRIV (focus_child)->iter;
-
- if (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD)
- iter = gtk_flow_box_get_previous_focusable (box, iter);
- else if (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD)
- iter = gtk_flow_box_get_next_focusable (box, iter);
- else if (direction == GTK_DIR_UP)
- iter = gtk_flow_box_get_above_focusable (box, iter);
- else if (direction == GTK_DIR_DOWN)
- iter = gtk_flow_box_get_below_focusable (box, iter);
-
- if (iter != NULL)
- next_focus_child = g_sequence_get (iter);
- }
- else
- {
- if (BOX_PRIV (box)->selected_child)
- next_focus_child = BOX_PRIV (box)->selected_child;
- else
- {
- if (direction == GTK_DIR_UP || direction == GTK_DIR_TAB_BACKWARD)
- iter = gtk_flow_box_get_last_focusable (box);
- else
- iter = gtk_flow_box_get_first_focusable (box);
-
- if (iter != NULL)
- next_focus_child = g_sequence_get (iter);
- }
- }
-
- if (next_focus_child == NULL)
- {
- if (direction == GTK_DIR_UP || direction == GTK_DIR_DOWN ||
- direction == GTK_DIR_LEFT || direction == GTK_DIR_RIGHT)
- {
- if (gtk_widget_keynav_failed (GTK_WIDGET (box), direction))
- return TRUE;
- }
-
- return FALSE;
- }
-
- if (gtk_widget_child_focus (GTK_WIDGET (next_focus_child), direction))
- return TRUE;
-
- return TRUE;
-}
-
-static void
-gtk_flow_box_add_move_binding (GtkBindingSet *binding_set,
- guint keyval,
- GdkModifierType modmask,
- GtkMovementStep step,
- gint count)
-{
- GdkDisplay *display;
- GdkModifierType extend_mod_mask = GDK_SHIFT_MASK;
- GdkModifierType modify_mod_mask = GDK_CONTROL_MASK;
-
- display = gdk_display_get_default ();
- if (display)
- {
- extend_mod_mask = gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display),
- GDK_MODIFIER_INTENT_EXTEND_SELECTION);
- modify_mod_mask = gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display),
- GDK_MODIFIER_INTENT_MODIFY_SELECTION);
- }
-
- gtk_binding_entry_add_signal (binding_set, keyval, modmask,
- "move-cursor", 2,
- GTK_TYPE_MOVEMENT_STEP, step,
- G_TYPE_INT, count,
- NULL);
- gtk_binding_entry_add_signal (binding_set, keyval, modmask | extend_mod_mask,
- "move-cursor", 2,
- GTK_TYPE_MOVEMENT_STEP, step,
- G_TYPE_INT, count,
- NULL);
- gtk_binding_entry_add_signal (binding_set, keyval, modmask | modify_mod_mask,
- "move-cursor", 2,
- GTK_TYPE_MOVEMENT_STEP, step,
- G_TYPE_INT, count,
- NULL);
- gtk_binding_entry_add_signal (binding_set, keyval, modmask | extend_mod_mask | modify_mod_mask,
- "move-cursor", 2,
- GTK_TYPE_MOVEMENT_STEP, step,
- G_TYPE_INT, count,
- NULL);
-}
-
-static void
-gtk_flow_box_activate_cursor_child (GtkFlowBox *box)
-{
- gtk_flow_box_select_and_activate (box, BOX_PRIV (box)->cursor_child);
-}
-
-static void
-gtk_flow_box_toggle_cursor_child (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (priv->cursor_child == NULL)
- return;
-
- if ((priv->selection_mode == GTK_SELECTION_SINGLE ||
- priv->selection_mode == GTK_SELECTION_MULTIPLE) &&
- CHILD_PRIV (priv->cursor_child)->selected)
- gtk_flow_box_unselect_child_internal (box, priv->cursor_child);
- else
- gtk_flow_box_select_and_activate (box, priv->cursor_child);
-}
-
-static void
-gtk_flow_box_move_cursor (GtkFlowBox *box,
- GtkMovementStep step,
- gint count)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gboolean modify;
- gboolean extend;
- GtkFlowBoxChild *child;
- GtkFlowBoxChild *prev;
- GtkFlowBoxChild *next;
- GtkAllocation allocation;
- gint page_size;
- GSequenceIter *iter;
- gint start;
- GtkAdjustment *adjustment;
- gboolean vertical;
-
- vertical = priv->orientation == GTK_ORIENTATION_VERTICAL;
-
- if (vertical)
- {
- switch (step)
- {
- case GTK_MOVEMENT_VISUAL_POSITIONS:
- step = GTK_MOVEMENT_DISPLAY_LINES;
- break;
- case GTK_MOVEMENT_DISPLAY_LINES:
- step = GTK_MOVEMENT_VISUAL_POSITIONS;
- break;
- default: ;
- }
- }
-
- child = NULL;
- switch (step)
- {
- case GTK_MOVEMENT_VISUAL_POSITIONS:
- if (priv->cursor_child != NULL)
- {
- iter = CHILD_PRIV (priv->cursor_child)->iter;
- if (gtk_widget_get_direction (GTK_WIDGET (box)) == GTK_TEXT_DIR_RTL)
- count = - count;
-
- while (count < 0 && iter != NULL)
- {
- iter = gtk_flow_box_get_previous_focusable (box, iter);
- count = count + 1;
- }
- while (count > 0 && iter != NULL)
- {
- iter = gtk_flow_box_get_next_focusable (box, iter);
- count = count - 1;
- }
-
- if (iter != NULL && !g_sequence_iter_is_end (iter))
- child = g_sequence_get (iter);
- }
- break;
-
- case GTK_MOVEMENT_BUFFER_ENDS:
- if (count < 0)
- iter = gtk_flow_box_get_first_focusable (box);
- else
- iter = gtk_flow_box_get_last_focusable (box);
- if (iter != NULL)
- child = g_sequence_get (iter);
- break;
-
- case GTK_MOVEMENT_DISPLAY_LINES:
- if (priv->cursor_child != NULL)
- {
- iter = CHILD_PRIV (priv->cursor_child)->iter;
-
- while (count < 0 && iter != NULL)
- {
- iter = gtk_flow_box_get_above_focusable (box, iter);
- count = count + 1;
- }
- while (count > 0 && iter != NULL)
- {
- iter = gtk_flow_box_get_below_focusable (box, iter);
- count = count - 1;
- }
-
- if (iter != NULL)
- child = g_sequence_get (iter);
- }
- break;
-
- case GTK_MOVEMENT_PAGES:
- page_size = 100;
- adjustment = vertical ? priv->hadjustment : priv->vadjustment;
- if (adjustment)
- page_size = gtk_adjustment_get_page_increment (adjustment);
-
- if (priv->cursor_child != NULL)
- {
- child = priv->cursor_child;
- iter = CHILD_PRIV (child)->iter;
- gtk_widget_get_allocation (GTK_WIDGET (child), &allocation);
- start = vertical ? allocation.x : allocation.y;
-
- if (count < 0)
- {
- gint i = 0;
-
- /* Up */
- while (iter != NULL)
- {
- iter = gtk_flow_box_get_previous_focusable (box, iter);
- if (iter == NULL)
- break;
-
- prev = g_sequence_get (iter);
-
- /* go up an even number of rows */
- if (i % priv->cur_children_per_line == 0)
- {
- gtk_widget_get_allocation (GTK_WIDGET (prev), &allocation);
- if ((vertical ? allocation.x : allocation.y) < start - page_size)
- break;
- }
-
- child = prev;
- i++;
- }
- }
- else
- {
- gint i = 0;
-
- /* Down */
- while (!g_sequence_iter_is_end (iter))
- {
- iter = gtk_flow_box_get_next_focusable (box, iter);
- if (g_sequence_iter_is_end (iter))
- break;
-
- next = g_sequence_get (iter);
-
- if (i % priv->cur_children_per_line == 0)
- {
- gtk_widget_get_allocation (GTK_WIDGET (next), &allocation);
- if ((vertical ? allocation.x : allocation.y) > start + page_size)
- break;
- }
-
- child = next;
- i++;
- }
- }
- gtk_widget_get_allocation (GTK_WIDGET (child), &allocation);
- }
- break;
-
- default:
- g_assert_not_reached ();
- }
-
- if (child == NULL || child == priv->cursor_child)
- {
- GtkDirectionType direction = count < 0 ? GTK_DIR_UP : GTK_DIR_DOWN;
-
- if (!gtk_widget_keynav_failed (GTK_WIDGET (box), direction))
- {
- GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (box));
-
- if (toplevel)
- gtk_widget_child_focus (toplevel,
- direction == GTK_DIR_UP ?
- GTK_DIR_TAB_BACKWARD :
- GTK_DIR_TAB_FORWARD);
-
- }
-
- return;
- }
-
- get_current_selection_modifiers (GTK_WIDGET (box), &modify, &extend);
-
- gtk_flow_box_update_cursor (box, child);
- if (!modify)
- gtk_flow_box_update_selection (box, child, FALSE, extend);
-}
-
-/* Selection {{{2 */
-
-static void
-gtk_flow_box_selected_children_changed (GtkFlowBox *box)
-{
-}
-
-/* GObject implementation {{{2 */
-
-static void
-gtk_flow_box_get_property (GObject *object,
- guint prop_id,
- GValue *value,
- GParamSpec *pspec)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (object);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- switch (prop_id)
- {
- case PROP_ORIENTATION:
- g_value_set_enum (value, priv->orientation);
- break;
- case PROP_HOMOGENEOUS:
- g_value_set_boolean (value, priv->homogeneous);
- break;
- case PROP_COLUMN_SPACING:
- g_value_set_uint (value, priv->column_spacing);
- break;
- case PROP_ROW_SPACING:
- g_value_set_uint (value, priv->row_spacing);
- break;
- case PROP_MIN_CHILDREN_PER_LINE:
- g_value_set_uint (value, priv->min_children_per_line);
- break;
- case PROP_MAX_CHILDREN_PER_LINE:
- g_value_set_uint (value, priv->max_children_per_line);
- break;
- case PROP_SELECTION_MODE:
- g_value_set_enum (value, priv->selection_mode);
- break;
- case PROP_ACTIVATE_ON_SINGLE_CLICK:
- g_value_set_boolean (value, priv->activate_on_single_click);
- break;
- default:
- G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
- break;
- }
-}
-
-static void
-gtk_flow_box_set_property (GObject *object,
- guint prop_id,
- const GValue *value,
- GParamSpec *pspec)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (object);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- switch (prop_id)
- {
- case PROP_ORIENTATION:
- priv->orientation = g_value_get_enum (value);
- /* Re-box the children in the new orientation */
- gtk_widget_queue_resize (GTK_WIDGET (box));
- break;
- case PROP_HOMOGENEOUS:
- gtk_flow_box_set_homogeneous (box, g_value_get_boolean (value));
- break;
- case PROP_COLUMN_SPACING:
- gtk_flow_box_set_column_spacing (box, g_value_get_uint (value));
- break;
- case PROP_ROW_SPACING:
- gtk_flow_box_set_row_spacing (box, g_value_get_uint (value));
- break;
- case PROP_MIN_CHILDREN_PER_LINE:
- gtk_flow_box_set_min_children_per_line (box, g_value_get_uint (value));
- break;
- case PROP_MAX_CHILDREN_PER_LINE:
- gtk_flow_box_set_max_children_per_line (box, g_value_get_uint (value));
- break;
- case PROP_SELECTION_MODE:
- gtk_flow_box_set_selection_mode (box, g_value_get_enum (value));
- break;
- case PROP_ACTIVATE_ON_SINGLE_CLICK:
- gtk_flow_box_set_activate_on_single_click (box, g_value_get_boolean (value));
- break;
- default:
- G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
- break;
- }
-}
-
-static void
-gtk_flow_box_finalize (GObject *obj)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (obj);
-
- if (priv->filter_destroy != NULL)
- priv->filter_destroy (priv->filter_data);
- if (priv->sort_destroy != NULL)
- priv->sort_destroy (priv->sort_data);
-
- g_sequence_free (priv->children);
- g_clear_object (&priv->hadjustment);
- g_clear_object (&priv->vadjustment);
-
- G_OBJECT_CLASS (gtk_flow_box_parent_class)->finalize (obj);
-}
-
-static void
-gtk_flow_box_class_init (GtkFlowBoxClass *class)
-{
- GObjectClass *object_class = G_OBJECT_CLASS (class);
- GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
- GtkContainerClass *container_class = GTK_CONTAINER_CLASS (class);
- GtkBindingSet *binding_set;
-
- object_class->finalize = gtk_flow_box_finalize;
- object_class->get_property = gtk_flow_box_get_property;
- object_class->set_property = gtk_flow_box_set_property;
-
- widget_class->enter_notify_event = gtk_flow_box_enter_notify_event;
- widget_class->leave_notify_event = gtk_flow_box_leave_notify_event;
- widget_class->motion_notify_event = gtk_flow_box_motion_notify_event;
- widget_class->size_allocate = gtk_flow_box_size_allocate;
- widget_class->realize = gtk_flow_box_realize;
- widget_class->unmap = gtk_flow_box_unmap;
- widget_class->focus = gtk_flow_box_focus;
- widget_class->draw = gtk_flow_box_draw;
- widget_class->button_press_event = gtk_flow_box_button_press_event;
- widget_class->button_release_event = gtk_flow_box_button_release_event;
- widget_class->key_press_event = gtk_flow_box_key_press_event;
- widget_class->grab_notify = gtk_flow_box_grab_notify;
- widget_class->get_request_mode = gtk_flow_box_get_request_mode;
- widget_class->get_preferred_width = gtk_flow_box_get_preferred_width;
- widget_class->get_preferred_height = gtk_flow_box_get_preferred_height;
- widget_class->get_preferred_height_for_width = gtk_flow_box_get_preferred_height_for_width;
- widget_class->get_preferred_width_for_height = gtk_flow_box_get_preferred_width_for_height;
-
- container_class->add = gtk_flow_box_add;
- container_class->remove = gtk_flow_box_remove;
- container_class->forall = gtk_flow_box_forall;
- container_class->child_type = gtk_flow_box_child_type;
- gtk_container_class_handle_border_width (container_class);
-
- class->activate_cursor_child = gtk_flow_box_activate_cursor_child;
- class->toggle_cursor_child = gtk_flow_box_toggle_cursor_child;
- class->move_cursor = gtk_flow_box_move_cursor;
- class->select_all = gtk_flow_box_select_all;
- class->unselect_all = gtk_flow_box_unselect_all;
- class->selected_children_changed = gtk_flow_box_selected_children_changed;
-
- g_object_class_override_property (object_class, PROP_ORIENTATION, "orientation");
-
- /**
- * GtkFlowBox:selection-mode:
- *
- * The selection mode used by the flow box.
- */
- g_object_class_install_property (object_class,
- PROP_SELECTION_MODE,
- g_param_spec_enum ("selection-mode",
- "Selection mode",
- "The selection mode",
- GTK_TYPE_SELECTION_MODE,
- GTK_SELECTION_SINGLE,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:activate-on-single-click:
- *
- * Determines whether children can be activated with a single
- * click, or require a double-click.
- */
- g_object_class_install_property (object_class,
- PROP_ACTIVATE_ON_SINGLE_CLICK,
- g_param_spec_boolean ("activate-on-single-click",
- "Activate on Single Click",
- "Activate row on a single click",
- TRUE,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:homogeneous:
- *
- * Determines whether all children should be allocated the
- * same size.
- */
- g_object_class_install_property (object_class,
- PROP_HOMOGENEOUS,
- g_param_spec_boolean ("homogeneous",
- "Homogeneous",
- "Whether the children should all be the same size",
- FALSE,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:min-children-per-line:
- *
- * The minimum number of children to allocate consecutively
- * in the given orientation.
- *
- * Setting the minimum children per line ensures
- * that a reasonably small height will be requested
- * for the overall minimum width of the box.
- */
- g_object_class_install_property (object_class,
- PROP_MIN_CHILDREN_PER_LINE,
- g_param_spec_uint ("min-children-per-line",
- "Minimum Children Per Line",
- "The minimum number of children to allocate "
- "consecutively in the given orientation.",
- 0,
- G_MAXUINT,
- 0,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:max-children-per-line:
- *
- * The maximum amount of children to request space for consecutively
- * in the given orientation.
- */
- g_object_class_install_property (object_class,
- PROP_MAX_CHILDREN_PER_LINE,
- g_param_spec_uint ("max-children-per-line",
- "Maximum Children Per Line",
- "The maximum amount of children to request space for "
- "consecutively in the given orientation.",
- 0,
- G_MAXUINT,
- DEFAULT_MAX_CHILDREN_PER_LINE,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:row-spacing:
- *
- * The amount of vertical space between two children.
- */
- g_object_class_install_property (object_class,
- PROP_ROW_SPACING,
- g_param_spec_uint ("row-spacing",
- "Vertical spacing",
- "The amount of vertical space between two children",
- 0,
- G_MAXUINT,
- 0,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:column-spacing:
- *
- * The amount of horizontal space between two children.
- */
- g_object_class_install_property (object_class,
- PROP_COLUMN_SPACING,
- g_param_spec_uint ("column-spacing",
- "Horizontal spacing",
- "The amount of horizontal space between two children",
- 0,
- G_MAXUINT,
- 0,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox::child-activated:
- * @box: the #GtkFlowBox on which the signal is emitted
- * @child: the child that is activated
- *
- * The ::child-activated signal is emitted when a child has been
- * activated by the user.
- */
- signals[CHILD_ACTIVATED] = g_signal_new ("child-activated",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST,
- G_STRUCT_OFFSET (GtkFlowBoxClass, child_activated),
- NULL, NULL,
- g_cclosure_marshal_VOID__OBJECT,
- G_TYPE_NONE, 1,
- GTK_TYPE_FLOW_BOX_CHILD);
-
- /**
- * GtkFlowBox::selected-children-changed:
- * @box: the #GtkFlowBox on wich the signal is emitted
- *
- * The ::selected-children-changed signal is emitted when the
- * set of selected children changes.
- *
- * Use gtk_flow_box_selected_foreach() or
- * gtk_flow_box_get_selected_children() to obtain the
- * selected children.
- */
- signals[SELECTED_CHILDREN_CHANGED] = g_signal_new ("selected-children-changed",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_FIRST,
- G_STRUCT_OFFSET (GtkFlowBoxClass, selected_children_changed),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
-
- /**
- * GtkFlowBox::activate-cursor-child:
- * @box: the #GtkFlowBox on which the signal is emitted
- *
- * The ::activate-cursor-child signal is a
- * [keybinding signal][GtkBindingSignal]
- * which gets emitted when the user activates the @box.
- */
- signals[ACTIVATE_CURSOR_CHILD] = g_signal_new ("activate-cursor-child",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxClass, activate_cursor_child),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
-
- /**
- * GtkFlowBox::toggle-cursor-child:
- * @box: the #GtkFlowBox on which the signal is emitted
- *
- * The ::toggle-cursor-child signal is a
- * [keybinding signal][GtkBindingSignal]
- * which toggles the selection of the child that has the focus.
- *
- * The default binding for this signal is Ctrl-Space.
- */
- signals[TOGGLE_CURSOR_CHILD] = g_signal_new ("toggle-cursor-child",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxClass, toggle_cursor_child),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
-
- /**
- * GtkFlowBox::move-cursor:
- * @box: the #GtkFlowBox on which the signal is emitted
- * @step: the granularity fo the move, as a #GtkMovementStep
- * @count: the number of @step units to move
- *
- * The ::move-cursor signal is a
- * [keybinding signal][GtkBindingSignal]
- * which gets emitted when the user initiates a cursor movement.
- * If the cursor is not visible in @text_view, this signal causes
- * the viewport to be moved instead.
- *
- * Applications should not connect to it, but may emit it with
- * g_signal_emit_by_name() if they need to control the cursor
- * programmatically.
- *
- * The default bindings for this signal come in two variants,
- * the variant with the Shift modifier extends the selection,
- * the variant without the Shift modifer does not.
- * There are too many key combinations to list them all here.
- * - Arrow keys move by individual children
- * - Home/End keys move to the ends of the box
- * - PageUp/PageDown keys move vertically by pages
- */
- signals[MOVE_CURSOR] = g_signal_new ("move-cursor",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxClass, move_cursor),
- NULL, NULL,
- NULL,
- G_TYPE_NONE, 2,
- GTK_TYPE_MOVEMENT_STEP, G_TYPE_INT);
- /**
- * GtkFlowBox::select-all:
- * @box: the #GtkFlowBox on which the signal is emitted
- *
- * The ::select-all signal is a
- * [keybinding signal][GtkBindingSignal]
- * which gets emitted to select all children of the box, if
- * the selection mode permits it.
- *
- * The default bindings for this signal is Ctrl-a.
- */
- signals[SELECT_ALL] = g_signal_new ("select-all",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxClass, select_all),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
-
- /**
- * GtkFlowBox::unselect-all:
- * @box: the #GtkFlowBox on which the signal is emitted
- *
- * The ::unselect-all signal is a
- * [keybinding signal][GtkBindingSignal]
- * which gets emitted to unselect all children of the box, if
- * the selection mode permits it.
- *
- * The default bindings for this signal is Ctrl-Shift-a.
- */
- signals[UNSELECT_ALL] = g_signal_new ("unselect-all",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxClass, unselect_all),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
-
- widget_class->activate_signal = signals[ACTIVATE_CURSOR_CHILD];
-
- binding_set = gtk_binding_set_by_class (class);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Home, 0,
- GTK_MOVEMENT_BUFFER_ENDS, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Home, 0,
- GTK_MOVEMENT_BUFFER_ENDS, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_End, 0,
- GTK_MOVEMENT_BUFFER_ENDS, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_End, 0,
- GTK_MOVEMENT_BUFFER_ENDS, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Up, 0,
- GTK_MOVEMENT_DISPLAY_LINES, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Up, 0,
- GTK_MOVEMENT_DISPLAY_LINES, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Down, 0,
- GTK_MOVEMENT_DISPLAY_LINES, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Down, 0,
- GTK_MOVEMENT_DISPLAY_LINES, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Page_Up, 0,
- GTK_MOVEMENT_PAGES, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Page_Up, 0,
- GTK_MOVEMENT_PAGES, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Page_Down, 0,
- GTK_MOVEMENT_PAGES, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Page_Down, 0,
- GTK_MOVEMENT_PAGES, 1);
-
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Right, 0,
- GTK_MOVEMENT_VISUAL_POSITIONS, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Right, 0,
- GTK_MOVEMENT_VISUAL_POSITIONS, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Left, 0,
- GTK_MOVEMENT_VISUAL_POSITIONS, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Left, 0,
- GTK_MOVEMENT_VISUAL_POSITIONS, -1);
-
- gtk_binding_entry_add_signal (binding_set, GDK_KEY_space, GDK_CONTROL_MASK,
- "toggle-cursor-child", 0, NULL);
- gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Space, GDK_CONTROL_MASK,
- "toggle-cursor-child", 0, NULL);
-
- gtk_binding_entry_add_signal (binding_set, GDK_KEY_a, GDK_CONTROL_MASK,
- "select-all", 0);
- gtk_binding_entry_add_signal (binding_set, GDK_KEY_a, GDK_CONTROL_MASK | GDK_SHIFT_MASK,
- "unselect-all", 0);
-}
-
-static void
-gtk_flow_box_init (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- gtk_widget_set_has_window (GTK_WIDGET (box), TRUE);
- gtk_widget_set_redraw_on_allocate (GTK_WIDGET (box), TRUE);
-
- priv->orientation = GTK_ORIENTATION_HORIZONTAL;
- priv->selection_mode = GTK_SELECTION_SINGLE;
- priv->max_children_per_line = DEFAULT_MAX_CHILDREN_PER_LINE;
- priv->column_spacing = 0;
- priv->row_spacing = 0;
- priv->activate_on_single_click = TRUE;
-
- priv->children = g_sequence_new (NULL);
-}
-
- /* Public API {{{2 */
-
-/**
- * gtk_flow_box_new:
- *
- * Creates a GtkFlowBox.
- *
- * Returns: a new #GtkFlowBox container
- *
- * Since: 3.12
- */
-GtkWidget *
-gtk_flow_box_new (void)
-{
- return (GtkWidget *)g_object_new (GTK_TYPE_FLOW_BOX, NULL);
-}
-
-/**
- * gtk_flow_box_insert:
- * @box: a #GtkFlowBox
- * @widget: the #GtkWidget to add
- * @position: the position to insert @child in
- *
- * Inserts the @widget into @box at @position.
- *
- * If a sort function is set, the widget will actually be inserted
- * at the calculated position and this function has the same effect
- * as gtk_container_add().
- *
- * If @position is -1, or larger than the total number of children
- * in the @box, then the @widget will be appended to the end.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_insert (GtkFlowBox *box,
- GtkWidget *widget,
- gint position)
-{
- GtkFlowBoxPrivate *priv;
- GtkFlowBoxChild *child;
- GSequenceIter *iter;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
- g_return_if_fail (GTK_IS_WIDGET (widget));
-
- priv = BOX_PRIV (box);
-
- if (GTK_IS_FLOW_BOX_CHILD (widget))
- child = GTK_FLOW_BOX_CHILD (widget);
- else
- {
- child = GTK_FLOW_BOX_CHILD (gtk_flow_box_child_new ());
- gtk_widget_show (GTK_WIDGET (child));
- gtk_container_add (GTK_CONTAINER (child), widget);
- }
-
- if (priv->sort_func != NULL)
- iter = g_sequence_insert_sorted (priv->children, child,
- (GCompareDataFunc)gtk_flow_box_sort, box);
- else if (position == 0)
- iter = g_sequence_prepend (priv->children, child);
- else if (position == -1)
- iter = g_sequence_append (priv->children, child);
- else
- {
- GSequenceIter *pos;
- pos = g_sequence_get_iter_at_pos (priv->children, position);
- iter = g_sequence_insert_before (pos, child);
- }
-
- CHILD_PRIV (child)->iter = iter;
- gtk_widget_set_parent (GTK_WIDGET (child), GTK_WIDGET (box));
- gtk_flow_box_apply_filter (box, child);
-}
-
-/**
- * gtk_flow_box_get_child_at_index:
- * @box: a #GtkFlowBox
- * @idx: the position of the child
- *
- * Gets the nth child in the @box.
- *
- * Returns: (transfer none): the child widget, which will
- * always be a #GtkFlowBoxChild
- *
- * Since: 3.12
- */
-GtkFlowBoxChild *
-gtk_flow_box_get_child_at_index (GtkFlowBox *box,
- gint idx)
-{
- GSequenceIter *iter;
-
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), NULL);
-
- iter = g_sequence_get_iter_at_pos (BOX_PRIV (box)->children, idx);
- if (iter)
- return g_sequence_get (iter);
-
- return NULL;
-}
-
-/**
- * gtk_flow_box_set_hadjustment:
- * @box: a #GtkFlowBox
- * @adjustment: an adjustment which should be adjusted
- * when the focus is moved among the descendents of @container
- *
- * Hooks up an adjustment to focus handling in @box.
- * The adjustment is also used for autoscrolling during
- * rubberband selection. See gtk_scrolled_window_get_hadjustment()
- * for a typical way of obtaining the adjustment, and
- * gtk_flow_box_set_vadjustment()for setting the vertical
- * adjustment.
- *
- * The adjustments have to be in pixel units and in the same
- * coordinate system as the allocation for immediate children
- * of the box.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_hadjustment (GtkFlowBox *box,
- GtkAdjustment *adjustment)
-{
- GtkFlowBoxPrivate *priv;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
- g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment));
-
- priv = BOX_PRIV (box);
-
- g_object_ref (adjustment);
- if (priv->hadjustment)
- g_object_unref (priv->hadjustment);
- priv->hadjustment = adjustment;
- gtk_container_set_focus_hadjustment (GTK_CONTAINER (box), adjustment);
-}
-
-/**
- * gtk_flow_box_set_vadjustment:
- * @box: a #GtkFlowBox
- * @adjustment: an adjustment which should be adjusted
- * when the focus is moved among the descendents of @container
- *
- * Hooks up an adjustment to focus handling in @box.
- * The adjustment is also used for autoscrolling during
- * rubberband selection. See gtk_scrolled_window_get_vadjustment()
- * for a typical way of obtaining the adjustment, and
- * gtk_flow_box_set_hadjustment()for setting the horizontal
- * adjustment.
- *
- * The adjustments have to be in pixel units and in the same
- * coordinate system as the allocation for immediate children
- * of the box.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_vadjustment (GtkFlowBox *box,
- GtkAdjustment *adjustment)
-{
- GtkFlowBoxPrivate *priv;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
- g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment));
-
- priv = BOX_PRIV (box);
-
- g_object_ref (adjustment);
- if (priv->vadjustment)
- g_object_unref (priv->vadjustment);
- priv->vadjustment = adjustment;
- gtk_container_set_focus_vadjustment (GTK_CONTAINER (box), adjustment);
-}
-
-/* Setters and getters {{{2 */
-
-/**
- * gtk_flow_box_get_homogeneous:
- * @box: a #GtkFlowBox
- *
- * Returns whether the box is homogeneous (all children are the
- * same size). See gtk_box_set_homogeneous().
- *
- * Returns: %TRUE if the box is homogeneous.
- *
- * Since: 3.12
- */
-gboolean
-gtk_flow_box_get_homogeneous (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->homogeneous;
-}
-
-/**
- * gtk_flow_box_set_homogeneous:
- * @box: a #GtkFlowBox
- * @homogeneous: %TRUE to create equal allotments,
- * %FALSE for variable allotments
- *
- * Sets the #GtkFlowBox:homogeneous property of @box, controlling
- * whether or not all children of @box are given equal space
- * in the box.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_homogeneous (GtkFlowBox *box,
- gboolean homogeneous)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- homogeneous = homogeneous != FALSE;
-
- if (BOX_PRIV (box)->homogeneous != homogeneous)
- {
- BOX_PRIV (box)->homogeneous = homogeneous;
-
- g_object_notify (G_OBJECT (box), "homogeneous");
- gtk_widget_queue_resize (GTK_WIDGET (box));
- }
-}
-
-/**
- * gtk_flow_box_set_row_spacing:
- * @box: a #GtkFlowBox
- * @spacing: the spacing to use
- *
- * Sets the vertical space to add between children.
- * See the #GtkFlowBox:row-spacing property.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_row_spacing (GtkFlowBox *box,
- guint spacing)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->row_spacing != spacing)
- {
- BOX_PRIV (box)->row_spacing = spacing;
-
- gtk_widget_queue_resize (GTK_WIDGET (box));
- g_object_notify (G_OBJECT (box), "row-spacing");
- }
-}
-
-/**
- * gtk_flow_box_get_row_spacing:
- * @box: a #GtkFlowBox
- *
- * Gets the vertical spacing.
- *
- * Returns: the vertical spacing
- *
- * Since: 3.12
- */
-guint
-gtk_flow_box_get_row_spacing (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->row_spacing;
-}
-
-/**
- * gtk_flow_box_set_column_spacing:
- * @box: a #GtkFlowBox
- * @spacing: the spacing to use
- *
- * Sets the horizontal space to add between children.
- * See the #GtkFlowBox:column-spacing property.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_column_spacing (GtkFlowBox *box,
- guint spacing)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->column_spacing != spacing)
- {
- BOX_PRIV (box)->column_spacing = spacing;
-
- gtk_widget_queue_resize (GTK_WIDGET (box));
- g_object_notify (G_OBJECT (box), "column-spacing");
- }
-}
-
-/**
- * gtk_flow_box_get_column_spacing:
- * @box: a #GtkFlowBox
- *
- * Gets the horizontal spacing.
- *
- * Returns: the horizontal spacing
- *
- * Since: 3.12
- */
-guint
-gtk_flow_box_get_column_spacing (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->column_spacing;
-}
-
-/**
- * gtk_flow_box_set_min_children_per_line:
- * @box: a #GtkFlowBox
- * @n_children: the minimum number of children per line
- *
- * Sets the minimum number of children to line up
- * in @box’s orientation before flowing.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_min_children_per_line (GtkFlowBox *box,
- guint n_children)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->min_children_per_line != n_children)
- {
- BOX_PRIV (box)->min_children_per_line = n_children;
-
- gtk_widget_queue_resize (GTK_WIDGET (box));
- g_object_notify (G_OBJECT (box), "min-children-per-line");
- }
-}
-
-/**
- * gtk_flow_box_get_min_children_per_line:
- * @box: a #GtkFlowBox
- *
- * Gets the minimum number of children per line.
- *
- * Returns: the minimum number of children per line
- *
- * Since: 3.12
- */
-guint
-gtk_flow_box_get_min_children_per_line (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->min_children_per_line;
-}
-
-/**
- * gtk_flow_box_set_max_children_per_line:
- * @box: a #GtkFlowBox
- * @n_children: the maximum number of children per line
- *
- * Sets the maximum number of children to request and
- * allocate space for in @box’s orientation.
- *
- * Setting the maximum number of children per line
- * limits the overall natural size request to be no more
- * than @n_children children long in the given orientation.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_max_children_per_line (GtkFlowBox *box,
- guint n_children)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->max_children_per_line != n_children)
- {
- BOX_PRIV (box)->max_children_per_line = n_children;
-
- gtk_widget_queue_resize (GTK_WIDGET (box));
- g_object_notify (G_OBJECT (box), "max-children-per-line");
- }
-}
-
-/**
- * gtk_flow_box_get_max_children_per_line:
- * @box: a #GtkFlowBox
- *
- * Gets the maximum number of children per line.
- *
- * Returns: the maximum number of children per line
- *
- * Since: 3.12
- */
-guint
-gtk_flow_box_get_max_children_per_line (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->max_children_per_line;
-}
-
-/**
- * gtk_flow_box_set_activate_on_single_click:
- * @box: a #GtkFlowBox
- * @single: %TRUE to emit child-activated on a single click
- *
- * If @single is %TRUE, children will be activated when you click
- * on them, otherwise you need to double-click.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_activate_on_single_click (GtkFlowBox *box,
- gboolean single)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- single = single != FALSE;
-
- if (BOX_PRIV (box)->activate_on_single_click != single)
- {
- BOX_PRIV (box)->activate_on_single_click = single;
- g_object_notify (G_OBJECT (box), "activate-on-single-click");
- }
-}
-
-/**
- * gtk_flow_box_get_activate_on_single_click:
- * @box: a #GtkFlowBox
- *
- * Returns whether children activate on single clicks.
- *
- * Returns: %TRUE if children are activated on single click,
- * %FALSE otherwise
- *
- * Since: 3.12
- */
-gboolean
-gtk_flow_box_get_activate_on_single_click (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->activate_on_single_click;
-}
-
- /* Selection handling {{{2 */
-
-/**
- * gtk_flow_box_get_selected_children:
- * @box: a #GtkFlowBox
- *
- * Creates a list of all selected children.
- *
- * Returns: (element-type GtkFlowBoxChild) (transfer container):
- * A #GList containing the #GtkWidget for each selected child.
- * Free with g_list_free() when done.
- *
- * Since: 3.12
- */
-GList *
-gtk_flow_box_get_selected_children (GtkFlowBox *box)
-{
- GtkFlowBoxChild *child;
- GSequenceIter *iter;
- GList *selected = NULL;
-
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), NULL);
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- child = g_sequence_get (iter);
- if (CHILD_PRIV (child)->selected)
- selected = g_list_prepend (selected, child);
- }
-
- return g_list_reverse (selected);
-}
-
-/**
- * gtk_flow_box_select_child:
- * @box: a #GtkFlowBox
- * @child: a child of @box
- *
- * Selects a single child of @box, if the selection
- * mode allows it.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_select_child (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
- g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child));
-
- gtk_flow_box_select_child_internal (box, child);
-}
-
-/**
- * gtk_flow_box_unselect_child:
- * @box: a #GtkFlowBox
- * @child: a child of @box
- *
- * Unselects a single child of @box, if the selection
- * mode allows it.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_unselect_child (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
- g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child));
-
- gtk_flow_box_unselect_child_internal (box, child);
-}
-
-/**
- * gtk_flow_box_select_all:
- * @box: a #GtkFlowBox
- *
- * Select all children of @box, if the selection
- * mode allows it.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_select_all (GtkFlowBox *box)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE)
- return;
-
- if (g_sequence_get_length (BOX_PRIV (box)->children) > 0)
- {
- gtk_flow_box_select_all_between (box, NULL, NULL, FALSE);
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
- }
-}
-
-/**
- * gtk_flow_box_unselect_all:
- * @box: a #GtkFlowBox
- *
- * Unselect all children of @box, if the selection
- * mode allows it.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_unselect_all (GtkFlowBox *box)
-{
- gboolean dirty = FALSE;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_BROWSE)
- return;
-
- dirty = gtk_flow_box_unselect_all_internal (box);
-
- if (dirty)
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-/**
- * GtkFlowBoxForeachFunc:
- * @box: a #GtkFlowBox
- * @child: a #GtkFlowBoxChild
- * @user_data: (closure): user data
- *
- * A function used by gtk_flow_box_selected_foreach().
- * It will be called on every selected child of the @box.
- *
- * Since: 3.12
- */
-
-/**
- * gtk_flow_box_selected_foreach:
- * @box: a #GtkFlowBox
- * @func: (scope call): the function to call for each selected child
- * @data: user data to pass to the function
- *
- * Calls a function for each selected child.
- *
- * Note that the selection cannot be modified from within
- * this function.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_selected_foreach (GtkFlowBox *box,
- GtkFlowBoxForeachFunc func,
- gpointer data)
-{
- GtkFlowBoxChild *child;
- GSequenceIter *iter;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- child = g_sequence_get (iter);
- if (CHILD_PRIV (child)->selected)
- (*func) (box, child, data);
- }
-}
-
-/**
- * gtk_flow_box_set_selection_mode:
- * @box: a #GtkFlowBox
- * @mode: the new selection mode
- *
- * Sets how selection works in @box.
- * See #GtkSelectionMode for details.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_selection_mode (GtkFlowBox *box,
- GtkSelectionMode mode)
-{
- gboolean dirty = FALSE;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (mode == BOX_PRIV (box)->selection_mode)
- return;
-
- if (mode == GTK_SELECTION_NONE ||
- BOX_PRIV (box)->selection_mode == GTK_SELECTION_MULTIPLE)
- {
- dirty = gtk_flow_box_unselect_all_internal (box);
- BOX_PRIV (box)->selected_child = NULL;
- }
-
- BOX_PRIV (box)->selection_mode = mode;
-
- g_object_notify (G_OBJECT (box), "selection-mode");
-
- if (dirty)
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-/**
- * gtk_flow_box_get_selection_mode:
- * @box: a #GtkFlowBox
- *
- * Gets the selection mode of @box.
- *
- * Returns: the #GtkSelectionMode
- *
- * Since: 3.12
- */
-GtkSelectionMode
-gtk_flow_box_get_selection_mode (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), GTK_SELECTION_SINGLE);
-
- return BOX_PRIV (box)->selection_mode;
-}
-
-/* Filtering {{{2 */
-
-/**
- * GtkFlowBoxFilterFunc:
- * @child: a #GtkFlowBoxChild that may be filtered
- * @user_data: (closure): user data
- *
- * A function that will be called whenrever a child changes
- * or is added. It lets you control if the child should be
- * visible or not.
- *
- * Returns: %TRUE if the row should be visible, %FALSE otherwise
- *
- * Since: 3.12
- */
-
-/**
- * gtk_flow_box_set_filter_func:
- * @box: a #GtkFlowBox
- * @filter_func: (closure user_data) (allow-none): callback that
- * lets you filter which children to show
- * @user_data: user data passed to @filter_func
- * @destroy: destroy notifier for @user_data
- *
- * By setting a filter function on the @box one can decide dynamically
- * which of the children to show. For instance, to implement a search
- * function that only shows the children matching the search terms.
- *
- * The @filter_func will be called for each child after the call, and
- * it will continue to be called each time a child changes (via
- * gtk_flow_box_child_changed()) or when gtk_flow_box_invalidate_filter()
- * is called.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_filter_func (GtkFlowBox *box,
- GtkFlowBoxFilterFunc filter_func,
- gpointer user_data,
- GDestroyNotify destroy)
-{
- GtkFlowBoxPrivate *priv;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- priv = BOX_PRIV (box);
-
- if (priv->filter_destroy != NULL)
- priv->filter_destroy (priv->filter_data);
-
- priv->filter_func = filter_func;
- priv->filter_data = user_data;
- priv->filter_destroy = destroy;
-
- gtk_flow_box_apply_filter_all (box);
-}
-
-/**
- * gtk_flow_box_invalidate_filter:
- * @box: a #GtkFlowBox
- *
- * Updates the filtering for all children.
- *
- * Call this function when the result of the filter
- * function on the @box is changed due ot an external
- * factor. For instance, this would be used if the
- * filter function just looked for a specific search
- * term, and the entry with the string has changed.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_invalidate_filter (GtkFlowBox *box)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->filter_func != NULL)
- gtk_flow_box_apply_filter_all (box);
-}
-
-/* Sorting {{{2 */
-
-/**
- * GtkFlowBoxSortFunc:
- * @child1: the first child
- * @child2: the second child
- * @user_data: (closure): user data
- *
- * A function to compare two children to determine which
- * should come first.
- *
- * Returns: < 0 if @child1 should be before @child2, 0 if
- * the are equal, and > 0 otherwise
- *
- * Since: 3.12
- */
-
-/**
- * gtk_flow_box_set_sort_func:
- * @box: a #GtkFlowBox
- * @sort_func: (closure user_data) (allow-none): the sort function
- * @user_data: user data passed to @sort_func
- * @destroy: destroy notifier for @user_data
- *
- * By setting a sort function on the @box, one can dynamically
- * reorder the children of the box, based on the contents of
- * the children.
- *
- * The @sort_func will be called for each child after the call,
- * and will continue to be called each time a child changes (via
- * gtk_flow_box_child_changed()) and when gtk_flow_box_invalidate_sort()
- * is called.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_sort_func (GtkFlowBox *box,
- GtkFlowBoxSortFunc sort_func,
- gpointer user_data,
- GDestroyNotify destroy)
-{
- GtkFlowBoxPrivate *priv;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- priv = BOX_PRIV (box);
-
- if (priv->sort_destroy != NULL)
- priv->sort_destroy (priv->sort_data);
-
- priv->sort_func = sort_func;
- priv->sort_data = user_data;
- priv->sort_destroy = destroy;
-
- gtk_flow_box_invalidate_sort (box);
-}
-
-static gint
-gtk_flow_box_sort (GtkFlowBoxChild *a,
- GtkFlowBoxChild *b,
- GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- return priv->sort_func (a, b, priv->sort_data);
-}
-
-/**
- * gtk_flow_box_invalidate_sort:
- * @box: a #GtkFlowBox
- *
- * Updates the sorting for all children.
- *
- * Call this when the result of the sort function on
- * @box is changed due to an external factor.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_invalidate_sort (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- priv = BOX_PRIV (box);
-
- if (priv->sort_func != NULL)
- {
- g_sequence_sort (priv->children,
- (GCompareDataFunc)gtk_flow_box_sort, box);
- gtk_widget_queue_resize (GTK_WIDGET (box));
- }
-}
-
-/* vim:set foldmethod=marker expandtab: */
diff --git a/src/interface-gtk/gtkflowbox.h b/src/interface-gtk/gtkflowbox.h
deleted file mode 100644
index 6f0549f..0000000
--- a/src/interface-gtk/gtkflowbox.h
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2010 Openismus GmbH
- * Copyright (C) 2013 Red Hat, Inc.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Library General Public
- * License as published by the Free Software Foundation; either
- * version 2 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Library General Public License for more details.
- *
- * You should have received a copy of the GNU Library General Public
- * License along with this library; if not, see <http://www.gnu.org/licenses/>.
-
- *
- * Authors:
- * Tristan Van Berkom <tristanvb@openismus.com>
- * Matthias Clasen <mclasen@redhat.com>
- * William Jon McCann <jmccann@redhat.com>
- */
-
-#ifndef __GTK_FLOW_BOX_H__
-#define __GTK_FLOW_BOX_H__
-
-#include <gtk/gtk.h>
-
-G_BEGIN_DECLS
-
-
-#define GTK_TYPE_FLOW_BOX (gtk_flow_box_get_type ())
-#define GTK_FLOW_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_FLOW_BOX, GtkFlowBox))
-#define GTK_FLOW_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_FLOW_BOX, GtkFlowBoxClass))
-#define GTK_IS_FLOW_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_FLOW_BOX))
-#define GTK_IS_FLOW_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_FLOW_BOX))
-#define GTK_FLOW_BOX_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_FLOW_BOX, GtkFlowBoxClass))
-
-typedef struct _GtkFlowBox GtkFlowBox;
-typedef struct _GtkFlowBoxClass GtkFlowBoxClass;
-
-typedef struct _GtkFlowBoxChild GtkFlowBoxChild;
-typedef struct _GtkFlowBoxChildClass GtkFlowBoxChildClass;
-
-struct _GtkFlowBox
-{
- GtkContainer container;
-};
-
-struct _GtkFlowBoxClass
-{
- GtkContainerClass parent_class;
-
- void (*child_activated) (GtkFlowBox *box,
- GtkFlowBoxChild *child);
- void (*selected_children_changed) (GtkFlowBox *box);
- void (*activate_cursor_child) (GtkFlowBox *box);
- void (*toggle_cursor_child) (GtkFlowBox *box);
- void (*move_cursor) (GtkFlowBox *box,
- GtkMovementStep step,
- gint count);
- void (*select_all) (GtkFlowBox *box);
- void (*unselect_all) (GtkFlowBox *box);
-
- /* Padding for future expansion */
- void (*_gtk_reserved1) (void);
- void (*_gtk_reserved2) (void);
- void (*_gtk_reserved3) (void);
- void (*_gtk_reserved4) (void);
- void (*_gtk_reserved5) (void);
- void (*_gtk_reserved6) (void);
-};
-
-#define GTK_TYPE_FLOW_BOX_CHILD (gtk_flow_box_child_get_type ())
-#define GTK_FLOW_BOX_CHILD(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChild))
-#define GTK_FLOW_BOX_CHILD_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChildClass))
-#define GTK_IS_FLOW_BOX_CHILD(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_FLOW_BOX_CHILD))
-#define GTK_IS_FLOW_BOX_CHILD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_FLOW_BOX_CHILD))
-#define GTK_FLOW_BOX_CHILD_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EG_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChildClass))
-
-struct _GtkFlowBoxChild
-{
- GtkBin parent_instance;
-};
-
-struct _GtkFlowBoxChildClass
-{
- GtkBinClass parent_class;
-
- void (* activate) (GtkFlowBoxChild *child);
-
- /* Padding for future expansion */
- void (*_gtk_reserved1) (void);
- void (*_gtk_reserved2) (void);
-};
-
-GType gtk_flow_box_child_get_type (void) G_GNUC_CONST;
-GtkWidget* gtk_flow_box_child_new (void);
-gint gtk_flow_box_child_get_index (GtkFlowBoxChild *child);
-gboolean gtk_flow_box_child_is_selected (GtkFlowBoxChild *child);
-void gtk_flow_box_child_changed (GtkFlowBoxChild *child);
-
-
-GType gtk_flow_box_get_type (void) G_GNUC_CONST;
-
-GtkWidget *gtk_flow_box_new (void);
-void gtk_flow_box_set_homogeneous (GtkFlowBox *box,
- gboolean homogeneous);
-gboolean gtk_flow_box_get_homogeneous (GtkFlowBox *box);
-void gtk_flow_box_set_row_spacing (GtkFlowBox *box,
- guint spacing);
-guint gtk_flow_box_get_row_spacing (GtkFlowBox *box);
-
-void gtk_flow_box_set_column_spacing (GtkFlowBox *box,
- guint spacing);
-guint gtk_flow_box_get_column_spacing (GtkFlowBox *box);
-
-void gtk_flow_box_set_min_children_per_line (GtkFlowBox *box,
- guint n_children);
-guint gtk_flow_box_get_min_children_per_line (GtkFlowBox *box);
-
-void gtk_flow_box_set_max_children_per_line (GtkFlowBox *box,
- guint n_children);
-guint gtk_flow_box_get_max_children_per_line (GtkFlowBox *box);
-void gtk_flow_box_set_activate_on_single_click (GtkFlowBox *box,
- gboolean single);
-gboolean gtk_flow_box_get_activate_on_single_click (GtkFlowBox *box);
-
-void gtk_flow_box_insert (GtkFlowBox *box,
- GtkWidget *widget,
- gint position);
-GtkFlowBoxChild *gtk_flow_box_get_child_at_index (GtkFlowBox *box,
- gint idx);
-
-typedef void (* GtkFlowBoxForeachFunc) (GtkFlowBox *box,
- GtkFlowBoxChild *child,
- gpointer user_data);
-
-void gtk_flow_box_selected_foreach (GtkFlowBox *box,
- GtkFlowBoxForeachFunc func,
- gpointer data);
-GList *gtk_flow_box_get_selected_children (GtkFlowBox *box);
-void gtk_flow_box_select_child (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-void gtk_flow_box_unselect_child (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-void gtk_flow_box_select_all (GtkFlowBox *box);
-void gtk_flow_box_unselect_all (GtkFlowBox *box);
-void gtk_flow_box_set_selection_mode (GtkFlowBox *box,
- GtkSelectionMode mode);
-GtkSelectionMode gtk_flow_box_get_selection_mode (GtkFlowBox *box);
-void gtk_flow_box_set_hadjustment (GtkFlowBox *box,
- GtkAdjustment *adjustment);
-void gtk_flow_box_set_vadjustment (GtkFlowBox *box,
- GtkAdjustment *adjustment);
-
-typedef gboolean (*GtkFlowBoxFilterFunc) (GtkFlowBoxChild *child,
- gpointer user_data);
-
-void gtk_flow_box_set_filter_func (GtkFlowBox *box,
- GtkFlowBoxFilterFunc filter_func,
- gpointer user_data,
- GDestroyNotify destroy);
-void gtk_flow_box_invalidate_filter (GtkFlowBox *box);
-
-typedef gint (*GtkFlowBoxSortFunc) (GtkFlowBoxChild *child1,
- GtkFlowBoxChild *child2,
- gpointer user_data);
-
-void gtk_flow_box_set_sort_func (GtkFlowBox *box,
- GtkFlowBoxSortFunc sort_func,
- gpointer user_data,
- GDestroyNotify destroy);
-void gtk_flow_box_invalidate_sort (GtkFlowBox *box);
-
-G_END_DECLS
-
-
-#endif /* __GTK_FLOW_BOX_H__ */
diff --git a/src/interface-gtk/interface-gtk.cpp b/src/interface-gtk/interface-gtk.cpp
deleted file mode 100644
index 9486802..0000000
--- a/src/interface-gtk/interface-gtk.cpp
+++ /dev/null
@@ -1,1132 +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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <stdarg.h>
-#include <string.h>
-#include <signal.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-/*
- * FIXME: Because of gdk_threads_enter().
- * The only way to do it in Gtk3 style would be using
- * idle callbacks into the main thread and sync barriers (inefficient!)
- * or doing it single-threaded and ticking the Gtk main loop
- * (may be inefficient since gtk_events_pending() is doing
- * syscalls; however that may be ailed by doing it less frequently).
- */
-#define GDK_DISABLE_DEPRECATION_WARNINGS
-#include <gdk/gdk.h>
-#include <gdk-pixbuf/gdk-pixbuf.h>
-
-#include <gtk/gtk.h>
-
-#include <gio/gio.h>
-
-#include <Scintilla.h>
-#include <ScintillaWidget.h>
-
-#include "gtk-info-popup.h"
-#include "gtk-canonicalized-label.h"
-
-#include "sciteco.h"
-#include "string-utils.h"
-#include "cmdline.h"
-#include "qregisters.h"
-#include "ring.h"
-#include "interface.h"
-#include "interface-gtk.h"
-
-/*
- * Signal handlers (e.g. for handling SIGTERM) are only
- * available on Unix and beginning with v2.30, while
- * we still support v2.28.
- * Handlers using `signal()` cannot be used easily for
- * this purpose.
- */
-#if defined(G_OS_UNIX) && GLIB_CHECK_VERSION(2,30,0)
-#include <glib-unix.h>
-#define SCITECO_HANDLE_SIGNALS
-#endif
-
-namespace SciTECO {
-
-extern "C" {
-
-static void scintilla_notify(ScintillaObject *sci, uptr_t idFrom,
- SCNotification *notify, gpointer user_data);
-
-static gpointer exec_thread_cb(gpointer data);
-static gboolean cmdline_key_pressed_cb(GtkWidget *widget, GdkEventKey *event,
- gpointer user_data);
-static gboolean window_delete_cb(GtkWidget *w, GdkEventAny *e,
- gpointer user_data);
-
-static gboolean sigterm_handler(gpointer user_data) G_GNUC_UNUSED;
-
-static gboolean
-g_object_unref_idle_cb(gpointer user_data)
-{
- g_object_unref(user_data);
- return G_SOURCE_REMOVE;
-}
-
-} /* extern "C" */
-
-#define UNNAMED_FILE "(Unnamed)"
-
-#define USER_CSS_FILE ".teco_css"
-
-/** printf() format for CSS RGB colors given as guint32 */
-#define CSS_COLOR_FORMAT "#%06" G_GINT32_MODIFIER "X"
-
-/**
- * Convert Scintilla-style BGR color triple to
- * RGB.
- */
-static inline guint32
-bgr2rgb(guint32 bgr)
-{
- return ((bgr & 0x0000FF) << 16) |
- ((bgr & 0x00FF00) << 0) |
- ((bgr & 0xFF0000) >> 16);
-}
-
-void
-ViewGtk::initialize_impl(void)
-{
- gint events;
-
- gdk_threads_enter();
-
- sci = SCINTILLA(scintilla_new());
- /*
- * We don't want the object to be destroyed
- * when it is removed from the vbox.
- */
- g_object_ref_sink(sci);
-
- scintilla_set_id(sci, 0);
-
- gtk_widget_set_size_request(get_widget(), 500, 300);
-
- /*
- * This disables mouse and key events on this view.
- * For some strange reason, masking events on
- * the event box does NOT work.
- * NOTE: Scroll events are still allowed - scrolling
- * is currently not under direct control of SciTECO
- * (i.e. it is OK the side effects of scrolling are not
- * tracked).
- */
- gtk_widget_set_can_focus(get_widget(), FALSE);
- events = gtk_widget_get_events(get_widget());
- events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
- events &= ~(GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK);
- gtk_widget_set_events(get_widget(), events);
-
- g_signal_connect(sci, SCINTILLA_NOTIFY,
- G_CALLBACK(scintilla_notify), NULL);
-
- /*
- * setup() calls Scintilla messages, so we must unlock
- * here already to avoid deadlocks.
- */
- gdk_threads_leave();
-
- setup();
-}
-
-ViewGtk::~ViewGtk()
-{
- /*
- * This does NOT destroy the Scintilla object
- * and GTK widget, if it is the current view
- * (and therefore added to the vbox).
- * FIXME: This only uses an idle watcher
- * because the destructor can be called with
- * the Gdk lock held and without.
- * Once the threading model is revised this
- * can be simplified and inlined again.
- */
- if (sci)
- gdk_threads_add_idle(g_object_unref_idle_cb, sci);
-}
-
-GOptionGroup *
-InterfaceGtk::get_options(void)
-{
- const GOptionEntry entries[] = {
- {"no-csd", 0, G_OPTION_FLAG_IN_MAIN | G_OPTION_FLAG_REVERSE,
- G_OPTION_ARG_NONE, &use_csd,
- "Disable client-side decorations.", NULL},
- {NULL}
- };
-
- /*
- * Parsing the option context with the Gtk option group
- * will automatically initialize Gtk, but we do not yet
- * open the default display.
- */
- GOptionGroup *group = gtk_get_option_group(FALSE);
-
- g_option_group_add_entries(group, entries);
-
- return group;
-}
-
-void
-InterfaceGtk::init(void)
-{
- static const Cmdline empty_cmdline;
-
- GtkWidget *vbox;
- GtkWidget *overlay_widget, *overlay_vbox;
- GtkWidget *message_bar_content;
-
- /*
- * g_thread_init() is required prior to v2.32
- * (we still support v2.28) but generates a warning
- * on newer versions.
- */
-#if !GLIB_CHECK_VERSION(2,32,0)
- g_thread_init(NULL);
-#endif
- gdk_threads_init();
-
- /*
- * gtk_init() is not necessary when using gtk_get_option_group(),
- * but this will open the default display.
- * FIXME: Perhaps it is possible to defer this until we initialize
- * interactive mode!?
- */
- gtk_init(NULL, NULL);
-
- /*
- * Register clipboard registers.
- * Unfortunately, we cannot find out which
- * clipboards/selections are supported on this system,
- * so we register only some default ones.
- */
- QRegisters::globals.insert(new QRegisterClipboard());
- QRegisters::globals.insert(new QRegisterClipboard("P"));
- QRegisters::globals.insert(new QRegisterClipboard("S"));
- QRegisters::globals.insert(new QRegisterClipboard("C"));
-
- /*
- * The event queue is initialized now, so we can
- * pass it as user data to C-linkage callbacks.
- */
- event_queue = g_async_queue_new();
-
- window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
- g_signal_connect(G_OBJECT(window), "delete-event",
- G_CALLBACK(window_delete_cb), event_queue);
-
- vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-
- info_current = g_strdup("");
-
- /*
- * The info bar is tried to be made the title bar of the
- * window which also disables the default window decorations
- * (client-side decorations) unless --no-csd was specified.
- * NOTE: Client-side decoations could fail, leaving us with a
- * standard title bar and the info bar with close buttons.
- * Other window managers have undesirable side-effects.
- */
- info_bar_widget = gtk_header_bar_new();
- gtk_widget_set_name(info_bar_widget, "sciteco-info-bar");
- info_name_widget = gtk_canonicalized_label_new(NULL);
- gtk_widget_set_valign(info_name_widget, GTK_ALIGN_CENTER);
- gtk_style_context_add_class(gtk_widget_get_style_context(info_name_widget),
- "name-label");
- gtk_label_set_selectable(GTK_LABEL(info_name_widget), TRUE);
- /* NOTE: Header bar does not resize for multi-line labels */
- //gtk_label_set_line_wrap(GTK_LABEL(info_name_widget), TRUE);
- //gtk_label_set_lines(GTK_LABEL(info_name_widget), 2);
- gtk_header_bar_set_custom_title(GTK_HEADER_BAR(info_bar_widget), info_name_widget);
- info_image = gtk_image_new();
- gtk_header_bar_pack_start(GTK_HEADER_BAR(info_bar_widget), info_image);
- info_type_widget = gtk_label_new(NULL);
- gtk_widget_set_valign(info_type_widget, GTK_ALIGN_CENTER);
- gtk_style_context_add_class(gtk_widget_get_style_context(info_type_widget),
- "type-label");
- gtk_header_bar_pack_start(GTK_HEADER_BAR(info_bar_widget), info_type_widget);
- if (use_csd) {
- /* use client-side decorations */
- gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(info_bar_widget), TRUE);
- gtk_window_set_titlebar(GTK_WINDOW(window), info_bar_widget);
- } else {
- /* fall back to adding the info bar as an ordinary widget */
- gtk_box_pack_start(GTK_BOX(vbox), info_bar_widget, FALSE, FALSE, 0);
- }
-
- /*
- * Overlay widget will allow overlaying the Scintilla view
- * and message widgets with the info popup.
- * Therefore overlay_vbox (containing the view and popup)
- * will be the main child of the overlay.
- */
- overlay_widget = gtk_overlay_new();
- overlay_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-
- /*
- * The event box is the parent of all Scintilla views
- * that should be displayed.
- * This is handy when adding or removing current views,
- * enabling and disabling GDK updates and in order to filter
- * mouse and keyboard events going to Scintilla.
- */
- event_box_widget = gtk_event_box_new();
- gtk_event_box_set_above_child(GTK_EVENT_BOX(event_box_widget), TRUE);
- gtk_box_pack_start(GTK_BOX(overlay_vbox), event_box_widget,
- TRUE, TRUE, 0);
-
- message_bar_widget = gtk_info_bar_new();
- gtk_widget_set_name(message_bar_widget, "sciteco-message-bar");
- message_bar_content = gtk_info_bar_get_content_area(GTK_INFO_BAR(message_bar_widget));
- /* NOTE: Messages are always pre-canonicalized */
- message_widget = gtk_label_new(NULL);
- gtk_label_set_selectable(GTK_LABEL(message_widget), TRUE);
- gtk_label_set_line_wrap(GTK_LABEL(message_widget), TRUE);
- gtk_container_add(GTK_CONTAINER(message_bar_content), message_widget);
- gtk_box_pack_start(GTK_BOX(overlay_vbox), message_bar_widget,
- FALSE, FALSE, 0);
-
- gtk_container_add(GTK_CONTAINER(overlay_widget), overlay_vbox);
- gtk_box_pack_start(GTK_BOX(vbox), overlay_widget, TRUE, TRUE, 0);
-
- cmdline_widget = gtk_entry_new();
- gtk_widget_set_name(cmdline_widget, "sciteco-cmdline");
- gtk_entry_set_has_frame(GTK_ENTRY(cmdline_widget), FALSE);
- gtk_editable_set_editable(GTK_EDITABLE(cmdline_widget), FALSE);
- g_signal_connect(G_OBJECT(cmdline_widget), "key-press-event",
- G_CALLBACK(cmdline_key_pressed_cb), event_queue);
- gtk_box_pack_start(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0);
-
- gtk_container_add(GTK_CONTAINER(window), vbox);
-
- /*
- * Popup widget will be shown in the bottom
- * of the overlay widget (i.e. the Scintilla views),
- * filling the entire width.
- */
- popup_widget = gtk_info_popup_new();
- gtk_widget_set_name(popup_widget, "sciteco-info-popup");
- gtk_overlay_add_overlay(GTK_OVERLAY(overlay_widget), popup_widget);
- g_signal_connect(overlay_widget, "get-child-position",
- G_CALLBACK(gtk_info_popup_get_position_in_overlay), NULL);
-
- gtk_widget_grab_focus(cmdline_widget);
-
- cmdline_update(&empty_cmdline);
-}
-
-void
-InterfaceGtk::vmsg_impl(MessageType type, const gchar *fmt, va_list ap)
-{
- /*
- * The message types are chosen such that there is a CSS class
- * for every one of them. GTK_MESSAGE_OTHER does not have
- * a CSS class.
- */
- static const GtkMessageType type2gtk[] = {
- /* [MSG_USER] = */ GTK_MESSAGE_QUESTION,
- /* [MSG_INFO] = */ GTK_MESSAGE_INFO,
- /* [MSG_WARNING] = */ GTK_MESSAGE_WARNING,
- /* [MSG_ERROR] = */ GTK_MESSAGE_ERROR
- };
-
- va_list aq;
- gchar buf[255];
-
- /*
- * stdio_vmsg() leaves `ap` undefined and we are expected
- * to do the same and behave like vprintf().
- */
- va_copy(aq, ap);
- stdio_vmsg(type, fmt, ap);
- g_vsnprintf(buf, sizeof(buf), fmt, aq);
- va_end(aq);
-
- gdk_threads_enter();
-
- gtk_info_bar_set_message_type(GTK_INFO_BAR(message_bar_widget),
- type2gtk[type]);
- gtk_label_set_text(GTK_LABEL(message_widget), buf);
-
- if (type == MSG_ERROR)
- gtk_widget_error_bell(window);
-
- gdk_threads_leave();
-}
-
-void
-InterfaceGtk::msg_clear(void)
-{
- gdk_threads_enter();
-
- gtk_info_bar_set_message_type(GTK_INFO_BAR(message_bar_widget),
- GTK_MESSAGE_QUESTION);
- gtk_label_set_text(GTK_LABEL(message_widget), "");
-
- gdk_threads_leave();
-}
-
-void
-InterfaceGtk::show_view_impl(ViewGtk *view)
-{
- current_view = view;
-}
-
-void
-InterfaceGtk::refresh_info(void)
-{
- GtkStyleContext *style = gtk_widget_get_style_context(info_bar_widget);
- const gchar *info_type_str = PACKAGE;
- gchar *info_current_temp = g_strdup(info_current);
- gchar *info_current_canon;
- GIcon *icon;
- gchar *title;
-
- gtk_style_context_remove_class(style, "info-qregister");
- gtk_style_context_remove_class(style, "info-buffer");
- gtk_style_context_remove_class(style, "dirty");
-
- if (info_type == INFO_TYPE_BUFFER_DIRTY)
- String::append(info_current_temp, "*");
- gtk_canonicalized_label_set_text(GTK_CANONICALIZED_LABEL(info_name_widget),
- info_current_temp);
- info_current_canon = String::canonicalize_ctl(info_current_temp);
- g_free(info_current_temp);
-
- switch (info_type) {
- case INFO_TYPE_QREGISTER:
- gtk_style_context_add_class(style, "info-qregister");
-
- info_type_str = PACKAGE_NAME " - <QRegister> ";
- gtk_label_set_text(GTK_LABEL(info_type_widget), "QRegister");
- gtk_label_set_ellipsize(GTK_LABEL(info_name_widget),
- PANGO_ELLIPSIZE_START);
-
- /* FIXME: Use a Q-Register icon */
- gtk_image_clear(GTK_IMAGE(info_image));
- break;
-
- case INFO_TYPE_BUFFER_DIRTY:
- gtk_style_context_add_class(style, "dirty");
- /* fall through */
- case INFO_TYPE_BUFFER:
- gtk_style_context_add_class(style, "info-buffer");
-
- info_type_str = PACKAGE_NAME " - <Buffer> ";
- gtk_label_set_text(GTK_LABEL(info_type_widget), "Buffer");
- gtk_label_set_ellipsize(GTK_LABEL(info_name_widget),
- PANGO_ELLIPSIZE_MIDDLE);
-
- icon = gtk_info_popup_get_icon_for_path(info_current,
- "text-x-generic");
- if (!icon)
- break;
- gtk_image_set_from_gicon(GTK_IMAGE(info_image),
- icon, GTK_ICON_SIZE_LARGE_TOOLBAR);
- g_object_unref(icon);
- break;
- }
-
- title = g_strconcat(info_type_str, info_current_canon, NIL);
- gtk_window_set_title(GTK_WINDOW(window), title);
- g_free(title);
- g_free(info_current_canon);
-}
-
-void
-InterfaceGtk::info_update_impl(const QRegister *reg)
-{
- g_free(info_current);
- info_type = INFO_TYPE_QREGISTER;
- /* NOTE: will contain control characters */
- info_current = g_strdup(reg->name);
-}
-
-void
-InterfaceGtk::info_update_impl(const Buffer *buffer)
-{
- g_free(info_current);
- info_type = buffer->dirty ? INFO_TYPE_BUFFER_DIRTY
- : INFO_TYPE_BUFFER;
- info_current = g_strdup(buffer->filename ? : UNNAMED_FILE);
-}
-
-void
-InterfaceGtk::cmdline_insert_chr(gint &pos, gchar chr)
-{
- gchar buffer[5+1];
-
- /*
- * NOTE: This mapping is similar to
- * View::set_representations()
- */
- switch (chr) {
- case CTL_KEY_ESC:
- strcpy(buffer, "$");
- break;
- case '\r':
- strcpy(buffer, "<CR>");
- break;
- case '\n':
- strcpy(buffer, "<LF>");
- break;
- case '\t':
- strcpy(buffer, "<TAB>");
- break;
- default:
- if (IS_CTL(chr)) {
- buffer[0] = '^';
- buffer[1] = CTL_ECHO(chr);
- buffer[2] = '\0';
- } else {
- buffer[0] = chr;
- buffer[1] = '\0';
- }
- }
-
- gtk_editable_insert_text(GTK_EDITABLE(cmdline_widget),
- buffer, -1, &pos);
-}
-
-void
-InterfaceGtk::cmdline_update_impl(const Cmdline *cmdline)
-{
- gint pos = 1;
- gint cmdline_len;
-
- gdk_threads_enter();
-
- /*
- * We don't know if the new command line is similar to
- * the old one, so we can just as well rebuild it.
- */
- gtk_entry_set_text(GTK_ENTRY(cmdline_widget), "*");
-
- /* format effective command line */
- for (guint i = 0; i < cmdline->len; i++)
- cmdline_insert_chr(pos, (*cmdline)[i]);
- /* save end of effective command line */
- cmdline_len = pos;
-
- /* format rubbed out command line */
- for (guint i = cmdline->len; i < cmdline->len+cmdline->rubout_len; i++)
- cmdline_insert_chr(pos, (*cmdline)[i]);
-
- /* set cursor after effective command line */
- gtk_editable_set_position(GTK_EDITABLE(cmdline_widget), cmdline_len);
-
- gdk_threads_leave();
-}
-
-static GdkAtom
-get_selection_by_name(const gchar *name)
-{
- /*
- * We can use gdk_atom_intern() to support arbitrary X11 selection
- * names. However, since we cannot find out which selections are
- * registered, we are only providing QRegisters for the three default
- * selections.
- * Checking them here avoids expensive X server roundtrips.
- */
- switch (*name) {
- case '\0': return GDK_NONE;
- case 'P': return GDK_SELECTION_PRIMARY;
- case 'S': return GDK_SELECTION_SECONDARY;
- case 'C': return GDK_SELECTION_CLIPBOARD;
- default: break;
- }
-
- return gdk_atom_intern(name, FALSE);
-}
-
-void
-InterfaceGtk::set_clipboard(const gchar *name, const gchar *str, gssize str_len)
-{
- GtkClipboard *clipboard;
-
- gdk_threads_enter();
-
- clipboard = gtk_clipboard_get(get_selection_by_name(name));
-
- /*
- * NOTE: function has compatible semantics for str_len < 0.
- */
- gtk_clipboard_set_text(clipboard, str, str_len);
-
- gdk_threads_leave();
-}
-
-gchar *
-InterfaceGtk::get_clipboard(const gchar *name, gsize *str_len)
-{
- GtkClipboard *clipboard;
- gchar *str;
-
- gdk_threads_enter();
-
- clipboard = gtk_clipboard_get(get_selection_by_name(name));
- /*
- * Could return NULL for an empty clipboard.
- * NOTE: This converts to UTF8 and we loose the ability
- * to get clipboard with embedded nulls.
- */
- str = gtk_clipboard_wait_for_text(clipboard);
-
- gdk_threads_leave();
-
- if (str_len)
- *str_len = str ? strlen(str) : 0;
- return str;
-}
-
-void
-InterfaceGtk::popup_add_impl(PopupEntryType type,
- const gchar *name, bool highlight)
-{
- static const GtkInfoPopupEntryType type2gtk[] = {
- /* [POPUP_PLAIN] = */ GTK_INFO_POPUP_PLAIN,
- /* [POPUP_FILE] = */ GTK_INFO_POPUP_FILE,
- /* [POPUP_DIRECTORY] = */ GTK_INFO_POPUP_DIRECTORY
- };
-
- gdk_threads_enter();
-
- gtk_info_popup_add(GTK_INFO_POPUP(popup_widget),
- type2gtk[type], name, highlight);
-
- gdk_threads_leave();
-}
-
-void
-InterfaceGtk::popup_show_impl(void)
-{
- gdk_threads_enter();
-
- if (gtk_widget_get_visible(popup_widget))
- gtk_info_popup_scroll_page(GTK_INFO_POPUP(popup_widget));
- else
- gtk_widget_show(popup_widget);
-
- gdk_threads_leave();
-}
-
-void
-InterfaceGtk::popup_clear_impl(void)
-{
- gdk_threads_enter();
-
- if (gtk_widget_get_visible(popup_widget)) {
- gtk_widget_hide(popup_widget);
- gtk_info_popup_clear(GTK_INFO_POPUP(popup_widget));
- }
-
- gdk_threads_leave();
-}
-
-void
-InterfaceGtk::set_css_variables_from_view(ViewGtk *view)
-{
- guint font_size;
- gchar buffer[256];
-
- /*
- * Unfortunately, we cannot use CSS variables to pass around
- * font names and sizes, necessary for styling the command line
- * widget.
- * Therefore we just style it using generated CSS here.
- * This one of the few non-deprecated ways that Gtk leaves us
- * to set a custom font name.
- * CSS customizations have to take that into account.
- * NOTE: We don't actually know apriori how
- * large our buffer should be, but luckily STYLEGETFONT with
- * a sptr==0 will return only the size.
- * This is undocumented in the Scintilla docs.
- */
- gchar font_name[view->ssm(SCI_STYLEGETFONT, STYLE_DEFAULT) + 1];
- view->ssm(SCI_STYLEGETFONT, STYLE_DEFAULT, (sptr_t)font_name);
- font_size = view->ssm(SCI_STYLEGETSIZEFRACTIONAL, STYLE_DEFAULT);
-
- /*
- * Generates a CSS that sets some predefined color variables.
- * This effectively "exports" Scintilla styles into the CSS
- * world.
- * Those colors are used by the fallback.css shipping with SciTECO
- * in order to apply the SciTECO-controlled color scheme to all the
- * predefined UI elements.
- * They can also be used in user-customizations.
- */
- g_snprintf(buffer, sizeof(buffer),
- "@define-color sciteco_default_fg_color " CSS_COLOR_FORMAT ";"
- "@define-color sciteco_default_bg_color " CSS_COLOR_FORMAT ";"
- "@define-color sciteco_calltip_fg_color " CSS_COLOR_FORMAT ";"
- "@define-color sciteco_calltip_bg_color " CSS_COLOR_FORMAT ";"
- "#%s{"
- "font: %s %u.%u"
- "}",
- bgr2rgb(view->ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)),
- bgr2rgb(view->ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)),
- bgr2rgb(view->ssm(SCI_STYLEGETFORE, STYLE_CALLTIP)),
- bgr2rgb(view->ssm(SCI_STYLEGETBACK, STYLE_CALLTIP)),
- gtk_widget_get_name(cmdline_widget),
- font_name,
- font_size / SC_FONT_SIZE_MULTIPLIER,
- font_size % SC_FONT_SIZE_MULTIPLIER);
-
- /*
- * The GError and return value has been deprecated.
- * A CSS parsing error would point to a programming
- * error anyway.
- */
- gtk_css_provider_load_from_data(css_var_provider, buffer, -1, NULL);
-}
-
-void
-InterfaceGtk::event_loop_impl(void)
-{
- static const gchar *icon_files[] = {
- SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-16.png",
- SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-32.png",
- SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-48.png",
- NULL
- };
-
- GdkScreen *default_screen = gdk_screen_get_default();
- GtkCssProvider *user_css_provider;
- gchar *config_path, *user_css_file;
-
- GList *icon_list = NULL;
- GThread *thread;
-
- /*
- * Assign an icon to the window.
- * If the file could not be found, we fail silently.
- * FIXME: On Windows, it may be better to load the icon compiled
- * as a resource into the binary.
- */
- for (const gchar **file = icon_files; *file; file++) {
- GdkPixbuf *icon_pixbuf = gdk_pixbuf_new_from_file(*file, NULL);
-
- /* fail silently if there's a problem with one of the icons */
- if (icon_pixbuf)
- icon_list = g_list_append(icon_list, icon_pixbuf);
- }
-
- gtk_window_set_default_icon_list(icon_list);
-
- if (icon_list)
- g_list_free_full(icon_list, g_object_unref);
-
- refresh_info();
-
- /*
- * Initialize the CSS variable provider and the CSS provider
- * for the included fallback.css.
- * NOTE: The return value of gtk_css_provider_load() is deprecated.
- * Instead we could register for the "parsing-error" signal.
- * For the time being we just silently ignore parsing errors.
- * They will be printed to stderr by Gtk anyway.
- */
- css_var_provider = gtk_css_provider_new();
- if (current_view)
- /* set CSS variables initially */
- set_css_variables_from_view(current_view);
- gtk_style_context_add_provider_for_screen(default_screen,
- GTK_STYLE_PROVIDER(css_var_provider),
- GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
- user_css_provider = gtk_css_provider_new();
- /* get path of $SCITECOCONFIG/.teco_css */
- config_path = QRegisters::globals["$SCITECOCONFIG"]->get_string();
- user_css_file = g_build_filename(config_path, USER_CSS_FILE, NIL);
- if (g_file_test(user_css_file, G_FILE_TEST_IS_REGULAR))
- /* open user CSS */
- gtk_css_provider_load_from_path(user_css_provider,
- user_css_file, NULL);
- else
- /* use fallback CSS */
- gtk_css_provider_load_from_path(user_css_provider,
- SCITECODATADIR G_DIR_SEPARATOR_S
- "fallback.css",
- NULL);
- g_free(user_css_file);
- g_free(config_path);
- gtk_style_context_add_provider_for_screen(default_screen,
- GTK_STYLE_PROVIDER(user_css_provider),
- GTK_STYLE_PROVIDER_PRIORITY_USER);
-
- /*
- * When changing views, the new widget is not
- * added immediately to avoid flickering in the GUI.
- * It is only updated once per key press and only
- * if it really changed.
- * Therefore we must add the current view to the
- * window initially.
- * For the same reason, window title updates are
- * deferred to once after every key press, so we must
- * set the window title initially.
- */
- if (current_view) {
- current_view_widget = current_view->get_widget();
- gtk_container_add(GTK_CONTAINER(event_box_widget),
- current_view_widget);
- }
-
- gtk_widget_show_all(window);
- /* don't show popup by default */
- gtk_widget_hide(popup_widget);
-
- /*
- * SIGTERM emulates the "Close" key just like when
- * closing the window if supported by this version of glib.
- * Note that this replaces SciTECO's default SIGTERM handler
- * so it will additionally raise(SIGINT).
- */
-#ifdef SCITECO_HANDLE_SIGNALS
- g_unix_signal_add(SIGTERM, sigterm_handler, event_queue);
-#endif
-
- /*
- * Start up SciTECO execution thread.
- * Whenever it needs to send a Scintilla message
- * it locks the GDK mutex.
- */
- thread = g_thread_new("sciteco-exec",
- exec_thread_cb, event_queue);
-
- /*
- * NOTE: The watchers do not modify any GTK objects
- * using one of the methods that lock the GDK mutex.
- * This is from now on reserved to the execution
- * thread. Therefore there can be no dead-locks.
- */
- gdk_threads_enter();
- gtk_main();
- gdk_threads_leave();
-
- /*
- * This usually means that the user requested program
- * termination and the execution thread called
- * gtk_main_quit().
- * We still wait for the execution thread to shut down
- * properly. This also frees `thread`.
- */
- g_thread_join(thread);
-
- /*
- * Make sure the window is hidden
- * now already, as there may be code that has to be
- * executed in batch mode.
- */
- gtk_widget_hide(window);
-}
-
-static gpointer
-exec_thread_cb(gpointer data)
-{
- GAsyncQueue *event_queue = (GAsyncQueue *)data;
-
- for (;;) {
- GdkEventKey *event = (GdkEventKey *)g_async_queue_pop(event_queue);
-
- bool is_shift = event->state & GDK_SHIFT_MASK;
- bool is_ctl = event->state & GDK_CONTROL_MASK;
-
- try {
- sigint_occurred = FALSE;
- interface.handle_key_press(is_shift, is_ctl, event->keyval);
- sigint_occurred = FALSE;
- } catch (Quit) {
- /*
- * SciTECO should terminate, so we exit
- * this thread.
- * The main loop will terminate and
- * event_loop() will return.
- */
- gdk_event_free((GdkEvent *)event);
-
- gdk_threads_enter();
- gtk_main_quit();
- gdk_threads_leave();
- break;
- }
-
- gdk_event_free((GdkEvent *)event);
- }
-
- return NULL;
-}
-
-void
-InterfaceGtk::handle_key_press(bool is_shift, bool is_ctl, guint keyval)
-{
- GdkWindow *view_window;
- ViewGtk *last_view = current_view;
-
- /*
- * Avoid redraws of the current view by freezing updates
- * on the view's GDK window (we're running in parallel
- * to the main loop so there could be frequent redraws).
- * By freezing updates, the behaviour is similar to
- * the Curses UI.
- */
- gdk_threads_enter();
- view_window = gtk_widget_get_parent_window(event_box_widget);
- gdk_window_freeze_updates(view_window);
- gdk_threads_leave();
-
- switch (keyval) {
- case GDK_KEY_Escape:
- cmdline.keypress(CTL_KEY_ESC);
- break;
- case GDK_KEY_BackSpace:
- cmdline.keypress(CTL_KEY('H'));
- break;
- case GDK_KEY_Tab:
- cmdline.keypress('\t');
- break;
- case GDK_KEY_Return:
- cmdline.keypress('\n');
- break;
-
- /*
- * Function key macros
- */
-#define FN(KEY, MACRO) \
- case GDK_KEY_##KEY: cmdline.fnmacro(#MACRO); break
-#define FNS(KEY, MACRO) \
- case GDK_KEY_##KEY: cmdline.fnmacro(is_shift ? "S" #MACRO : #MACRO); break
- FN(Down, DOWN); FN(Up, UP);
- FNS(Left, LEFT); FNS(Right, RIGHT);
- FN(KP_Down, DOWN); FN(KP_Up, UP);
- FNS(KP_Left, LEFT); FNS(KP_Right, RIGHT);
- FNS(Home, HOME);
- case GDK_KEY_F1...GDK_KEY_F35: {
- gchar macro_name[3+1];
-
- g_snprintf(macro_name, sizeof(macro_name),
- "F%d", keyval - GDK_KEY_F1 + 1);
- cmdline.fnmacro(macro_name);
- break;
- }
- FNS(Delete, DC);
- FNS(Insert, IC);
- FN(Page_Down, NPAGE); FN(Page_Up, PPAGE);
- FNS(Print, PRINT);
- FN(KP_Home, A1); FN(KP_Prior, A3);
- FN(KP_Begin, B2);
- FN(KP_End, C1); FN(KP_Next, C3);
- FNS(End, END);
- FNS(Help, HELP);
- FN(Close, CLOSE);
-#undef FNS
-#undef FN
-
- /*
- * Control keys and keys with printable representation
- */
- default:
- gunichar u = gdk_keyval_to_unicode(keyval);
-
- if (u && g_unichar_to_utf8(u, NULL) == 1) {
- gchar key;
-
- g_unichar_to_utf8(u, &key);
- if (key > 0x7F)
- break;
- if (is_ctl)
- key = CTL_KEY(g_ascii_toupper(key));
-
- cmdline.keypress(key);
- }
- }
-
- /*
- * The styles configured via Scintilla might change
- * with every keypress.
- */
- set_css_variables_from_view(current_view);
-
- /*
- * The info area is updated very often and setting the
- * window title each time it is updated is VERY costly.
- * So we set it here once after every keypress even if the
- * info line did not change.
- * View changes are also only applied here to the GTK
- * window even though GDK updates have been frozen since
- * the size reallocations are very costly.
- */
- gdk_threads_enter();
-
- refresh_info();
-
- if (current_view != last_view) {
- /*
- * The last view's object is not guaranteed to
- * still exist.
- * However its widget is, due to reference counting.
- */
- if (current_view_widget)
- gtk_container_remove(GTK_CONTAINER(event_box_widget),
- current_view_widget);
-
- current_view_widget = current_view->get_widget();
-
- gtk_container_add(GTK_CONTAINER(event_box_widget),
- current_view_widget);
- gtk_widget_show(current_view_widget);
- }
-
- gdk_window_thaw_updates(view_window);
-
- gdk_threads_leave();
-}
-
-InterfaceGtk::~InterfaceGtk()
-{
- g_free(info_current);
-
- if (window)
- gtk_widget_destroy(window);
-
- scintilla_release_resources();
-
- if (event_queue) {
- GdkEvent *e;
-
- while ((e = (GdkEvent *)g_async_queue_try_pop(event_queue)))
- gdk_event_free(e);
-
- g_async_queue_unref(event_queue);
- }
-
- if (css_var_provider)
- g_object_unref(css_var_provider);
-}
-
-/*
- * GTK+ callbacks
- */
-
-static void
-scintilla_notify(ScintillaObject *sci, uptr_t idFrom,
- SCNotification *notify, gpointer user_data)
-{
- interface.process_notify(notify);
-}
-
-static gboolean
-cmdline_key_pressed_cb(GtkWidget *widget, GdkEventKey *event,
- gpointer user_data)
-{
- GAsyncQueue *event_queue = (GAsyncQueue *)user_data;
-
- bool is_ctl = event->state & GDK_CONTROL_MASK;
-
-#ifdef DEBUG
- g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n",
- event->string, *event->string,
- event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK);
-#endif
-
- g_async_queue_lock(event_queue);
-
- if (g_async_queue_length_unlocked(event_queue) >= 0 &&
- is_ctl && gdk_keyval_to_upper(event->keyval) == GDK_KEY_C) {
- /*
- * Handle asynchronous interruptions if CTRL+C is pressed.
- * This will usually send SIGINT to the entire process
- * group and set `sigint_occurred`.
- * If the execution thread is currently blocking,
- * the key is delivered like an ordinary key press.
- */
- interrupt();
- } else {
- /*
- * Copies the key-press event, since it must be evaluated
- * by the exec_thread_cb. This is costly, but since we're
- * using the event queue as a kind of keyboard buffer,
- * who cares?
- */
- g_async_queue_push_unlocked(event_queue,
- gdk_event_copy((GdkEvent *)event));
- }
-
- g_async_queue_unlock(event_queue);
-
- return TRUE;
-}
-
-static gboolean
-window_delete_cb(GtkWidget *w, GdkEventAny *e, gpointer user_data)
-{
- GAsyncQueue *event_queue = (GAsyncQueue *)user_data;
- GdkEventKey *close_event;
-
- /*
- * Emulate that the "close" key was pressed
- * which may then be handled by the execution thread
- * which invokes the appropriate "function key macro"
- * if it exists. Its default action will ensure that
- * the execution thread shuts down and the main loop
- * will eventually terminate.
- */
- close_event = (GdkEventKey *)gdk_event_new(GDK_KEY_PRESS);
- close_event->window = gtk_widget_get_parent_window(w);
- close_event->keyval = GDK_KEY_Close;
-
- g_async_queue_push(event_queue, close_event);
-
- return TRUE;
-}
-
-static gboolean
-sigterm_handler(gpointer user_data)
-{
- GAsyncQueue *event_queue = (GAsyncQueue *)user_data;
- GdkEventKey *close_event;
-
- /*
- * Since this handler replaces the default one, we
- * also have to make sure it interrupts.
- */
- interrupt();
-
- /*
- * Similar to window deletion - emulate "close" key press.
- */
- close_event = (GdkEventKey *)gdk_event_new(GDK_KEY_PRESS);
- close_event->keyval = GDK_KEY_Close;
-
- g_async_queue_push(event_queue, close_event);
-
- return G_SOURCE_CONTINUE;
-}
-
-} /* namespace SciTECO */
diff --git a/src/interface-gtk/interface-gtk.h b/src/interface-gtk/interface-gtk.h
deleted file mode 100644
index 82ed96b..0000000
--- a/src/interface-gtk/interface-gtk.h
+++ /dev/null
@@ -1,179 +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 <http://www.gnu.org/licenses/>.
- */
-
-#ifndef __INTERFACE_GTK_H
-#define __INTERFACE_GTK_H
-
-#include <stdarg.h>
-
-#include <glib.h>
-
-/* FIXME: see interface-gtk.cpp */
-#define GDK_DISABLE_DEPRECATION_WARNINGS
-#include <gdk/gdk.h>
-#include <gtk/gtk.h>
-
-#include <Scintilla.h>
-#include <ScintillaWidget.h>
-
-#include "interface.h"
-
-namespace SciTECO {
-
-typedef class ViewGtk : public View<ViewGtk> {
- ScintillaObject *sci;
-
-public:
- ViewGtk() : sci(NULL) {}
-
- /* implementation of View::initialize() */
- void initialize_impl(void);
-
- ~ViewGtk();
-
- inline GtkWidget *
- get_widget(void)
- {
- return GTK_WIDGET(sci);
- }
-
- /* implementation of View::ssm() */
- inline sptr_t
- ssm_impl(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0)
- {
- sptr_t ret;
-
- gdk_threads_enter();
- ret = scintilla_send_message(sci, iMessage, wParam, lParam);
- gdk_threads_leave();
-
- return ret;
- }
-} ViewCurrent;
-
-typedef class InterfaceGtk : public Interface<InterfaceGtk, ViewGtk> {
- GtkCssProvider *css_var_provider;
-
- GtkWidget *window;
-
- enum {
- INFO_TYPE_BUFFER = 0,
- INFO_TYPE_BUFFER_DIRTY,
- INFO_TYPE_QREGISTER
- } info_type;
- gchar *info_current;
- gboolean use_csd;
- GtkWidget *info_bar_widget;
- GtkWidget *info_image;
- GtkWidget *info_type_widget;
- GtkWidget *info_name_widget;
-
- GtkWidget *event_box_widget;
-
- GtkWidget *message_bar_widget;
- GtkWidget *message_widget;
-
- GtkWidget *cmdline_widget;
-
- GtkWidget *popup_widget;
-
- GtkWidget *current_view_widget;
-
- GAsyncQueue *event_queue;
-
-public:
- InterfaceGtk() : css_var_provider(NULL),
- window(NULL),
- info_type(INFO_TYPE_BUFFER), info_current(NULL),
- use_csd(TRUE),
- info_bar_widget(NULL),
- info_image(NULL), info_type_widget(NULL), info_name_widget(NULL),
- event_box_widget(NULL),
- message_bar_widget(NULL), message_widget(NULL),
- cmdline_widget(NULL),
- popup_widget(NULL),
- current_view_widget(NULL),
- event_queue(NULL) {}
- ~InterfaceGtk();
-
- /* overrides Interface::get_options() */
- GOptionGroup *get_options(void);
-
- /* override of Interface::init() */
- void init(void);
-
- /* implementation of Interface::vmsg() */
- void vmsg_impl(MessageType type, const gchar *fmt, va_list ap);
- /* overrides Interface::msg_clear() */
- void msg_clear(void);
-
- /* implementation of Interface::show_view() */
- void show_view_impl(ViewGtk *view);
-
- /* implementation of Interface::info_update() */
- void info_update_impl(const QRegister *reg);
- void info_update_impl(const Buffer *buffer);
-
- /* implementation of Interface::cmdline_update() */
- void cmdline_update_impl(const Cmdline *cmdline);
-
- /* override of Interface::set_clipboard() */
- void set_clipboard(const gchar *name,
- const gchar *str = NULL, gssize str_len = -1);
- /* override of Interface::get_clipboard() */
- gchar *get_clipboard(const gchar *name, gsize *str_len = NULL);
-
- /* implementation of Interface::popup_add() */
- void popup_add_impl(PopupEntryType type,
- const gchar *name, bool highlight = false);
- /* implementation of Interface::popup_show() */
- void popup_show_impl(void);
-
- /* implementation of Interface::popup_is_shown() */
- inline bool
- popup_is_shown_impl(void)
- {
- bool ret;
-
- gdk_threads_enter();
- ret = gtk_widget_get_visible(popup_widget);
- gdk_threads_leave();
-
- return ret;
- }
- /* implementation of Interface::popup_clear() */
- void popup_clear_impl(void);
-
- /* main entry point (implementation) */
- void event_loop_impl(void);
-
- /*
- * FIXME: This is for internal use only and could be
- * hidden in a nested forward-declared friend struct.
- */
- void handle_key_press(bool is_shift, bool is_ctl, guint keyval);
-
-private:
- void set_css_variables_from_view(ViewGtk *view);
-
- void refresh_info(void);
- void cmdline_insert_chr(gint &pos, gchar chr);
-} InterfaceCurrent;
-
-} /* namespace SciTECO */
-
-#endif
diff --git a/src/interface-gtk/interface.c b/src/interface-gtk/interface.c
new file mode 100644
index 0000000..afc8fe3
--- /dev/null
+++ b/src/interface-gtk/interface.c
@@ -0,0 +1,1203 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <stdarg.h>
+#include <string.h>
+#include <signal.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#ifdef G_OS_UNIX
+#include <glib-unix.h>
+#endif
+
+#include <gdk/gdk.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+#include <gtk/gtk.h>
+
+#include <gio/gio.h>
+
+#include <Scintilla.h>
+#include <ScintillaWidget.h>
+
+#include "teco-gtk-info-popup.h"
+#include "teco-gtk-label.h"
+
+#include "sciteco.h"
+#include "error.h"
+#include "string-utils.h"
+#include "cmdline.h"
+#include "qreg.h"
+#include "ring.h"
+#include "interface.h"
+
+//#define DEBUG
+
+static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget,
+ GdkRectangle *allocation,
+ gpointer user_data);
+static gboolean teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event,
+ gpointer user_data);
+static gboolean teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event,
+ gpointer user_data);
+static gboolean teco_interface_sigterm_handler(gpointer user_data) G_GNUC_UNUSED;
+
+#define UNNAMED_FILE "(Unnamed)"
+
+#define USER_CSS_FILE ".teco_css"
+
+/** printf() format for CSS RGB colors given as guint32 */
+#define CSS_COLOR_FORMAT "#%06" G_GINT32_MODIFIER "X"
+
+/** Style used for the asterisk at the beginning of the command line */
+#define STYLE_ASTERISK 16
+
+/** Indicator number used for control characters in the command line */
+#define INDIC_CONTROLCHAR (INDIC_CONTAINER+0)
+/** Indicator number used for the rubbed out part of the command line */
+#define INDIC_RUBBEDOUT (INDIC_CONTAINER+1)
+
+/** Convert Scintilla-style BGR color triple to RGB. */
+static inline guint32
+teco_bgr2rgb(guint32 bgr)
+{
+ return GUINT32_SWAP_LE_BE(bgr) >> 8;
+}
+
+/*
+ * NOTE: The teco_view_t pointer is reused to directly
+ * point to the ScintillaObject.
+ * This saves one heap object per view.
+ */
+
+static void
+teco_view_scintilla_notify(ScintillaObject *sci, gint id,
+ struct SCNotification *notify, gpointer user_data)
+{
+ teco_interface_process_notify(notify);
+}
+
+teco_view_t *
+teco_view_new(void)
+{
+ ScintillaObject *sci = SCINTILLA(scintilla_new());
+ /*
+ * We don't want the object to be destroyed
+ * when it is removed from the vbox.
+ */
+ g_object_ref_sink(sci);
+
+ scintilla_set_id(sci, 0);
+
+ gtk_widget_set_size_request(GTK_WIDGET(sci), 500, 300);
+
+ /*
+ * This disables mouse and key events on this view.
+ * For some strange reason, masking events on
+ * the event box does NOT work.
+ *
+ * NOTE: Scroll events are still allowed - scrolling
+ * is currently not under direct control of SciTECO
+ * (i.e. it is OK the side effects of scrolling are not
+ * tracked).
+ */
+ gtk_widget_set_can_focus(GTK_WIDGET(sci), FALSE);
+ gint events = gtk_widget_get_events(GTK_WIDGET(sci));
+ events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
+ events &= ~(GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK);
+ gtk_widget_set_events(GTK_WIDGET(sci), events);
+
+ g_signal_connect(sci, SCINTILLA_NOTIFY,
+ G_CALLBACK(teco_view_scintilla_notify), NULL);
+
+ return (teco_view_t *)sci;
+}
+
+static inline GtkWidget *
+teco_view_get_widget(teco_view_t *ctx)
+{
+ return GTK_WIDGET(ctx);
+}
+
+sptr_t
+teco_view_ssm(teco_view_t *ctx, unsigned int iMessage, uptr_t wParam, sptr_t lParam)
+{
+ return scintilla_send_message(SCINTILLA(ctx), iMessage, wParam, lParam);
+}
+
+static gboolean
+teco_view_free_idle_cb(gpointer user_data)
+{
+ /*
+ * This does NOT destroy the Scintilla object
+ * and GTK widget, if it is the current view
+ * (and therefore added to the vbox).
+ */
+ g_object_unref(user_data);
+ return G_SOURCE_REMOVE;
+}
+
+void
+teco_view_free(teco_view_t *ctx)
+{
+ /*
+ * FIXME: The widget is unreffed only in an idle watcher because
+ * Scintilla may have idle callbacks activated (see ScintillaGTK.cxx)
+ * and we must prevent use-after-frees.
+ * A simple g_idle_remove_by_data() does not suffice for some strange reason
+ * (perhaps it does not prevent the invocation of already activated watchers).
+ * This is a bug should better be fixed by reference counting in
+ * ScintillaGTK.cxx itself.
+ */
+ g_idle_add_full(G_PRIORITY_LOW, teco_view_free_idle_cb, SCINTILLA(ctx), NULL);
+}
+
+static struct {
+ GtkCssProvider *css_var_provider;
+
+ GtkWidget *window;
+
+ enum {
+ TECO_INFO_TYPE_BUFFER = 0,
+ TECO_INFO_TYPE_BUFFER_DIRTY,
+ TECO_INFO_TYPE_QREG
+ } info_type;
+ teco_string_t info_current;
+
+ gboolean no_csd;
+ GtkWidget *info_bar_widget;
+ GtkWidget *info_image;
+ GtkWidget *info_type_widget;
+ GtkWidget *info_name_widget;
+
+ GtkWidget *event_box_widget;
+
+ GtkWidget *message_bar_widget;
+ GtkWidget *message_widget;
+
+ teco_view_t *cmdline_view;
+
+ GtkWidget *popup_widget;
+
+ GtkWidget *current_view_widget;
+
+ GQueue *event_queue;
+} teco_interface;
+
+void
+teco_interface_init(void)
+{
+ /*
+ * gtk_init() is not necessary when using gtk_get_option_group(),
+ * but this will open the default display.
+ *
+ * FIXME: Perhaps it is possible to defer this until we initialize
+ * interactive mode!?
+ */
+ gtk_init(NULL, NULL);
+
+ /*
+ * Register clipboard registers.
+ * Unfortunately, we cannot find out which
+ * clipboards/selections are supported on this system,
+ * so we register only some default ones.
+ */
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("P"));
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("S"));
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("C"));
+
+ teco_interface.event_queue = g_queue_new();
+
+ teco_interface.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ g_signal_connect(teco_interface.window, "delete-event",
+ G_CALLBACK(teco_interface_window_delete_cb), NULL);
+
+ g_signal_connect(teco_interface.window, "key-press-event",
+ G_CALLBACK(teco_interface_key_pressed_cb), NULL);
+
+ GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+
+ /*
+ * The info bar is tried to be made the title bar of the
+ * window which also disables the default window decorations
+ * (client-side decorations) unless --no-csd was specified.
+ *
+ * NOTE: Client-side decoations could fail, leaving us with a
+ * standard title bar and the info bar with close buttons.
+ * Other window managers have undesirable side-effects.
+ */
+ teco_interface.info_bar_widget = gtk_header_bar_new();
+ gtk_widget_set_name(teco_interface.info_bar_widget, "sciteco-info-bar");
+ teco_interface.info_name_widget = teco_gtk_label_new(NULL, 0);
+ gtk_widget_set_valign(teco_interface.info_name_widget, GTK_ALIGN_CENTER);
+ /* eases writing portable fallback.css that avoids CSS element names */
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_name_widget),
+ "label");
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_name_widget),
+ "name-label");
+ gtk_label_set_selectable(GTK_LABEL(teco_interface.info_name_widget), TRUE);
+ /* NOTE: Header bar does not resize for multi-line labels */
+ //gtk_label_set_line_wrap(GTK_LABEL(teco_interface.info_name_widget), TRUE);
+ //gtk_label_set_lines(GTK_LABEL(teco_interface.info_name_widget), 2);
+ gtk_header_bar_set_custom_title(GTK_HEADER_BAR(teco_interface.info_bar_widget),
+ teco_interface.info_name_widget);
+ teco_interface.info_image = gtk_image_new();
+ gtk_header_bar_pack_start(GTK_HEADER_BAR(teco_interface.info_bar_widget),
+ teco_interface.info_image);
+ teco_interface.info_type_widget = gtk_label_new(NULL);
+ gtk_widget_set_valign(teco_interface.info_type_widget, GTK_ALIGN_CENTER);
+ /* eases writing portable fallback.css that avoids CSS element names */
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_type_widget),
+ "label");
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_type_widget),
+ "type-label");
+ gtk_header_bar_pack_start(GTK_HEADER_BAR(teco_interface.info_bar_widget),
+ teco_interface.info_type_widget);
+ if (teco_interface.no_csd) {
+ /* fall back to adding the info bar as an ordinary widget */
+ gtk_box_pack_start(GTK_BOX(vbox), teco_interface.info_bar_widget,
+ FALSE, FALSE, 0);
+ } else {
+ /* use client-side decorations */
+ gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(teco_interface.info_bar_widget), TRUE);
+ gtk_window_set_titlebar(GTK_WINDOW(teco_interface.window),
+ teco_interface.info_bar_widget);
+ }
+
+ /*
+ * Overlay widget will allow overlaying the Scintilla view
+ * and message widgets with the info popup.
+ * Therefore overlay_vbox (containing the view and popup)
+ * will be the main child of the overlay.
+ */
+ GtkWidget *overlay_widget = gtk_overlay_new();
+ GtkWidget *overlay_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+
+ /*
+ * The event box is the parent of all Scintilla views
+ * that should be displayed.
+ * This is handy when adding or removing current views,
+ * enabling and disabling GDK updates and in order to filter
+ * mouse and keyboard events going to Scintilla.
+ */
+ teco_interface.event_box_widget = gtk_event_box_new();
+ gtk_event_box_set_above_child(GTK_EVENT_BOX(teco_interface.event_box_widget), TRUE);
+ gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.event_box_widget,
+ TRUE, TRUE, 0);
+
+ teco_interface.message_bar_widget = gtk_info_bar_new();
+ gtk_widget_set_name(teco_interface.message_bar_widget, "sciteco-message-bar");
+ GtkWidget *message_bar_content =
+ gtk_info_bar_get_content_area(GTK_INFO_BAR(teco_interface.message_bar_widget));
+ /* NOTE: Messages are always pre-canonicalized */
+ teco_interface.message_widget = gtk_label_new(NULL);
+ /* eases writing portable fallback.css that avoids CSS element names */
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.message_widget),
+ "label");
+ gtk_label_set_selectable(GTK_LABEL(teco_interface.message_widget), TRUE);
+ gtk_label_set_line_wrap(GTK_LABEL(teco_interface.message_widget), TRUE);
+ gtk_container_add(GTK_CONTAINER(message_bar_content), teco_interface.message_widget);
+ gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.message_bar_widget,
+ FALSE, FALSE, 0);
+
+ gtk_container_add(GTK_CONTAINER(overlay_widget), overlay_vbox);
+ gtk_box_pack_start(GTK_BOX(vbox), overlay_widget, TRUE, TRUE, 0);
+
+ teco_interface.cmdline_view = teco_view_new();
+ teco_view_setup(teco_interface.cmdline_view);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETUNDOCOLLECTION, FALSE, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETVSCROLLBAR, FALSE, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETMARGINTYPEN, 1, SC_MARGIN_TEXT);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETSTYLE, 0, STYLE_ASTERISK);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETMARGINWIDTHN, 1,
+ teco_view_ssm(teco_interface.cmdline_view, SCI_TEXTWIDTH, STYLE_ASTERISK, (sptr_t)"*"));
+ teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETTEXT, 0, (sptr_t)"*");
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETSTYLE, INDIC_CONTROLCHAR, INDIC_ROUNDBOX);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETALPHA, INDIC_CONTROLCHAR, 128);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETSTYLE, INDIC_RUBBEDOUT, INDIC_STRIKE);
+
+ GtkWidget *cmdline_widget = teco_view_get_widget(teco_interface.cmdline_view);
+ gtk_widget_set_name(cmdline_widget, "sciteco-cmdline");
+ g_signal_connect(cmdline_widget, "size-allocate",
+ G_CALLBACK(teco_interface_cmdline_size_allocate_cb), NULL);
+ gtk_box_pack_start(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0);
+
+ gtk_container_add(GTK_CONTAINER(teco_interface.window), vbox);
+
+ /*
+ * Popup widget will be shown in the bottom
+ * of the overlay widget (i.e. the Scintilla views),
+ * filling the entire width.
+ */
+ teco_interface.popup_widget = teco_gtk_info_popup_new();
+ gtk_widget_set_name(teco_interface.popup_widget, "sciteco-info-popup");
+ gtk_overlay_add_overlay(GTK_OVERLAY(overlay_widget), teco_interface.popup_widget);
+ g_signal_connect(overlay_widget, "get-child-position",
+ G_CALLBACK(teco_gtk_info_popup_get_position_in_overlay), NULL);
+
+ /*
+ * FIXME: Nothing can really take the focus, so it will end up in the
+ * selectable labels unless we explicitly prevent it.
+ */
+ gtk_widget_set_can_focus(teco_interface.message_widget, FALSE);
+ gtk_widget_set_can_focus(teco_interface.info_name_widget, FALSE);
+
+ teco_cmdline_t empty_cmdline;
+ memset(&empty_cmdline, 0, sizeof(empty_cmdline));
+ teco_interface_cmdline_update(&empty_cmdline);
+}
+
+GOptionGroup *
+teco_interface_get_options(void)
+{
+ /*
+ * FIXME: On platforms where you want to disable CSD, you usually
+ * want to disable it always, so it should be configurable in the SciTECO
+ * profile.
+ * On the other hand, you could just install gtk3-nocsd.
+ */
+ static const GOptionEntry entries[] = {
+ {"no-csd", 0, G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, &teco_interface.no_csd,
+ "Disable client-side decorations.", NULL},
+ {NULL}
+ };
+
+ /*
+ * Parsing the option context with the Gtk option group
+ * will automatically initialize Gtk, but we do not yet
+ * open the default display.
+ */
+ GOptionGroup *group = gtk_get_option_group(FALSE);
+
+ g_option_group_add_entries(group, entries);
+
+ return group;
+}
+
+void teco_interface_init_color(guint color, guint32 rgb) {}
+
+void
+teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
+{
+ /*
+ * The message types are chosen such that there is a CSS class
+ * for every one of them. GTK_MESSAGE_OTHER does not have
+ * a CSS class.
+ */
+ static const GtkMessageType type2gtk[] = {
+ [TECO_MSG_USER] = GTK_MESSAGE_QUESTION,
+ [TECO_MSG_INFO] = GTK_MESSAGE_INFO,
+ [TECO_MSG_WARNING] = GTK_MESSAGE_WARNING,
+ [TECO_MSG_ERROR] = GTK_MESSAGE_ERROR
+ };
+
+ g_assert(type < G_N_ELEMENTS(type2gtk));
+
+ gchar buf[256];
+
+ /*
+ * stdio_vmsg() leaves `ap` undefined and we are expected
+ * to do the same and behave like vprintf().
+ */
+ va_list aq;
+ va_copy(aq, ap);
+ teco_interface_stdio_vmsg(type, fmt, ap);
+ g_vsnprintf(buf, sizeof(buf), fmt, aq);
+ va_end(aq);
+
+ gtk_info_bar_set_message_type(GTK_INFO_BAR(teco_interface.message_bar_widget),
+ type2gtk[type]);
+ gtk_label_set_text(GTK_LABEL(teco_interface.message_widget), buf);
+
+ if (type == TECO_MSG_ERROR)
+ gtk_widget_error_bell(teco_interface.window);
+}
+
+void
+teco_interface_msg_clear(void)
+{
+ gtk_info_bar_set_message_type(GTK_INFO_BAR(teco_interface.message_bar_widget),
+ GTK_MESSAGE_QUESTION);
+ gtk_label_set_text(GTK_LABEL(teco_interface.message_widget), "");
+}
+
+void
+teco_interface_show_view(teco_view_t *view)
+{
+ teco_interface_current_view = view;
+}
+
+static void
+teco_interface_refresh_info(void)
+{
+ GtkStyleContext *style = gtk_widget_get_style_context(teco_interface.info_bar_widget);
+
+ gtk_style_context_remove_class(style, "info-qregister");
+ gtk_style_context_remove_class(style, "info-buffer");
+ gtk_style_context_remove_class(style, "dirty");
+
+ g_auto(teco_string_t) info_current_temp;
+ teco_string_init(&info_current_temp,
+ teco_interface.info_current.data, teco_interface.info_current.len);
+ if (teco_interface.info_type == TECO_INFO_TYPE_BUFFER_DIRTY)
+ teco_string_append_c(&info_current_temp, '*');
+ teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.info_name_widget),
+ info_current_temp.data, info_current_temp.len);
+ g_autofree gchar *info_current_canon =
+ teco_string_echo(info_current_temp.data, info_current_temp.len);
+
+ const gchar *info_type_str = PACKAGE;
+ g_autoptr(GIcon) icon = NULL;
+
+ switch (teco_interface.info_type) {
+ case TECO_INFO_TYPE_QREG:
+ gtk_style_context_add_class(style, "info-qregister");
+
+ info_type_str = PACKAGE_NAME " - <QRegister> ";
+ gtk_label_set_text(GTK_LABEL(teco_interface.info_type_widget), "QRegister");
+ gtk_label_set_ellipsize(GTK_LABEL(teco_interface.info_name_widget),
+ PANGO_ELLIPSIZE_START);
+
+ /*
+ * FIXME: Perhaps we should use the SciTECO icon for Q-Registers.
+ */
+ icon = g_icon_new_for_string("emblem-generic", NULL);
+ break;
+
+ case TECO_INFO_TYPE_BUFFER_DIRTY:
+ gtk_style_context_add_class(style, "dirty");
+ /* fall through */
+ case TECO_INFO_TYPE_BUFFER:
+ gtk_style_context_add_class(style, "info-buffer");
+
+ info_type_str = PACKAGE_NAME " - <Buffer> ";
+ gtk_label_set_text(GTK_LABEL(teco_interface.info_type_widget), "Buffer");
+ gtk_label_set_ellipsize(GTK_LABEL(teco_interface.info_name_widget),
+ PANGO_ELLIPSIZE_MIDDLE);
+
+ icon = teco_gtk_info_popup_get_icon_for_path(teco_interface.info_current.data,
+ "text-x-generic");
+ break;
+ }
+
+ g_autofree gchar *title = g_strconcat(info_type_str, info_current_canon, NULL);
+ gtk_window_set_title(GTK_WINDOW(teco_interface.window), title);
+
+ if (icon) {
+ gint width, height;
+ gtk_icon_size_lookup(GTK_ICON_SIZE_LARGE_TOOLBAR, &width, &height);
+
+ gtk_image_set_from_gicon(GTK_IMAGE(teco_interface.info_image),
+ icon, GTK_ICON_SIZE_LARGE_TOOLBAR);
+ /* This is necessary so that oversized icons get scaled down. */
+ gtk_image_set_pixel_size(GTK_IMAGE(teco_interface.info_image), height);
+ }
+}
+
+void
+teco_interface_info_update_qreg(const teco_qreg_t *reg)
+{
+ teco_string_clear(&teco_interface.info_current);
+ teco_string_init(&teco_interface.info_current,
+ reg->head.name.data, reg->head.name.len);
+ teco_interface.info_type = TECO_INFO_TYPE_QREG;
+}
+
+void
+teco_interface_info_update_buffer(const teco_buffer_t *buffer)
+{
+ const gchar *filename = buffer->filename ? : UNNAMED_FILE;
+
+ teco_string_clear(&teco_interface.info_current);
+ teco_string_init(&teco_interface.info_current, filename, strlen(filename));
+ teco_interface.info_type = buffer->dirty ? TECO_INFO_TYPE_BUFFER_DIRTY
+ : TECO_INFO_TYPE_BUFFER;
+}
+
+/**
+ * Insert a single character into the command line.
+ *
+ * @fixme
+ * Control characters should be inserted verbatim since the Scintilla
+ * representations of them should be preferred.
+ * However, Scintilla would break the line on every CR/LF and there is
+ * currently no way to prevent this.
+ * Scintilla needs to be patched.
+ *
+ * @see teco_view_set_representations()
+ * @see teco_curses_format_str()
+ */
+static void
+teco_interface_cmdline_insert_c(gchar chr)
+{
+ gchar buffer[3+1] = "";
+
+ /*
+ * NOTE: This mapping is similar to teco_view_set_representations()
+ */
+ switch (chr) {
+ case '\e': strcpy(buffer, "$"); break;
+ case '\r': strcpy(buffer, "CR"); break;
+ case '\n': strcpy(buffer, "LF"); break;
+ case '\t': strcpy(buffer, "TAB"); break;
+ default:
+ if (TECO_IS_CTL(chr)) {
+ buffer[0] = '^';
+ buffer[1] = TECO_CTL_ECHO(chr);
+ buffer[2] = '\0';
+ }
+ }
+
+ if (*buffer) {
+ gsize len = strlen(buffer);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_APPENDTEXT, len, (sptr_t)buffer);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETINDICATORCURRENT, INDIC_CONTROLCHAR, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICATORFILLRANGE,
+ teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0) - len, len);
+ } else {
+ teco_view_ssm(teco_interface.cmdline_view, SCI_APPENDTEXT, 1, (sptr_t)&chr);
+ }
+}
+
+void
+teco_interface_cmdline_update(const teco_cmdline_t *cmdline)
+{
+ /*
+ * We don't know if the new command line is similar to
+ * the old one, so we can just as well rebuild it.
+ *
+ * NOTE: teco_view_ssm() already locks the GDK lock.
+ */
+ teco_view_ssm(teco_interface.cmdline_view, SCI_CLEARALL, 0, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SCROLLCARET, 0, 0);
+
+ /* format effective command line */
+ for (guint i = 0; i < cmdline->effective_len; i++)
+ teco_interface_cmdline_insert_c(cmdline->str.data[i]);
+
+ /* cursor should be after effective command line */
+ guint pos = teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_GOTOPOS, pos, 0);
+
+ /* format rubbed out command line */
+ for (guint i = cmdline->effective_len; i < cmdline->str.len; i++)
+ teco_interface_cmdline_insert_c(cmdline->str.data[i]);
+
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETINDICATORCURRENT, INDIC_RUBBEDOUT, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICATORFILLRANGE, pos,
+ teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0) - pos);
+
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SCROLLCARET, 0, 0);
+}
+
+static GdkAtom
+teco_interface_get_selection_by_name(const gchar *name)
+{
+ /*
+ * We can use gdk_atom_intern() to support arbitrary X11 selection
+ * names. However, since we cannot find out which selections are
+ * registered, we are only providing QRegisters for the three default
+ * selections.
+ * Checking them here avoids expensive X server roundtrips.
+ */
+ switch (*name) {
+ case '\0': return GDK_NONE;
+ case 'P': return GDK_SELECTION_PRIMARY;
+ case 'S': return GDK_SELECTION_SECONDARY;
+ case 'C': return GDK_SELECTION_CLIPBOARD;
+ default: break;
+ }
+
+ return gdk_atom_intern(name, FALSE);
+}
+
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, GError **error)
+{
+ GtkClipboard *clipboard = gtk_clipboard_get(teco_interface_get_selection_by_name(name));
+
+ /*
+ * NOTE: function has compatible semantics for str_len < 0.
+ */
+ gtk_clipboard_set_text(clipboard, str, str_len);
+
+ return TRUE;
+}
+
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
+{
+ GtkClipboard *clipboard = gtk_clipboard_get(teco_interface_get_selection_by_name(name));
+ /*
+ * Could return NULL for an empty clipboard.
+ *
+ * FIXME: This converts to UTF8 and we loose the ability
+ * to get clipboard with embedded nulls.
+ */
+ g_autofree gchar *contents = gtk_clipboard_wait_for_text(clipboard);
+
+ *len = contents ? strlen(contents) : 0;
+ if (str)
+ *str = g_steal_pointer(&contents);
+
+ return TRUE;
+}
+
+void
+teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len,
+ gboolean highlight)
+{
+ teco_gtk_info_popup_add(TECO_GTK_INFO_POPUP(teco_interface.popup_widget),
+ type, name, name_len, highlight);
+}
+
+void
+teco_interface_popup_show(void)
+{
+ if (gtk_widget_get_visible(teco_interface.popup_widget))
+ teco_gtk_info_popup_scroll_page(TECO_GTK_INFO_POPUP(teco_interface.popup_widget));
+ else
+ gtk_widget_show(teco_interface.popup_widget);
+}
+
+gboolean
+teco_interface_popup_is_shown(void)
+{
+ return gtk_widget_get_visible(teco_interface.popup_widget);
+}
+
+void
+teco_interface_popup_clear(void)
+{
+ if (gtk_widget_get_visible(teco_interface.popup_widget)) {
+ gtk_widget_hide(teco_interface.popup_widget);
+ teco_gtk_info_popup_clear(TECO_GTK_INFO_POPUP(teco_interface.popup_widget));
+ }
+}
+
+/**
+ * Whether the execution has been interrupted (CTRL+C).
+ *
+ * This is called regularily, so it is used to drive the
+ * main loop so that we can still process key presses.
+ *
+ * This approach is significantly slower in interactive mode
+ * than executing in a separate thread probably due to the
+ * system call overhead.
+ * But the GDK lock that would be necessary for synchronization
+ * has been deprecated.
+ */
+gboolean
+teco_interface_is_interrupted(void)
+{
+ if (gtk_main_level() > 0)
+ gtk_main_iteration_do(FALSE);
+
+ return teco_sigint_occurred != FALSE;
+}
+
+static void
+teco_interface_set_css_variables(teco_view_t *view)
+{
+ guint32 default_fg_color = teco_view_ssm(view, SCI_STYLEGETFORE, STYLE_DEFAULT, 0);
+ guint32 default_bg_color = teco_view_ssm(view, SCI_STYLEGETBACK, STYLE_DEFAULT, 0);
+ guint32 calltip_fg_color = teco_view_ssm(view, SCI_STYLEGETFORE, STYLE_CALLTIP, 0);
+ guint32 calltip_bg_color = teco_view_ssm(view, SCI_STYLEGETBACK, STYLE_CALLTIP, 0);
+
+ /*
+ * FIXME: Font and colors of Scintilla views cannot be set via CSS.
+ * But some day, there will be a way to send messages to the commandline view
+ * from SciTECO code via ES.
+ * Configuration will then be in the hands of color schemes.
+ *
+ * NOTE: We don't actually know apriori how large the font_size buffer should be,
+ * but luckily SCI_STYLEGETFONT with a sptr==0 will return only the size.
+ * This is undocumented in the Scintilla docs.
+ */
+ g_autofree gchar *font_name = g_malloc(teco_view_ssm(view, SCI_STYLEGETFONT, STYLE_DEFAULT, 0) + 1);
+ teco_view_ssm(view, SCI_STYLEGETFONT, STYLE_DEFAULT, (sptr_t)font_name);
+
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFORE, STYLE_DEFAULT, default_fg_color);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBACK, STYLE_DEFAULT, default_bg_color);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)font_name);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETSIZE, STYLE_DEFAULT,
+ teco_view_ssm(view, SCI_STYLEGETSIZE, STYLE_DEFAULT, 0));
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLECLEARALL, 0, 0);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFORE, STYLE_CALLTIP, calltip_fg_color);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBACK, STYLE_CALLTIP, calltip_bg_color);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SETCARETFORE, default_fg_color, 0);
+ /* used for the asterisk at the beginning of the command line */
+ teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBOLD, STYLE_ASTERISK, TRUE);
+ /* used for character representations */
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETFORE, INDIC_CONTROLCHAR, default_fg_color);
+ /* used for the rubbed out command line */
+ teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETFORE, INDIC_RUBBEDOUT, default_fg_color);
+ /* this somehow gets reset */
+ teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETTEXT, 0, (sptr_t)"*");
+
+ guint text_height = teco_view_ssm(teco_interface.cmdline_view, SCI_TEXTHEIGHT, 0, 0);
+
+ /*
+ * Generates a CSS that sets some predefined color variables.
+ * This effectively "exports" Scintilla styles into the CSS
+ * world.
+ * Those colors are used by the fallback.css shipping with SciTECO
+ * in order to apply the SciTECO-controlled color scheme to all the
+ * predefined UI elements.
+ * They can also be used in user-customizations.
+ */
+ gchar css[256];
+ g_snprintf(css, sizeof(css),
+ "@define-color sciteco_default_fg_color " CSS_COLOR_FORMAT ";"
+ "@define-color sciteco_default_bg_color " CSS_COLOR_FORMAT ";"
+ "@define-color sciteco_calltip_fg_color " CSS_COLOR_FORMAT ";"
+ "@define-color sciteco_calltip_bg_color " CSS_COLOR_FORMAT ";",
+ teco_bgr2rgb(default_fg_color), teco_bgr2rgb(default_bg_color),
+ teco_bgr2rgb(calltip_fg_color), teco_bgr2rgb(calltip_bg_color));
+
+ /*
+ * The GError and return value has been deprecated.
+ * A CSS parsing error would point to a programming
+ * error anyway.
+ */
+ gtk_css_provider_load_from_data(teco_interface.css_var_provider, css, -1, NULL);
+
+ /*
+ * The font and size of the commandline view might have changed,
+ * so we resize it.
+ * This cannot be done via CSS or Scintilla messages.
+ * Currently, it is always exactly one line high in order to mimic the Curses UI.
+ */
+ gtk_widget_set_size_request(teco_view_get_widget(teco_interface.cmdline_view), -1, text_height);
+}
+
+static gboolean
+teco_interface_handle_key_press(guint keyval, guint state, GError **error)
+{
+ teco_view_t *last_view = teco_interface_current_view;
+
+ switch (keyval) {
+ case GDK_KEY_Escape:
+ if (!teco_cmdline_keypress_c('\e', error))
+ return FALSE;
+ break;
+ case GDK_KEY_BackSpace:
+ if (!teco_cmdline_keypress_c(TECO_CTL_KEY('H'), error))
+ return FALSE;
+ break;
+ case GDK_KEY_Tab:
+ if (!teco_cmdline_keypress_c('\t', error))
+ return FALSE;
+ break;
+ case GDK_KEY_Return:
+ if (!teco_cmdline_keypress_c('\n', error))
+ return FALSE;
+ break;
+
+ /*
+ * Function key macros
+ */
+#define FN(KEY, MACRO) \
+ case GDK_KEY_##KEY: \
+ if (!teco_cmdline_fnmacro(#MACRO, error)) \
+ return FALSE; \
+ break
+#define FNS(KEY, MACRO) \
+ case GDK_KEY_##KEY: \
+ if (!teco_cmdline_fnmacro(state & GDK_SHIFT_MASK ? "S" #MACRO : #MACRO, error)) \
+ return FALSE; \
+ break
+ FN(Down, DOWN); FN(Up, UP);
+ FNS(Left, LEFT); FNS(Right, RIGHT);
+ FN(KP_Down, DOWN); FN(KP_Up, UP);
+ FNS(KP_Left, LEFT); FNS(KP_Right, RIGHT);
+ FNS(Home, HOME);
+ case GDK_KEY_F1...GDK_KEY_F35: {
+ gchar macro_name[3+1];
+
+ g_snprintf(macro_name, sizeof(macro_name),
+ "F%d", keyval - GDK_KEY_F1 + 1);
+ if (!teco_cmdline_fnmacro(macro_name, error))
+ return FALSE;
+ break;
+ }
+ FNS(Delete, DC);
+ FNS(Insert, IC);
+ FN(Page_Down, NPAGE); FN(Page_Up, PPAGE);
+ FNS(Print, PRINT);
+ FN(KP_Home, A1); FN(KP_Prior, A3);
+ FN(KP_Begin, B2);
+ FN(KP_End, C1); FN(KP_Next, C3);
+ FNS(End, END);
+ FNS(Help, HELP);
+ FN(Close, CLOSE);
+#undef FNS
+#undef FN
+
+ /*
+ * Control keys and keys with printable representation
+ */
+ default: {
+ gunichar u = gdk_keyval_to_unicode(keyval);
+
+ if (!u || g_unichar_to_utf8(u, NULL) != 1)
+ break;
+
+ gchar key;
+
+ g_unichar_to_utf8(u, &key);
+ if (key > 0x7F)
+ break;
+ if (state & GDK_CONTROL_MASK)
+ key = TECO_CTL_KEY(g_ascii_toupper(key));
+
+ if (!teco_cmdline_keypress_c(key, error))
+ return FALSE;
+ }
+ }
+
+ /*
+ * The styles configured via Scintilla might change
+ * with every keypress.
+ */
+ teco_interface_set_css_variables(teco_interface_current_view);
+
+ /*
+ * The info area is updated very often and setting the
+ * window title each time it is updated is VERY costly.
+ * So we set it here once after every keypress even if the
+ * info line did not change.
+ * View changes are also only applied here to the GTK
+ * window even though GDK updates have been frozen since
+ * the size reallocations are very costly.
+ */
+ teco_interface_refresh_info();
+
+ if (teco_interface_current_view != last_view) {
+ /*
+ * The last view's object is not guaranteed to
+ * still exist.
+ * However its widget is, due to reference counting.
+ */
+ if (teco_interface.current_view_widget)
+ gtk_container_remove(GTK_CONTAINER(teco_interface.event_box_widget),
+ teco_interface.current_view_widget);
+
+ teco_interface.current_view_widget = teco_view_get_widget(teco_interface_current_view);
+
+ gtk_container_add(GTK_CONTAINER(teco_interface.event_box_widget),
+ teco_interface.current_view_widget);
+ gtk_widget_show(teco_interface.current_view_widget);
+ }
+
+ return TRUE;
+}
+
+gboolean
+teco_interface_event_loop(GError **error)
+{
+ static const gchar *icon_files[] = {
+ SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-48.png",
+ SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-32.png",
+ SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-16.png",
+ NULL
+ };
+
+ /*
+ * Assign an icon to the window.
+ *
+ * FIXME: On Windows, it may be better to load the icon compiled
+ * as a resource into the binary.
+ */
+ GList *icon_list = NULL;
+
+ for (const gchar **file = icon_files; *file; file++) {
+ GdkPixbuf *icon_pixbuf = gdk_pixbuf_new_from_file(*file, NULL);
+
+ /* fail silently if there's a problem with one of the icons */
+ if (icon_pixbuf)
+ icon_list = g_list_append(icon_list, icon_pixbuf);
+ }
+
+ gtk_window_set_default_icon_list(icon_list);
+
+ g_list_free_full(icon_list, g_object_unref);
+
+ teco_interface_refresh_info();
+
+ /*
+ * Initialize the CSS variable provider and the CSS provider
+ * for the included fallback.css.
+ */
+ teco_interface.css_var_provider = gtk_css_provider_new();
+ if (teco_interface_current_view)
+ /* set CSS variables initially */
+ teco_interface_set_css_variables(teco_interface_current_view);
+ GdkScreen *default_screen = gdk_screen_get_default();
+ gtk_style_context_add_provider_for_screen(default_screen,
+ GTK_STYLE_PROVIDER(teco_interface.css_var_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ /* get path of $SCITECOCONFIG/.teco_css */
+ teco_qreg_t *config_path_reg = teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECOCONFIG", 14);
+ g_assert(config_path_reg != NULL);
+ g_auto(teco_string_t) config_path = {NULL, 0};
+ if (!config_path_reg->vtable->get_string(config_path_reg, &config_path.data, &config_path.len, error))
+ return FALSE;
+ if (teco_string_contains(&config_path, '\0')) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Null-character not allowed in filenames");
+ return FALSE;
+ }
+ g_autofree gchar *user_css_file = g_build_filename(config_path.data, USER_CSS_FILE, NULL);
+
+ GtkCssProvider *user_css_provider = gtk_css_provider_new();
+ /*
+ * NOTE: The return value of gtk_css_provider_load() is deprecated.
+ * Instead we could register for the "parsing-error" signal.
+ * For the time being we just silently ignore parsing errors.
+ * They will be printed to stderr by Gtk anyway.
+ */
+ if (g_file_test(user_css_file, G_FILE_TEST_IS_REGULAR))
+ /* open user CSS */
+ gtk_css_provider_load_from_path(user_css_provider, user_css_file, NULL);
+ else
+ /* use fallback CSS */
+ gtk_css_provider_load_from_path(user_css_provider,
+ SCITECODATADIR G_DIR_SEPARATOR_S "fallback.css",
+ NULL);
+ gtk_style_context_add_provider_for_screen(default_screen,
+ GTK_STYLE_PROVIDER(user_css_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
+
+ /*
+ * When changing views, the new widget is not
+ * added immediately to avoid flickering in the GUI.
+ * It is only updated once per key press and only
+ * if it really changed.
+ * Therefore we must add the current view to the
+ * window initially.
+ * For the same reason, window title updates are
+ * deferred to once after every key press, so we must
+ * set the window title initially.
+ */
+ if (teco_interface_current_view) {
+ teco_interface.current_view_widget = teco_view_get_widget(teco_interface_current_view);
+ gtk_container_add(GTK_CONTAINER(teco_interface.event_box_widget),
+ teco_interface.current_view_widget);
+ }
+
+ gtk_widget_show_all(teco_interface.window);
+ /* don't show popup by default */
+ gtk_widget_hide(teco_interface.popup_widget);
+
+ /*
+ * SIGTERM emulates the "Close" key just like when
+ * closing the window if supported by this version of glib.
+ * Note that this replaces SciTECO's default SIGTERM handler
+ * so it will additionally raise(SIGINT).
+ *
+ * FIXME: On ^Z, we do not suspend properly. The window is still shown.
+ * Perhaps we should try to catch SIGTSTP?
+ * This does not work with g_unix_signal_add(), though, so any
+ * workaround would be tricky.
+ * We could create a pipe via g_unix_open_pipe() which we
+ * write to using write() in a normal signal handler.
+ * We can then add a watcher using g_unix_fd_add() which will
+ * hide the main window.
+ */
+#ifdef G_OS_UNIX
+ g_unix_signal_add(SIGTERM, teco_interface_sigterm_handler, NULL);
+#endif
+
+ gtk_main();
+
+ /*
+ * Make sure the window is hidden
+ * now already, as there may be code that has to be
+ * executed in batch mode.
+ */
+ gtk_widget_hide(teco_interface.window);
+
+ return TRUE;
+}
+
+void
+teco_interface_cleanup(void)
+{
+ teco_string_clear(&teco_interface.info_current);
+
+ if (teco_interface.window)
+ gtk_widget_destroy(teco_interface.window);
+
+ scintilla_release_resources();
+
+ if (teco_interface.event_queue)
+ g_queue_free_full(teco_interface.event_queue,
+ (GDestroyNotify)gdk_event_free);
+
+ if (teco_interface.css_var_provider)
+ g_object_unref(teco_interface.css_var_provider);
+}
+
+/*
+ * GTK+ callbacks
+ */
+
+/**
+ * Called when the commandline widget is resized.
+ * This should ensure that the caret jumps to the middle of the command line,
+ * imitating the behaviour of the current Curses command line.
+ */
+static void
+teco_interface_cmdline_size_allocate_cb(GtkWidget *widget,
+ GdkRectangle *allocation, gpointer user_data)
+{
+ /*
+ * The GDK lock is already held, so we avoid using teco_view_ssm().
+ */
+ scintilla_send_message(SCINTILLA(widget), SCI_SETXCARETPOLICY,
+ CARET_SLOP, allocation->width/2);
+}
+
+static gboolean
+teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
+{
+ g_autoptr(GError) error = NULL;
+
+#ifdef DEBUG
+ g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n",
+ event->string, *event->string,
+ event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK);
+#endif
+
+ if (teco_cmdline.pc < teco_cmdline.effective_len) {
+ /*
+ * We're already executing, so this event is processed
+ * from gtk_main_iteration_do().
+ * Unfortunately, gtk_main_level() is still 1 in this case.
+ *
+ * We might also completely replace the watchers
+ * during execution, but the current implementation is
+ * probably easier.
+ */
+ if (event->state & GDK_CONTROL_MASK &&
+ gdk_keyval_to_upper(event->keyval) == GDK_KEY_C)
+ /*
+ * Handle asynchronous interruptions if CTRL+C is pressed.
+ * This will usually send SIGINT to the entire process
+ * group and set `teco_sigint_occurred`.
+ * If the execution thread is currently blocking,
+ * the key is delivered like an ordinary key press.
+ */
+ teco_interrupt();
+ else
+ g_queue_push_tail(teco_interface.event_queue,
+ gdk_event_copy((GdkEvent *)event));
+
+ return TRUE;
+ }
+
+ g_queue_push_tail(teco_interface.event_queue, gdk_event_copy((GdkEvent *)event));
+
+ /*
+ * Avoid redraws of the current view by freezing updates
+ * on the view's GDK window (we're running in parallel
+ * to the main loop so there could be frequent redraws).
+ * By freezing updates, the behaviour is similar to
+ * the Curses UI.
+ */
+ GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window));
+ /*
+ * FIXME: A simple freeze will not suffice to prevent updates in code like <Sx$;>.
+ * gdk_window_freeze_toplevel_updates_libgtk_only() is deprecated, though.
+ * Perhaps this hack is no longer required after upgrading Scintilla.
+ *
+ * For the time being, we just live with the expected deprecation warnings,
+ * although they could theoretically be suppressed using
+ * `#pragma GCC diagnostic ignored`.
+ */
+ //gdk_window_freeze_updates(top_window);
+ gdk_window_freeze_toplevel_updates_libgtk_only(top_window);
+
+ /*
+ * The event queue might be filled when pressing keys when SciTECO
+ * is busy executing code.
+ */
+ do {
+ g_autoptr(GdkEvent) event = g_queue_pop_head(teco_interface.event_queue);
+
+ teco_sigint_occurred = FALSE;
+ teco_interface_handle_key_press(event->key.keyval, event->key.state, &error);
+ teco_sigint_occurred = FALSE;
+
+ if (g_error_matches(error, TECO_ERROR, TECO_ERROR_QUIT)) {
+ gtk_main_quit();
+ break;
+ }
+ } while (!g_queue_is_empty(teco_interface.event_queue));
+
+ gdk_window_thaw_toplevel_updates_libgtk_only(top_window);
+ //gdk_window_thaw_updates(top_window);
+
+ return TRUE;
+}
+
+static gboolean
+teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer user_data)
+{
+ /*
+ * Emulate that the "close" key was pressed
+ * which may then be handled by the execution thread
+ * which invokes the appropriate "function key macro"
+ * if it exists. Its default action will ensure that
+ * the execution thread shuts down and the main loop
+ * will eventually terminate.
+ */
+ g_autoptr(GdkEvent) close_event = gdk_event_new(GDK_KEY_PRESS);
+ close_event->key.window = gtk_widget_get_parent_window(widget);
+ close_event->key.keyval = GDK_KEY_Close;
+
+ return teco_interface_key_pressed_cb(widget, &close_event->key, NULL);
+}
+
+static gboolean
+teco_interface_sigterm_handler(gpointer user_data)
+{
+ /*
+ * Since this handler replaces the default signal handler,
+ * we also have to make sure it interrupts.
+ */
+ teco_interrupt();
+
+ /*
+ * Similar to window deletion - emulate "close" key press.
+ */
+ g_autoptr(GdkEvent) close_event = gdk_event_new(GDK_KEY_PRESS);
+ close_event->key.keyval = GDK_KEY_Close;
+
+ return teco_interface_key_pressed_cb(teco_interface.window, &close_event->key, NULL);
+}
diff --git a/src/interface-gtk/teco-gtk-info-popup.gob b/src/interface-gtk/teco-gtk-info-popup.gob
new file mode 100644
index 0000000..f08b0d7
--- /dev/null
+++ b/src/interface-gtk/teco-gtk-info-popup.gob
@@ -0,0 +1,446 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+requires 2.0.20
+
+%ctop{
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+#include <math.h>
+
+#include <glib/gprintf.h>
+
+#include "list.h"
+#include "string-utils.h"
+#include "teco-gtk-label.h"
+%}
+
+%h{
+#include <gtk/gtk.h>
+#include <gdk/gdk.h>
+
+#include <gio/gio.h>
+
+#include "interface.h"
+%}
+
+%{
+/*
+ * FIXME: This is redundant with curses-info-popup.c.
+ */
+typedef struct {
+ teco_stailq_entry_t entry;
+
+ teco_popup_entry_type_t type;
+ teco_string_t name;
+ gboolean highlight;
+} teco_popup_entry_t;
+%}
+
+/*
+ * NOTE: Deriving from GtkEventBox ensures that we can
+ * set a background on the entire popup widget.
+ */
+class Teco:Gtk:Info:Popup from Gtk:Event:Box {
+ /* These are added to other widgets, so they don't have to be destroyed */
+ public GtkAdjustment *hadjustment = {gtk_adjustment_new(0, 0, 0, 0, 0, 0)};
+ public GtkAdjustment *vadjustment = {gtk_adjustment_new(0, 0, 0, 0, 0, 0)};
+
+ private GtkWidget *flow_box = {gtk_flow_box_new()};
+
+ private GStringChunk *chunk = {g_string_chunk_new(32)}
+ destroywith g_string_chunk_free;
+ private teco_stailq_head_t list
+ destroy {
+ teco_stailq_entry_t *entry;
+ while ((entry = teco_stailq_remove_head(&VAR)))
+ g_free(entry);
+ };
+ private guint idle_id = 0;
+ private gboolean frozen = FALSE;
+
+ init(self)
+ {
+ /*
+ * A box containing a viewport and scrollbar will
+ * "emulate" a scrolled window.
+ * We cannot use a scrolled window since it ignores
+ * the preferred height of its viewport which breaks
+ * height-for-width management.
+ */
+ GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+
+ GtkWidget *scrollbar = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL,
+ self->vadjustment);
+ /* show/hide the scrollbar dynamically */
+ g_signal_connect(self->vadjustment, "changed",
+ G_CALLBACK(self_vadjustment_changed), scrollbar);
+
+ /* take as little height as necessary */
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(selfp->flow_box),
+ GTK_ORIENTATION_HORIZONTAL);
+ //gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(selfp->flow_box), TRUE);
+ /* this for focus handling only, not for scrolling */
+ gtk_flow_box_set_hadjustment(GTK_FLOW_BOX(selfp->flow_box),
+ self->hadjustment);
+ gtk_flow_box_set_vadjustment(GTK_FLOW_BOX(selfp->flow_box),
+ self->vadjustment);
+
+ GtkWidget *viewport = gtk_viewport_new(self->hadjustment, self->vadjustment);
+ gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
+ gtk_container_add(GTK_CONTAINER(viewport), selfp->flow_box);
+
+ gtk_box_pack_start(GTK_BOX(box), viewport, TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(box), scrollbar, FALSE, FALSE, 0);
+ gtk_widget_show_all(box);
+
+ /*
+ * NOTE: Everything shown except the top-level container.
+ * Therefore a gtk_widget_show() is enough to show our popup.
+ */
+ gtk_container_add(GTK_CONTAINER(self), box);
+
+ selfp->list = TECO_STAILQ_HEAD_INITIALIZER(&selfp->list);
+ }
+
+ /**
+ * Allocate position in an overlay.
+ *
+ * This function can be used as the "get-child-position" signal
+ * handler of a GtkOverlay in order to position the popup at the
+ * bottom of the overlay's main child, spanning the entire width.
+ * In contrast to the GtkOverlay's default allocation schemes,
+ * this makes sure that the widget will not be larger than the
+ * main child, so the popup properly scrolls when becoming too large
+ * in height.
+ *
+ * @param user_data unused by this callback
+ */
+ public gboolean
+ get_position_in_overlay(Gtk:Overlay *overlay, Gtk:Widget *widget,
+ Gdk:Rectangle *allocation, gpointer user_data)
+ {
+ GtkWidget *main_child = gtk_bin_get_child(GTK_BIN(overlay));
+ GtkAllocation main_child_alloc;
+ gint natural_height;
+
+ gtk_widget_get_allocation(main_child, &main_child_alloc);
+ gtk_widget_get_preferred_height_for_width(widget,
+ main_child_alloc.width,
+ NULL, &natural_height);
+
+ /*
+ * FIXME: Probably due to some bug in the height-for-width
+ * calculation of Gtk (at least in 3.10 or in the GtkFlowBox
+ * fallback included with SciTECO), the natural height
+ * is a bit too small to accommodate the entire GtkFlowBox,
+ * resulting in the GtkViewport always scrolling.
+ * This hack fixes it up in a NONPORTABLE manner.
+ */
+ natural_height += 5;
+
+ allocation->width = main_child_alloc.width;
+ allocation->height = MIN(natural_height, main_child_alloc.height);
+ allocation->x = 0;
+ allocation->y = main_child_alloc.height - allocation->height;
+
+ return TRUE;
+ }
+
+ /*
+ * Adapted from GtkScrolledWindow's gtk_scrolled_window_scroll_event()
+ * since the viewport does not react to scroll events.
+ * This is registered for our container widget instead of only for
+ * GtkViewport since this is what GtkScrolledWindow does.
+ *
+ * FIXME: May need to handle non-delta scrolling, i.e. GDK_SCROLL_UP
+ * and GDK_SCROLL_DOWN.
+ */
+ override (Gtk:Widget) gboolean
+ scroll_event(Gtk:Widget *widget, Gdk:Event:Scroll *event)
+ {
+ Self *self = SELF(widget);
+ gdouble delta_x, delta_y;
+
+ if (!gdk_event_get_scroll_deltas((GdkEvent *)event,
+ &delta_x, &delta_y))
+ return FALSE;
+
+ GtkAdjustment *adj = self->vadjustment;
+ gdouble page_size = gtk_adjustment_get_page_size(adj);
+ gdouble scroll_unit = pow(page_size, 2.0 / 3.0);
+ gdouble new_value;
+
+ new_value = CLAMP(gtk_adjustment_get_value(adj) + delta_y * scroll_unit,
+ gtk_adjustment_get_lower(adj),
+ gtk_adjustment_get_upper(adj) -
+ gtk_adjustment_get_page_size(adj));
+
+ gtk_adjustment_set_value(adj, new_value);
+
+ return TRUE;
+ }
+
+ private void
+ vadjustment_changed(Gtk:Adjustment *vadjustment, Gtk:Widget *scrollbar)
+ {
+ /*
+ * This shows/hides the widget using opacity instead of using
+ * gtk_widget_set_visibility() since the latter would influence
+ * size allocations. A widget with opacity 0 keeps its size.
+ */
+ gtk_widget_set_opacity(scrollbar,
+ gtk_adjustment_get_upper(vadjustment) -
+ gtk_adjustment_get_lower(vadjustment) >
+ gtk_adjustment_get_page_size(vadjustment) ? 1 : 0);
+ }
+
+ public GtkWidget *
+ new(void)
+ {
+ return GTK_WIDGET(GET_NEW);
+ }
+
+ public GIcon *
+ get_icon_for_path(const gchar *path, const gchar *fallback_name)
+ {
+ GIcon *icon = NULL;
+
+ g_autoptr(GFile) file = g_file_new_for_path(path);
+ g_autoptr(GFileInfo) info = g_file_query_info(file, "standard::icon", 0, NULL, NULL);
+ if (info) {
+ icon = g_file_info_get_icon(info);
+ g_object_ref(icon);
+ } else {
+ /* fall back to standard icon, but this can still return NULL! */
+ icon = g_icon_new_for_string(fallback_name, NULL);
+ }
+
+ return icon;
+ }
+
+ public void
+ add(self, teco_popup_entry_type_t type,
+ const gchar *name, gssize len, gboolean highlight)
+ {
+ teco_popup_entry_t *entry = g_new(teco_popup_entry_t, 1);
+ entry->type = type;
+ /*
+ * Popup entries aren't removed individually, so we can
+ * more efficiently store them via GStringChunk.
+ */
+ teco_string_init_chunk(&entry->name, name, len < 0 ? strlen(name) : len,
+ selfp->chunk);
+ entry->highlight = highlight;
+
+ /*
+ * NOTE: We don't immediately create the Gtk+ widget and add it
+ * to the GtkFlowBox since it would be too slow for very large
+ * numbers of popup entries.
+ * Instead, we queue and process them in idle time only once the widget
+ * is shown. This ensures a good reactivity, even though the popup may
+ * not yet be complete when first shown.
+ *
+ * While it would be possible to show the widget before the first
+ * add() call to achieve the same effect, this would prevent keyboard
+ * interaction unless we add support for interruptions or drive
+ * the event loop manually.
+ */
+ teco_stailq_insert_tail(&selfp->list, &entry->entry);
+ }
+
+ override (Gtk:Widget) void
+ show(Gtk:Widget *widget)
+ {
+ Self *self = SELF(widget);
+
+ if (!selfp->idle_id) {
+ selfp->idle_id = gdk_threads_add_idle((GSourceFunc)self_idle_cb, self);
+
+ /*
+ * To prevent a visible popup build-up for small popups,
+ * the display is frozen until the popup is large enough for
+ * scrolling or until all entries have been added.
+ */
+ GdkWindow *window = gtk_widget_get_window(widget);
+ if (window) {
+ gdk_window_freeze_updates(window);
+ selfp->frozen = TRUE;
+ }
+ }
+
+ PARENT_HANDLER(widget);
+ }
+
+ private gboolean
+ idle_cb(self)
+ {
+ /*
+ * The more often this is repeated, the faster we will add all popup entries,
+ * but at the same time, the UI will be less responsive.
+ */
+ for (gint i = 0; i < 5; i++) {
+ teco_popup_entry_t *head = (teco_popup_entry_t *)teco_stailq_remove_head(&selfp->list);
+ if (G_UNLIKELY(!head)) {
+ if (selfp->frozen)
+ gdk_window_thaw_updates(gtk_widget_get_window(GTK_WIDGET(self)));
+ selfp->frozen = FALSE;
+ selfp->idle_id = 0;
+ return G_SOURCE_REMOVE;
+ }
+
+ self_idle_add(self, head->type, head->name.data, head->name.len, head->highlight);
+
+ /* All teco_popup_entry_t::names are freed via GStringChunk */
+ g_free(head);
+ }
+
+ if (selfp->frozen &&
+ gtk_adjustment_get_upper(self->vadjustment) -
+ gtk_adjustment_get_lower(self->vadjustment) > gtk_adjustment_get_page_size(self->vadjustment)) {
+ /* the GtkFlowBox needs scrolling - time to thaw */
+ gdk_window_thaw_updates(gtk_widget_get_window(GTK_WIDGET(self)));
+ selfp->frozen = FALSE;
+ }
+
+ return G_SOURCE_CONTINUE;
+ }
+
+ private void
+ idle_add(self, teco_popup_entry_type_t type,
+ const gchar *name, gssize len, gboolean highlight)
+ {
+ GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
+ if (highlight)
+ gtk_style_context_add_class(gtk_widget_get_style_context(hbox),
+ "highlight");
+
+ /*
+ * FIXME: The icon fetching takes about 1/3 of the time required to
+ * add all widgets.
+ * Perhaps it's possible to optimize this.
+ */
+ if (type == TECO_POPUP_FILE || type == TECO_POPUP_DIRECTORY) {
+ const gchar *fallback = type == TECO_POPUP_FILE ? "text-x-generic"
+ : "folder";
+
+ /*
+ * `name` is not guaranteed to be null-terminated.
+ */
+ g_autofree gchar *path = len < 0 ? g_strdup(name) : g_strndup(name, len);
+
+ g_autoptr(GIcon) icon = self_get_icon_for_path(path, fallback);
+ if (icon) {
+ gint width, height;
+ gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height);
+
+ GtkWidget *image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU);
+ /* This is necessary so that oversized icons get scaled down. */
+ gtk_image_set_pixel_size(GTK_IMAGE(image), height);
+ gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);
+ }
+ }
+
+ GtkWidget *label = teco_gtk_label_new(name, len);
+ /*
+ * Gtk v3.20 changed the CSS element names.
+ * Adding a style class eases writing a portable fallback.css.
+ */
+ gtk_style_context_add_class(gtk_widget_get_style_context(label), "label");
+ gtk_widget_set_halign(label, GTK_ALIGN_START);
+ gtk_widget_set_valign(label, GTK_ALIGN_CENTER);
+
+ /*
+ * FIXME: This makes little sense once we've got mouse support.
+ * But for the time being, it's a useful setting.
+ */
+ gtk_label_set_selectable(GTK_LABEL(label), TRUE);
+
+ switch (type) {
+ case TECO_POPUP_PLAIN:
+ gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_START);
+ break;
+ case TECO_POPUP_FILE:
+ case TECO_POPUP_DIRECTORY:
+ gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_MIDDLE);
+ break;
+ }
+
+ gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0);
+
+ gtk_widget_show_all(hbox);
+ gtk_container_add(GTK_CONTAINER(selfp->flow_box), hbox);
+ }
+
+ public void
+ scroll_page(self)
+ {
+ GtkAdjustment *adj = self->vadjustment;
+ gdouble new_value;
+
+ if (gtk_adjustment_get_value(adj) + gtk_adjustment_get_page_size(adj) ==
+ gtk_adjustment_get_upper(adj)) {
+ /* wrap and scroll back to the top */
+ new_value = gtk_adjustment_get_lower(adj);
+ } else {
+ /* scroll one page */
+ new_value = gtk_adjustment_get_value(adj) +
+ gtk_adjustment_get_page_size(adj);
+
+ /*
+ * Adjust this so only complete entries are shown.
+ * Effectively, this rounds down to the line height.
+ */
+ GList *child_list = gtk_container_get_children(GTK_CONTAINER(selfp->flow_box));
+ if (child_list) {
+ new_value -= (gint)new_value %
+ gtk_widget_get_allocated_height(GTK_WIDGET(child_list->data));
+ g_list_free(child_list);
+ }
+
+ /* clip to the maximum possible value */
+ new_value = MIN(new_value, gtk_adjustment_get_upper(adj));
+ }
+
+ gtk_adjustment_set_value(adj, new_value);
+ }
+
+ private void
+ destroy_cb(Gtk:Widget *widget, gpointer user_data)
+ {
+ gtk_widget_destroy(widget);
+ }
+
+ public void
+ clear(self)
+ {
+ gtk_container_foreach(GTK_CONTAINER(selfp->flow_box), self_destroy_cb, NULL);
+
+ /*
+ * If there are still queued popoup entries, the next self_idle_cb()
+ * invocation will also stop the GSource.
+ */
+ teco_stailq_entry_t *entry;
+ while ((entry = teco_stailq_remove_head(&selfp->list)))
+ g_free(entry);
+
+ g_string_chunk_clear(selfp->chunk);
+ }
+}
diff --git a/src/interface-gtk/gtk-canonicalized-label.gob b/src/interface-gtk/teco-gtk-label.gob
index c6adeeb..2167dbc 100644
--- a/src/interface-gtk/gtk-canonicalized-label.gob
+++ b/src/interface-gtk/teco-gtk-label.gob
@@ -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
@@ -28,68 +28,86 @@ requires 2.0.20
#include <gdk/gdk.h>
-/*
- * NOTE: These definitions are also in sciteco.h,
- * but we cannot include them from a plain C file.
- */
-#define IS_CTL(C) ((C) < ' ')
-#define CTL_ECHO(C) ((C) | 0x40)
-#define CTL_KEY_ESC 27
+#include "sciteco.h"
+#include "string-utils.h"
-#define GDK_TO_PANGO_COLOR(X) \
- ((guint16)((X) * G_MAXUINT16))
+#define GDK_TO_PANGO_COLOR(X) ((guint16)((X) * G_MAXUINT16))
%}
%h{
#include <gtk/gtk.h>
%}
-class Gtk:Canonicalized:Label from Gtk:Label {
+class Teco:Gtk:Label from Gtk:Label {
private PangoColor fg;
private guint16 fg_alpha;
private PangoColor bg;
private guint16 bg_alpha;
+ private teco_string_t string
+ destroy {
+ teco_string_clear(&VAR);
+ };
+
override (Gtk:Widget) void
style_updated(Gtk:Widget *widget)
{
Self *self = SELF(widget);
- GtkStyleContext *style;
- GdkRGBA normal_color;
-
PARENT_HANDLER(widget);
- style = gtk_widget_get_style_context(widget);
+ GtkStyleContext *style = gtk_widget_get_style_context(widget);
+ GdkRGBA normal_color;
gtk_style_context_get_color(style, GTK_STATE_NORMAL, &normal_color);
- self->_priv->bg.red = GDK_TO_PANGO_COLOR(normal_color.red);
- self->_priv->bg.green = GDK_TO_PANGO_COLOR(normal_color.green);
- self->_priv->bg.blue = GDK_TO_PANGO_COLOR(normal_color.blue);
- self->_priv->bg_alpha = GDK_TO_PANGO_COLOR(normal_color.alpha);
+ selfp->bg.red = GDK_TO_PANGO_COLOR(normal_color.red);
+ selfp->bg.green = GDK_TO_PANGO_COLOR(normal_color.green);
+ selfp->bg.blue = GDK_TO_PANGO_COLOR(normal_color.blue);
+ selfp->bg_alpha = GDK_TO_PANGO_COLOR(normal_color.alpha);
/*
* If Pango does not support transparent foregrounds,
* it will at least use a high-contrast foreground.
+ *
* NOTE: It would be very hard to get an appropriate background
* color even if Gtk supports it since the label itself may
* not have one but one of its parents.
+ *
* FIXME: We may want to honour the background color,
* so we can at least get decent reverse text when setting
* the background color in the CSS.
*/
- self->_priv->fg.red = G_MAXUINT16 - self->_priv->bg.red;
- self->_priv->fg.green = G_MAXUINT16 - self->_priv->bg.green;
- self->_priv->fg.blue = G_MAXUINT16 - self->_priv->bg.blue;
+ selfp->fg.red = normal_color.red > 0.5 ? 0 : G_MAXUINT16;
+ selfp->fg.green = normal_color.green > 0.5 ? 0 : G_MAXUINT16;
+ selfp->fg.blue = normal_color.blue > 0.5 ? 0 : G_MAXUINT16;
/* try hard to get a transparent foreground anyway */
- self->_priv->fg_alpha = G_MAXUINT16;
+ selfp->fg_alpha = 0;
+
+ /*
+ * The widget might be styled after the text has been set on it,
+ * we must recreate the Pango attributes.
+ */
+ if (selfp->string.len > 0) {
+ PangoAttrList *attribs = NULL;
+ g_autofree gchar *plaintext = NULL;
+
+ self_parse_string(selfp->string.data, selfp->string.len,
+ &selfp->fg, selfp->fg_alpha,
+ &selfp->bg, selfp->bg_alpha,
+ &attribs, &plaintext);
+
+ gtk_label_set_attributes(GTK_LABEL(self), attribs);
+ pango_attr_list_unref(attribs);
+
+ g_assert(!g_strcmp0(plaintext, gtk_label_get_text(GTK_LABEL(self))));
+ }
}
public GtkWidget *
- new(const gchar *str)
+ new(const gchar *str, gssize len)
{
Self *widget = GET_NEW;
- self_set_text(widget, str);
+ self_set_text(widget, str, len);
return GTK_WIDGET(widget);
}
@@ -102,6 +120,11 @@ class Gtk:Canonicalized:Label from Gtk:Label {
{
PangoAttribute *attr;
+ /*
+ * NOTE: Transparent foreground do not seem to work,
+ * even in Pango v1.38.
+ * Perhaps, this has been fixed in later versions.
+ */
#if PANGO_VERSION_CHECK(1,38,0)
attr = pango_attr_foreground_alpha_new(fg_alpha);
attr->start_index = index;
@@ -131,28 +154,27 @@ class Gtk:Canonicalized:Label from Gtk:Label {
Pango:Color *bg, guint16 bg_alpha,
Pango:Attr:List **attribs, gchar **text)
{
- gsize text_len = 1; /* for trailing 0 */
- gint index = 0;
-
if (len < 0)
len = strlen(str);
/*
* Approximate size of unformatted text.
*/
+ gsize text_len = 1; /* for trailing 0 */
for (gint i = 0; i < len; i++)
- text_len += IS_CTL(str[i]) ? 3 : 1;
+ text_len += TECO_IS_CTL(str[i]) ? 3 : 1;
*attribs = pango_attr_list_new();
*text = g_malloc(text_len);
+ gint index = 0;
while (len > 0) {
/*
* NOTE: This mapping is similar to
- * View::set_presentations()
+ * teco_view_set_presentations()
*/
switch (*str) {
- case CTL_KEY_ESC:
+ case '\e':
self_add_highlight_attribs(*attribs,
fg, fg_alpha,
bg, bg_alpha,
@@ -185,13 +207,13 @@ class Gtk:Canonicalized:Label from Gtk:Label {
(*text)[index++] = 'B';
break;
default:
- if (IS_CTL(*str)) {
+ if (TECO_IS_CTL(*str)) {
self_add_highlight_attribs(*attribs,
fg, fg_alpha,
bg, bg_alpha,
index, 2);
(*text)[index++] = '^';
- (*text)[index++] = CTL_ECHO(*str);
+ (*text)[index++] = TECO_CTL_ECHO(*str);
} else {
(*text)[index++] = *str;
}
@@ -207,20 +229,25 @@ class Gtk:Canonicalized:Label from Gtk:Label {
}
public void
- set_text(self, const gchar *str)
+ set_text(self, const gchar *str, gssize len)
{
- PangoAttrList *attribs = NULL;
- gchar *plaintext = NULL;
+ teco_string_clear(&selfp->string);
+ teco_string_init(&selfp->string, str, len < 0 ? strlen(str) : len);
+
+ g_autofree gchar *plaintext = NULL;
- if (str)
- self_parse_string(str, -1,
- &self->_priv->fg, self->_priv->fg_alpha,
- &self->_priv->bg, self->_priv->bg_alpha,
+ if (selfp->string.len > 0) {
+ PangoAttrList *attribs = NULL;
+
+ self_parse_string(selfp->string.data, selfp->string.len,
+ &selfp->fg, selfp->fg_alpha,
+ &selfp->bg, selfp->bg_alpha,
&attribs, &plaintext);
- gtk_label_set_attributes(GTK_LABEL(self), attribs);
- pango_attr_list_unref(attribs);
+ gtk_label_set_attributes(GTK_LABEL(self), attribs);
+ pango_attr_list_unref(attribs);
+ }
+
gtk_label_set_text(GTK_LABEL(self), plaintext);
- g_free(plaintext);
}
}
diff --git a/src/interface.c b/src/interface.c
new file mode 100644
index 0000000..21a83ff
--- /dev/null
+++ b/src/interface.c
@@ -0,0 +1,120 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <stdarg.h>
+#include <stdio.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include <Scintilla.h>
+#include <SciLexer.h>
+
+#include "sciteco.h"
+#include "undo.h"
+#include "view.h"
+#include "interface.h"
+
+//#define DEBUG
+
+teco_view_t *teco_interface_current_view = NULL;
+
+TECO_DEFINE_UNDO_CALL(teco_interface_show_view, teco_view_t *);
+TECO_DEFINE_UNDO_CALL(teco_interface_ssm, unsigned int, uptr_t, sptr_t);
+TECO_DEFINE_UNDO_CALL(teco_interface_info_update_qreg, const teco_qreg_t *);
+TECO_DEFINE_UNDO_CALL(teco_interface_info_update_buffer, const teco_buffer_t *);
+
+typedef struct {
+ teco_string_t str;
+ gchar name[];
+} teco_undo_set_clipboard_t;
+
+static void
+teco_undo_set_clipboard_action(teco_undo_set_clipboard_t *ctx, gboolean run)
+{
+ if (run)
+ teco_interface_set_clipboard(ctx->name, ctx->str.data, ctx->str.len, NULL);
+ teco_string_clear(&ctx->str);
+}
+
+/**
+ * Set the clipboard upon rubout.
+ *
+ * This passes ownership of the clipboard content string
+ * to the undo token object.
+ */
+void
+teco_interface_undo_set_clipboard(const gchar *name, gchar *str, gsize len)
+{
+ teco_undo_set_clipboard_t *ctx;
+
+ ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_set_clipboard_action,
+ sizeof(*ctx) + strlen(name) + 1);
+ if (ctx) {
+ ctx->str.data = str;
+ ctx->str.len = len;
+ strcpy(ctx->name, name);
+ } else {
+ g_free(str);
+ }
+}
+
+/**
+ * Print a message to the appropriate stdio streams.
+ *
+ * This method has similar semantics to `vprintf`, i.e.
+ * it leaves `ap` undefined. Therefore to pass the format
+ * string and arguments to another `vprintf`-like function,
+ * you have to copy the arguments via `va_copy`.
+ */
+void
+teco_interface_stdio_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
+{
+ FILE *stream = stdout;
+
+ switch (type) {
+ case TECO_MSG_USER:
+ break;
+ case TECO_MSG_INFO:
+ fputs("Info: ", stream);
+ break;
+ case TECO_MSG_WARNING:
+ stream = stderr;
+ fputs("Warning: ", stream);
+ break;
+ case TECO_MSG_ERROR:
+ stream = stderr;
+ fputs("Error: ", stream);
+ break;
+ }
+
+ g_vfprintf(stream, fmt, ap);
+ fputc('\n', stream);
+}
+
+void
+teco_interface_process_notify(struct SCNotification *notify)
+{
+#ifdef DEBUG
+ g_printf("SCINTILLA NOTIFY: code=%d\n", notify->nmhdr.code);
+#endif
+}
diff --git a/src/interface.cpp b/src/interface.cpp
deleted file mode 100644
index cc8b069..0000000
--- a/src/interface.cpp
+++ /dev/null
@@ -1,180 +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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <stdarg.h>
-#include <stdio.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-#include <Scintilla.h>
-#include <SciLexer.h>
-
-#include "sciteco.h"
-#include "interface.h"
-
-namespace SciTECO {
-
-template <class ViewImpl>
-void
-View<ViewImpl>::set_representations(void)
-{
- 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'};
- ssm(SCI_SETREPRESENTATION, (uptr_t)buf, (sptr_t)reps[cc]);
- }
-}
-
-template <class ViewImpl>
-void
-View<ViewImpl>::setup(void)
-{
- /*
- * Start with or without undo collection,
- * depending on undo.enabled.
- */
- ssm(SCI_SETUNDOCOLLECTION, undo.enabled);
-
- ssm(SCI_SETFOCUS, TRUE);
-
- /*
- * Some Scintilla implementations show the horizontal
- * scroll bar by default.
- * Ensure it is never displayed by default.
- */
- ssm(SCI_SETHSCROLLBAR, FALSE);
-
- /*
- * Only margin 1 is given a width by default.
- * To provide a minimalist default view, it is disabled.
- */
- ssm(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).
- */
- ssm(SCI_SETCARETSTYLE, CARETSTYLE_BLOCK);
- ssm(SCI_SETCARETPERIOD, 0);
- ssm(SCI_SETCARETFORE, 0xFFFFFF);
-
- ssm(SCI_STYLESETFORE, STYLE_DEFAULT, 0xFFFFFF);
- ssm(SCI_STYLESETBACK, STYLE_DEFAULT, 0x000000);
- ssm(SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)"Courier");
- ssm(SCI_STYLECLEARALL);
-
- /*
- * FIXME: The line number background is apparently not
- * affected by SCI_STYLECLEARALL
- */
- ssm(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).
- */
- ssm(SCI_STYLESETFORE, STYLE_CALLTIP, 0x000000);
- ssm(SCI_STYLESETBACK, STYLE_CALLTIP, 0xFFFFFF);
-}
-
-template class View<ViewCurrent>;
-
-template <class InterfaceImpl, class ViewImpl>
-void
-Interface<InterfaceImpl, ViewImpl>::UndoTokenShowView::run(void)
-{
- /*
- * Implementing this here allows us to reference
- * `interface`
- */
- interface.show_view(view);
-}
-
-template <class InterfaceImpl, class ViewImpl>
-template <class Type>
-void
-Interface<InterfaceImpl, ViewImpl>::UndoTokenInfoUpdate<Type>::run(void)
-{
- interface.info_update(obj);
-}
-
-/**
- * Print a message to the appropriate stdio streams.
- *
- * This method has similar semantics to `vprintf`, i.e.
- * it leaves `ap` undefined. Therefore to pass the format
- * string and arguments to another `vprintf`-like function,
- * you have to copy the arguments via `va_copy`.
- */
-template <class InterfaceImpl, class ViewImpl>
-void
-Interface<InterfaceImpl, ViewImpl>::stdio_vmsg(MessageType type, const gchar *fmt, va_list ap)
-{
- FILE *stream = stdout;
-
- switch (type) {
- case MSG_USER:
- break;
- case MSG_INFO:
- fputs("Info: ", stream);
- break;
- case MSG_WARNING:
- stream = stderr;
- fputs("Warning: ", stream);
- break;
- case MSG_ERROR:
- stream = stderr;
- fputs("Error: ", stream);
- break;
- }
-
- g_vfprintf(stream, fmt, ap);
- fputc('\n', stream);
-}
-
-template <class InterfaceImpl, class ViewImpl>
-void
-Interface<InterfaceImpl, ViewImpl>::process_notify(SCNotification *notify)
-{
-#ifdef DEBUG
- g_printf("SCINTILLA NOTIFY: code=%d\n", notify->nmhdr.code);
-#endif
-}
-
-template class Interface<InterfaceCurrent, ViewCurrent>;
-
-} /* namespace SciTECO */
diff --git a/src/interface.h b/src/interface.h
index 607a42c..c396225 100644
--- a/src/interface.h
+++ b/src/interface.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,9 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __INTERFACE_H
-#define __INTERFACE_H
+#pragma once
#include <stdarg.h>
#include <signal.h>
@@ -25,349 +23,151 @@
#include <Scintilla.h>
-#include "memory.h"
-#include "undo.h"
-#include "error.h"
-
-namespace SciTECO {
-
-/* avoid include dependency conflict */
-class QRegister;
-class Buffer;
-class Cmdline;
-extern sig_atomic_t sigint_occurred;
+#include "sciteco.h"
+#include "qreg.h"
+#include "ring.h"
+#include "cmdline.h"
+#include "view.h"
/**
- * Base class for all SciTECO views. This is a minimal
- * abstraction where the implementor only has to provide
- * a method for dispatching Scintilla messages.
- * Everything else is handled by other SciTECO classes.
+ * @file
+ * Abstract user interface.
*
- * This interface employs the Curiously Recurring Template
- * Pattern (CRTP). To implement it, one must derive from
- * View<DerivedClass>. The methods to implement actually
- * have an "_impl" suffix so as to avoid infinite recursion
- * if an implementation is missing.
- * Externally however, the methods as given in this interface
- * may be called.
+ * Interface of all SciTECO user interfaces (e.g. Curses or GTK+).
+ * All functions that must be provided are marked with the \@pure tag.
*
- * The CRTP has a runtime overhead at low optimization levels
- * (additional non-inlined calls), but should provide a
- * significant performance boost when inlining is enabled.
- *
- * Note that not all methods have to be defined in the
- * class. Explicit template instantiation is used to outsource
- * base-class implementations to interface.cpp.
+ * @note
+ * We do not provide default implementations for any of the interface
+ * functions by declaring them "weak" since this is a non-portable linker
+ * feature.
*/
-template <class ViewImpl>
-class View : public Object {
- inline ViewImpl &
- impl(void)
- {
- return *(ViewImpl *)this;
- }
-
- class UndoTokenMessage : public UndoToken {
- ViewImpl &view;
- unsigned int iMessage;
- uptr_t wParam;
- sptr_t lParam;
+/** @protected */
+extern teco_view_t *teco_interface_current_view;
- public:
- UndoTokenMessage(ViewImpl &_view, unsigned int _iMessage,
- uptr_t _wParam = 0, sptr_t _lParam = 0)
- : view(_view), iMessage(_iMessage),
- wParam(_wParam), lParam(_lParam) {}
+/** @pure */
+void teco_interface_init(void);
- void
- run(void)
- {
- view.ssm(iMessage, wParam, lParam);
- }
- };
+/** @pure */
+GOptionGroup *teco_interface_get_options(void);
- class UndoTokenSetRepresentations : public UndoToken {
- ViewImpl &view;
+/** @pure makes sense only on Curses */
+void teco_interface_init_color(guint color, guint32 rgb);
- public:
- UndoTokenSetRepresentations(ViewImpl &_view)
- : view(_view) {}
+typedef enum {
+ TECO_MSG_USER,
+ TECO_MSG_INFO,
+ TECO_MSG_WARNING,
+ TECO_MSG_ERROR
+} teco_msg_t;
- void
- run(void)
- {
- view.set_representations();
- }
- };
+/** @pure */
+void teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap);
-public:
- /*
- * called after Interface initialization.
- * Should call setup()
- */
- inline void
- initialize(void)
- {
- impl().initialize_impl();
- }
+static inline void G_GNUC_PRINTF(2, 3)
+teco_interface_msg(teco_msg_t type, const gchar *fmt, ...)
+{
+ va_list ap;
- inline sptr_t
- ssm(unsigned int iMessage,
- uptr_t wParam = 0, sptr_t lParam = 0)
- {
- return impl().ssm_impl(iMessage, wParam, lParam);
- }
+ va_start(ap, fmt);
+ teco_interface_vmsg(type, fmt, ap);
+ va_end(ap);
+}
- inline void
- undo_ssm(unsigned int iMessage,
- uptr_t wParam = 0, sptr_t lParam = 0)
- {
- undo.push<UndoTokenMessage>(impl(), iMessage, wParam, lParam);
- }
+/** @pure */
+void teco_interface_msg_clear(void);
- void set_representations(void);
- inline void
- undo_set_representations(void)
- {
- undo.push<UndoTokenSetRepresentations>(impl());
- }
+/** @pure */
+void teco_interface_show_view(teco_view_t *view);
+void undo__teco_interface_show_view(teco_view_t *);
- inline void
- set_scintilla_undo(bool state)
- {
- ssm(SCI_EMPTYUNDOBUFFER);
- ssm(SCI_SETUNDOCOLLECTION, state);
- }
+static inline sptr_t
+teco_interface_ssm(unsigned int iMessage, uptr_t wParam, sptr_t lParam)
+{
+ return teco_view_ssm(teco_interface_current_view, iMessage, wParam, lParam);
+}
-protected:
- void setup(void);
-};
-
-/**
- * Base class and interface of all SciTECO user interfaces
- * (e.g. Curses or GTK+).
- *
- * This uses the same Curiously Recurring Template Pattern (CRTP)
- * as the View interface above, as there is only one type of
- * user interface at runtime.
+/*
+ * NOTE: You could simply call undo__teco_view_ssm(teco_interface_current_view, ...).
+ * undo__teco_interface_ssm(...) exists for brevity and aestethics.
*/
-template <class InterfaceImpl, class ViewImpl>
-class Interface : public Object {
- inline InterfaceImpl &
- impl(void)
- {
- return *(InterfaceImpl *)this;
- }
-
- class UndoTokenShowView : public UndoToken {
- ViewImpl *view;
-
- public:
- UndoTokenShowView(ViewImpl *_view)
- : view(_view) {}
-
- void run(void);
- };
-
- template <class Type>
- class UndoTokenInfoUpdate : public UndoToken {
- const Type *obj;
-
- public:
- UndoTokenInfoUpdate(const Type *_obj)
- : obj(_obj) {}
-
- void run(void);
- };
-
-protected:
- ViewImpl *current_view;
-
-public:
- Interface() : current_view(NULL) {}
-
- /* default implementation */
- inline GOptionGroup *
- get_options(void)
- {
- return NULL;
- }
-
- /* default implementation */
- inline void init(void) {}
-
- /* makes sense only on Curses */
- inline void init_color(guint color, guint32 rgb) {}
+void undo__teco_interface_ssm(unsigned int, uptr_t, sptr_t);
- enum MessageType {
- MSG_USER,
- MSG_INFO,
- MSG_WARNING,
- MSG_ERROR
- };
- inline void
- vmsg(MessageType type, const gchar *fmt, va_list ap)
- {
- impl().vmsg_impl(type, fmt, ap);
- }
- inline void
- msg(MessageType type, const gchar *fmt, ...) G_GNUC_PRINTF(3, 4)
- {
- va_list ap;
+/** @pure */
+void teco_interface_info_update_qreg(const teco_qreg_t *reg);
+/** @pure */
+void teco_interface_info_update_buffer(const teco_buffer_t *buffer);
- va_start(ap, fmt);
- vmsg(type, fmt, ap);
- va_end(ap);
- }
- /* default implementation */
- inline void msg_clear(void) {}
+#define teco_interface_info_update(X) \
+ (_Generic((X), teco_qreg_t * : teco_interface_info_update_qreg, \
+ const teco_qreg_t * : teco_interface_info_update_qreg, \
+ teco_buffer_t * : teco_interface_info_update_buffer, \
+ const teco_buffer_t * : teco_interface_info_update_buffer)(X))
- inline void
- show_view(ViewImpl *view)
- {
- impl().show_view_impl(view);
- }
- inline void
- undo_show_view(ViewImpl *view)
- {
- undo.push<UndoTokenShowView>(view);
- }
+void undo__teco_interface_info_update_qreg(const teco_qreg_t *);
+void undo__teco_interface_info_update_buffer(const teco_buffer_t *);
- inline ViewImpl *
- get_current_view(void)
- {
- return current_view;
- }
+/** @pure */
+void teco_interface_cmdline_update(const teco_cmdline_t *cmdline);
- inline sptr_t
- ssm(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0)
- {
- return current_view->ssm(iMessage, wParam, lParam);
- }
- inline void
- undo_ssm(unsigned int iMessage,
- uptr_t wParam = 0, sptr_t lParam = 0)
- {
- current_view->undo_ssm(iMessage, wParam, lParam);
- }
-
- /*
- * NOTE: could be rolled into a template, but
- * this way it is explicit what must be implemented
- * by the deriving class.
- */
- inline void
- info_update(const QRegister *reg)
- {
- impl().info_update_impl(reg);
- }
- inline void
- info_update(const Buffer *buffer)
- {
- impl().info_update_impl(buffer);
- }
-
- inline void
- undo_info_update(const QRegister *reg)
- {
- undo.push<UndoTokenInfoUpdate<QRegister>>(reg);
- }
- inline void
- undo_info_update(const Buffer *buffer)
- {
- undo.push<UndoTokenInfoUpdate<Buffer>>(buffer);
- }
-
- inline void
- cmdline_update(const Cmdline *cmdline)
- {
- impl().cmdline_update_impl(cmdline);
- }
-
- /* default implementation */
- inline void
- set_clipboard(const gchar *name,
- const gchar *str = NULL, gssize str_len = -1)
- {
- throw Error("Setting clipboard unsupported");
- }
-
- /* default implementation */
- inline gchar *
- get_clipboard(const gchar *name, gsize *str_len = NULL)
- {
- throw Error("Getting clipboard unsupported");
- }
-
- enum PopupEntryType {
- POPUP_PLAIN,
- POPUP_FILE,
- POPUP_DIRECTORY
- };
- inline void
- popup_add(PopupEntryType type,
- const gchar *name, bool highlight = false)
- {
- impl().popup_add_impl(type, name, highlight);
- }
- inline void
- popup_show(void)
- {
- impl().popup_show_impl();
- }
- inline bool
- popup_is_shown(void)
- {
- return impl().popup_is_shown_impl();
- }
- inline void
- popup_clear(void)
- {
- impl().popup_clear_impl();
- }
-
- /* default implementation */
- inline bool
- is_interrupted(void)
- {
- return sigint_occurred != FALSE;
- }
-
- /* main entry point */
- inline void
- event_loop(void)
- {
- impl().event_loop_impl();
- }
-
- /*
- * Interfacing to the external SciTECO world
- */
-protected:
- void stdio_vmsg(MessageType type, const gchar *fmt, va_list ap);
-public:
- void process_notify(SCNotification *notify);
-};
-
-} /* namespace SciTECO */
-
-#ifdef INTERFACE_GTK
-#include "interface-gtk/interface-gtk.h"
-#elif defined(INTERFACE_CURSES)
-#include "interface-curses/interface-curses.h"
-#else
-#error No interface selected!
-#endif
-
-namespace SciTECO {
+/** @pure */
+gboolean teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len,
+ GError **error);
+void teco_interface_undo_set_clipboard(const gchar *name, gchar *str, gsize len);
+/**
+ * Semantics are compatible with teco_qreg_vtable_t::get_string() since that is the
+ * main user of this function.
+ *
+ * @pure
+ */
+gboolean teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error);
+
+typedef enum {
+ TECO_POPUP_PLAIN,
+ TECO_POPUP_FILE,
+ TECO_POPUP_DIRECTORY
+} teco_popup_entry_type_t;
+
+/** @pure */
+void teco_interface_popup_add(teco_popup_entry_type_t type,
+ const gchar *name, gsize name_len, gboolean highlight);
+/** @pure */
+void teco_interface_popup_show(void);
+/** @pure */
+gboolean teco_interface_popup_is_shown(void);
+/** @pure */
+void teco_interface_popup_clear(void);
+
+/** @pure */
+gboolean teco_interface_is_interrupted(void);
+
+/** @pure main entry point */
+gboolean teco_interface_event_loop(GError **error);
-/* object defined in main.cpp */
-extern InterfaceCurrent interface;
+/*
+ * Interfacing to the external SciTECO world
+ */
+/** @protected */
+void teco_interface_stdio_vmsg(teco_msg_t type, const gchar *fmt, va_list ap);
+void teco_interface_process_notify(struct SCNotification *notify);
-extern template class View<ViewCurrent>;
-extern template class Interface<InterfaceCurrent, ViewCurrent>;
+/** @pure */
+void teco_interface_cleanup(void);
-} /* namespace SciTECO */
+/*
+ * The following functions are here for lack of a better place.
+ * They could also be in sciteco.h, but only if declared as non-inline
+ * since sciteco.h should not depend on interface.h.
+ */
-#endif
+static inline gboolean
+teco_validate_pos(teco_int_t n)
+{
+ return 0 <= n && n <= teco_interface_ssm(SCI_GETLENGTH, 0, 0);
+}
+
+static inline gboolean
+teco_validate_line(teco_int_t n)
+{
+ return 0 <= n && n < teco_interface_ssm(SCI_GETLINECOUNT, 0, 0);
+}
diff --git a/src/ioview.cpp b/src/ioview.cpp
deleted file mode 100644
index 383b9bb..0000000
--- a/src/ioview.cpp
+++ /dev/null
@@ -1,512 +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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <limits.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-#include <errno.h>
-#include <sys/stat.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-#include <Scintilla.h>
-
-#include "sciteco.h"
-#include "interface.h"
-#include "undo.h"
-#include "error.h"
-#include "qregisters.h"
-#include "eol.h"
-#include "ioview.h"
-
-#ifdef HAVE_WINDOWS_H
-/* here it shouldn't cause conflicts with other headers */
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#endif
-
-namespace SciTECO {
-
-#ifdef G_OS_WIN32
-
-typedef DWORD FileAttributes;
-/* INVALID_FILE_ATTRIBUTES already defined */
-
-static inline FileAttributes
-get_file_attributes(const gchar *filename)
-{
- return GetFileAttributes((LPCTSTR)filename);
-}
-
-static inline void
-set_file_attributes(const gchar *filename, FileAttributes attrs)
-{
- SetFileAttributes((LPCTSTR)filename, attrs);
-}
-
-#else
-
-typedef int FileAttributes;
-#define INVALID_FILE_ATTRIBUTES (-1)
-
-static inline FileAttributes
-get_file_attributes(const gchar *filename)
-{
- struct stat buf;
-
- return g_stat(filename, &buf) ? INVALID_FILE_ATTRIBUTES : buf.st_mode;
-}
-
-static inline void
-set_file_attributes(const gchar *filename, FileAttributes attrs)
-{
- g_chmod(filename, attrs);
-}
-
-#endif /* !G_OS_WIN32 */
-
-/**
- * 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 channel Channel to read from.
- */
-void
-IOView::load(GIOChannel *channel)
-{
- GStatBuf stat_buf;
-
- EOLReaderGIO reader(channel);
-
- ssm(SCI_BEGINUNDOACTION);
- ssm(SCI_CLEARALL);
-
- /*
- * 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.
- */
- stat_buf.st_size = 0;
- if (!fstat(g_io_channel_unix_get_fd(channel), &stat_buf) &&
- stat_buf.st_size > 0)
- ssm(SCI_ALLOCATE, stat_buf.st_size);
-
- try {
- const gchar *data;
- gsize data_len;
-
- while ((data = reader.convert(data_len)))
- ssm(SCI_APPENDTEXT, data_len, (sptr_t)data);
- } catch (...) {
- ssm(SCI_ENDUNDOACTION);
- throw; /* forward */
- }
-
- /*
- * 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)
- ssm(SCI_SETEOLMODE, reader.eol_style);
-
- if (reader.eol_style_inconsistent)
- interface.msg(InterfaceCurrent::MSG_WARNING,
- "Inconsistent EOL styles normalized");
-
- ssm(SCI_ENDUNDOACTION);
-}
-
-/**
- * Load view's document from file.
- */
-void
-IOView::load(const gchar *filename)
-{
- GError *error = NULL;
- GIOChannel *channel;
-
- channel = g_io_channel_new_file(filename, "r", &error);
- if (!channel) {
- Error err("Error opening file \"%s\" for reading: %s",
- filename, error->message);
- g_error_free(error);
- throw err;
- }
-
- /*
- * 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);
-
- try {
- load(channel);
- } catch (Error &e) {
- Error err("Error reading file \"%s\": %s",
- filename, e.description);
- g_io_channel_unref(channel);
- throw err;
- }
-
- /* also closes file: */
- g_io_channel_unref(channel);
-}
-
-#if 0
-
-/*
- * TODO: on UNIX it may be better to open() the current file, unlink() it
- * and keep the file descriptor in the UndoToken.
- * 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;
-
-class UndoTokenRestoreSavePoint : public UndoToken {
- gchar *savepoint;
- gchar *filename;
-
-#ifdef G_OS_WIN32
- FileAttributes orig_attrs;
-#endif
-
-public:
- UndoTokenRestoreSavePoint(gchar *_savepoint, const gchar *_filename)
- : savepoint(_savepoint), filename(g_strdup(_filename))
- {
-#ifdef G_OS_WIN32
- orig_attrs = get_file_attributes(filename);
- if (orig_attrs != INVALID_FILE_ATTRIBUTES)
- set_file_attributes(savepoint,
- orig_attrs | FILE_ATTRIBUTE_HIDDEN);
-#endif
- }
-
- ~UndoTokenRestoreSavePoint()
- {
- if (savepoint) {
- g_unlink(savepoint);
- g_free(savepoint);
- }
- g_free(filename);
-
- savepoint_id--;
- }
-
- void
- run(void)
- {
- if (!g_rename(savepoint, filename)) {
- g_free(savepoint);
- savepoint = NULL;
-#ifdef G_OS_WIN32
- if (orig_attrs != INVALID_FILE_ATTRIBUTES)
- set_file_attributes(filename,
- orig_attrs);
-#endif
- } else {
- interface.msg(InterfaceCurrent::MSG_WARNING,
- "Unable to restore save point file \"%s\"",
- savepoint);
- }
- }
-};
-
-static void
-make_savepoint(const gchar *filename)
-{
- gchar *dirname, *basename, *savepoint;
- gchar savepoint_basename[FILENAME_MAX];
-
- basename = g_path_get_basename(filename);
- g_snprintf(savepoint_basename, sizeof(savepoint_basename),
- ".teco-%d-%s~", savepoint_id, basename);
- g_free(basename);
- dirname = g_path_get_dirname(filename);
- savepoint = g_build_filename(dirname, savepoint_basename, NIL);
- g_free(dirname);
-
- if (g_rename(filename, savepoint)) {
- interface.msg(InterfaceCurrent::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.
- */
- undo.push_own<UndoTokenRestoreSavePoint>(savepoint, filename);
-}
-
-#endif
-
-void
-IOView::save(GIOChannel *channel)
-{
- EOLWriterGIO writer(channel, ssm(SCI_GETEOLMODE));
- sptr_t gap;
- gsize size;
- const gchar *buffer;
- gsize bytes_written;
-
- /* write part of buffer before gap */
- gap = ssm(SCI_GETGAPPOSITION);
- if (gap > 0) {
- buffer = (const gchar *)ssm(SCI_GETRANGEPOINTER, 0, gap);
- bytes_written = writer.convert(buffer, gap);
- g_assert(bytes_written == (gsize)gap);
- }
-
- /* write part of buffer after gap */
- size = ssm(SCI_GETLENGTH) - gap;
- if (size > 0) {
- buffer = (const gchar *)ssm(SCI_GETRANGEPOINTER, gap, (sptr_t)size);
- bytes_written = writer.convert(buffer, size);
- g_assert(bytes_written == size);
- }
-}
-
-void
-IOView::save(const gchar *filename)
-{
- GError *error = NULL;
- GIOChannel *channel;
-
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
- GStatBuf file_stat;
- file_stat.st_uid = -1;
- file_stat.st_gid = -1;
-#endif
- FileAttributes attributes = INVALID_FILE_ATTRIBUTES;
-
- if (undo.enabled) {
- if (g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
- g_stat(filename, &file_stat);
-#endif
- attributes = get_file_attributes(filename);
- make_savepoint(filename);
- } else {
- undo.push<UndoTokenRemoveFile>(filename);
- }
- }
-
- /* leaves access mode intact if file still exists */
- channel = g_io_channel_new_file(filename, "w", &error);
- if (!channel)
- throw GlibError(error);
-
- /*
- * save(GIOChannel *, const gchar *) expects a buffered
- * and blocking channel
- */
- g_io_channel_set_encoding(channel, NULL, NULL);
- g_io_channel_set_buffered(channel, TRUE);
-
- try {
- save(channel);
- } catch (Error &e) {
- Error err("Error writing file \"%s\": %s", filename, e.description);
- g_io_channel_unref(channel);
- throw err;
- }
-
- /* if file existed but has been renamed, restore attributes */
- if (attributes != INVALID_FILE_ATTRIBUTES)
- set_file_attributes(filename, attributes);
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
- /*
- * 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))
- interface.msg(InterfaceCurrent::MSG_WARNING,
- "Unable to preserve owner of \"%s\": %s",
- filename, g_strerror(errno));
-#endif
-
- /* also closes file */
- g_io_channel_unref(channel);
-}
-
-/*
- * Auxiliary functions
- */
-
-/**
- * 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 *
-expand_path(const gchar *path)
-{
- gchar *home, *ret;
-
- if (!path)
- path = "";
-
- 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).
- */
- home = QRegisters::globals["$HOME"]->get_string();
- ret = g_build_filename(home, path+1, NIL);
- g_free(home);
-
- return ret;
-}
-
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
-
-gchar *
-get_absolute_path(const gchar *path)
-{
- gchar buf[PATH_MAX];
- gchar *resolved;
-
- if (!path)
- return NULL;
-
- if (realpath(path, buf)) {
- resolved = g_strdup(buf);
- } else if (g_path_is_absolute(path)) {
- resolved = g_strdup(path);
- } else {
- gchar *cwd = g_get_current_dir();
- resolved = g_build_filename(cwd, path, NIL);
- g_free(cwd);
- }
-
- return resolved;
-}
-
-bool
-file_is_visible(const gchar *path)
-{
- gchar *basename = g_path_get_basename(path);
- bool ret = *basename != '.';
-
- g_free(basename);
- return ret;
-}
-
-#elif defined(G_OS_WIN32)
-
-gchar *
-get_absolute_path(const gchar *path)
-{
- TCHAR buf[MAX_PATH];
- gchar *resolved = NULL;
-
- if (path && GetFullPathName(path, sizeof(buf), buf, NULL))
- resolved = g_strdup(buf);
-
- return resolved;
-}
-
-bool
-file_is_visible(const gchar *path)
-{
- return !(get_file_attributes(path) & FILE_ATTRIBUTE_HIDDEN);
-}
-
-#else /* !G_OS_UNIX && !G_OS_HAIKU && !G_OS_WIN32 */
-
-/*
- * 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 *
-get_absolute_path(const gchar *path)
-{
- gchar *resolved;
-
- if (!path)
- return NULL;
-
- if (g_path_is_absolute(path)) {
- resolved = g_strdup(path);
- } else {
- gchar *cwd = g_get_current_dir();
- resolved = g_build_filename(cwd, path, NIL);
- g_free(cwd);
- }
-
- return resolved;
-}
-
-/*
- * There's no platform-independent way to determine if a file
- * is visible/hidden, so we just assume that all files are
- * visible.
- */
-bool
-file_is_visible(const gchar *path)
-{
- return true;
-}
-
-#endif /* !G_OS_UNIX && !G_OS_HAIKU && !G_OS_WIN32 */
-
-} /* namespace SciTECO */
diff --git a/src/list.h b/src/list.h
new file mode 100644
index 0000000..7249f9f
--- /dev/null
+++ b/src/list.h
@@ -0,0 +1,94 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+typedef struct teco_stailq_entry_t {
+ struct teco_stailq_entry_t *next;
+} teco_stailq_entry_t;
+
+typedef struct {
+ /** Pointer to the first element or NULL */
+ teco_stailq_entry_t *first;
+ /** Pointer to the last element's `next` field or the head's `first` field */
+ teco_stailq_entry_t **last;
+} teco_stailq_head_t;
+
+#define TECO_STAILQ_HEAD_INITIALIZER(HEAD) ((teco_stailq_head_t){NULL, &(HEAD)->first})
+
+static inline void
+teco_stailq_insert_tail(teco_stailq_head_t *head, teco_stailq_entry_t *entry)
+{
+ entry->next = NULL;
+ *head->last = entry;
+ head->last = &entry->next;
+}
+
+static inline teco_stailq_entry_t *
+teco_stailq_remove_head(teco_stailq_head_t *head)
+{
+ teco_stailq_entry_t *first = head->first;
+ if (first && !(head->first = first->next))
+ head->last = &head->first;
+ return first;
+}
+
+/** Can be both a tail queue head or an entry (tail queue element). */
+typedef union teco_tailq_entry_t {
+ struct {
+ /** Pointer to the next entry or NULL */
+ union teco_tailq_entry_t *next;
+ /** Pointer to the previous entry or to the queue head */
+ union teco_tailq_entry_t *prev;
+ };
+
+ struct {
+ /** Pointer to the first entry or NULL */
+ union teco_tailq_entry_t *first;
+ /** Pointer to the last entry or to the queue head */
+ union teco_tailq_entry_t *last;
+ };
+} teco_tailq_entry_t;
+
+#define TECO_TAILQ_HEAD_INITIALIZER(HEAD) ((teco_tailq_entry_t){.first = NULL, .last = (HEAD)})
+
+static inline void
+teco_tailq_insert_before(teco_tailq_entry_t *entry_a, teco_tailq_entry_t *entry_b)
+{
+ entry_b->prev = entry_a->prev;
+ entry_b->next = entry_a;
+ entry_a->prev->next = entry_b;
+ entry_a->prev = entry_b;
+}
+
+static inline void
+teco_tailq_insert_tail(teco_tailq_entry_t *head, teco_tailq_entry_t *entry)
+{
+ entry->next = NULL;
+ entry->prev = head->last;
+ head->last->next = entry;
+ head->last = entry;
+}
+
+static inline void
+teco_tailq_remove(teco_tailq_entry_t *head, teco_tailq_entry_t *entry)
+{
+ if (entry->next)
+ entry->next->prev = entry->prev;
+ else
+ head->last = entry->prev;
+ entry->prev->next = entry->next;
+}
diff --git a/src/main.cpp b/src/main.c
index e7c87d4..3d149d4 100644
--- a/src/main.cpp
+++ b/src/main.c
@@ -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
@@ -29,12 +29,12 @@
#include <glib/gstdio.h>
#include "sciteco.h"
+#include "file-utils.h"
#include "cmdline.h"
#include "interface.h"
-#include "ioview.h"
#include "parser.h"
#include "goto.h"
-#include "qregisters.h"
+#include "qreg.h"
#include "ring.h"
#include "undo.h"
#include "error.h"
@@ -48,47 +48,16 @@
*/
//#define DEBUG_PAUSE
-namespace SciTECO {
-
#define INI_FILE ".teco_ini"
-/*
- * defining the global objects here ensures
- * a ctor/dtor order without depending on the
- * GCC init_priority() attribute
- */
-InterfaceCurrent interface;
-IOView QRegisters::view;
-
-/*
- * Scintilla will be initialized after these
- * ctors (in main()), but dtors are guaranteed
- * to be executed before Scintilla's
- * destruction
- */
-QRegisterTable QRegisters::globals;
-Ring ring;
-
-namespace Flags {
- tecoInt ed = ED_AUTOEOL;
-}
-
-static gchar *eval_macro = NULL;
-static gboolean mung_file = FALSE;
-static gboolean mung_profile = TRUE;
+teco_int_t teco_ed = TECO_ED_AUTOEOL;
-sig_atomic_t sigint_occurred = FALSE;
+volatile sig_atomic_t teco_sigint_occurred = FALSE;
-extern "C" {
-
-static void sigint_handler(int signal);
-
-} /* extern "C" */
-
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
+#ifdef G_OS_UNIX
void
-interrupt(void)
+teco_interrupt(void)
{
/*
* This sends SIGINT to the entire process group,
@@ -96,34 +65,23 @@ interrupt(void)
* even when called from the wrong thread.
*/
if (kill(0, SIGINT))
- sigint_occurred = TRUE;
+ teco_sigint_occurred = TRUE;
}
-#else /* !G_OS_UNIX && !G_OS_HAIKU */
+#else /* !G_OS_UNIX */
void
-interrupt(void)
+teco_interrupt(void)
{
if (raise(SIGINT))
- sigint_occurred = TRUE;
+ teco_sigint_occurred = TRUE;
}
#endif
-const gchar *
-get_eol_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";
- }
-}
-
+/*
+ * FIXME: Move this into file-utils.c?
+ */
#ifdef G_OS_WIN32
/*
@@ -133,60 +91,59 @@ get_eol_seq(gint eol_mode)
* program's directory.
*/
static inline gchar *
-get_default_config_path(const gchar *program)
+teco_get_default_config_path(const gchar *program)
{
return g_path_get_dirname(program);
}
-#elif defined(G_OS_UNIX)
+#elif defined(G_OS_UNIX) && !defined(__HAIKU__)
-/*
- * NOTE: We explicitly do not handle
- * Haiku like UNIX here, since it appears to
- * be uncommon on Haiku to clutter the HOME directory
- * with config files.
- */
static inline gchar *
-get_default_config_path(const gchar *program)
+teco_get_default_config_path(const gchar *program)
{
- return g_strdup(g_getenv("HOME"));
+ return g_strdup(g_get_home_dir());
}
-#else
+#else /* !G_OS_WIN32 && (!G_OS_UNIX || __HAIKU__) */
+/*
+ * NOTE: We explicitly do not handle
+ * Haiku like UNIX, since it appears to
+ * be uncommon on Haiku to clutter the $HOME directory
+ * with config files.
+ */
static inline gchar *
-get_default_config_path(const gchar *program)
+teco_get_default_config_path(const gchar *program)
{
return g_strdup(g_get_user_config_dir());
}
#endif
-static inline gchar *
-process_options(int &argc, char **&argv)
+static gchar *teco_eval_macro = NULL;
+static gboolean teco_mung_file = FALSE;
+static gboolean teco_mung_profile = TRUE;
+
+static gchar *
+teco_process_options(gint *argc, gchar ***argv)
{
static const GOptionEntry option_entries[] = {
- {"eval", 'e', 0, G_OPTION_ARG_STRING, &eval_macro,
+ {"eval", 'e', 0, G_OPTION_ARG_STRING, &teco_eval_macro,
"Evaluate macro", "macro"},
- {"mung", 'm', 0, G_OPTION_ARG_NONE, &mung_file,
+ {"mung", 'm', 0, G_OPTION_ARG_NONE, &teco_mung_file,
"Mung script file (first non-option argument) instead of "
"$SCITECOCONFIG" G_DIR_SEPARATOR_S INI_FILE},
{"no-profile", 0, G_OPTION_FLAG_REVERSE,
- G_OPTION_ARG_NONE, &mung_profile,
+ G_OPTION_ARG_NONE, &teco_mung_profile,
"Do not mung "
"$SCITECOCONFIG" G_DIR_SEPARATOR_S INI_FILE " "
"even if it exists"},
{NULL}
};
- gchar *mung_filename = NULL;
-
- GError *gerror = NULL;
+ g_autoptr(GError) error = NULL;
- GOptionContext *options;
- GOptionGroup *interface_group = interface.get_options();
-
- options = g_option_context_new("[--] [SCRIPT] [ARGUMENT...]");
+ g_autoptr(GOptionContext) options = g_option_context_new("[--] [SCRIPT] [ARGUMENT...]");
g_option_context_set_summary(
options,
@@ -199,12 +156,13 @@ process_options(int &argc, char **&argv)
);
g_option_context_add_main_entries(options, option_entries, NULL);
+
+ GOptionGroup *interface_group = teco_interface_get_options();
if (interface_group)
g_option_context_add_group(options, interface_group);
-#if GLIB_CHECK_VERSION(2,44,0)
/*
- * If possible we parse in POSIX mode, which means that
+ * We parse in POSIX mode, which means that
* the first non-option argument terminates option parsing.
* SciTECO considers all non-option arguments to be script
* arguments and it makes little sense to mix script arguments
@@ -212,20 +170,16 @@ process_options(int &argc, char **&argv)
* in many situations.
* It is also strictly required to make hash-bang lines like
* #!/usr/bin/sciteco -m
- * work (see sciteco(1)).
+ * work.
*/
g_option_context_set_strict_posix(options, TRUE);
-#endif
- if (!g_option_context_parse(options, &argc, &argv, &gerror)) {
+ if (!g_option_context_parse(options, argc, argv, &error)) {
g_fprintf(stderr, "Option parsing failed: %s\n",
- gerror->message);
- g_error_free(gerror);
+ error->message);
exit(EXIT_FAILURE);
}
- g_option_context_free(options);
-
/*
* GOption will NOT remove "--" if followed by an
* option-argument, which may interfer with scripts
@@ -235,38 +189,41 @@ process_options(int &argc, char **&argv)
* and "--" is not the first non-option argument as in
* sciteco foo -- -C bar.
*/
- if (argc >= 2 && !strcmp(argv[1], "--")) {
- argv[1] = argv[0];
- argv++;
- argc--;
+ if (*argc >= 2 && !strcmp((*argv)[1], "--")) {
+ (*argv)[1] = (*argv)[0];
+ (*argv)++;
+ (*argc)--;
}
- if (mung_file) {
- if (argc < 2) {
+ gchar *mung_filename = NULL;
+
+ if (teco_mung_file) {
+ if (*argc < 2) {
g_fprintf(stderr, "Script to mung expected!\n");
exit(EXIT_FAILURE);
}
- if (!g_file_test(argv[1], G_FILE_TEST_IS_REGULAR)) {
+ if (!g_file_test((*argv)[1], G_FILE_TEST_IS_REGULAR)) {
g_fprintf(stderr, "Cannot mung \"%s\". File does not exist!\n",
- argv[1]);
+ (*argv)[1]);
exit(EXIT_FAILURE);
}
- mung_filename = g_strdup(argv[1]);
+ mung_filename = g_strdup((*argv)[1]);
- argv[1] = argv[0];
- argv++;
- argc--;
+ (*argv)[1] = (*argv)[0];
+ (*argv)++;
+ (*argc)--;
}
return mung_filename;
}
-static inline void
-initialize_environment(const gchar *program)
+static void
+teco_initialize_environment(const gchar *program)
{
- gchar *default_configpath, *abs_path;
+ g_autoptr(GError) error = NULL;
+ gchar *abs_path;
/*
* Initialize some "special" environment variables.
@@ -279,44 +236,41 @@ initialize_environment(const gchar *program)
* Initialize and canonicalize $HOME.
* Therefore we can refer to $HOME as the
* current user's home directory on any platform
- * and it can be re-configured even though g_get_home_dir()
- * evaluates $HOME only beginning with glib v2.36.
+ * and you can even start SciTECO with $HOME set to a relative
+ * path (sometimes useful for testing).
*/
g_setenv("HOME", g_get_home_dir(), FALSE);
- abs_path = get_absolute_path(g_getenv("HOME"));
+ abs_path = teco_file_get_absolute_path(g_getenv("HOME"));
g_setenv("HOME", abs_path, TRUE);
g_free(abs_path);
#ifdef G_OS_WIN32
- g_setenv("COMSPEC", "cmd.exe", FALSE);
-#elif defined(G_OS_UNIX) || defined(G_OS_HAIKU)
+ g_setenv("ComSpec", "cmd.exe", FALSE);
+#elif defined(G_OS_UNIX)
g_setenv("SHELL", "/bin/sh", FALSE);
#endif
/*
* Initialize $SCITECOCONFIG and $SCITECOPATH
*/
- default_configpath = get_default_config_path(program);
+ g_autofree gchar *default_configpath = teco_get_default_config_path(program);
g_setenv("SCITECOCONFIG", default_configpath, FALSE);
#ifdef G_OS_WIN32
- gchar *default_scitecopath;
- default_scitecopath = g_build_filename(default_configpath, "lib", NIL);
+ g_autofree gchar *default_scitecopath = g_build_filename(default_configpath, "lib", NULL);
g_setenv("SCITECOPATH", default_scitecopath, FALSE);
- g_free(default_scitecopath);
#else
g_setenv("SCITECOPATH", SCITECOLIBDIR, FALSE);
#endif
- g_free(default_configpath);
/*
* $SCITECOCONFIG and $SCITECOPATH may still be relative.
* They are canonicalized, so macros can use them even
* if the current working directory changes.
*/
- abs_path = get_absolute_path(g_getenv("SCITECOCONFIG"));
+ abs_path = teco_file_get_absolute_path(g_getenv("SCITECOCONFIG"));
g_setenv("SCITECOCONFIG", abs_path, TRUE);
g_free(abs_path);
- abs_path = get_absolute_path(g_getenv("SCITECOPATH"));
+ abs_path = teco_file_get_absolute_path(g_getenv("SCITECOPATH"));
g_setenv("SCITECOPATH", abs_path, TRUE);
g_free(abs_path);
@@ -332,7 +286,11 @@ initialize_environment(const gchar *program)
* the environment variables, the environment should
* be exported via QRegisters::globals.get_environ().
*/
- QRegisters::globals.set_environ();
+ if (!teco_qreg_table_set_environ(&teco_qreg_table_globals, &error)) {
+ g_fprintf(stderr, "Error intializing environment: %s\n",
+ error->message);
+ exit(EXIT_FAILURE);
+ }
}
/*
@@ -340,159 +298,162 @@ initialize_environment(const gchar *program)
*/
static void
-sigint_handler(int signal)
+teco_sigint_handler(int signal)
{
- sigint_occurred = TRUE;
+ teco_sigint_occurred = TRUE;
}
-} /* namespace SciTECO */
-
-/*
- * main() must be defined in the root
- * namespace, so we import the "SciTECO"
- * namespace. We have no more declarations
- * to make in the "SciTECO" namespace.
- */
-using namespace SciTECO;
-
int
main(int argc, char **argv)
{
- static GotoTable cmdline_goto_table;
- static QRegisterTable local_qregs;
-
- gchar *mung_filename;
+ g_autoptr(GError) error = NULL;
#ifdef DEBUG_PAUSE
/* Windows debugging hack (see above) */
system("pause");
#endif
- signal(SIGINT, sigint_handler);
- signal(SIGTERM, sigint_handler);
+ signal(SIGINT, teco_sigint_handler);
+ signal(SIGTERM, teco_sigint_handler);
- mung_filename = process_options(argc, argv);
+ g_autofree gchar *mung_filename = teco_process_options(&argc, &argv);
/*
* All remaining arguments in argv are arguments
* to the macro or munged file.
*/
- interface.init();
+
+ /*
+ * Theoretically, QReg tables should only be initialized
+ * after the interface, since they contain Scintilla documents.
+ * However, this would prevent the inialization of clipboard QRegs
+ * in teco_interface_init() and those should be available in batch mode
+ * as well.
+ * As long as the string parts are not accessed, that should be OK.
+ *
+ * FIXME: Perhaps it would be better to introduce something like
+ * teco_interface_init_clipboard()?
+ */
+ teco_qreg_table_init(&teco_qreg_table_globals, TRUE);
+
+ teco_interface_init();
/*
* QRegister view must be initialized only now
* (e.g. after Curses/GTK initialization).
*/
- QRegisters::view.initialize();
+ teco_qreg_view = teco_view_new();
+ teco_view_setup(teco_qreg_view);
- /* the default registers (A-Z and 0-9) */
- QRegisters::globals.insert_defaults();
/* search string and status register */
- QRegisters::globals.insert("_");
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("_", 1));
/* replacement string register */
- QRegisters::globals.insert("-");
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("-", 1));
/* current buffer name and number ("*") */
- QRegisters::globals.insert(new QRegisterBufferInfo());
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_bufferinfo_new());
/* current working directory ("$") */
- QRegisters::globals.insert(new QRegisterWorkingDir());
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_workingdir_new());
/* environment defaults and registers */
- initialize_environment(argv[0]);
+ teco_initialize_environment(argv[0]);
- /* the default registers (A-Z and 0-9) */
- local_qregs.insert_defaults();
- QRegisters::locals = &local_qregs;
+ teco_qreg_table_t local_qregs;
+ teco_qreg_table_init(&local_qregs, TRUE);
- ring.edit((const gchar *)NULL);
+ if (!teco_ring_edit_by_name(NULL, &error)) {
+ g_fprintf(stderr, "Error editing unnamed file: %s\n",
+ error->message);
+ exit(EXIT_FAILURE);
+ }
- /* add remaining arguments to unnamed buffer */
+ /*
+ * Add remaining arguments to unnamed buffer.
+ *
+ * FIXME: This is not really robust since filenames may contain linefeeds.
+ * Also, the Unnamed Buffer should be kept empty for piping.
+ * Therefore, it would be best to store the arguments in Q-Regs, e.g. $0,$1,$2...
+ */
for (gint i = 1; i < argc; i++) {
- /*
- * FIXME: arguments may contain line-feeds.
- * Once SciTECO is 8-byte clear, we can add the
- * command-line params null-terminated.
- */
- interface.ssm(SCI_APPENDTEXT, strlen(argv[i]), (sptr_t)argv[i]);
- interface.ssm(SCI_APPENDTEXT, 1, (sptr_t)"\n");
+ teco_interface_ssm(SCI_APPENDTEXT, strlen(argv[i]), (sptr_t)argv[i]);
+ teco_interface_ssm(SCI_APPENDTEXT, 1, (sptr_t)"\n");
}
/*
* Execute macro or mung file
*/
- try {
- if (eval_macro) {
- try {
- Execute::macro(eval_macro, false);
- } catch (Error &error) {
- error.add_frame(new Error::ToplevelFrame());
- throw; /* forward */
- } catch (Quit) {
- /*
- * ^C invoked, quit hook should still
- * be executed.
- */
- }
- QRegisters::hook(QRegisters::HOOK_QUIT);
- exit(EXIT_SUCCESS);
+ if (teco_eval_macro) {
+ if (!teco_execute_macro(teco_eval_macro, strlen(teco_eval_macro),
+ &local_qregs, &error) &&
+ !g_error_matches(error, TECO_ERROR, TECO_ERROR_QUIT)) {
+ teco_error_add_frame_toplevel();
+ goto error;
}
+ if (!teco_ed_hook(TECO_ED_HOOK_QUIT, &error))
+ goto error;
+ goto cleanup;
+ }
+
+ if (!mung_filename && teco_mung_profile)
+ /* NOTE: Still safe to use g_getenv() */
+ mung_filename = g_build_filename(g_getenv("SCITECOCONFIG"), INI_FILE, NULL);
- if (!mung_filename && mung_profile)
- /* NOTE: Still safe to use g_getenv() */
- mung_filename = g_build_filename(g_getenv("SCITECOCONFIG"),
- INI_FILE, NIL);
-
- if (mung_filename &&
- g_file_test(mung_filename, G_FILE_TEST_IS_REGULAR)) {
- try {
- Execute::file(mung_filename, false);
- } catch (Quit) {
- /*
- * ^C invoked, quit hook should still
- * be executed.
- */
- }
-
- if (quit_requested) {
- QRegisters::hook(QRegisters::HOOK_QUIT);
- exit(EXIT_SUCCESS);
- }
+ if (mung_filename && g_file_test(mung_filename, G_FILE_TEST_IS_REGULAR)) {
+ if (!teco_execute_file(mung_filename, &local_qregs, &error) &&
+ !g_error_matches(error, TECO_ERROR, TECO_ERROR_QUIT))
+ goto error;
+
+ if (teco_quit_requested) {
+ if (!teco_ed_hook(TECO_ED_HOOK_QUIT, &error))
+ goto error;
+ goto cleanup;
}
- } catch (Error &error) {
- error.display_full();
- exit(EXIT_FAILURE);
- } catch (...) {
- exit(EXIT_FAILURE);
}
/*
* If munged file didn't quit, switch into interactive mode
*/
/* commandline replacement string register */
- QRegisters::globals.insert(CTL_KEY_ESC_STR);
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("\e", 1));
+
+ teco_undo_enabled = TRUE;
+ teco_ring_set_scintilla_undo(TRUE);
+ teco_view_set_scintilla_undo(teco_qreg_view, TRUE);
- Goto::table = &cmdline_goto_table;
- undo.enabled = true;
- ring.set_scintilla_undo(true);
- QRegisters::view.set_scintilla_undo(true);
+ /*
+ * FIXME: Perhaps we should simply call teco_cmdline_init() and
+ * teco_cmdline_cleanup() here.
+ */
+ teco_machine_main_init(&teco_cmdline.machine, &local_qregs, TRUE);
+
+ if (!teco_interface_event_loop(&error))
+ goto error;
- interface.event_loop();
+ teco_machine_main_clear(&teco_cmdline.machine);
+ memset(&teco_cmdline.machine, 0, sizeof(teco_cmdline.machine));
/*
* Ordinary application termination:
* Interface is shut down, so we are
* in non-interactive mode again.
*/
- undo.enabled = false;
- undo.clear();
+ teco_undo_enabled = FALSE;
+ teco_undo_clear();
/* also empties all Scintilla undo buffers */
- ring.set_scintilla_undo(false);
- QRegisters::view.set_scintilla_undo(false);
-
- try {
- QRegisters::hook(QRegisters::HOOK_QUIT);
- } catch (Error &error) {
- error.display_full();
- exit(EXIT_FAILURE);
- }
-
- g_free(mung_filename);
+ teco_ring_set_scintilla_undo(FALSE);
+ teco_view_set_scintilla_undo(teco_qreg_view, FALSE);
+
+ if (!teco_ed_hook(TECO_ED_HOOK_QUIT, &error))
+ goto error;
+
+cleanup:
+#ifndef NDEBUG
+ teco_ring_cleanup();
+ teco_qreg_table_clear(&local_qregs);
+ teco_qreg_table_clear(&teco_qreg_table_globals);
+ teco_view_free(teco_qreg_view);
+#endif
+ teco_interface_cleanup();
return 0;
+
+error:
+ teco_error_display_full(error);
+ return EXIT_FAILURE;
}
diff --git a/src/memory.c b/src/memory.c
new file mode 100644
index 0000000..f8942fd
--- /dev/null
+++ b/src/memory.c
@@ -0,0 +1,672 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#define USE_DL_PREFIX /* for dlmalloc */
+
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#ifdef HAVE_MALLOC_H
+#include <malloc.h>
+#endif
+#ifdef HAVE_MALLOC_NP_H
+#include <malloc_np.h>
+#endif
+
+#ifdef HAVE_WINDOWS_H
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <psapi.h>
+#endif
+
+/*
+ * For task_info() on OS X.
+ */
+#ifdef HAVE_MACH_MACH_H
+#include <mach/mach.h>
+#endif
+#ifdef HAVE_MACH_MESSAGE_H
+#include <mach/message.h>
+#endif
+#ifdef HAVE_MACH_KERN_RETURN_H
+#include <mach/kern_return.h>
+#endif
+#ifdef HAVE_MACH_TASK_INFO_H
+#include <mach/task_info.h>
+#endif
+
+/*
+ * For sysctl() on FreeBSD.
+ */
+#ifdef HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+#ifdef HAVE_SYS_SYSCTL_H
+#include <sys/sysctl.h>
+#endif
+
+/*
+ * For sysconf() on Linux.
+ */
+#ifdef HAVE_SYS_TIME_H
+#include <sys/time.h>
+#endif
+#ifdef HAVE_SYS_RESOURCE_H
+#include <sys/resource.h>
+#endif
+
+#include <glib.h>
+
+/*
+ * For open() (currently only on Linux).
+ */
+#ifdef G_OS_UNIX
+#include <sys/stat.h>
+#include <fcntl.h>
+#endif
+
+#include "sciteco.h"
+#include "error.h"
+#include "undo.h"
+#include "memory.h"
+
+/**
+ * @file
+ * Memory measurement and limiting.
+ *
+ * A discussion of memory measurement techniques on Linux
+ * and UNIXoid operating systems is in order, since this
+ * problem turned out to be rather tricky.
+ *
+ * @par Size of the program break
+ * There is also the old-school technique of calculating the size
+ * of the program break, ie. the effective size of the DATA segment.
+ * This works under the assumption that all allocations are
+ * performed by extending the program break, as is __traditionally__
+ * done by malloc() and friends.
+ *
+ * - Unfortunately, modern malloc() implementations sometimes
+ * mmap() memory, especially for large allocations.
+ * SciTECO mostly allocates small chunks.
+ * Unfortunately, some malloc implementations like jemalloc
+ * only claim memory using mmap(), thus rendering sbrk(0)
+ * useless.
+ * - Furthermore, some malloc-implementations like glibc will
+ * only shrink the program break when told so explicitly
+ * using malloc_trim(0).
+ * - The sbrk(0) method thus depends on implementation details
+ * of the libc.
+ * - However, this might be a suitable backend on old UNIX-platforms
+ * or a as a fallback for teco_memory_get_usage().
+ *
+ * @par Resource limits
+ * UNIX has resource limits, which could be used to enforce
+ * the memory limit, but in case they are hit, malloc()
+ * will return NULL, so g_malloc() would abort().
+ * Wrapping malloc() to work around that has the same
+ * problems described below.
+ *
+ * @par Hooking malloc()
+ * malloc_usable_size() could be used to count the memory
+ * consumption by updating a counter after every malloc(),
+ * realloc() and free().
+ * malloc_usable_size() is libc-specific, but available at least in
+ * glibc and jemalloc (FreeBSD). Windows (MSVCRT) has `_msize()`.
+ * This would require overwriting or hooking all calls to
+ * malloc() and friends, though.
+ * For all other platforms, we'd have to rely on writing the
+ * heap object size into every heap object, thus wasting
+ * one word per heap object.
+ *
+ * - glibc has malloc hooks, but they are non-portable and
+ * deprecated.
+ * - It is possible to effectively wrap malloc() by overriding
+ * the libc's implementation, which will even work when
+ * statically linking in libc since malloc() is usually
+ * declared `weak`.
+ * This however does probably not work on all platforms and
+ * means you need to know the original function (pointers).
+ * It should work sufficiently when linking everything statically.
+ * - glibc exports symbols for the original malloc() implementation
+ * like `__libc_malloc()` that could be used for wrapping.
+ * This is undocumented and libc-specific, though.
+ * - The GNU ld --wrap option allows us to intercept calls,
+ * but obviously won't work for shared libraries.
+ * - The portable dlsym() could be used to look up the original
+ * library symbol, but it may and does call malloc functions,
+ * eg. calloc() on glibc.
+ * Some people work around this using bootstrap makeshift allocators
+ * used only during dlsym().
+ * __In other words, there is no way to portably and reliably
+ * wrap malloc() and friends when linking dynamically.__
+ * - Another difficulty is that, when free() is overridden, every
+ * function that can __independently__ allocate memory that
+ * can be passed to free() must also be overridden.
+ * This is impossible to know without making assumptions about the
+ * malloc implementation used.
+ * Otherwise the measurement is not precise and there can even
+ * be underruns. Thus we'd have to guard against underruns.
+ * - Unfortunately, it is undefined whether the "usable" size of
+ * a heap object can change unwittingly, ie. not by malloc() or
+ * realloc() on that same heap object, but for instance after a
+ * neighbouring heap object is freed.
+ * If this can happen, free() on that heap object might subtract
+ * more than was initially added for this heap object, resulting
+ * in measurement underruns.
+ * - malloc() and friends are MT-safe, so any replacement function
+ * would have to be MT-safe as well to avoid memory corruption.
+ *
+ * Memory counting using malloc_usable_size() in overwritten/wrapped
+ * malloc()/realloc()/free() calls has thus been deemed impractical.
+ *
+ * Overriding could only work if we store the allocated size
+ * at the beginning of each heap object and would link in an external
+ * malloc() implementation, so that the symbol names are known.
+ *
+ * Unfortunately, overwriting libc functions is also non-portable,
+ * so replacing the libc malloc with an external allocator is tricky.
+ * On Linux (and hopefully other UNIXes), you can simply link
+ * in the malloc replacement statically which will even let the
+ * dynamic linker pick the new implementation.
+ * On Windows however, we would apparently need incredibly hacky code
+ * to patch the symbol tables
+ * (see https://github.com/ned14/nedmalloc/blob/master/winpatcher.c).
+ * Alternatively, everything __including__ MSVCRT needs to be linked
+ * in statically. This is not supported by MinGW and would have certain
+ * disadvantages even if it worked.
+ *
+ * @par malloc() introspection
+ * glibc and some other platforms have mallinfo().
+ * But at least on glibc it can get unbearably slow on programs
+ * with a lot of (virtual/resident) memory.
+ * Besides, mallinfo's API is broken on 64-bit systems, effectively
+ * limiting the enforcable memory limit to 4GB.
+ * Other glibc-specific introspection functions like malloc_info()
+ * can be even slower because of the syscalls required.
+ *
+ * - FreeBSD/jemalloc has mallctl("stats.allocated") which even when
+ * optimized is significantly slower than the current implementation
+ * but generally acceptable.
+ * - dlmalloc has malloc_footprint() which is very fast.
+ * It was therefore considered to simply import dlmalloc as the default
+ * allocator on (almost) all platforms.
+ * Despite problems overwriting malloc() globally on some platforms,
+ * this turned out to be impractical since malloc_footprint() includes
+ * only the mmapped memory and memory is not always unmapped even when
+ * calling malloc_trim(), so we couldn't recover after hitting
+ * the memory limit.
+ * - rpmalloc has a cheap rpmalloc_global_statistics() but enabling it
+ * comes with a memory overhead.
+ * - There seems to be no other malloc() replacement with a constant-time
+ * function returning the footprint.
+ *
+ * @par Instrumenting all of SciTECO's and C++ allocations.
+ * If we don't want to count each and every allocation in the system,
+ * we could also use custom allocators/deallocators together with
+ * malloc_usable_size().
+ * For many objects, the size will also be known at free() time, so
+ * malloc_usable_size() can be avoided.
+ *
+ * - To track Scintilla's memory usage, custom C++ allocators/deallocators
+ * can be defined.
+ * - Beginning with C++14 (or earlier with -fsized-deallocation),
+ * it is possible to globally replace sized allocation/deallocation
+ * functions, which could be used to avoid the malloc_usable_size()
+ * workaround. Unfortunately, this may not be used for arrays,
+ * since the compiler may have to call non-sized variants if the
+ * original allocation size is unknown - and there is no way to detect
+ * that when the new[] call is made.
+ * What's worse is that at least G++ STL is broken seriously and
+ * some versions will call the non-sized delete() even when sized-deallocation
+ * is available. Again, this cannot be detected at new() time.
+ * Therefore, I had to remove the sized-deallocation based
+ * optimization.
+ * - This approach has the same disadvantages as wrapping malloc() because
+ * of the unreliability of malloc_usable_size().
+ * Furthermore, all allocations by glib (eg. g_strdup()) will be missed.
+ *
+ * @par Directly measuring the resident memory size
+ * It is of course possible to query the program's RSS via OS APIs.
+ * This has long been avoided because it is naturally platform-dependant and
+ * some of the APIs have proven to be too slow for frequent polling.
+ *
+ * - Windows has GetProcessMemoryInfo() which is quite slow.
+ * When polled on a separate thread, the slow down is very acceptable.
+ * - OS X has task_info().
+ * __Its performance is still untested!__
+ * - FreeBSD has sysctl().
+ * __Its performance is still untested!__
+ * - Linux has no APIs but /proc/self/statm.
+ * Reading it is naturally very slow, but at least of constant time.
+ * When polled on a separate thread, the slow down is very acceptable.
+ * Also, use of malloc_trim() after hitting the memory limit is crucial
+ * since the RSS will otherwise not decrease.
+ * - Haiku has no usable constant-time API.
+ *
+ * @par Conclusion
+ * Every approach sucks and no platform supports everything.
+ * We therefore now opted for a combined strategy:
+ * Most platforms will by default try to replace malloc() with dlmalloc.
+ * The dlmalloc functions are wrapped and the memory usage is counted via
+ * malloc_usable_size() which in the case of dlmalloc should never change
+ * for one heap object unless we realloc() it.
+ * This should be fastest, the most precise and there is a guaranteed
+ * malloc_trim().
+ * Malloc overriding can be disabled at compile time to aid in memory
+ * debugging.
+ * On Windows, we never even try to link in dlmalloc.
+ * If disabled, we try to directly measure memory consumption using
+ * OS APIs.
+ * Polling of the RSS takes place in a dedicated thread that is started
+ * on demand and paused whenever the main thread is idle (eg. waits for
+ * user input), so we don't waste cycles.
+ */
+
+/**
+ * Current memory usage.
+ * Access must be synchronized using atomic operations.
+ */
+static gint teco_memory_usage = 0;
+
+/*
+ * NOTE: This implementation based on malloc_usable_size() might
+ * also work with other malloc libraries, given that they provide
+ * a malloc_usable_size() which does not change for a heap object
+ * (unless it is reallocated of course).
+ */
+#ifdef REPLACE_MALLOC
+
+void * __attribute__((used))
+malloc(size_t size)
+{
+ void *ptr = dlmalloc(size);
+ if (G_LIKELY(ptr != NULL))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(ptr));
+ return ptr;
+}
+
+void __attribute__((used))
+free(void *ptr)
+{
+ if (!ptr)
+ return;
+ g_atomic_int_add(&teco_memory_usage, -dlmalloc_usable_size(ptr));
+ dlfree(ptr);
+}
+
+void * __attribute__((used))
+calloc(size_t nmemb, size_t size)
+{
+ void *ptr = dlcalloc(nmemb, size);
+ if (G_LIKELY(ptr != NULL))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(ptr));
+ return ptr;
+}
+
+void * __attribute__((used))
+realloc(void *ptr, size_t size)
+{
+ if (ptr)
+ g_atomic_int_add(&teco_memory_usage, -dlmalloc_usable_size(ptr));
+ ptr = dlrealloc(ptr, size);
+ if (G_LIKELY(ptr != NULL))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(ptr));
+ return ptr;
+}
+
+void * __attribute__((used))
+memalign(size_t alignment, size_t size)
+{
+ void *ptr = dlmemalign(alignment, size);
+ if (G_LIKELY(ptr != NULL))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(ptr));
+ return ptr;
+}
+
+void *aligned_alloc(size_t, size_t) __attribute__((used, alias("memalign")));
+
+int __attribute__((used))
+posix_memalign(void **memptr, size_t alignment, size_t size)
+{
+ int ret = dlposix_memalign(memptr, alignment, size);
+ if (G_LIKELY(!ret))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(*memptr));
+ return ret;
+}
+
+void * __attribute__((used))
+valloc(size_t size)
+{
+ void *ptr = dlvalloc(size);
+ if (G_LIKELY(ptr != NULL))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(ptr));
+ return ptr;
+}
+
+/*
+ * The glibc manual claims we have to replace this function
+ * but we'd need sysconf(_SC_PAGESIZE) to implement it.
+ */
+void * __attribute__((used))
+pvalloc(size_t size)
+{
+ g_assert_not_reached();
+ return NULL;
+}
+
+size_t __attribute__((used))
+malloc_usable_size(void *ptr)
+{
+ return dlmalloc_usable_size(ptr);
+}
+
+int __attribute__((used))
+malloc_trim(size_t pad)
+{
+ return dlmalloc_trim(pad);
+}
+
+/*
+ * FIXME: Which platforms might need malloc_trim() to
+ * recover from hitting the memory limit?
+ * In other words, which platform's teco_memory_get_usage()
+ * might return a large value even if most memory has already
+ * been deallocated?
+ */
+#elif defined(G_OS_WIN32)
+
+/*
+ * On Windows, we never link in dlmalloc.
+ *
+ * NOTE: At least on Windows 2000, we run twice as fast than
+ * when polling from a dedicated thread.
+ */
+static gsize
+teco_memory_get_usage(void)
+{
+ PROCESS_MEMORY_COUNTERS info;
+
+ /*
+ * This __should__ not fail since the current process has
+ * PROCESS_ALL_ACCESS, but who knows...
+ * Since memory limiting cannot be turned off when this
+ * happens, we can just as well terminate abnormally.
+ */
+ if (G_UNLIKELY(!GetProcessMemoryInfo(GetCurrentProcess(),
+ &info, sizeof(info)))) {
+ g_autofree gchar *msg = g_win32_error_message(GetLastError());
+ g_error("Cannot get memory usage: %s", msg);
+ return 0;
+ }
+
+ return info.WorkingSetSize;
+}
+
+#define NEED_POLL_THREAD
+
+#elif defined(HAVE_TASK_INFO)
+
+/*
+ * Practically only for Mac OS X.
+ *
+ * FIXME: Benchmark whether polling in a thread really
+ * improves performances as much as on Linux.
+ * Is this even critical or can we link in dlmalloc?
+ */
+static gsize
+teco_memory_get_usage(void)
+{
+ struct mach_task_basic_info info;
+ mach_msg_type_number_t info_count = MACH_TASK_BASIC_INFO_COUNT;
+
+ if (G_UNLIKELY(task_info(mach_task_self(), MACH_TASK_BASIC_INFO,
+ (task_info_t)&info, &info_count) != KERN_SUCCESS))
+ return 0; // FIXME
+
+ return info.resident_size;
+}
+
+#define NEED_POLL_THREAD
+
+#elif defined(G_OS_UNIX) && defined(HAVE_SYSCTL)
+
+/*
+ * Practically only for FreeBSD.
+ *
+ * FIXME: Is this even critical or can we link in dlmalloc?
+ */
+static gsize
+teco_memory_get_usage(void)
+{
+ struct kinfo_proc procstk;
+ size_t len = sizeof(procstk);
+ int pidinfo[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
+
+ sysctl(pidinfo, G_N_ELEMENTS(pidinfo), &procstk, &len, NULL, 0);
+
+ return procstk.ki_rssize; // FIXME: Which unit?
+}
+
+#define NEED_POLL_THREAD
+
+#elif defined(G_OS_UNIX) && defined(HAVE_SYSCONF) && defined(HAVE_PROCFS)
+
+#ifndef HAVE_MALLOC_TRIM
+#warning malloc_trim() missing - Might not recover from hitting the memory limit!
+#endif
+
+/*
+ * Mainly for Linux, but there might be other UNIXoids supporting procfs.
+ * This would be ridiculously slow if polled from the main thread.
+ *
+ * Since Linux supports dlmalloc(), this will usually not be required
+ * unless you disable it explicitly.
+ *
+ * NOTE: This is conciously avoiding glib and stdio APIs since we run in
+ * a very tight loop and should avoid any unnecessary allocations which could
+ * significantly slow down the main thread.
+ */
+static gsize
+teco_memory_get_usage(void)
+{
+ static long page_size = 0;
+
+ if (G_UNLIKELY(!page_size))
+ page_size = sysconf(_SC_PAGESIZE);
+
+ int fd = open("/proc/self/statm", O_RDONLY);
+ if (fd < 0)
+ /* procfs might not be mounted */
+ return 0;
+
+ gchar buf[256];
+ ssize_t len = read(fd, buf, sizeof(buf)-1);
+ close(fd);
+ if (G_UNLIKELY(len < 0))
+ return 0;
+ buf[len] = '\0';
+
+ gsize memory_usage = 0;
+ sscanf(buf, "%*u %" G_GSIZE_FORMAT, &memory_usage);
+
+ return memory_usage * page_size;
+}
+
+#define NEED_POLL_THREAD
+
+#else
+
+/*
+ * We've got neither dlmalloc, nor any particular OS backend.
+ */
+#warning dlmalloc is disabled and there is no memory counting backend - memory limiting will be unavailable!
+
+#endif
+
+#ifdef NEED_POLL_THREAD
+
+static GThread *teco_memory_thread = NULL;
+
+static enum {
+ TECO_MEMORY_STATE_ON,
+ TECO_MEMORY_STATE_OFF,
+ TECO_MEMORY_STATE_SHUTDOWN
+} teco_memory_state = TECO_MEMORY_STATE_ON;
+
+static GMutex teco_memory_mutex;
+static GCond teco_memory_cond;
+
+/*
+ * FIXME: What if we activated the thread only whenever the
+ * usage is queried in the main thread?
+ * This would automatically "clock" the threaded polling at the same rate
+ * as the main thread is polling.
+ * On the downside, the value of teco_memory_usage would be more outdated,
+ * so a memory overrun would be detected with even more delay.
+ */
+static gpointer
+teco_memory_poll_thread_cb(gpointer data)
+{
+ g_mutex_lock(&teco_memory_mutex);
+
+ for (;;) {
+ while (teco_memory_state == TECO_MEMORY_STATE_ON) {
+ g_mutex_unlock(&teco_memory_mutex);
+ /*
+ * NOTE: teco_memory_mutex is not used for teco_memory_usage
+ * since it is locked most of the time which would extremely slow
+ * down the main thread.
+ */
+ g_atomic_int_set(&teco_memory_usage, teco_memory_get_usage());
+ g_thread_yield();
+ g_mutex_lock(&teco_memory_mutex);
+ }
+ if (G_UNLIKELY(teco_memory_state == TECO_MEMORY_STATE_SHUTDOWN))
+ break;
+
+ g_cond_wait(&teco_memory_cond, &teco_memory_mutex);
+ /* teco_memory_mutex is locked */
+ }
+
+ g_mutex_unlock(&teco_memory_mutex);
+ return NULL;
+}
+
+void __attribute__((constructor))
+teco_memory_start_limiting(void)
+{
+ if (!teco_memory_limit)
+ return;
+
+ /*
+ * FIXME: Setting a low thread priority would certainly help.
+ * This would be less important for platforms like Linux where
+ * we usually don't need a polling thread at all.
+ */
+ if (G_UNLIKELY(!teco_memory_thread))
+ teco_memory_thread = g_thread_new(NULL, teco_memory_poll_thread_cb, NULL);
+
+ g_mutex_lock(&teco_memory_mutex);
+ teco_memory_state = TECO_MEMORY_STATE_ON;
+ g_cond_signal(&teco_memory_cond);
+ g_mutex_unlock(&teco_memory_mutex);
+}
+
+void
+teco_memory_stop_limiting(void)
+{
+ g_mutex_lock(&teco_memory_mutex);
+ teco_memory_state = TECO_MEMORY_STATE_OFF;
+ g_mutex_unlock(&teco_memory_mutex);
+}
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_memory_cleanup(void)
+{
+ if (!teco_memory_thread)
+ return;
+
+ g_mutex_lock(&teco_memory_mutex);
+ teco_memory_state = TECO_MEMORY_STATE_SHUTDOWN;
+ g_cond_signal(&teco_memory_cond);
+ g_mutex_unlock(&teco_memory_mutex);
+
+ g_thread_join(teco_memory_thread);
+}
+#endif
+
+#else /* !NEED_POLL_THREAD */
+
+void teco_memory_start_limiting(void) {}
+void teco_memory_stop_limiting(void) {}
+
+#endif
+
+/**
+ * Memory limit in bytes (500mb by default, assuming SI units).
+ * 0 means no limiting.
+ */
+gsize teco_memory_limit = 500*1000*1000;
+
+gboolean
+teco_memory_set_limit(gsize new_limit, GError **error)
+{
+ gsize memory_usage = g_atomic_int_get(&teco_memory_usage);
+
+ if (G_UNLIKELY(new_limit && memory_usage > new_limit)) {
+ g_autofree gchar *usage_str = g_format_size(memory_usage);
+ g_autofree gchar *limit_str = g_format_size(new_limit);
+
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Cannot set undo memory limit (%s): "
+ "Current usage too large (%s).",
+ limit_str, usage_str);
+ return FALSE;
+ }
+
+ teco_undo_gsize(teco_memory_limit) = new_limit;
+
+ if (teco_memory_limit)
+ teco_memory_start_limiting();
+ else
+ teco_memory_stop_limiting();
+
+ return TRUE;
+}
+
+gboolean
+teco_memory_check(GError **error)
+{
+ gsize memory_usage = g_atomic_int_get(&teco_memory_usage);
+
+ if (G_UNLIKELY(teco_memory_limit && memory_usage > teco_memory_limit)) {
+ g_autofree gchar *limit_str = g_format_size(memory_usage);
+
+ g_set_error(error, TECO_ERROR, TECO_ERROR_MEMLIMIT,
+ "Memory limit (%s) exceeded. See <EJ> command.",
+ limit_str);
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/src/memory.cpp b/src/memory.cpp
deleted file mode 100644
index fd7adf7..0000000
--- a/src/memory.cpp
+++ /dev/null
@@ -1,350 +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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-/* for malloc_usable_size() */
-#ifdef HAVE_MALLOC_H
-#include <malloc.h>
-#endif
-#ifdef HAVE_MALLOC_NP_H
-#include <malloc_np.h>
-#endif
-
-#include <new>
-
-#include <glib.h>
-
-#include "sciteco.h"
-#include "memory.h"
-#include "error.h"
-#include "undo.h"
-
-#ifdef HAVE_WINDOWS_H
-/* here it shouldn't cause conflicts with other headers */
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#include <psapi.h>
-#endif
-
-namespace SciTECO {
-
-/*
- * Define this to prefix each heap object allocated
- * by the custom allocators with a magic value.
- * This helps to detect non-matching calls to the
- * overridden new/delete operators which can cause
- * underruns of the memory counter.
- */
-//#define DEBUG_MAGIC ((guintptr)0xDEAD15DE5E1BEAF0)
-
-MemoryLimit memlimit;
-
-/*
- * A discussion of memory measurement techniques on Linux
- * and UNIXoid operating systems is in order, since this
- * problem turned out to be rather tricky.
- *
- * - UNIX has resource limits, which could be used to enforce
- * the memory limit, but in case they are hit, malloc()
- * will return NULL, so g_malloc() would abort().
- * Wrapping malloc() to work around that has the same
- * problems described below.
- * - glibc has malloc hooks, but they are non-portable and
- * deprecated.
- * - It is possible to effectively wrap malloc() by overriding
- * the libc's implementation, which will even work when
- * statically linking in libc since malloc() is usually
- * delcared `weak`.
- * - When wrapping malloc(), malloc_usable_size() could be
- * used to count the memory consumption.
- * This is libc-specific, but available at least in
- * glibc and jemalloc (FreeBSD).
- * - glibc exports symbols for the original malloc() implementation
- * like __libc_malloc() that could be used for wrapping.
- * This is undocumented and libc-specific, though.
- * - The GNU ld --wrap option allows us to intercept calls,
- * but obviously won't work for shared libraries.
- * - The portable dlsym() could be used to look up the original
- * library symbol, but it may and does call malloc functions,
- * eg. calloc() on glibc.
- * In other words, there is no way to portably and reliably
- * wrap malloc() and friends when linking dynamically.
- * - Another difficulty is that, when free() is overridden, every
- * function that can __independently__ allocate memory that
- * can be passed to free() must also be overridden.
- * Otherwise the measurement is not precise and there can even
- * be underruns. Thus we'd have to guard against underruns.
- * - malloc() and friends are MT-safe, so any replacement function
- * would have to be MT-safe as well to avoid memory corruption.
- * E.g. even in single-threaded builds, glib might use
- * threads internally.
- * - There is also the old-school technique of calculating the size
- * of the program break, ie. the effective size of the DATA segment.
- * This works under the assumption that all allocations are
- * performed by extending the program break, as is __traditionally__
- * done by malloc() and friends.
- * - Unfortunately, modern malloc() implementations sometimes
- * mmap() memory, especially for large allocations.
- * SciTECO mostly allocates small chunks.
- * Unfortunately, some malloc implementations like jemalloc
- * only claim memory using mmap(), thus rendering sbrk(0)
- * useless.
- * - Furthermore, some malloc-implementations like glibc will
- * only shrink the program break when told so explicitly
- * using malloc_trim(0).
- * - The sbrk(0) method thus depends on implementation details
- * of the libc.
- * - glibc and some other platforms have mallinfo().
- * But at least on glibc it can get unbearably slow on programs
- * with a lot of (virtual/resident) memory.
- * Besides, mallinfo's API is broken on 64-bit systems, effectively
- * limiting the enforcable memory limit to 4GB.
- * Other glibc-specific introspection functions like malloc_info()
- * can be even slower because of the syscalls required.
- * - Linux has /proc/self/stat and /proc/self/statm but polling them
- * is very inefficient.
- * - FreeBSD/jemalloc has mallctl("stats.allocated") which even when
- * optimized is significantly slower than the fallback but generally
- * acceptable.
- * - On all other platforms we (have to) rely on the fallback
- * implementation based on C++ allocators/deallocators.
- * They have been improved significantly to count as much memory
- * as possible, even using libc-specific APIs like malloc_usable_size().
- * Since this has been proven to work sufficiently well even on FreeBSD,
- * there is no longer any UNIX-specific implementation.
- * Even the malloc_usable_size() workaround for old or non-GNU
- * compilers is still faster than mallctl() on FreeBSD.
- * This might need to change in the future.
- * - Beginning with C++14 (or earlier with -fsized-deallocation),
- * it is possible to globally replace sized allocation/deallocation
- * functions, which could be used to avoid the malloc_usable_size()
- * workaround. Unfortunately, this may not be used for arrays,
- * since the compiler may have to call non-sized variants if the
- * original allocation size is unknown - and there is no way to detect
- * that when the new[] call is made.
- * What's worse is that at least G++ STL is broken seriously and
- * some versions will call the non-sized delete() even when sized-deallocation
- * is available. Again, this cannot be detected at new() time.
- * Therefore, I had to remove the sized-deallocation based
- * optimization.
- */
-
-#ifdef G_OS_WIN32
-/*
- * Uses the Windows-specific GetProcessMemoryInfo(),
- * so the entire process heap is measured.
- *
- * FIXME: Unfortunately, this is much slower than the portable
- * fallback implementation.
- * It may be possible to overwrite malloc() and friends,
- * counting the chunks with the MSVCRT-specific _minfo().
- * Since we will always run against MSVCRT, the disadvantages
- * discussed above for the UNIX-case may not be important.
- * We might also just use the fallback implementation with some
- * additional support for _msize().
- */
-
-gsize
-MemoryLimit::get_usage(void)
-{
- PROCESS_MEMORY_COUNTERS info;
-
- /*
- * This __should__ not fail since the current process has
- * PROCESS_ALL_ACCESS, but who knows...
- * Since memory limiting cannot be turned off when this
- * happens, we can just as well terminate abnormally.
- */
- if (G_UNLIKELY(!GetProcessMemoryInfo(GetCurrentProcess(),
- &info, sizeof(info)))) {
- gchar *msg = g_win32_error_message(GetLastError());
- g_error("Cannot get memory usage: %s", msg);
- /* shouldn't be reached */
- g_free(msg);
- return 0;
- }
-
- return info.WorkingSetSize;
-}
-
-#else
-/*
- * Portable fallback-implementation relying on C++11 sized allocators.
- *
- * Unfortunately, in the worst case, this will only measure the heap used
- * by C++ objects in SciTECO's sources; not even Scintilla, nor all
- * g_malloc() calls.
- * Usually, we will be able to use global non-sized deallocators with
- * libc-specific support to get more accurate results, though.
- */
-
-#define MEMORY_USAGE_FALLBACK
-
-/**
- * Current memory usage in bytes.
- *
- * @bug This only works in single-threaded applications.
- * Should SciTECO or Scintilla ever use multiple threads,
- * it will be necessary to use atomic operations.
- */
-static gsize memory_usage = 0;
-
-gsize
-MemoryLimit::get_usage(void)
-{
- return memory_usage;
-}
-
-#endif /* MEMORY_USAGE_FALLBACK */
-
-void
-MemoryLimit::set_limit(gsize new_limit)
-{
- gsize memory_usage = get_usage();
-
- if (G_UNLIKELY(new_limit && memory_usage > new_limit)) {
- gchar *usage_str = g_format_size(memory_usage);
- gchar *limit_str = g_format_size(new_limit);
-
- Error err("Cannot set undo memory limit (%s): "
- "Current usage too large (%s).",
- limit_str, usage_str);
-
- g_free(limit_str);
- g_free(usage_str);
- throw err;
- }
-
- undo.push_var(limit) = new_limit;
-}
-
-void
-MemoryLimit::check(void)
-{
- if (G_UNLIKELY(limit && get_usage() > limit)) {
- gchar *limit_str = g_format_size(limit);
-
- Error err("Memory limit (%s) exceeded. See <EJ> command.",
- limit_str);
-
- g_free(limit_str);
- throw err;
- }
-}
-
-/*
- * The object-specific sized deallocators allow memory
- * counting portably, even in strict C++11 mode.
- * Once we depend on C++14, they and the entire `Object`
- * class hack may be avoided.
- * But see above - due to broken STLs, this may not actually
- * be safe!
- */
-
-void *
-Object::operator new(size_t size) noexcept
-{
-#ifdef MEMORY_USAGE_FALLBACK
- memory_usage += size;
-#endif
-
-#ifdef DEBUG_MAGIC
- guintptr *ptr = (guintptr *)g_malloc(sizeof(guintptr) + size);
- *ptr = DEBUG_MAGIC;
- return ptr + 1;
-#else
- /*
- * Since we've got the sized-delete operator
- * below, we could allocate via g_slice.
- *
- * Using g_slice however would render malloc_trim()
- * ineffective. Also, it has been shown to be
- * unnecessary on Linux/glibc.
- * Glib is guaranteed to use the system malloc(),
- * so g_malloc() cooperates with malloc_trim().
- *
- * On Windows (even Windows 2000), the slice allocator
- * did not show any significant performance boost
- * either. Also, since g_slice never seems to return
- * memory to the OS and we cannot force it to do so,
- * it will not cooperate with the Windows-specific
- * memory measurement and it is hard to recover
- * from memory limit exhaustions.
- */
- return g_malloc(size);
-#endif
-}
-
-void
-Object::operator delete(void *ptr, size_t size) noexcept
-{
-#ifdef DEBUG_MAGIC
- if (ptr) {
- ptr = (guintptr *)ptr - 1;
- g_assert(*(guintptr *)ptr == DEBUG_MAGIC);
- }
-#endif
-
- g_free(ptr);
-
-#ifdef MEMORY_USAGE_FALLBACK
- memory_usage -= size;
-#endif
-}
-
-} /* namespace SciTECO */
-
-/*
- * In strict C++11, we can still use global non-sized
- * deallocators.
- *
- * On their own, they bring little benefit, but with
- * some libc-specific functionality, they can be used
- * to improve the fallback memory measurements to include
- * all allocations (including Scintilla).
- * This comes with a moderate runtime penalty.
- *
- * Unfortunately, even in C++14, defining replacement
- * sized deallocators may be very dangerous, so this
- * seems to be as best as we can get (see above).
- */
-
-void *
-operator new(size_t size)
-{
- void *ptr = g_malloc(size);
-
-#if defined(MEMORY_USAGE_FALLBACK) && defined(HAVE_MALLOC_USABLE_SIZE)
- /* NOTE: g_malloc() should always use the system malloc(). */
- SciTECO::memory_usage += malloc_usable_size(ptr);
-#endif
-
- return ptr;
-}
-
-void
-operator delete(void *ptr) noexcept
-{
-#if defined(MEMORY_USAGE_FALLBACK) && defined(HAVE_MALLOC_USABLE_SIZE)
- if (ptr)
- SciTECO::memory_usage -= malloc_usable_size(ptr);
-#endif
- g_free(ptr);
-}
diff --git a/src/memory.h b/src/memory.h
index 693a208..58705a7 100644
--- a/src/memory.h
+++ b/src/memory.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,77 +14,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __MEMORY_H
-#define __MEMORY_H
+#pragma once
#include <glib.h>
-/**
- * Default memory limit (500mb, assuming SI units).
- */
-#define MEMORY_LIMIT_DEFAULT (500*1000*1000)
-
-namespace SciTECO {
-
-/**
- * Common base class for all objects in SciTECO.
- * This is currently only used to provide custom new/delete
- * replacements in order to support unified allocation via
- * Glib (g_malloc and g_slice) and as a memory usage
- * counting fallback.
- *
- * This approach has certain drawbacks, e.g. you cannot
- * derive from Object privately; nor is it possible to
- * influence allocations in other libraries or even of
- * scalars (e.g. new char[5]).
- *
- * C++14 (supported by GCC >= 5) has global sized delete
- * replacements which would be effective in the entire application.
- * We're using them too if support is detected and there is
- * also a fallback using malloc_usable_size().
- * Another fallback with a size field would be possible
- * but is probably not worth the trouble.
- */
-class Object {
-public:
- static void *operator new(size_t size) noexcept;
- static inline void *
- operator new[](size_t size) noexcept
- {
- return operator new(size);
- }
- static inline void *
- operator new(size_t size, void *ptr) noexcept
- {
- return ptr;
- }
-
- static void operator delete(void *ptr, size_t size) noexcept;
- static inline void
- operator delete[](void *ptr, size_t size) noexcept
- {
- operator delete(ptr, size);
- }
-};
-
-extern class MemoryLimit : public Object {
-public:
- /**
- * Undo stack memory limit in bytes.
- * 0 means no limiting.
- */
- gsize limit;
-
- MemoryLimit() : limit(MEMORY_LIMIT_DEFAULT) {}
-
- static gsize get_usage(void);
-
- void set_limit(gsize new_limit = 0);
+extern gsize teco_memory_limit;
- void check(void);
-} memlimit;
+void teco_memory_start_limiting(void);
+void teco_memory_stop_limiting(void);
-} /* namespace SciTECO */
+gboolean teco_memory_set_limit(gsize new_limit, GError **error);
-#endif
+gboolean teco_memory_check(GError **error);
diff --git a/src/parser.c b/src/parser.c
new file mode 100644
index 0000000..ff1fd18
--- /dev/null
+++ b/src/parser.c
@@ -0,0 +1,902 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include "sciteco.h"
+#include "memory.h"
+#include "string-utils.h"
+#include "interface.h"
+#include "undo.h"
+#include "expressions.h"
+#include "qreg.h"
+#include "ring.h"
+#include "glob.h"
+#include "error.h"
+#include "core-commands.h"
+#include "goto-commands.h"
+#include "parser.h"
+
+//#define DEBUG
+
+GArray *teco_loop_stack;
+
+static void __attribute__((constructor))
+teco_loop_stack_init(void)
+{
+ teco_loop_stack = g_array_sized_new(FALSE, FALSE, sizeof(teco_loop_context_t), 1024);
+}
+
+TECO_DEFINE_ARRAY_UNDO_INSERT_VAL(teco_loop_stack, teco_loop_context_t);
+TECO_DEFINE_ARRAY_UNDO_REMOVE_INDEX(teco_loop_stack);
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_loop_stack_cleanup(void)
+{
+ g_array_free(teco_loop_stack, TRUE);
+}
+#endif
+
+gboolean
+teco_machine_input(teco_machine_t *ctx, gchar chr, GError **error)
+{
+ teco_state_t *next = ctx->current->input_cb(ctx, chr, error);
+ if (!next)
+ return FALSE;
+
+ if (next != ctx->current) {
+ if (ctx->must_undo)
+ teco_undo_ptr(ctx->current);
+ ctx->current = next;
+
+ if (ctx->current->initial_cb && !ctx->current->initial_cb(ctx, error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+teco_state_end_of_macro(teco_machine_t *ctx, GError **error)
+{
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Unterminated command");
+ return FALSE;
+}
+
+/**
+ * Handles all expected exceptions and preparing them for stack frame insertion.
+ */
+gboolean
+teco_machine_main_step(teco_machine_main_t *ctx, const gchar *macro, gint stop_pos, GError **error)
+{
+ while (ctx->macro_pc < stop_pos) {
+#ifdef DEBUG
+ g_printf("EXEC(%d): input='%c'/%x, state=%p, mode=%d\n",
+ ctx->macro_pc, macro[ctx->macro_pc], macro[ctx->macro_pc],
+ ctx->parent.current, ctx->mode);
+#endif
+
+ if (teco_interface_is_interrupted()) {
+ teco_error_interrupted_set(error);
+ goto error_attach;
+ }
+
+ if (!teco_memory_check(error))
+ goto error_attach;
+
+ if (!teco_machine_input(&ctx->parent, macro[ctx->macro_pc], error))
+ goto error_attach;
+ ctx->macro_pc++;
+ }
+
+ /*
+ * Provide interactive feedback when the
+ * PC is at the end of the command line.
+ * This will actually be called in other situations,
+ * like at the end of macros but that does not hurt.
+ * It should perhaps be in teco_cmdline_insert(),
+ * but doing it here ensures that exceptions get
+ * normalized.
+ */
+ if (ctx->parent.current->refresh_cb &&
+ !ctx->parent.current->refresh_cb(&ctx->parent, error))
+ goto error_attach;
+
+ return TRUE;
+
+error_attach:
+ g_assert(!error || *error != NULL);
+ /*
+ * FIXME: Maybe this can be avoided altogether by passing in ctx->macro_pc
+ * from the callees?
+ */
+ teco_error_set_coord(macro, ctx->macro_pc);
+ return FALSE;
+}
+
+gboolean
+teco_execute_macro(const gchar *macro, gsize macro_len,
+ teco_qreg_table_t *qreg_table_locals, GError **error)
+{
+ /*
+ * This is not auto-cleaned up, so it can be initialized
+ * on demand.
+ */
+ teco_qreg_table_t macro_locals;
+
+ if (!qreg_table_locals)
+ teco_qreg_table_init(&macro_locals, FALSE);
+
+ guint parent_brace_level = teco_brace_level;
+
+ g_auto(teco_machine_main_t) macro_machine;
+ teco_machine_main_init(&macro_machine, qreg_table_locals ? : &macro_locals, FALSE);
+
+ GError *tmp_error = NULL;
+
+ if (!teco_machine_main_step(&macro_machine, macro, macro_len, &tmp_error)) {
+ if (!g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN)) {
+ /* passes ownership of tmp_error */
+ g_propagate_error(error, tmp_error);
+ goto error_cleanup;
+ }
+ g_error_free(tmp_error);
+
+ /*
+ * Macro returned - handle like regular
+ * end of macro, even though some checks
+ * are unnecessary here.
+ * macro_pc will still point to the return PC.
+ */
+ g_assert(macro_machine.parent.current == &teco_state_start);
+
+ /*
+ * Discard all braces, except the current one.
+ */
+ if (!teco_expressions_brace_return(parent_brace_level, teco_error_return_args, error))
+ goto error_cleanup;
+
+ /*
+ * Clean up the loop stack.
+ * We are allowed to return in loops.
+ * NOTE: This does not have to be undone.
+ */
+ g_array_remove_range(teco_loop_stack, macro_machine.loop_stack_fp,
+ teco_loop_stack->len - macro_machine.loop_stack_fp);
+ }
+
+ if (G_UNLIKELY(teco_loop_stack->len > macro_machine.loop_stack_fp)) {
+ const teco_loop_context_t *ctx = &g_array_index(teco_loop_stack, teco_loop_context_t, teco_loop_stack->len-1);
+ teco_error_set_coord(macro, ctx->pc);
+
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Unterminated loop");
+ goto error_cleanup;
+ }
+
+ if (G_UNLIKELY(teco_goto_skip_label.len > 0)) {
+ g_autofree gchar *label_printable = teco_string_echo(teco_goto_skip_label.data, teco_goto_skip_label.len);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Label \"%s\" not found", label_printable);
+ goto error_attach;
+ }
+
+ /*
+ * Some states (esp. commands involving a
+ * "lookahead") are valid at the end of a macro.
+ */
+ if (macro_machine.parent.current->end_of_macro_cb &&
+ !macro_machine.parent.current->end_of_macro_cb(&macro_machine.parent, error))
+ goto error_attach;
+
+ /*
+ * This handles the problem of Q-Registers
+ * local to the macro invocation being edited
+ * when the macro terminates without additional
+ * complexity.
+ * teco_qreg_table_empty() might leave the table
+ * half-empty, but it will eventually be completely
+ * cleared by teco_qreg_table_clear().
+ * This does not hurt since an error will rub out the
+ * macro invocation itself and macro_locals don't have
+ * to be preserved.
+ */
+ if (!qreg_table_locals && !teco_qreg_table_empty(&macro_locals, error))
+ goto error_attach;
+
+ return TRUE;
+
+error_attach:
+ teco_error_set_coord(macro, macro_machine.macro_pc);
+ /* fall through */
+error_cleanup:
+ if (!qreg_table_locals)
+ teco_qreg_table_clear(&macro_locals);
+ /* make sure teco_goto_skip_label will be NULL even in batch mode */
+ teco_string_truncate(&teco_goto_skip_label, 0);
+ return FALSE;
+}
+
+gboolean
+teco_execute_file(const gchar *filename, teco_qreg_table_t *qreg_table_locals, GError **error)
+{
+ g_auto(teco_string_t) macro = {NULL, 0};
+ if (!g_file_get_contents(filename, &macro.data, &macro.len, error))
+ return FALSE;
+
+ gchar *p;
+
+ /* only when executing files, ignore Hash-Bang line */
+ if (*macro.data == '#') {
+ /*
+ * NOTE: We assume that a file starting with Hash does not contain
+ * a null-byte in its first line.
+ */
+ p = strpbrk(macro.data, "\r\n");
+ if (G_UNLIKELY(!p))
+ /* empty script */
+ return TRUE;
+ p++;
+ } else {
+ p = macro.data;
+ }
+
+ if (!teco_execute_macro(p, macro.len - (p - macro.data),
+ qreg_table_locals, error)) {
+ /* correct error position for Hash-Bang line */
+ teco_error_pos += p - macro.data;
+ if (*macro.data == '#')
+ teco_error_line++;
+ teco_error_add_frame_file(filename);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+void
+teco_machine_main_init(teco_machine_main_t *ctx, teco_qreg_table_t *qreg_table_locals,
+ gboolean must_undo)
+{
+ memset(ctx, 0, sizeof(*ctx));
+ teco_machine_init(&ctx->parent, &teco_state_start, must_undo);
+ ctx->loop_stack_fp = teco_loop_stack->len;
+ teco_goto_table_init(&ctx->goto_table, must_undo);
+ ctx->qreg_table_locals = qreg_table_locals;
+
+ ctx->expectstring.nesting = 1;
+ teco_machine_stringbuilding_init(&ctx->expectstring.machine, '\e', qreg_table_locals, must_undo);
+}
+
+gboolean
+teco_machine_main_eval_colon(teco_machine_main_t *ctx)
+{
+ if (!ctx->modifier_colon)
+ return FALSE;
+
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->modifier_colon = FALSE;
+ return TRUE;
+}
+
+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)
+{
+ if (chr < 0 || chr >= len || !transitions[(guint)chr].next) {
+ teco_error_syntax_set(error, chr);
+ return NULL;
+ }
+
+ if (ctx->mode == TECO_MODE_NORMAL && transitions[(guint)chr].transition_cb) {
+ /*
+ * NOTE: We could also just let transition_cb return a boolean...
+ */
+ GError *tmp_error = NULL;
+ transitions[(guint)chr].transition_cb(ctx, &tmp_error);
+ if (tmp_error) {
+ g_propagate_error(error, tmp_error);
+ return NULL;
+ }
+ }
+
+ return transitions[(guint)chr].next;
+}
+
+void
+teco_machine_main_clear(teco_machine_main_t *ctx)
+{
+ teco_goto_table_clear(&ctx->goto_table);
+ teco_machine_stringbuilding_clear(&ctx->expectstring.machine);
+}
+
+/*
+ * FIXME: All teco_state_stringbuilding_* states could be static?
+ */
+static teco_state_t *teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx,
+ gchar chr, GError **error);
+TECO_DECLARE_STATE(teco_state_stringbuilding_ctl);
+
+static teco_state_t *teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx,
+ gchar chr, GError **error);
+TECO_DECLARE_STATE(teco_state_stringbuilding_escaped);
+
+TECO_DECLARE_STATE(teco_state_stringbuilding_lower);
+TECO_DECLARE_STATE(teco_state_stringbuilding_upper);
+
+TECO_DECLARE_STATE(teco_state_stringbuilding_ctle);
+TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_num);
+TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_u);
+TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_q);
+TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_quote);
+TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_n);
+
+static teco_state_t *
+teco_state_stringbuilding_start_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ if (chr == '^')
+ return &teco_state_stringbuilding_ctl;
+ if (TECO_IS_CTL(chr))
+ return teco_state_stringbuilding_ctl_input(ctx, TECO_CTL_ECHO(chr), error);
+
+ return teco_state_stringbuilding_escaped_input(ctx, chr, error);
+}
+
+/* in cmdline.c */
+gboolean teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
+ gchar key, GError **error);
+
+TECO_DEFINE_STATE(teco_state_stringbuilding_start,
+ .is_start = TRUE,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)
+ teco_state_stringbuilding_start_process_edit_cmd
+);
+
+static teco_state_t *
+teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ chr = teco_ascii_toupper(chr);
+
+ switch (chr) {
+ case '^': break;
+ case 'Q':
+ case 'R': return &teco_state_stringbuilding_escaped;
+ case 'V': return &teco_state_stringbuilding_lower;
+ case 'W': return &teco_state_stringbuilding_upper;
+ case 'E': return &teco_state_stringbuilding_ctle;
+ default:
+ chr = TECO_CTL_KEY(chr);
+ }
+
+ if (ctx->result)
+ teco_string_append_c(ctx->result, chr);
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctl);
+
+static teco_state_t *
+teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ switch (ctx->mode) {
+ case TECO_STRINGBUILDING_MODE_UPPER:
+ chr = g_ascii_toupper(chr);
+ break;
+ case TECO_STRINGBUILDING_MODE_LOWER:
+ chr = g_ascii_tolower(chr);
+ break;
+ default:
+ break;
+ }
+
+ teco_string_append_c(ctx->result, chr);
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE(teco_state_stringbuilding_escaped);
+
+static teco_state_t *
+teco_state_stringbuilding_lower_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ /*
+ * FIXME: This does not handle ^V^V typed with up-carets.
+ */
+ if (chr == TECO_CTL_KEY('V')) {
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->mode);
+ ctx->mode = TECO_STRINGBUILDING_MODE_LOWER;
+ } else {
+ teco_string_append_c(ctx->result, g_ascii_tolower(chr));
+ }
+
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE(teco_state_stringbuilding_lower);
+
+static teco_state_t *
+teco_state_stringbuilding_upper_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ /*
+ * FIXME: This does not handle ^W^W typed with up-carets.
+ */
+ if (chr == TECO_CTL_KEY('W')) {
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->mode);
+ ctx->mode = TECO_STRINGBUILDING_MODE_UPPER;
+ } else {
+ teco_string_append_c(ctx->result, g_ascii_toupper(chr));
+ }
+
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE(teco_state_stringbuilding_upper);
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_state_t *next;
+
+ switch (teco_ascii_toupper(chr)) {
+ case '\\': next = &teco_state_stringbuilding_ctle_num; break;
+ case 'U': next = &teco_state_stringbuilding_ctle_u; break;
+ case 'Q': next = &teco_state_stringbuilding_ctle_q; break;
+ case '@': next = &teco_state_stringbuilding_ctle_quote; break;
+ case 'N': next = &teco_state_stringbuilding_ctle_n; break;
+ default:
+ if (ctx->result) {
+ gchar buf[] = {TECO_CTL_KEY('E'), chr};
+ teco_string_append(ctx->result, buf, sizeof(buf));
+ }
+ return &teco_state_stringbuilding_start;
+ }
+
+ if (ctx->machine_qregspec)
+ teco_machine_qregspec_reset(ctx->machine_qregspec);
+ else
+ ctx->machine_qregspec = teco_machine_qregspec_new(TECO_QREG_REQUIRED,
+ ctx->qreg_table_locals,
+ ctx->parent.must_undo);
+ return next;
+}
+
+TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctle);
+
+/* in cmdline.c */
+gboolean teco_state_stringbuilding_qreg_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
+ gchar chr, GError **error);
+
+/**
+ * @interface TECO_DEFINE_STATE_STRINGBUILDING_QREG
+ * @implements TECO_DEFINE_STATE
+ * @ingroup states
+ */
+#define TECO_DEFINE_STATE_STRINGBUILDING_QREG(NAME, ...) \
+ TECO_DEFINE_STATE(NAME, \
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \
+ teco_state_stringbuilding_qreg_process_edit_cmd, \
+ ##__VA_ARGS__ \
+ )
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_num_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_qreg_t *qreg;
+
+ switch (teco_machine_qregspec_input(ctx->machine_qregspec, chr,
+ ctx->result ? &qreg : NULL, NULL, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+ case TECO_MACHINE_QREGSPEC_MORE:
+ return &teco_state_stringbuilding_ctle_num;
+ case TECO_MACHINE_QREGSPEC_DONE:
+ break;
+ }
+
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ teco_int_t value;
+ if (!qreg->vtable->get_integer(qreg, &value, error))
+ return NULL;
+
+ /*
+ * NOTE: Numbers can always be safely formatted as null-terminated strings.
+ */
+ gchar buffer[TECO_EXPRESSIONS_FORMAT_LEN];
+ const gchar *num = teco_expressions_format(buffer, value);
+ teco_string_append(ctx->result, num, strlen(num));
+
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_num);
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_u_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_qreg_t *qreg;
+
+ switch (teco_machine_qregspec_input(ctx->machine_qregspec, chr,
+ ctx->result ? &qreg : NULL, NULL, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+ case TECO_MACHINE_QREGSPEC_MORE:
+ return &teco_state_stringbuilding_ctle_u;
+ case TECO_MACHINE_QREGSPEC_DONE:
+ break;
+ }
+
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ teco_int_t value;
+ if (!qreg->vtable->get_integer(qreg, &value, error))
+ return NULL;
+ if (value < 0 || value > 0xFF) {
+ g_autofree gchar *name_printable = teco_string_echo(qreg->head.name.data, qreg->head.name.len);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Q-Register \"%s\" does not contain a valid character", name_printable);
+ return NULL;
+ }
+
+ teco_string_append_c(ctx->result, (gchar)value);
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_u);
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_q_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_qreg_t *qreg;
+
+ switch (teco_machine_qregspec_input(ctx->machine_qregspec, chr,
+ ctx->result ? &qreg : NULL, NULL, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+ case TECO_MACHINE_QREGSPEC_MORE:
+ return &teco_state_stringbuilding_ctle_q;
+ case TECO_MACHINE_QREGSPEC_DONE:
+ break;
+ }
+
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ /*
+ * FIXME: Should we have a special teco_qreg_get_string_append() function?
+ */
+ g_auto(teco_string_t) str = {NULL, 0};
+ if (!qreg->vtable->get_string(qreg, &str.data, &str.len, error))
+ return NULL;
+ teco_string_append(ctx->result, str.data, str.len);
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_q);
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_quote_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_qreg_t *qreg;
+ teco_qreg_table_t *table;
+
+ switch (teco_machine_qregspec_input(ctx->machine_qregspec, chr,
+ ctx->result ? &qreg : NULL, &table, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+ case TECO_MACHINE_QREGSPEC_MORE:
+ return &teco_state_stringbuilding_ctle_quote;
+ case TECO_MACHINE_QREGSPEC_DONE:
+ break;
+ }
+
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ g_auto(teco_string_t) str = {NULL, 0};
+ if (!qreg->vtable->get_string(qreg, &str.data, &str.len, error))
+ return NULL;
+ /*
+ * NOTE: g_shell_quote() expects a null-terminated string, so it is
+ * important to check that there are no embedded nulls.
+ * The restriction itself is probably valid since null-bytes are not allowed
+ * in command line arguments anyway.
+ * Otherwise, we'd have to implement our own POSIX shell escape function.
+ */
+ if (teco_string_contains(&str, '\0')) {
+ teco_error_qregcontainsnull_set(error, qreg->head.name.data, qreg->head.name.len,
+ table != &teco_qreg_table_globals);
+ return NULL;
+ }
+ g_autofree gchar *str_quoted = g_shell_quote(str.data);
+ teco_string_append(ctx->result, str_quoted, strlen(str_quoted));
+
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_quote);
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_n_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_qreg_t *qreg;
+ teco_qreg_table_t *table;
+
+ switch (teco_machine_qregspec_input(ctx->machine_qregspec, chr,
+ ctx->result ? &qreg : NULL, &table, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+ case TECO_MACHINE_QREGSPEC_MORE:
+ return &teco_state_stringbuilding_ctle_n;
+ case TECO_MACHINE_QREGSPEC_DONE:
+ break;
+ }
+
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ g_auto(teco_string_t) str = {NULL, 0};
+ if (!qreg->vtable->get_string(qreg, &str.data, &str.len, error))
+ return NULL;
+ if (teco_string_contains(&str, '\0')) {
+ teco_error_qregcontainsnull_set(error, qreg->head.name.data, qreg->head.name.len,
+ table != &teco_qreg_table_globals);
+ return NULL;
+ }
+
+ g_autofree gchar *str_escaped = teco_globber_escape_pattern(str.data);
+ teco_string_append(ctx->result, str_escaped, strlen(str_escaped));
+
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_n);
+
+void
+teco_machine_stringbuilding_init(teco_machine_stringbuilding_t *ctx, gchar escape_char,
+ teco_qreg_table_t *locals, gboolean must_undo)
+{
+ memset(ctx, 0, sizeof(*ctx));
+ teco_machine_init(&ctx->parent, &teco_state_stringbuilding_start, must_undo);
+ ctx->escape_char = escape_char;
+ ctx->qreg_table_locals = locals;
+}
+
+void
+teco_machine_stringbuilding_reset(teco_machine_stringbuilding_t *ctx)
+{
+ teco_machine_reset(&ctx->parent, &teco_state_stringbuilding_start);
+ if (ctx->machine_qregspec)
+ teco_machine_qregspec_reset(ctx->machine_qregspec);
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->mode);
+ ctx->mode = TECO_STRINGBUILDING_MODE_NORMAL;
+}
+
+void
+teco_machine_stringbuilding_escape(teco_machine_stringbuilding_t *ctx, const gchar *str, gsize len,
+ teco_string_t *target)
+{
+ target->data = g_malloc(len*2+1);
+ target->len = 0;
+
+ for (guint i = 0; i < len; i++) {
+ if (teco_ascii_toupper(str[i]) == ctx->escape_char ||
+ (ctx->escape_char == '[' && str[i] == ']') ||
+ (ctx->escape_char == '{' && str[i] == '}'))
+ target->data[target->len++] = TECO_CTL_KEY('Q');
+ target->data[target->len++] = str[i];
+ }
+
+ target->data[target->len] = '\0';
+}
+
+void
+teco_machine_stringbuilding_clear(teco_machine_stringbuilding_t *ctx)
+{
+ if (ctx->machine_qregspec)
+ teco_machine_qregspec_free(ctx->machine_qregspec);
+}
+
+teco_state_t *
+teco_state_expectstring_input(teco_machine_main_t *ctx, gchar chr, GError **error)
+{
+ teco_state_t *current = ctx->parent.current;
+
+ /*
+ * String termination handling
+ */
+ if (ctx->modifier_at) {
+ if (current->expectstring.last) {
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->modifier_at = FALSE;
+ }
+
+ /*
+ * FIXME: Exclude setting at least whitespace characters as the
+ * new string escape character to avoid accidental errors?
+ */
+ switch (ctx->expectstring.machine.escape_char) {
+ case '\e':
+ case '{':
+ if (ctx->parent.must_undo)
+ teco_undo_gchar(ctx->expectstring.machine.escape_char);
+ ctx->expectstring.machine.escape_char = teco_ascii_toupper(chr);
+ return current;
+ }
+ }
+
+ /*
+ * This makes sure that escape characters (or braces) within string-building
+ * constructs and Q-Register specifications do not have to be escaped.
+ * This makes also sure that string terminators can be escaped via ^Q/^R.
+ */
+ if (ctx->expectstring.machine.parent.current->is_start) {
+ if (ctx->expectstring.machine.escape_char == '{') {
+ switch (chr) {
+ case '{':
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->expectstring.nesting);
+ ctx->expectstring.nesting++;
+ break;
+ case '}':
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->expectstring.nesting);
+ ctx->expectstring.nesting--;
+ break;
+ }
+ } else if (teco_ascii_toupper(chr) == ctx->expectstring.machine.escape_char) {
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->expectstring.nesting);
+ ctx->expectstring.nesting--;
+ }
+ }
+
+ if (!ctx->expectstring.nesting) {
+ /*
+ * Call process_cb() even if interactive feedback
+ * has not been requested using refresh_cb().
+ * This is necessary since commands are either
+ * written for interactive execution or not,
+ * so they may do their main activity in process_cb().
+ */
+ if (ctx->expectstring.insert_len && current->expectstring.process_cb &&
+ !current->expectstring.process_cb(ctx, &ctx->expectstring.string,
+ ctx->expectstring.insert_len, error))
+ return NULL;
+
+ teco_state_t *next = current->expectstring.done_cb(ctx, &ctx->expectstring.string, error);
+
+ if (ctx->parent.must_undo)
+ teco_undo_string_own(ctx->expectstring.string);
+ else
+ teco_string_clear(&ctx->expectstring.string);
+ memset(&ctx->expectstring.string, 0, sizeof(ctx->expectstring.string));
+
+ if (current->expectstring.last) {
+ if (ctx->parent.must_undo)
+ teco_undo_gchar(ctx->expectstring.machine.escape_char);
+ ctx->expectstring.machine.escape_char = '\e';
+ }
+ ctx->expectstring.nesting = 1;
+
+ if (current->expectstring.string_building)
+ teco_machine_stringbuilding_reset(&ctx->expectstring.machine);
+
+ ctx->expectstring.insert_len = 0;
+ return next;
+ }
+
+ /*
+ * NOTE: Since we only ever append to `string`, this is more efficient
+ * than teco_undo_string(ctx->expectstring.string).
+ */
+ if (ctx->mode == TECO_MODE_NORMAL && ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->expectstring.string, ctx->expectstring.string.len);
+
+ /*
+ * String building characters and string argument accumulation.
+ */
+ gsize old_len = ctx->expectstring.string.len;
+ if (current->expectstring.string_building) {
+ teco_string_t *str = ctx->mode == TECO_MODE_NORMAL
+ ? &ctx->expectstring.string : NULL;
+ if (!teco_machine_stringbuilding_input(&ctx->expectstring.machine, chr, str, error))
+ return NULL;
+ } else if (ctx->mode == TECO_MODE_NORMAL) {
+ teco_string_append_c(&ctx->expectstring.string, chr);
+ }
+ /*
+ * NOTE: As an optimization insert_len is not
+ * restored on undo since that is only
+ * necessary in interactive mode and we get
+ * called once per character when this is necessary.
+ */
+ ctx->expectstring.insert_len += ctx->expectstring.string.len - old_len;
+
+ return current;
+}
+
+gboolean
+teco_state_expectstring_refresh(teco_machine_main_t *ctx, GError **error)
+{
+ teco_state_t *current = ctx->parent.current;
+
+ /* never calls process_cb() in parse-only mode */
+ if (ctx->expectstring.insert_len && current->expectstring.process_cb &&
+ !current->expectstring.process_cb(ctx, &ctx->expectstring.string,
+ ctx->expectstring.insert_len, error))
+ return FALSE;
+
+ ctx->expectstring.insert_len = 0;
+ return TRUE;
+}
+
+gboolean
+teco_state_expectfile_process(teco_machine_main_t *ctx, const teco_string_t *str,
+ gsize new_chars, GError **error)
+{
+ g_assert(str->data != NULL);
+
+ /*
+ * Null-chars must not ocur in filename/path strings and at some point
+ * teco_string_t has to be converted to a null-terminated C string
+ * as all the glib filename functions rely on null-terminated strings.
+ * Doing it here ensures that teco_file_expand_path() can be safely called
+ * from the done_cb().
+ */
+ if (memchr(str->data + str->len - new_chars, '\0', new_chars)) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Null-character not allowed in filenames");
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/src/parser.cpp b/src/parser.cpp
deleted file mode 100644
index fe22560..0000000
--- a/src/parser.cpp
+++ /dev/null
@@ -1,2883 +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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <string.h>
-#include <exception>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-#include "sciteco.h"
-#include "memory.h"
-#include "string-utils.h"
-#include "interface.h"
-#include "undo.h"
-#include "expressions.h"
-#include "goto.h"
-#include "qregisters.h"
-#include "ring.h"
-#include "parser.h"
-#include "symbols.h"
-#include "search.h"
-#include "spawn.h"
-#include "glob.h"
-#include "help.h"
-#include "cmdline.h"
-#include "ioview.h"
-#include "error.h"
-
-namespace SciTECO {
-
-//#define DEBUG
-
-gint macro_pc = 0;
-
-namespace States {
- StateStart start;
- StateControl control;
- StateASCII ascii;
- StateEscape escape;
- StateFCommand fcommand;
- StateChangeDir changedir;
- StateCondCommand condcommand;
- StateECommand ecommand;
- StateScintilla_symbols scintilla_symbols;
- StateScintilla_lParam scintilla_lparam;
- StateInsert insert_building(true);
- StateInsert insert_nobuilding(false);
- StateInsertIndent insert_indent;
-
- State *current = &start;
-}
-
-namespace Modifiers {
- static bool colon = false;
- static bool at = false;
-}
-
-enum Mode mode = MODE_NORMAL;
-
-/* FIXME: perhaps integrate into Mode */
-static bool skip_else = false;
-
-static gint nest_level = 0;
-
-gchar *strings[2] = {NULL, NULL};
-gchar escape_char = CTL_KEY_ESC;
-
-LoopStack loop_stack;
-
-/**
- * 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.
- */
-static guint loop_stack_fp = 0;
-
-/**
- * Handles all expected exceptions, converting them to
- * SciTECO::Error and preparing them for stack frame insertion.
- * This method will only throw SciTECO::Error and
- * SciTECO::Cmdline *.
- */
-void
-Execute::step(const gchar *macro, gint stop_pos)
-{
- try {
- /*
- * Convert bad_alloc and other C++ standard
- * library exceptions.
- * bad_alloc should no longer be thrown, though
- * since new/delete uses Glib allocations and we
- * uniformly terminate abnormally in case of OOM.
- */
- try {
- while (macro_pc < stop_pos) {
-#ifdef DEBUG
- g_printf("EXEC(%d): input='%c'/%x, state=%p, mode=%d\n",
- macro_pc, macro[macro_pc], macro[macro_pc],
- States::current, mode);
-#endif
-
- if (interface.is_interrupted())
- throw Error("Interrupted");
-
- memlimit.check();
-
- State::input(macro[macro_pc]);
- macro_pc++;
- }
-
- /*
- * Provide interactive feedback when the
- * PC is at the end of the command line.
- * This will actually be called in other situations,
- * like at the end of macros but that does not hurt.
- * It should perhaps be in Cmdline::insert(),
- * but doing it here ensures that exceptions get
- * normalized.
- */
- States::current->refresh();
- } catch (std::exception &error) {
- throw StdError(error);
- }
- } catch (Error &error) {
- error.set_coord(macro, macro_pc);
- throw; /* forward */
- }
-}
-
-/*
- * may throw non SciTECO::Error exceptions which are not to be
- * associated with the macro invocation stack frame
- */
-void
-Execute::macro(const gchar *macro, bool locals)
-{
- GotoTable *parent_goto_table = Goto::table;
- GotoTable macro_goto_table(false);
-
- QRegisterTable *parent_locals = QRegisters::locals;
- /*
- * NOTE: A local QReg table is not required
- * for local macro calls (:M).
- * However allocating it on the stack on-demand is
- * tricky (VLAs are not in standard C++ and alloca()
- * is buggy on MSCVRT), so we always reserve a
- * local Q-Reg table.
- * This is OK since the table object itself is very
- * small and it's empty by default.
- * Best would be to let Execute::macro() be a wrapper
- * around something like Execute::local_macro() which
- * cares about local Q-Reg allocation, but the special
- * handling of currently-edited local Q-Regs below
- * prevents this.
- */
- QRegisterTable macro_locals(false);
-
- State *parent_state = States::current;
- gint parent_pc = macro_pc;
- guint parent_loop_fp = loop_stack_fp;
-
- guint parent_brace_level = expressions.brace_level;
-
- /*
- * need this to fixup state on rubout: state machine emits undo token
- * resetting state to parent's one, but the macro executed also emitted
- * undo tokens resetting the state to StateStart
- */
- undo.push_var(States::current) = &States::start;
- macro_pc = 0;
- loop_stack_fp = loop_stack.items();
-
- Goto::table = &macro_goto_table;
-
- /*
- * Locals are only initialized when needed to
- * improve the speed of local macro calls.
- */
- if (locals) {
- macro_locals.insert_defaults();
- QRegisters::locals = &macro_locals;
- }
-
- try {
- try {
- step(macro, strlen(macro));
- } catch (Return &info) {
- /*
- * Macro returned - handle like regular
- * end of macro, even though some checks
- * are unnecessary here.
- * macro_pc will still point to the return PC.
- */
- g_assert(States::current == &States::start);
-
- /*
- * Discard all braces, except the current one.
- */
- expressions.brace_return(parent_brace_level, info.args);
-
- /*
- * Clean up the loop stack.
- * We are allowed to return in loops.
- * NOTE: This does not have to be undone.
- */
- loop_stack.clear(loop_stack_fp);
- }
-
- if (G_UNLIKELY(loop_stack.items() > loop_stack_fp)) {
- Error error("Unterminated loop");
- error.set_coord(macro, loop_stack.peek().pc);
- throw error;
- }
-
- /*
- * Subsequent errors must still be
- * attached to this macro invocation
- * via Error::set_coord()
- */
- try {
- if (G_UNLIKELY(Goto::skip_label))
- throw Error("Label \"%s\" not found",
- Goto::skip_label);
-
- /*
- * Some states (esp. commands involving a
- * "lookahead") are valid at the end of a macro.
- */
- States::current->end_of_macro();
-
- /*
- * This handles the problem of Q-Registers
- * local to the macro invocation being edited
- * when the macro terminates.
- * QRegisterTable::clear() throws an error
- * if this happens and the Q-Reg editing
- * is undone.
- */
- if (locals)
- QRegisters::locals->clear();
- } catch (Error &error) {
- error.set_coord(macro, macro_pc);
- throw; /* forward */
- }
- } catch (...) {
- g_free(Goto::skip_label);
- Goto::skip_label = NULL;
-
- QRegisters::locals = parent_locals;
- Goto::table = parent_goto_table;
-
- loop_stack_fp = parent_loop_fp;
- macro_pc = parent_pc;
- States::current = parent_state;
-
- throw; /* forward */
- }
-
- QRegisters::locals = parent_locals;
- Goto::table = parent_goto_table;
-
- loop_stack_fp = parent_loop_fp;
- macro_pc = parent_pc;
- States::current = parent_state;
-}
-
-void
-Execute::file(const gchar *filename, bool locals)
-{
- GError *gerror = NULL;
- gchar *macro_str, *p;
-
- if (!g_file_get_contents(filename, &macro_str, NULL, &gerror))
- throw GlibError(gerror);
-
- /* only when executing files, ignore Hash-Bang line */
- if (*macro_str == '#') {
- p = strpbrk(macro_str, "\r\n");
- if (G_UNLIKELY(!p))
- /* empty script */
- goto cleanup;
- p++;
- } else {
- p = macro_str;
- }
-
- try {
- macro(p, locals);
- } catch (Error &error) {
- error.pos += p - macro_str;
- if (*macro_str == '#')
- error.line++;
- error.add_frame(new Error::FileFrame(filename));
-
- g_free(macro_str);
- throw; /* forward */
- } catch (...) {
- g_free(macro_str);
- throw; /* forward */
- }
-
-cleanup:
- g_free(macro_str);
-}
-
-State::State()
-{
- for (guint i = 0; i < G_N_ELEMENTS(transitions); i++)
- transitions[i] = NULL;
-}
-
-bool
-State::eval_colon(void)
-{
- if (!Modifiers::colon)
- return false;
-
- undo.push_var<bool>(Modifiers::colon);
- Modifiers::colon = false;
- return true;
-}
-
-void
-State::input(gchar chr)
-{
- State *state = States::current;
-
- for (;;) {
- State *next = state->get_next_state(chr);
-
- if (next == state)
- break;
-
- state = next;
- chr = '\0';
- }
-
- if (state != States::current) {
- undo.push_var<State *>(States::current);
- States::current = state;
- }
-}
-
-State *
-State::get_next_state(gchar chr)
-{
- State *next = NULL;
- guint upper = String::toupper(chr);
-
- if (upper < G_N_ELEMENTS(transitions))
- next = transitions[upper];
- if (!next)
- next = custom(chr);
- if (!next)
- throw SyntaxError(chr);
-
- return next;
-}
-
-void
-StringBuildingMachine::reset(void)
-{
- MicroStateMachine<gchar *>::reset();
- undo.push_obj(qregspec_machine) = NULL;
- undo.push_var(mode) = MODE_NORMAL;
- undo.push_var(toctl) = false;
-}
-
-bool
-StringBuildingMachine::input(gchar chr, gchar *&result)
-{
- QRegister *reg;
- gchar *str;
-
- switch (mode) {
- case MODE_UPPER:
- chr = g_ascii_toupper(chr);
- break;
- case MODE_LOWER:
- chr = g_ascii_tolower(chr);
- break;
- default:
- break;
- }
-
- if (toctl) {
- if (chr != '^')
- chr = CTL_KEY(String::toupper(chr));
- undo.push_var(toctl) = false;
- } else if (chr == '^') {
- undo.push_var(toctl) = true;
- return false;
- }
-
-MICROSTATE_START;
- switch (chr) {
- case CTL_KEY('Q'):
- case CTL_KEY('R'): set(&&StateEscaped); break;
- case CTL_KEY('V'): set(&&StateLower); break;
- case CTL_KEY('W'): set(&&StateUpper); break;
- case CTL_KEY('E'): set(&&StateCtlE); break;
- default:
- goto StateEscaped;
- }
-
- return false;
-
-StateLower:
- set(StateStart);
-
- if (chr != CTL_KEY('V')) {
- result = String::chrdup(g_ascii_tolower(chr));
- return true;
- }
-
- undo.push_var(mode) = MODE_LOWER;
- return false;
-
-StateUpper:
- set(StateStart);
-
- if (chr != CTL_KEY('W')) {
- result = String::chrdup(g_ascii_toupper(chr));
- return true;
- }
-
- undo.push_var(mode) = MODE_UPPER;
- return false;
-
-StateCtlE:
- switch (String::toupper(chr)) {
- case '\\':
- undo.push_obj(qregspec_machine) = new QRegSpecMachine;
- set(&&StateCtlENum);
- break;
- case 'U':
- undo.push_obj(qregspec_machine) = new QRegSpecMachine;
- set(&&StateCtlEU);
- break;
- case 'Q':
- undo.push_obj(qregspec_machine) = new QRegSpecMachine;
- set(&&StateCtlEQ);
- break;
- case '@':
- undo.push_obj(qregspec_machine) = new QRegSpecMachine;
- set(&&StateCtlEQuote);
- break;
- case 'N':
- undo.push_obj(qregspec_machine) = new QRegSpecMachine;
- set(&&StateCtlEN);
- break;
- default:
- result = (gchar *)g_malloc(3);
-
- set(StateStart);
- result[0] = CTL_KEY('E');
- result[1] = chr;
- result[2] = '\0';
- return true;
- }
-
- return false;
-
-StateCtlENum:
- if (!qregspec_machine->input(chr, reg))
- return false;
-
- undo.push_obj(qregspec_machine) = NULL;
- set(StateStart);
- result = g_strdup(expressions.format(reg->get_integer()));
- return true;
-
-StateCtlEU:
- if (!qregspec_machine->input(chr, reg))
- return false;
-
- undo.push_obj(qregspec_machine) = NULL;
- set(StateStart);
- result = String::chrdup((gchar)reg->get_integer());
- return true;
-
-StateCtlEQ:
- if (!qregspec_machine->input(chr, reg))
- return false;
-
- undo.push_obj(qregspec_machine) = NULL;
- set(StateStart);
- result = reg->get_string();
- return true;
-
-StateCtlEQuote:
- if (!qregspec_machine->input(chr, reg))
- return false;
-
- undo.push_obj(qregspec_machine) = NULL;
- set(StateStart);
- str = reg->get_string();
- result = g_shell_quote(str);
- g_free(str);
- return true;
-
-StateCtlEN:
- if (!qregspec_machine->input(chr, reg))
- return false;
-
- undo.push_obj(qregspec_machine) = NULL;
- set(StateStart);
- str = reg->get_string();
- result = Globber::escape_pattern(str);
- g_free(str);
- return true;
-
-StateEscaped:
- set(StateStart);
- result = String::chrdup(chr);
- return true;
-}
-
-StringBuildingMachine::~StringBuildingMachine()
-{
- delete qregspec_machine;
-}
-
-State *
-StateExpectString::custom(gchar chr)
-{
- if (chr == '\0') {
- BEGIN_EXEC(this);
- initial();
- return this;
- }
-
- /*
- * String termination handling
- */
- if (Modifiers::at) {
- if (last)
- undo.push_var(Modifiers::at) = false;
-
- switch (escape_char) {
- case CTL_KEY_ESC:
- case '{':
- undo.push_var(escape_char) = String::toupper(chr);
- return this;
- }
- }
-
- if (escape_char == '{') {
- switch (chr) {
- case '{':
- undo.push_var(nesting)++;
- break;
- case '}':
- undo.push_var(nesting)--;
- break;
- }
- } else if (String::toupper(chr) == escape_char) {
- undo.push_var(nesting)--;
- }
-
- if (!nesting) {
- State *next;
- gchar *string = strings[0];
-
- undo.push_str(strings[0]) = NULL;
- if (last)
- undo.push_var(escape_char) = CTL_KEY_ESC;
- nesting = 1;
-
- if (string_building)
- machine.reset();
-
- try {
- /*
- * Call process() even if interactive feedback
- * has not been requested using refresh().
- * This is necessary since commands are either
- * written for interactive execution or not,
- * so they may do their main activity in process().
- */
- if (insert_len)
- process(string ? : "", insert_len);
- next = done(string ? : "");
- } catch (...) {
- g_free(string);
- throw;
- }
-
- g_free(string);
- insert_len = 0;
- return next;
- }
-
- BEGIN_EXEC(this);
-
- /*
- * String building characters and
- * string argument accumulation.
- *
- * NOTE: As an optimization insert_len is not
- * restored on undo since that is only
- * necessary in interactive mode and we get
- * called once per character when this is necessary.
- * If this gets too confusing, just undo changes
- * to insert_len.
- */
- if (string_building) {
- gchar *insert;
-
- if (!machine.input(chr, insert))
- return this;
-
- undo.push_str(strings[0]);
- String::append(strings[0], insert);
- insert_len += strlen(insert);
-
- g_free(insert);
- } else {
- undo.push_str(strings[0]);
- String::append(strings[0], chr);
- insert_len++;
- }
-
- return this;
-}
-
-void
-StateExpectString::refresh(void)
-{
- /* never calls process() in parse-only mode */
- if (insert_len)
- process(strings[0], insert_len);
- insert_len = 0;
-}
-
-State *
-StateExpectFile::done(const gchar *str)
-{
- gchar *filename = expand_path(str);
- State *next;
-
- try {
- next = got_file(filename);
- } catch (...) {
- g_free(filename);
- throw;
- }
-
- g_free(filename);
- return next;
-}
-
-StateStart::StateStart()
-{
- transitions['\0'] = this;
- init(" \f\r\n\v");
-
- transitions['$'] = &States::escape;
- transitions['!'] = &States::label;
- transitions['O'] = &States::gotocmd;
- transitions['^'] = &States::control;
- transitions['F'] = &States::fcommand;
- transitions['"'] = &States::condcommand;
- transitions['E'] = &States::ecommand;
- transitions['I'] = &States::insert_building;
- transitions['?'] = &States::gethelp;
- transitions['S'] = &States::search;
- transitions['N'] = &States::searchall;
-
- transitions['['] = &States::pushqreg;
- transitions[']'] = &States::popqreg;
- transitions['G'] = &States::getqregstring;
- transitions['Q'] = &States::queryqreg;
- transitions['U'] = &States::setqreginteger;
- transitions['%'] = &States::increaseqreg;
- transitions['M'] = &States::macro;
- transitions['X'] = &States::copytoqreg;
-}
-
-void
-StateStart::insert_integer(tecoInt v)
-{
- const gchar *str = expressions.format(v);
-
- interface.ssm(SCI_BEGINUNDOACTION);
- interface.ssm(SCI_ADDTEXT, strlen(str), (sptr_t)str);
- interface.ssm(SCI_SCROLLCARET);
- interface.ssm(SCI_ENDUNDOACTION);
- ring.dirtify();
-
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
-}
-
-tecoInt
-StateStart::read_integer(void)
-{
- uptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
- gchar c = (gchar)interface.ssm(SCI_GETCHARAT, pos);
- tecoInt v = 0;
- gint sign = 1;
-
- if (c == '-') {
- pos++;
- sign = -1;
- }
-
- for (;;) {
- c = String::toupper((gchar)interface.ssm(SCI_GETCHARAT, pos));
- if (c >= '0' && c <= '0' + MIN(expressions.radix, 10) - 1)
- v = (v*expressions.radix) + (c - '0');
- else if (c >= 'A' &&
- c <= 'A' + MIN(expressions.radix - 10, 26) - 1)
- v = (v*expressions.radix) + 10 + (c - 'A');
- else
- break;
-
- pos++;
- }
-
- return sign * v;
-}
-
-tecoBool
-StateStart::move_chars(tecoInt n)
-{
- sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
-
- if (!Validate::pos(pos + n))
- return FAILURE;
-
- interface.ssm(SCI_GOTOPOS, pos + n);
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_GOTOPOS, pos);
-
- return SUCCESS;
-}
-
-tecoBool
-StateStart::move_lines(tecoInt n)
-{
- sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
- sptr_t line = interface.ssm(SCI_LINEFROMPOSITION, pos) + n;
-
- if (!Validate::line(line))
- return FAILURE;
-
- interface.ssm(SCI_GOTOLINE, line);
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_GOTOPOS, pos);
-
- return SUCCESS;
-}
-
-tecoBool
-StateStart::delete_words(tecoInt n)
-{
- sptr_t pos, size;
-
- if (!n)
- return SUCCESS;
-
- pos = interface.ssm(SCI_GETCURRENTPOS);
- size = interface.ssm(SCI_GETLENGTH);
- interface.ssm(SCI_BEGINUNDOACTION);
- /*
- * 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 = interface.ssm(SCI_GETLENGTH);
- interface.ssm(SCI_DELWORDRIGHTEND);
- if (size == interface.ssm(SCI_GETLENGTH))
- break;
- }
- } else {
- n *= -1;
- while (n--) {
- sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
- //interface.ssm(SCI_DELWORDLEFTEND);
- interface.ssm(SCI_WORDLEFTEND);
- if (pos == interface.ssm(SCI_GETCURRENTPOS))
- break;
- interface.ssm(SCI_DELWORDRIGHTEND);
- }
- }
- interface.ssm(SCI_ENDUNDOACTION);
-
- if (n >= 0) {
- if (size != interface.ssm(SCI_GETLENGTH)) {
- interface.ssm(SCI_UNDO);
- interface.ssm(SCI_GOTOPOS, pos);
- }
- return FAILURE;
- }
-
- interface.undo_ssm(SCI_GOTOPOS, pos);
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
- ring.dirtify();
-
- return SUCCESS;
-}
-
-State *
-StateStart::custom(gchar chr)
-{
- tecoInt v;
- tecoBool rc;
-
- /*
- * <CTRL/x> commands implemented in StateControl
- */
- if (IS_CTL(chr))
- return States::control.get_next_state(CTL_ECHO(chr));
-
- /*
- * arithmetics
- */
- /*$ 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.
- */
- if (g_ascii_isdigit(chr)) {
- BEGIN_EXEC(this);
- expressions.add_digit(chr);
- return this;
- }
-
- chr = String::toupper(chr);
- switch (chr) {
- case '/':
- BEGIN_EXEC(this);
- expressions.push_calc(Expressions::OP_DIV);
- break;
-
- case '*':
- if (cmdline.len == 1 && cmdline[0] == '*')
- /* special save last commandline command */
- return &States::save_cmdline;
-
- BEGIN_EXEC(this);
- expressions.push_calc(Expressions::OP_MUL);
- break;
-
- case '+':
- BEGIN_EXEC(this);
- expressions.push_calc(Expressions::OP_ADD);
- break;
-
- case '-':
- BEGIN_EXEC(this);
- if (!expressions.args())
- expressions.set_num_sign(-expressions.num_sign);
- else
- expressions.push_calc(Expressions::OP_SUB);
- break;
-
- case '&':
- BEGIN_EXEC(this);
- expressions.push_calc(Expressions::OP_AND);
- break;
-
- case '#':
- BEGIN_EXEC(this);
- expressions.push_calc(Expressions::OP_OR);
- break;
-
- case '(':
- BEGIN_EXEC(this);
- if (expressions.num_sign < 0) {
- expressions.set_num_sign(1);
- expressions.eval();
- expressions.push(-1);
- expressions.push_calc(Expressions::OP_MUL);
- }
- expressions.brace_open();
- break;
-
- case ')':
- BEGIN_EXEC(this);
- expressions.brace_close();
- break;
-
- case ',':
- BEGIN_EXEC(this);
- expressions.eval();
- expressions.push(Expressions::OP_NEW);
- break;
-
- /*$ "." dot
- * \&. -> dot -- Return buffer position
- *
- * \(lq.\(rq pushes onto the stack, the current
- * position (also called <dot>) of the currently
- * selected buffer or Q-Register.
- */
- case '.':
- BEGIN_EXEC(this);
- expressions.eval();
- expressions.push(interface.ssm(SCI_GETCURRENTPOS));
- break;
-
- /*$ 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.
- */
- case 'Z':
- BEGIN_EXEC(this);
- expressions.eval();
- expressions.push(interface.ssm(SCI_GETLENGTH));
- break;
-
- /*$ 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.
- */
- case 'H':
- BEGIN_EXEC(this);
- expressions.eval();
- expressions.push(0);
- expressions.push(interface.ssm(SCI_GETLENGTH));
- break;
-
- /*$ "\\"
- * 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 <n> 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.
- */
- case '\\':
- BEGIN_EXEC(this);
- expressions.eval();
- if (expressions.args())
- insert_integer(expressions.pop_num_calc());
- else
- expressions.push(read_integer());
- break;
-
- /*
- * control structures (loops)
- */
- case '<':
- if (mode == MODE_PARSE_ONLY_LOOP) {
- undo.push_var(nest_level)++;
- } else {
- LoopContext ctx;
-
- BEGIN_EXEC(this);
-
- expressions.eval();
- ctx.pass_through = eval_colon();
- ctx.counter = expressions.pop_num_calc(0, -1);
- if (ctx.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 (!ctx.pass_through)
- expressions.brace_open();
-
- ctx.pc = macro_pc;
- loop_stack.push(ctx);
- LoopStack::undo_pop<loop_stack>();
- } else {
- /* skip to end of loop */
- undo.push_var(mode) = MODE_PARSE_ONLY_LOOP;
- }
- }
- break;
-
- case '>':
- if (mode == MODE_PARSE_ONLY_LOOP) {
- if (!nest_level)
- undo.push_var(mode) = MODE_NORMAL;
- else
- undo.push_var(nest_level)--;
- } else {
- BEGIN_EXEC(this);
-
- if (loop_stack.items() <= loop_stack_fp)
- throw Error("Loop end without corresponding "
- "loop start command");
- LoopContext &ctx = loop_stack.peek();
- bool colon_modified = eval_colon();
-
- /*
- * 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 (!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>(loop_stack.pop());
- } else {
- /*
- * Repeat loop:
- * NOTE: One undo token per iteration could
- * be avoided by saving the original counter
- * in the LoopContext.
- * We do however optimize the case of infinite loops
- * because the loop counter does not have to be
- * updated.
- */
- macro_pc = ctx.pc;
- if (ctx.counter >= 0)
- undo.push_var(ctx.counter) = ctx.counter - 1;
- }
- }
- break;
-
- /*$ ";" break
- * [bool]; -- Conditionally break from loop
- * [bool]:;
- *
- * Breaks from the current inner-most loop if <bool>
- * signifies failure (non-negative value).
- * If colon-modified, breaks from the loop if <bool>
- * 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.
- */
- case ';':
- BEGIN_EXEC(this);
-
- if (loop_stack.items() <= loop_stack_fp)
- throw Error("<;> only allowed in iterations");
-
- v = QRegisters::globals["_"]->get_integer();
- rc = expressions.pop_num_calc(0, v);
- if (eval_colon())
- rc = ~rc;
-
- if (IS_FAILURE(rc)) {
- LoopContext ctx = loop_stack.pop();
-
- expressions.discard_args();
- if (!ctx.pass_through)
- expressions.brace_close();
-
- LoopStack::undo_push<loop_stack>(ctx);
-
- /* skip to end of loop */
- undo.push_var(mode) = MODE_PARSE_ONLY_LOOP;
- }
- break;
-
- /*
- * control structures (conditionals)
- */
- case '|':
- if (mode == MODE_PARSE_ONLY_COND) {
- if (!skip_else && !nest_level) {
- undo.push_var<Mode>(mode);
- mode = MODE_NORMAL;
- }
- return this;
- }
- BEGIN_EXEC(this);
-
- /* skip to end of conditional; skip ELSE-part */
- undo.push_var<Mode>(mode);
- mode = MODE_PARSE_ONLY_COND;
- break;
-
- case '\'':
- if (mode != MODE_PARSE_ONLY_COND)
- break;
-
- if (!nest_level) {
- undo.push_var<Mode>(mode);
- mode = MODE_NORMAL;
- undo.push_var<bool>(skip_else);
- skip_else = false;
- } else {
- undo.push_var<gint>(nest_level);
- nest_level--;
- }
- break;
-
- /*
- * Command-line editing
- */
- /*$ "{" "}"
- * { -- 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).
- */
- case '{':
- BEGIN_EXEC(this);
- if (!undo.enabled)
- throw Error("Command-line editing only possible in "
- "interactive mode");
-
- current_doc_undo_edit();
- QRegisters::globals.edit(CTL_KEY_ESC_STR);
-
- interface.ssm(SCI_BEGINUNDOACTION);
- interface.ssm(SCI_CLEARALL);
- interface.ssm(SCI_ADDTEXT, cmdline.pc, (sptr_t)cmdline.str);
- interface.ssm(SCI_SCROLLCARET);
- interface.ssm(SCI_ENDUNDOACTION);
-
- /* must always support undo on global register */
- interface.undo_ssm(SCI_UNDO);
- break;
-
- case '}':
- BEGIN_EXEC(this);
- if (!undo.enabled)
- throw Error("Command-line editing only possible in "
- "interactive mode");
- if (QRegisters::current != QRegisters::globals[CTL_KEY_ESC_STR])
- throw Error("Command-line replacement only allowed when "
- "editing the replacement register");
-
- /* replace cmdline in the outer macro environment */
- cmdline.replace();
- /* never reached */
-
- /*
- * modifiers
- */
- case '@':
- /*
- * @ modifier has syntactic significance, so set it even
- * in PARSE_ONLY* modes
- */
- undo.push_var<bool>(Modifiers::at);
- Modifiers::at = true;
- break;
-
- case ':':
- BEGIN_EXEC(this);
- undo.push_var<bool>(Modifiers::colon);
- Modifiers::colon = true;
- break;
-
- /*
- * commands
- */
- /*$ J jump
- * [position]J -- Go to position in buffer
- * [position]:J -> Success|Failure
- *
- * Sets dot to <position>.
- * If <position> is omitted, 0 is implied and \(lqJ\(rq will
- * go to the beginning of the buffer.
- *
- * If <position> 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.
- */
- case 'J':
- BEGIN_EXEC(this);
- v = expressions.pop_num_calc(0, 0);
- if (Validate::pos(v)) {
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_GOTOPOS,
- interface.ssm(SCI_GETCURRENTPOS));
- interface.ssm(SCI_GOTOPOS, v);
-
- if (eval_colon())
- expressions.push(SUCCESS);
- } else if (eval_colon()) {
- expressions.push(FAILURE);
- } else {
- throw MoveError("J");
- }
- break;
-
- /*$ C move
- * [n]C -- Move dot <n> characters
- * -C
- * [n]:C -> Success|Failure
- *
- * Adds <n> to dot. 1 or -1 is implied if <n> is omitted.
- * Fails if <n> would move dot off-page.
- * The colon modifier results in a success-boolean being
- * returned instead.
- */
- case 'C':
- BEGIN_EXEC(this);
- rc = move_chars(expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw MoveError("C");
- break;
-
- /*$ R reverse
- * [n]R -- Move dot <n> characters backwards
- * -R
- * [n]:R -> Success|Failure
- *
- * Subtracts <n> from dot.
- * It is equivalent to \(lq-nC\(rq.
- */
- case 'R':
- BEGIN_EXEC(this);
- rc = move_chars(-expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw MoveError("R");
- break;
-
- /*$ L line
- * [n]L -- Move dot <n> 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 <n> goes to the
- * beginning of the current line, 1 will go to the
- * next line, -1 to the previous line etc.
- * If <n> is omitted, 1 or -1 is implied depending on
- * the sign prefix.
- *
- * If <n> would move dot off-page, the command yields
- * an error.
- * The colon-modifer results in a condition boolean
- * being returned instead.
- */
- case 'L':
- BEGIN_EXEC(this);
- rc = move_lines(expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw MoveError("L");
- break;
-
- /*$ B backwards
- * [n]B -- Move dot <n> lines backwards
- * -B
- * [n]:B -> Success|Failure
- *
- * Move dot to the beginning of the line <n>
- * lines before the current one.
- * It is equivalent to \(lq-nL\(rq.
- */
- case 'B':
- BEGIN_EXEC(this);
- rc = move_lines(-expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw MoveError("B");
- break;
-
- /*$ W word
- * [n]W -- Move dot by words
- * -W
- * [n]:W -> Success|Failure
- *
- * Move dot <n> words forward.
- * - If <n> is positive, dot is positioned at the beginning
- * of the word <n> words after the current one.
- * - If <n> is negative, dot is positioned at the end
- * of the word <n> words before the current one.
- * - If <n> 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.
- */
- case 'W': {
- sptr_t pos;
- unsigned int msg = SCI_WORDRIGHTEND;
-
- BEGIN_EXEC(this);
- v = expressions.pop_num_calc();
-
- pos = interface.ssm(SCI_GETCURRENTPOS);
- /*
- * 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 (v < 0) {
- v *= -1;
- msg = SCI_WORDLEFTEND;
- }
- while (v--) {
- sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
- interface.ssm(msg);
- if (pos == interface.ssm(SCI_GETCURRENTPOS))
- break;
- }
- if (v < 0) {
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_GOTOPOS, pos);
- if (eval_colon())
- expressions.push(SUCCESS);
- } else {
- interface.ssm(SCI_GOTOPOS, pos);
- if (eval_colon())
- expressions.push(FAILURE);
- else
- throw MoveError("W");
- }
- break;
- }
-
- /*$ V
- * [n]V -- Delete words forward
- * -V
- * [n]:V -> Success|Failure
- *
- * Deletes the next <n> words until the end of the
- * n'th word after the current one.
- * If <n> is negative, deletes up to end of the
- * n'th word before the current one.
- * If <n> 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.
- */
- case 'V':
- BEGIN_EXEC(this);
- rc = delete_words(expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw Error("Not enough words to delete with <V>");
- break;
-
- /*$ Y
- * [n]Y -- Delete word backwards
- * -Y
- * [n]:Y -> Success|Failure
- *
- * Delete <n> words backward.
- * <n>Y is equivalent to \(lq-nV\(rq.
- */
- case 'Y':
- BEGIN_EXEC(this);
- rc = delete_words(-expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw Error("Not enough words to delete with <Y>");
- break;
-
- /*$ "=" print
- * <n>= -- Show value as message
- *
- * Shows integer <n> 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 <n> is not given.
- */
- /**
- * @todo perhaps care about current radix
- * @todo colon-modifier to suppress line-break on console?
- */
- case '=':
- BEGIN_EXEC(this);
- expressions.eval();
- if (!expressions.args())
- throw ArgExpectedError('=');
- interface.msg(InterfaceCurrent::MSG_USER,
- "%" TECO_INTEGER_FORMAT,
- expressions.pop_num_calc());
- break;
-
- /*$ 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 <n> lines after or before the current one.
- * If <n> is 0, \(lqK\(rq will delete up to the beginning
- * of the current line.
- * If <n> 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 <from> and <to> are available, the
- * command is synonymous to <from>,<to>D.
- */
- case 'K':
- /*$ D delete
- * [n]D -- Delete characters
- * -D
- * from,to D
- * [n]:D -> Success|Failure
- * from,to:D -> Success|Failure
- *
- * If <n> is positive, the next <n> characters (up to and
- * character .+<n>) are deleted.
- * If <n> is negative, the previous <n> characters are
- * deleted.
- * If <n> 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 <from> up to <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.
- */
- case 'D': {
- tecoInt from, len;
-
- BEGIN_EXEC(this);
- expressions.eval();
-
- if (expressions.args() <= 1) {
- from = interface.ssm(SCI_GETCURRENTPOS);
- if (chr == 'D') {
- len = expressions.pop_num_calc();
- rc = TECO_BOOL(Validate::pos(from + len));
- } else /* chr == 'K' */ {
- sptr_t line;
- line = interface.ssm(SCI_LINEFROMPOSITION, from) +
- expressions.pop_num_calc();
- len = interface.ssm(SCI_POSITIONFROMLINE, line)
- - from;
- rc = TECO_BOOL(Validate::line(line));
- }
- if (len < 0) {
- len *= -1;
- from -= len;
- }
- } else {
- tecoInt to = expressions.pop_num();
- from = expressions.pop_num();
- len = to - from;
- rc = TECO_BOOL(len >= 0 && Validate::pos(from) &&
- Validate::pos(to));
- }
-
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw RangeError(chr);
-
- if (len == 0 || IS_FAILURE(rc))
- break;
-
- if (current_doc_must_undo()) {
- interface.undo_ssm(SCI_GOTOPOS, interface.ssm(SCI_GETCURRENTPOS));
- interface.undo_ssm(SCI_UNDO);
- }
-
- interface.ssm(SCI_BEGINUNDOACTION);
- interface.ssm(SCI_DELETERANGE, from, len);
- interface.ssm(SCI_ENDUNDOACTION);
- ring.dirtify();
- break;
- }
-
- /*$ A
- * [n]A -> code -- Get character code from buffer
- * -A -> code
- *
- * Returns the character <code> of the character
- * <n> relative to dot from the buffer.
- * This can be an ASCII <code> or Unicode codepoint
- * depending on Scintilla's encoding of the current
- * buffer.
- * - If <n> is 0, return the <code> of the character
- * pointed to by dot.
- * - If <n> is 1, return the <code> of the character
- * immediately after dot.
- * - If <n> is -1, return the <code> of the character
- * immediately preceding dot, ecetera.
- * - If <n> 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??? */
- case 'A':
- BEGIN_EXEC(this);
- v = interface.ssm(SCI_GETCURRENTPOS) +
- expressions.pop_num_calc();
- /*
- * NOTE: We cannot use Validate::pos() here since
- * the end of the buffer is not a valid position for <A>.
- */
- 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>(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 = MODE_PARSE_ONLY_COND;
- undo.push_var<bool>(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 = 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 <directory> 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 <directory> 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<UndoTokenChangeDir>(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 <radix>.
- * If <radix> 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) <n> 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 <c>
- * 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: ^[, <CTRL/[>, <ESC>, $ (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.
- * <bool> may be a specified to enforce closing dirty
- * buffers.
- * If it is a Failure condition boolean (negative),
- * the buffer will be closed unconditionally.
- * If <bool> 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.
- * <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.
- * <off> is a bitmap of flags to disable (set to 0 in ED
- * flags) and <on> is a bitmap of flags that is ORed into
- * the flags variable.
- * If <off> is omitted, the value 0^_ is implied.
- * In otherwords, all flags are turned off before turning
- * on the <on> 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 <EJ>", 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 <EJ>", 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 <EJ>", 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, <EL> 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 <EL>");
- }
- } 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 <EL>",
- 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 <bool> as a success/truth condition
- * boolean, EX will not check whether there are modified
- * buffers and will always succeed.
- * If <bool> 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, <bool> 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 <message>, <wParam> and <lParam>.
- * <wParam> may be symbolic when specified as part of the
- * first string argument.
- * If not it is popped from the stack.
- * <lParam> 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, <lParam> 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 <wParam>,
- * 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("<ES> 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 <n>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.
- * <c1> is inserted before <c2>, ecetera.
- * Secondly, the command inserts <text>.
- * In interactive mode, <text> 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, <CTRL/I>, <TAB>
- */
-/*$ ^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 <text> 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 <text> 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 <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __PARSER_H
-#define __PARSER_H
-
-#include <string.h>
+#pragma once
#include <glib.h>
+#include <Scintilla.h>
+
#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 <typename Type>
-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<gchar *> {
- enum Mode {
- MODE_NORMAL,
- MODE_UPPER,
- MODE_LOWER
- } mode;
-
- bool toctl;
-
-public:
- QRegSpecMachine *qregspec_machine;
+/** @} */
- StringBuildingMachine() : MicroStateMachine<gchar *>(),
- 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<LoopContext> 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 <http://www.gnu.org/licenses/>.
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib.h>
+
+#include <Scintilla.h>
+
+#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 <q> 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 <q> 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 <q>
+ * 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 <file> string argument,
+ * EQ makes <q> the currently edited Q-Register.
+ * Otherwise, when <file> is specified, it is the
+ * name of a file to read into Q-Register <q>.
+ * When loading a file, the currently edited
+ * buffer/register is not changed and the edit position
+ * of register <q> is reset to 0.
+ *
+ * Undefined Q-Registers will be defined.
+ * The command fails if <file> 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<file>$ -- Save Q-Register string to file
+ *
+ * Saves the string contents of Q-Register <q> to
+ * <file>.
+ * The <file> 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 <file>.
+ * 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
+ * <position>Qq -> character
+ * :Qq -> -1 | size
+ *
+ * Without any arguments, get and return the integer-part of
+ * Q-Register <q>.
+ *
+ * With one argument, return the <character> code at <position>
+ * from the string-part of Q-Register <q>.
+ * 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 <q>
+ * to be defined and fail otherwise.
+ *
+ * When colon-modified, Q does not pop any arguments from
+ * the expression stack and returns the <size> of the string
+ * in Q-Register <q> if register <q> exists (i.e. is defined).
+ * Naturally, for empty strings, 0 is returned.
+ * When colon-modified and Q-Register <q> 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 <q> 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 <q>.
+ * If <q> 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 <q>
+ * with all the values on the expression stack (interpreted as
+ * codepoints).
+ * It does so in the order of the arguments, i.e.
+ * <c1> will be the first character in <q>, <c2> the second, etc.
+ * Eventually the <string> argument is appended to the
+ * register.
+ * Any existing string value in <q> is overwritten by this operation.
+ *
+ * In the colon-modified form ^U does not overwrite existing
+ * contents of <q> but only appends to it.
+ *
+ * If <q> 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 <q> into the buffer
+ * at its current position.
+ * Specifying an undefined <q> 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 <q> to <n>.
+ * \(lq-U\(rq is equivalent to \(lq-1U\(rq, otherwise
+ * the command fails if <n> is missing.
+ *
+ * If the command is colon-modified, it returns a success
+ * boolean if <n> or \(lq-\(rq is given.
+ * Otherwise it returns a failure boolean and does not
+ * modify <q>.
+ *
+ * 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 <n> to the integer part of register <q>, returning
+ * its new value.
+ * <q> 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 <q>.
+ * 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 <q> will be copied upon macro execution,
+ * so subsequent changes to Q-Register <q> 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 <file> into memory and execute its contents
+ * as a macro.
+ * It is otherwise similar to the \(lqM\(rq command.
+ *
+ * If <file> 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 <lines> from the buffer
+ * into the Q-Register <q> string.
+ * If <lines> is omitted, the sign prefix is implied.
+ * If two arguments are specified, the characters beginning
+ * at position <from> up to the character at position <to>
+ * 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 <q> instead.
+ *
+ * Register <q> 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 <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#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 <http://www.gnu.org/licenses/>.
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <Scintilla.h>
+
+#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, &macro.data, &macro.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(&reg->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 <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <string.h>
-#include <bsd/sys/queue.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-#include <Scintilla.h>
-
-#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 &reg)
-{
- 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<UndoTokenChangeDir>(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 &reg)
-{
- 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 &reg)
-{
- 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<UndoTokenSetClipboard>(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 &reg)
-{
- 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 &reg)
-{
- 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<UndoTokenRemoveGlobal>(reg);
- else
- undo.push<UndoTokenRemoveLocal>(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 &reg)
-{
- 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<UndoTokenPop>(this);
-}
-
-bool
-QRegisterStack::pop(QRegister &reg)
-{
- 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<UndoTokenPush>(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<QRegister *>::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 <q> 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 <q> 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 <q>
- * 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 <file> string argument,
- * EQ makes <q> the currently edited Q-Register.
- * Otherwise, when <file> is specified, it is the
- * name of a file to read into Q-Register <q>.
- * When loading a file, the currently edited
- * buffer/register is not changed and the edit position
- * of register <q> is reset to 0.
- *
- * Undefined Q-Registers will be defined.
- * The command fails if <file> 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<file>$ -- Save Q-Register string to file
- *
- * Saves the string contents of Q-Register <q> to
- * <file>.
- * The <file> 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 <file>.
- * 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
- * <position>Qq -> character
- * :Qq -> -1 | size
- *
- * Without any arguments, get and return the integer-part of
- * Q-Register <q>.
- *
- * With one argument, return the <character> code at <position>
- * from the string-part of Q-Register <q>.
- * 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 <q>
- * to be defined and fail otherwise.
- *
- * When colon-modified, Q does not pop any arguments from
- * the expression stack and returns the <size> of the string
- * in Q-Register <q> if register <q> exists (i.e. is defined).
- * Naturally, for empty strings, 0 is returned.
- * When colon-modified and Q-Register <q> 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 <q> 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 <q>.
- * If <q> 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 <q>
- * with all the values on the expression stack (interpreted as
- * codepoints).
- * It does so in the order of the arguments, i.e.
- * <c1> will be the first character in <q>, <c2> the second, etc.
- * Eventually the <string> argument is appended to the
- * register.
- * Any existing string value in <q> is overwritten by this operation.
- *
- * In the colon-modified form ^U does not overwrite existing
- * contents of <q> but only appends to it.
- *
- * If <q> 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 <q> into the buffer
- * at its current position.
- * Specifying an undefined <q> 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 <q> to <n>.
- * \(lq-U\(rq is equivalent to \(lq-1U\(rq, otherwise
- * the command fails if <n> is missing.
- *
- * If the command is colon-modified, it returns a success
- * boolean if <n> or \(lq-\(rq is given.
- * Otherwise it returns a failure boolean and does not
- * modify <q>.
- *
- * 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 <n> to the integer part of register <q>, returning
- * its new value.
- * <q> 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 <q>.
- * 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 <q> will be copied upon macro execution,
- * so subsequent changes to Q-Register <q> 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 <file> into memory and execute its contents
- * as a macro.
- * It is otherwise similar to the \(lqM\(rq command.
- *
- * If <file> 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 <lines> from the buffer
- * into the Q-Register <q> string.
- * If <lines> is omitted, the sign prefix is implied.
- * If two arguments are specified, the characters beginning
- * at position <from> up to the character at position <to>
- * 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 <q> instead.
- *
- * Register <q> 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 <http://www.gnu.org/licenses/>.
- */
-
-#ifndef __QREGISTERS_H
-#define __QREGISTERS_H
-
-#include <string.h>
-
-#include <bsd/sys/queue.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#include <Scintilla.h>
-
-#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 &reg)
- {
- string.exchange(reg.string);
- }
- virtual void undo_exchange_string(QRegisterData &reg);
-
- /*
- * 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 &reg);
- void undo_exchange_string(QRegisterData &reg);
-};
-
-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 &reg);
- void undo_exchange_string(QRegisterData &reg);
-};
-
-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 &reg);
- bool pop(QRegister &reg);
-};
-
-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<QRegister *> {
- StringBuildingMachine string_machine;
- QRegSpecType type;
-
- bool is_local;
- gint nesting;
- gchar *name;
-
-public:
- QRegSpecMachine(QRegSpecType _type = QREG_REQUIRED)
- : MicroStateMachine<QRegister *>(),
- 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib.h>
+
+/*
+ * NOTE: Must be included only once.
+ */
+//#include <rb3ptr.h>
+
+#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 <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#include <rb3ptr.h>
+
+#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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <string.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#include "sciteco.h"
-#include "rbtree.h"
-#include "interface.h"
-#include "string-utils.h"
-
-namespace SciTECO {
-
-template <StringCmpFunc StringCmp, StringNCmpFunc StringNCmp>
-gchar *
-RBTreeStringT<StringCmp, StringNCmp>::
-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<strcmp, strncmp>;
-template class RBTreeStringT<g_ascii_strcasecmp, g_ascii_strncasecmp>;
-
-} /* 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 <http://www.gnu.org/licenses/>.
- */
-
-#ifndef __RBTREE_H
-#define __RBTREE_H
-
-#include <string.h>
-
-#include <bsd/sys/tree.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#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 RBEntryType>
-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 <StringCmpFunc StringCmp>
-class RBEntryStringT : public RBTree<RBEntryStringT<StringCmp>>::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 <StringCmpFunc StringCmp, StringNCmpFunc StringNCmp>
-class RBTreeStringT : public RBTree<RBEntryStringT<StringCmp>> {
-public:
- typedef RBEntryStringT<StringCmp> 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<RBEntryString>::find(&entry);
- }
- inline RBEntryString *
- operator [](const gchar *name)
- {
- return find(name);
- }
-
- inline RBEntryString *
- nfind(const gchar *str)
- {
- RBEntryString entry((gchar *)str);
- return RBTree<RBEntryString>::nfind(&entry);
- }
-
- gchar *auto_complete(const gchar *key, gchar completed = '\0',
- gsize restrict_len = 0);
-};
-
-typedef RBTreeStringT<strcmp, strncmp> RBTreeString;
-typedef RBTreeStringT<g_ascii_strcasecmp, g_ascii_strncasecmp> 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<strcmp, strncmp>;
-extern template class RBTreeStringT<g_ascii_strcasecmp, g_ascii_strncasecmp>;
-
-} /* 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include <Scintilla.h>
+
+#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 <EB> "
+ "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 <file>.
+ * If <file> 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.
+ * <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.
+ *
+ * <file> 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).
+ *
+ * <file> does not have to exist on disk.
+ * In this case, an empty buffer is created and its
+ * name is guessed from <file>.
+ * 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 <n> is greater than zero, the string argument
+ * must be empty.
+ * Instead <n> 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 <file> 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 <file>.
+ * Q-Registers have no notion of associated file names,
+ * so <file> 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 <filename> is the name of the saved file and <n> 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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <bsd/sys/queue.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#include <Scintilla.h>
-
-#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<UndoTokenEdit>(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 <file>.
- * If <file> 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.
- * <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.
- *
- * <file> 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).
- *
- * <file> does not have to exist on disk.
- * In this case, an empty buffer is created and its
- * name is guessed from <file>.
- * 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 <n> is greater than zero, the string argument
- * must be empty.
- * Instead <n> 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 <EB> "
- "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 <file> 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 <file>.
- * Q-Registers have no notion of associated file names,
- * so <file> 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 <filename> is the name of the saved file and <n> 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 <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __RING_H
-#define __RING_H
-
-#include <bsd/sys/queue.h>
+#pragma once
#include <glib.h>
-#include <Scintilla.h>
-
#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<UndoTokenClose>(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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <glib.h>
+
+#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,
+ "<ES> 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 <message>, <wParam> and <lParam>.
+ * <wParam> may be symbolic when specified as part of the
+ * first string argument.
+ * If not it is popped from the stack.
+ * <lParam> 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, <lParam> 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 <wParam>,
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#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 <http://www.gnu.org/licenses/>.
*/
+#pragma once
-#ifndef __SCITECO_H
-#define __SCITECO_H
-
+#include <stdio.h>
#include <signal.h>
#include <glib.h>
-#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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#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 <CTRL/S> */
+ 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, &reg, 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 <CTRL/X> */
+ 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 <pattern> 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 <n> perform backward searches.
+ * If missing, the sign prefix is implied for <n>.
+ * Therefore \(lq-S\(rq will search for the first occurrence
+ * of <pattern> before dot.
+ *
+ * If two arguments are specified on the command,
+ * search will be bounded in the character range <from> up to
+ * <to>, and only the first occurrence will be searched.
+ * <from> might be larger than <to> in which case a backward
+ * search is performed in the selected range.
+ *
+ * After performing the search, the search <pattern> is saved
+ * in the global search Q-Register \(lq_\(rq.
+ * A success/failure condition boolean is saved in that
+ * register's integer part.
+ * <pattern> 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
+ * <Sfoo$; ...>
+ * .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 <pattern> 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 <pattern> 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 <pattern> 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 <from>, up to the buffer with number
+ * <to>.
+ * When specifying buffer ranges, the entire buffers are searched
+ * from beginning to end.
+ * <from> may be greater than <to> in which case, searching starts
+ * at the end of buffer <from> and continues backwards until the
+ * beginning of buffer <to> 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 <pattern> 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 <pattern> 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 <pattern> 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 <pattern> just like the regular search command
+ * (S) does but replace it with <string> if found.
+ * If <string> 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 <pattern> just like the regular search
+ * command (S) and replaces the occurrence with <string>
+ * 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 <string> 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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <string.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#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 <pattern> 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 <n> perform backward searches.
- * If missing, the sign prefix is implied for <n>.
- * Therefore \(lq-S\(rq will search for the first occurrence
- * of <pattern> before dot.
- *
- * If two arguments are specified on the command,
- * search will be bounded in the character range <from> up to
- * <to>, and only the first occurrence will be searched.
- * <from> might be larger than <to> in which case a backward
- * search is performed in the selected range.
- *
- * After performing the search, the search <pattern> is saved
- * in the global search Q-Register \(lq_\(rq.
- * A success/failure condition boolean is saved in that
- * register's integer part.
- * <pattern> 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
- * <Sfoo$; ...>
- * .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 <pattern> 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 <CTRL/S> */
- 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 <CTRL/X> */
- 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 <pattern> 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 <pattern> 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 <from>, up to the buffer with number
- * <to>.
- * When specifying buffer ranges, the entire buffers are searched
- * from beginning to end.
- * <from> may be greater than <to> in which case, searching starts
- * at the end of buffer <from> and continues backwards until the
- * beginning of buffer <to> 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 <pattern> 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 <pattern> 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 <pattern> 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 <pattern> just like the regular search command
- * (S) does but replace it with <string> if found.
- * If <string> 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 <pattern> just like the regular search
- * command (S) and replaces the occurrence with <string>
- * 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 <string> 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 <http://www.gnu.org/licenses/>.
*/
+#pragma once
-#ifndef __SEARCH_H
-#define __SEARCH_H
-
-#include <glib.h>
-
-#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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib.h>
+
+#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 <lines> are piped from the buffer into the program and
+ * its output replaces these <lines>.
+ * This effectively runs <command> as a filter over <lines>.
+ * \(lq-EC\(rq may be written as a short-cut for \(lq-1EC\(rq.
+ * When invoked with two parameters, the characters beginning
+ * at position <from> up to the character at position <to>
+ * are piped into the program and replaced with its output.
+ * This effectively runs <command> 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 <command> 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.
+ *
+ * <command> execution is by default platform-dependent.
+ * On DOS-like systems like Windows, <command> 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, <command> 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 <command> and
+ * quoting of parameters within <command> is somewhat platform
+ * dependent.
+ * On all other platforms, \*(ST will uniformly parse
+ * <command> just as an UNIX98 \(lq/bin/sh\(rq would, but without
+ * performing any expansions.
+ * The program specified in <command> 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 <command> might have had on your system.
+ *
+ * Note also that the EC command blocks indefinitely until
+ * the <command> 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 <command> 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 <command> and set Q-Register
+ * <q> to the data read from its standard output stream.
+ * Data may be fed to <command> from the current buffer/document.
+ * The interpretation of the parameters and <command> 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 <q>.
+ * In other words, the current buffer is not modified by EG.
+ * Also since EG replaces the string value of <q>, the register's
+ * EOL mode is set to the mode guessed from the external program's
+ * output.
+ *
+ * The register <q> 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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <glib.h>
-
-#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 <sys/types.h>
-#include <sys/wait.h>
-
-#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 <lines> are piped from the buffer into the program and
- * its output replaces these <lines>.
- * This effectively runs <command> as a filter over <lines>.
- * \(lq-EC\(rq may be written as a short-cut for \(lq-1EC\(rq.
- * When invoked with two parameters, the characters beginning
- * at position <from> up to the character at position <to>
- * are piped into the program and replaced with its output.
- * This effectively runs <command> 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 <command> 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.
- *
- * <command> execution is by default platform-dependent.
- * On DOS-like systems like Windows, <command> 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, <command> 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 <command> and
- * quoting of parameters within <command> is somewhat platform
- * dependent.
- * On all other platforms, \*(ST will uniformly parse
- * <command> just as an UNIX98 \(lq/bin/sh\(rq would, but without
- * performing any expansions.
- * The program specified in <command> 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 <command> might have had on your system.
- *
- * Note also that the EC command blocks indefinitely until
- * the <command> 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 <command> 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 <command> and set Q-Register
- * <q> to the data read from its standard output stream.
- * Data may be fed to <command> from the current buffer/document.
- * The interpretation of the parameters and <command> 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 <q>.
- * In other words, the current buffer is not modified by EG.
- * Also since EG replaces the string value of <q>, the register's
- * EOL mode is set to the mode guessed from the external program's
- * output.
- *
- * The register <q> 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 <http://www.gnu.org/licenses/>.
*/
+#pragma once
-#ifndef __SPAWN_H
-#define __SPAWN_H
-
-#include <glib.h>
-
-#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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib.h>
+
+#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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <glib.h>
-
-#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 <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __STRING_UTILS_H
-#define __STRING_UTILS_H
+#pragma once
#include <string.h>
#include <glib.h>
-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.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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <string.h>
-
-#include <glib.h>
-
-#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 <http://www.gnu.org/licenses/>.
- */
-
-#ifndef __SYMBOLS_H
-#define __SYMBOLS_H
-
-#include <string.h>
-#include <glib.h>
-
-#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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <stdio.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#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 <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <stdio.h>
-#include <bsd/sys/queue.h>
-
-#include <glib.h>
-#include <glib/gstdio.h>
-
-#include <Scintilla.h>
-
-#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 <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __UNDO_H
-#define __UNDO_H
-
-#include <string.h>
-
-#include <bsd/sys/queue.h>
+#pragma once
#include <glib.h>
-#include <glib/gprintf.h>
-#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 <typename Type>
-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 Type>
-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 <class TokenType, typename... Params>
- 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 <class TokenType, typename... Params>
- 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 <typename Type>
- inline Type &
- push_var(Type &variable, Type value)
- {
- push<UndoTokenVariable<Type>>(variable, value);
- return variable;
- }
+TECO_DECLARE_UNDO_SCALAR(gsize);
+#define teco_undo_gsize(VAR) (*teco_undo_object_gsize_push(&(VAR)))
- template <typename Type>
- inline Type &
- push_var(Type &variable)
- {
- return push_var<Type>(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<UndoTokenString>(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 <class Type>
- inline Type *&
- push_obj(Type *&variable, Type *obj)
- {
- /* pass ownership of original object */
- push_own<UndoTokenObject<Type>>(variable, obj);
- return variable;
- }
+TECO_DECLARE_UNDO_SCALAR(gconstpointer);
+#define teco_undo_ptr(VAR) \
+ (*(typeof(VAR) *)teco_undo_object_gconstpointer_push((gconstpointer *)&(VAR)))
- template <class Type>
- inline Type *&
- push_obj(Type *&variable)
- {
- return push_obj<Type>(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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/stat.h>
+
+#ifdef HAVE_WINDOWS_H
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#endif
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include <Scintilla.h>
+
+#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 <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#include <Scintilla.h>
+
+#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{>} <Ma'], 1, ignore, ignore)
AT_CLEANUP
+AT_SETUP([String arguments])
+AT_CHECK([$SCITECO -e $'Ifoo^Q\e(0/0)\e'], 0, ignore, ignore)
+AT_CHECK([$SCITECO -e '@I"foo^Q"(0/0)"'], 0, ignore, ignore)
+AT_CHECK([$SCITECO -e '@I{foo{bar}foo^Q{(0/0)}'], 0, ignore, ignore)
+AT_CHECK([$SCITECO -e '@Ia^EQa(0/0)a'], 0, ignore, ignore)
+# TODO: String building characters
+AT_CLEANUP
+
+AT_SETUP([Q-Register definitions])
+AT_CHECK([$SCITECO -e '0Ua'], 0, ignore, ignore)
+AT_CHECK([$SCITECO -e '0U.a'], 0, ignore, ignore)
+AT_CHECK([$SCITECO -e '0U#ab'], 0, ignore, ignore)
+AT_CHECK([$SCITECO -e '0U.#ab'], 0, ignore, ignore)
+AT_CHECK([$SCITECO -e '0U[[AB]]'], 0, ignore, ignore)
+AT_CHECK([$SCITECO -e '0U.[[AB]]'], 0, ignore, ignore)
+# TODO: String building in Q-Register definitions
+# TODO: Escaping of braces.
+# Unfortunately, braces are significant for M4. Perhaps use $'...'.
+AT_CLEANUP
+
+AT_SETUP([8-bit cleanlyness])
+AT_CHECK([$SCITECO -e "0:@EUa/f^@^@/ :Qa-4\"N(0/0)' GaZ-4\"N(0/0)'"], 0, ignore, ignore)
+AT_CLEANUP
+
AT_SETUP([Automatic EOL normalization])
AT_CHECK([$SCITECO -e "@EB'${srcdir}/autoeol-input.txt' EL-2\"N(0/0)' 2LR 13@I'' 0EL @EW'autoeol-sciteco.txt'"],
0, ignore, ignore)
AT_CHECK([cmp autoeol-sciteco.txt ${srcdir}/autoeol-output.txt], 0, ignore, ignore)
AT_CLEANUP
+AT_BANNER([Regression Tests])
+
AT_SETUP([Glob patterns with character classes])
# Also checks closing brackets as part of the character set.
# NOTE: The worse-than-average escaping of the square brackets with
@@ -32,3 +60,34 @@ AT_CLEANUP
AT_SETUP([Glob patterns with unclosed trailing brackets])
AT_CHECK([$SCITECO -e "91U< :@EN/*.^EU<h/foo.^EU<h/\"F(0/0)'"], 0, ignore, ignore)
AT_CLEANUP
+
+AT_SETUP([Searching with large counts])
+# Even though the search will be unsuccessful, it will not be considered
+# a proper error, so the process return code is still 0.
+AT_CHECK([$SCITECO -e "2147483647@S/foo/"], 0, ignore, ignore)
+# NOTE: In case of crashes, the return code should be >= 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