diff options
author | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2016-08-16 05:04:54 +0200 |
---|---|---|
committer | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2016-08-19 03:29:11 +0200 |
commit | 61ff6e97c57f62ee3ad4ffc2166e433bc060e7cb (patch) | |
tree | 64c032ebdd040809d1b7e822e627e199a9c2476e | |
parent | 94b041ec331427fd63cdae3e943efe825d1bbf14 (diff) | |
download | sciteco-61ff6e97c57f62ee3ad4ffc2166e433bc060e7cb.tar.gz |
Integrated clipboard support
* mapped to different registers beginning with "~"
* on supported platforms accessing the clipboard is as easy as
X~ or G~.
Naturally this also allows clipboards to be pasted in
string arguments/insertions (^EQ~).
* Currently, Gtk+, PDCurses and ncurses/XTerm are supported.
For XTerm clipboard support, users must set 0,256ED to enable
it since we cannot check for XTerm window ops programmatically
(at least without libX11).
* When clipboard regs exist, the clipboard can also be deemed functional.
This allows macros to fall back to xclip(1) if necessary.
* EOL handling has been moved into a new file eol.c and eol.h.
EOL translation no longer depends on GIOChannels but can be
memory-backed as well.
-rw-r--r-- | TODO | 22 | ||||
-rw-r--r-- | doc/sciteco.7.template | 47 | ||||
-rw-r--r-- | sample.teco_ini | 3 | ||||
-rw-r--r-- | src/Makefile.am | 1 | ||||
-rw-r--r-- | src/eol.cpp | 360 | ||||
-rw-r--r-- | src/eol.h | 160 | ||||
-rw-r--r-- | src/error.h | 2 | ||||
-rw-r--r-- | src/interface-curses/interface-curses.cpp | 364 | ||||
-rw-r--r-- | src/interface-curses/interface-curses.h | 8 | ||||
-rw-r--r-- | src/interface-gtk/interface-gtk.cpp | 74 | ||||
-rw-r--r-- | src/interface-gtk/interface-gtk.h | 6 | ||||
-rw-r--r-- | src/interface.h | 16 | ||||
-rw-r--r-- | src/ioview.cpp | 353 | ||||
-rw-r--r-- | src/ioview.h | 14 | ||||
-rw-r--r-- | src/parser.cpp | 4 | ||||
-rw-r--r-- | src/qregisters.cpp | 187 | ||||
-rw-r--r-- | src/qregisters.h | 79 | ||||
-rw-r--r-- | src/sciteco.h | 9 | ||||
-rw-r--r-- | src/spawn.cpp | 180 | ||||
-rw-r--r-- | src/spawn.h | 12 |
20 files changed, 1460 insertions, 441 deletions
@@ -29,6 +29,13 @@ Known Bugs: We should fall back silently to an (inefficient) memory copy or temporary file strategy if this is detected. * crashes on large files: S^EM^X$ (regexp: .*) + 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. * the glib allocators are fundamentally broken: throwing exceptions is unsafe from C-linkage callbacks. We should abandon the custom allocators and rely on @@ -43,6 +50,13 @@ Known Bugs: savepoint restoration. On Windows we could work around this using MoveFileEx(file, NULL, MOVEFILE_DELAY_UNTIL_REBOOT) + * 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. Features: * :$ and :$$ to pop/return only single values @@ -172,14 +186,6 @@ Features: Each Scintilla view could then be associated with at most one curses screen. GTK+ would simply manage a list of windows. - * Add a command for manipulating the clipboard. - Can be done with ECxclip$ on Unix, but other platforms have - better methods (e.g. PDCurses has clipboard extensions, GTK+ - has native clipboard support). On ncurses/UNIX we can fall back - to spawning xclip or copy xclip's functionality. - Unfortunately, not every UNIX has X11 (e.g. linux console, - OS X). What do we do on those systems? - This will also allow us to reverse clipboard modifications. Optimizations: * Add G_UNLIKELY to all error throws. diff --git a/doc/sciteco.7.template b/doc/sciteco.7.template index 9b31c7d..6ed89db 100644 --- a/doc/sciteco.7.template +++ b/doc/sciteco.7.template @@ -1267,6 +1267,53 @@ Their numeric parts are currently unused. The mechanisms involved are documented more elaborately in .BR sciteco (1). .TP +.BI ~ clipboard +These registers constitute \*(ST's support for system clipboards. +Clipboard support is highly UI-specific, so different +UIs might support different clipboards (or X11 selections) +or no clipboard at all. +\*(ST thus initializes registers beginning with \(lq~\(rq for +every available clipboard either on startup or only when +entering interactive mode. +The register \(lq~\(rq refers to the default clipboard which +will always exist if clipboards are supported. +Other commonly used clipboard registers are \(lq~P\(rq for the +primary selection, \(lq~S\(rq for the secondary selection and +\(lq~C\(rq for the clipboard selection. +The existence of a clipboard register can thus be checked +in macros to determine whether getting and modifying that +particular clipboard is supported natively. +.br +\*(ST does \fBnot\fP generally support clipboards on ncurses, +but has special support when used with a sufficiently recent version +of \fBxterm\fP(1). +Since the operability of XTerm clipboards cannot be tested +automatically, users will have to set the flag 256 of the +\fBED\fP flags if and only if their XTerm is configured for allowing +the \fISetSelection\fP and \fIGetSelection\fP window operations. +\*(ST will still check whether XTerm is actually used in +a particular session. +If native clipboard support is unavailable, users may +still fall back to using external tools like \fBxclip\fP(1) +with the \fBEC\fP command. +.br +Setting the string part of a clipboard register will set that +clipboard. \*(ST will perform automatic EOL-translation according +to the EOL-mode of the Q-Register view, so e.g. on Windows +clipboards will usually be set with the expected DOS linebreaks. +Appending to clipboard registers is currently not supported. +Furthermore, \*(ST will restore the contents of the clipboard +at the time of the operation when an assignment is rubbed out in +interactive mode. +When retrieving a clipboard register, the contents of the +clipboard at the time of the operation are returned. +EOL normalization will take place (if enabled), so that pasting +clipboards does not introduce unexpected EOL sequences. +The Q-Register view's EOL mode will \fBnot\fP be guessed from +the original clipboard contents, though. +The numeric parts of the clipboard registers are currently +not used by \*(ST. +.TP .BI ^F key Function key registers as documented in section \fBKEY TRANSLATION\fP. diff --git a/sample.teco_ini b/sample.teco_ini index fb02d34..6360d14 100644 --- a/sample.teco_ini +++ b/sample.teco_ini @@ -46,6 +46,9 @@ EMQ[$SCITECOPATH]/session.tes ! Uncomment to enable default keyboard macros and function keys ! ! EMQ[$SCITECOPATH]/fnkeys.tes ! +! Uncomment if XTerm allows clipboard operations ! +! 0,256ED ! + ! Uncomment to tweak the undo stack memory limit ! ! 500*1024*1024,2EJ ! diff --git a/src/Makefile.am b/src/Makefile.am index 6da8d84..515accd 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -34,6 +34,7 @@ libsciteco_base_la_SOURCES = main.cpp sciteco.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 \ diff --git a/src/eol.cpp b/src/eol.cpp new file mode 100644 index 0000000..3b42547 --- /dev/null +++ b/src/eol.cpp @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2012-2016 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. + * @parem buffer_len The length of the data in buffer. + * @return The number of bytes written to the data sink, + * i.e. the size of the EOL-normalized data written. + */ +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 new file mode 100644 index 0000000..81ed4ef --- /dev/null +++ b/src/eol.h @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2012-2016 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 __EOL_H +#define __EOL_H + +#include <string.h> + +#include <glib.h> + +#include "sciteco.h" + +namespace SciTECO { + +class EOLReader { + gchar *buffer; + 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; +}; + +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(); + } +}; + +class EOLReaderMem : public EOLReader { + gsize buffer_len; + + bool read(gchar *buffer, gsize &read_len); + +public: + EOLReaderMem(gchar *buffer, gsize _buffer_len) + : EOLReader(buffer), buffer_len(_buffer_len) {} + + gchar *convert_all(gsize *out_len = NULL); +}; + +class EOLWriter { + enum { + STATE_START = 0, + 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() {} + + gsize convert(const gchar *buffer, gsize buffer_len); + +protected: + virtual gsize write(const gchar *buffer, gsize buffer_len) = 0; +}; + +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(); + } +}; + +class EOLWriterMem : public EOLWriter { + GString *str; + + gsize write(const gchar *buffer, gsize buffer_len); + +public: + EOLWriterMem(GString *_str, gint eol_mode) + : EOLWriter(eol_mode), str(_str) {} +}; + +} /* namespace SciTECO */ + +#endif diff --git a/src/error.h b/src/error.h index f4dbfbf..62063ff 100644 --- a/src/error.h +++ b/src/error.h @@ -47,10 +47,10 @@ public: }; class Error { - gchar *description; GSList *frames; public: + gchar *description; gint pos; gint line, column; diff --git a/src/interface-curses/interface-curses.cpp b/src/interface-curses/interface-curses.cpp index fa1cc97..d592b89 100644 --- a/src/interface-curses/interface-curses.cpp +++ b/src/interface-curses/interface-curses.cpp @@ -54,6 +54,7 @@ #include "cmdline.h" #include "qregisters.h" #include "ring.h" +#include "error.h" #include "interface.h" #include "interface-curses.h" #include "curses-utils.h" @@ -158,6 +159,8 @@ console_ctrl_handler(DWORD type) } /* extern "C" */ +static gint xterm_version(void) G_GNUC_UNUSED; + #define UNNAMED_FILE "(Unnamed)" /** @@ -245,6 +248,46 @@ rgb2curses(guint32 rgb) return COLOR_WHITE; } +static gint +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) + return xterm_patch; + xterm_patch = -1; + + if (!term || !g_str_has_prefix(term, "xterm")) + /* no XTerm */ + return -1; + + /* + * Terminal might claim to be XTerm-compatible, + * but this only refers to the terminfo database. + * XTERM_VERSION however should be sufficient to tell + * whether we are running under a real XTerm. + */ + xterm_version = g_getenv("XTERM_VERSION"); + if (!xterm_version) + /* no XTerm */ + return -1; + xterm_patch = 0; + + xterm_version = strrchr(xterm_version, '('); + if (!xterm_version) + /* Invalid XTERM_VERSION, assume some XTerm */ + return 0; + + xterm_patch = atoi(xterm_version+1); + return xterm_patch; +} + void ViewCurses::initialize_impl(void) { @@ -286,6 +329,14 @@ InterfaceCurses::main_impl(int &argc, char **&argv) * even if info_update() is never called. */ info_current = g_strdup(PACKAGE_NAME); + + /* + * On all platforms except NCurses/XTerm, it's + * safe to initialize the clipboards now. + */ +#ifndef NCURSES_UNIX + init_clipboard(); +#endif } void @@ -341,7 +392,7 @@ InterfaceCurses::restore_colors(void) * will return bogus "default" values and only for the first 8 colors. * It would do more damage to restore the palette returned by * color_content() than it helps. - * xterm has the escape sequence "\e]104\x07" which restores + * xterm has the escape sequence "\e]104\a" which restores * the palette from Xdefaults but not all terminal emulators * claiming to be "xterm" via $TERM support this escape sequence. * lxterminal for instance will print gibberish instead. @@ -359,15 +410,14 @@ InterfaceCurses::restore_colors(void) void InterfaceCurses::restore_colors(void) { - if (g_str_has_prefix(g_getenv("TERM") ? : "", "xterm") && - g_getenv("XTERM_VERSION")) { - /* - * Looks like a real xterm. $TERM alone is not - * sufficient to tell. - */ - fputs("\e]104\x07", screen_tty); - fflush(screen_tty); - } + if (xterm_version() < 0) + return; + + /* + * Looks like a real XTerm + */ + fputs("\e]104\a", screen_tty); + fflush(screen_tty); } #else /* !PDCURSES_WIN32 && !NCURSES_UNIX */ @@ -537,8 +587,6 @@ InterfaceCurses::init_interactive(void) /* * Disable all magic function keys. - * NOTE: This could also be used to assign - * a "shutdown" key when program termination is requested. */ for (int i = 0; i < N_FUNCTION_KEYS; i++) PDC_set_function_key(i, 0); @@ -591,6 +639,16 @@ InterfaceCurses::init_interactive(void) init_color_safe(i, (guint32)color_table[i]); } } + + /* + * Only now (in interactive mode), it's safe to initialize + * the clipboard Q-Registers on ncurses with a compatible terminal + * emulator since clipboard operations will no longer interfer + * with stdout. + */ +#ifdef NCURSES_UNIX + init_clipboard(); +#endif } void @@ -983,6 +1041,288 @@ InterfaceCurses::draw_cmdline(void) 0, disp_offset, 0, 1, 0, disp_len, FALSE); } +#ifdef __PDCURSES__ + +/* + * At least on PDCurses, a single clipboard + * can be supported. We register it as the + * default clipboard ("~") as we do not know whether + * it corresponds to the X11 PRIMARY, SECONDARY or + * CLIPBOARD selections. + */ +void +InterfaceCurses::init_clipboard(void) +{ + char *contents; + long length; + int rc; + + /* + * Even on PDCurses, while the clipboard functions are + * available, the clipboard might not actually be supported. + * Since the existence of the QReg serves as an indication + * of clipboard support in SciTECO, we must first probe the + * usability of the clipboard. + * This could be done at compile time, but this way is more + * generic (albeit inefficient). + */ + 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()); +} + +void +InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len) +{ + int rc; + + if (str) { + if (str_len < 0) + str_len = strlen(str); + + rc = PDC_setclipboard(str, str_len); + } else { + rc = PDC_clearclipboard(); + } + + if (rc != PDC_CLIP_SUCCESS) + throw Error("Error %d copying to clipboard", rc); +} + +gchar * +InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len) +{ + 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; + if (rc == PDC_CLIP_EMPTY) + return NULL; + if (rc != PDC_CLIP_SUCCESS) + throw Error("Error %d retrieving clipboard", rc); + + /* + * 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). + */ + str = g_malloc(length + 1); + memcpy(str, contents, length); + str[length] = '\0'; + + PDC_freeclipboard(contents); + return str; +} + +#elif defined(NCURSES_UNIX) + +void +InterfaceCurses::init_clipboard(void) +{ + /* + * At least on XTerm, there are escape sequences + * for modifying the clipboard (OSC-52). + * This is not standardized in terminfo, so we add special + * XTerm support here. Unfortunately, it is pretty hard to find out + * whether clipboard operations will actually work. + * XTerm must be at least at v203 and the corresponding window operations + * must be enabled. + * There is no way to find out if they are but we must + * 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) + return; + + QRegisters::globals.insert(new QRegisterClipboard()); + QRegisters::globals.insert(new QRegisterClipboard("P")); + QRegisters::globals.insert(new QRegisterClipboard("S")); + QRegisters::globals.insert(new QRegisterClipboard("C")); +} + +static inline gchar +get_selection_by_name(const gchar *name) +{ + /* + * Only the first letter of name is significant. + * We allow to address the XTerm cut buffers as well + * (everything gets passed down), but currently we + * only register the three standard registers + * "~", "~P", "~S" and "~C". + */ + return g_ascii_tolower(*name) ? : 'c'; +} + +void +InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len) +{ + /* + * 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("\e]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); + + while (str_len > 0) { + gsize step_len = MIN(1024, str_len); + + /* + * This could be simplified using g_base64_encode(). + * However, doing it step-wise avoids an allocation. + */ + out_len = g_base64_encode_step((const guchar *)str, + step_len, FALSE, + buffer, &state, &save); + fwrite(buffer, 1, out_len, 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); + + fputc('\a', screen_tty); + fflush(screen_tty); +} + +gchar * +InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len) +{ + /* + * 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("\e]52;", screen_tty); + fputc(get_selection_by_name(name), screen_tty); + fputs(";?\a", screen_tty); + fflush(screen_tty); + + /* + * It is very well possible that the XTerm clipboard + * is not working because it is disabled, so we + * must be prepared for timeouts when reading. + * That's why we're using the Curses API here, instead + * of accessing screen_tty directly. It gives us a relatively + * simple way to read with timeouts. + * We restore all changed Curses settings before returning + * to be on the safe side. + */ + halfdelay(1); /* 100ms timeout */ + keypad(stdscr, FALSE); + + /* + * Skip "\e]52;x;" (7 characters). + */ + for (gint i = 0; i < 7; i++) { + if (getch() == ERR) { + /* timeout */ + cbreak(); + throw Error("Timed out reading XTerm clipboard"); + } + } + + str_base64 = g_string_new(""); + + for (;;) { + gchar c; + gsize out_len; + + c = (gchar)getch(); + if (c == ERR) { + /* timeout */ + cbreak(); + g_string_free(str_base64, TRUE); + throw Error("Timed out reading XTerm clipboard"); + } + if (c == '\a') + break; + + /* + * 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 + * read character-wise using getch() anyway.) + */ + 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 the clipboard answer is empty, return NULL. + */ + return g_string_free(str_base64, str_base64->len == 0); +} + +#else + +void +InterfaceCurses::init_clipboard(void) +{ + /* + * No native clipboard support, so no clipboard Q-Regs are + * registered. + */ +} + +void +InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len) +{ + throw Error("Setting clipboard unsupported"); +} + +gchar * +InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len) +{ + throw Error("Getting clipboard unsupported"); +} + +#endif /* !__PDCURSES__ && !NCURSES_UNIX */ + void InterfaceCurses::popup_show_impl(void) { diff --git a/src/interface-curses/interface-curses.h b/src/interface-curses/interface-curses.h index f29b1b4..d036d37 100644 --- a/src/interface-curses/interface-curses.h +++ b/src/interface-curses/interface-curses.h @@ -142,6 +142,12 @@ public: /* 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, @@ -177,6 +183,8 @@ private: void init_interactive(void); void restore_batch(void); + void init_clipboard(void); + void resize_all_windows(void); void set_window_title(const gchar *title); diff --git a/src/interface-gtk/interface-gtk.cpp b/src/interface-gtk/interface-gtk.cpp index 49c3154..4ef0b38 100644 --- a/src/interface-gtk/interface-gtk.cpp +++ b/src/interface-gtk/interface-gtk.cpp @@ -211,6 +211,17 @@ InterfaceGtk::main_impl(int &argc, char **&argv) gtk_init(&argc, &argv); /* + * 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. */ @@ -381,7 +392,7 @@ void InterfaceGtk::refresh_info(void) { GtkStyleContext *style = gtk_widget_get_style_context(info_bar_widget); - const gchar *info_type_str; + const gchar *info_type_str = PACKAGE; gchar *info_current_temp = g_strdup(info_current); gchar *info_current_canon; GIcon *icon; @@ -523,6 +534,67 @@ InterfaceGtk::cmdline_update_impl(const Cmdline *cmdline) 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) diff --git a/src/interface-gtk/interface-gtk.h b/src/interface-gtk/interface-gtk.h index 80692f9..0145ff4 100644 --- a/src/interface-gtk/interface-gtk.h +++ b/src/interface-gtk/interface-gtk.h @@ -131,6 +131,12 @@ public: /* 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); diff --git a/src/interface.h b/src/interface.h index 84182ea..98254ac 100644 --- a/src/interface.h +++ b/src/interface.h @@ -26,6 +26,7 @@ #include <Scintilla.h> #include "undo.h" +#include "error.h" namespace SciTECO { @@ -287,6 +288,21 @@ public: 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, diff --git a/src/ioview.cpp b/src/ioview.cpp index 1643a29..a5685fe 100644 --- a/src/ioview.cpp +++ b/src/ioview.cpp @@ -37,6 +37,7 @@ #include "undo.h" #include "error.h" #include "qregisters.h" +#include "eol.h" #include "ioview.h" #ifdef HAVE_WINDOWS_H @@ -86,173 +87,6 @@ set_file_attributes(const gchar *filename, FileAttributes attrs) #endif /* !G_OS_WIN32 */ /** - * A wrapper around g_io_channel_read_chars() that also - * performs automatic EOL translation (if enabled) in a - * more or less efficient manner. - * Unlike g_io_channel_read_chars(), this returns an - * offset and length into the buffer with normalized - * EOL character. - * The function must therefore be called iteratively on - * on the same buffer while it returns G_IO_STATUS_NORMAL. - * - * @param channel The GIOChannel to read from. - ' @param buffer Used to store blocks. - * @param buffer_len Size of buffer. - * @param read_len Total number of bytes read into buffer. - * Must be provided over the lifetime of buffer - * and initialized with 0. - * @param offset If a block could be read (G_IO_STATUS_NORMAL), - * this will be set to indicate its beginning in - * buffer. Should be initialized to 0. - * @param block_len Will be set to the block length. - * Should be initialized to 0. - * @param state Opaque state that must persist for the lifetime - * of the channel. Must be initialized with 0. - * @param eol_style Will be set to the EOL style guessed from - * the data in channel (if the data allows it). - * Should be initialized with -1 (unknown). - * @param eol_style_inconsistent Will be set to TRUE if - * inconsistent EOL styles are detected. - * @param error If not NULL and an error occurred, it is set to - * the error. It should be initialized to -1. - * @return A GIOStatus as returned by g_io_channel_read_chars() - */ -GIOStatus -IOView::channel_read_with_eol(GIOChannel *channel, - gchar *buffer, gsize buffer_len, - gsize &read_len, - guint &offset, gsize &block_len, - gint &state, gint &eol_style, - gboolean &eol_style_inconsistent, - GError **error) -{ - GIOStatus status; - - if (state < 0) { - /* a CRLF was last translated */ - block_len++; - state = '\n'; - } - offset += block_len; - - if (offset == read_len) { - offset = 0; - - status = g_io_channel_read_chars(channel, buffer, buffer_len, - &read_len, error); - if (status == G_IO_STATUS_EOF && state == '\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; - } - if (status != G_IO_STATUS_NORMAL) - return status; - - if (!(Flags::ed & Flags::ED_AUTOEOL)) { - /* - * No EOL translation - always return entire - * buffer - */ - block_len = read_len; - 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 = offset; i < read_len; i++) { - switch (buffer[i]) { - case '\n': - if (state == '\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`. - */ - block_len = i-offset; - /* next call will skip the CR */ - state = -1; - return G_IO_STATUS_NORMAL; - } - - 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. - */ - state = '\n'; - break; - - case '\r': - if (state == '\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'; - state = '\r'; - break; - - default: - if (state == '\r') { - if (eol_style < 0) - eol_style = SC_EOL_CR; - else if (eol_style != SC_EOL_CR) - eol_style_inconsistent = TRUE; - } - state = buffer[i]; - break; - } - } - - /* - * Return remaining block. - * With UNIX/MAC EOLs, this will usually be the - * entire `buffer` - */ - block_len = read_len-offset; - return G_IO_STATUS_NORMAL; -} - -/** * Loads the view's document by reading all data from * a GIOChannel. * The EOL style is guessed from the channel's data @@ -261,23 +95,17 @@ IOView::channel_read_with_eol(GIOChannel *channel, * 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. - * @param error Glib error or NULL. - * @returns A GIOStatus as returned by g_io_channel_read_chars() */ -GIOStatus -IOView::load(GIOChannel *channel, GError **error) +void +IOView::load(GIOChannel *channel) { - GIOStatus status; GStatBuf stat_buf; - gchar buffer[1024]; - gsize read_len = 0; - guint offset = 0; - gsize block_len = 0; - gint state = 0; /* opaque state */ - gint eol_style = -1; /* yet unknown */ - gboolean eol_style_inconsistent = FALSE; + EOLReaderGIO reader(channel); ssm(SCI_BEGINUNDOACTION); ssm(SCI_CLEARALL); @@ -294,17 +122,15 @@ IOView::load(GIOChannel *channel, GError **error) stat_buf.st_size > 0) ssm(SCI_ALLOCATE, stat_buf.st_size); - for (;;) { - status = channel_read_with_eol( - channel, buffer, sizeof(buffer), - read_len, offset, block_len, state, - eol_style, eol_style_inconsistent, - error - ); - if (status != G_IO_STATUS_NORMAL) - break; - - ssm(SCI_APPENDTEXT, block_len, (sptr_t)(buffer+offset)); + 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 */ } /* @@ -317,15 +143,14 @@ IOView::load(GIOChannel *channel, GError **error) * If it is enabled but the stream does not contain any * EOL characters, the platform default is still assumed. */ - if (eol_style >= 0) - ssm(SCI_SETEOLMODE, eol_style); + if (reader.eol_style >= 0) + ssm(SCI_SETEOLMODE, reader.eol_style); - if (eol_style_inconsistent) + if (reader.eol_style_inconsistent) interface.msg(InterfaceCurrent::MSG_WARNING, "Inconsistent EOL styles normalized"); ssm(SCI_ENDUNDOACTION); - return status; } /** @@ -336,7 +161,6 @@ IOView::load(const gchar *filename) { GError *error = NULL; GIOChannel *channel; - GIOStatus status; channel = g_io_channel_new_file(filename, "r", &error); if (!channel) { @@ -354,15 +178,17 @@ IOView::load(const gchar *filename) g_io_channel_set_encoding(channel, NULL, NULL); g_io_channel_set_buffered(channel, FALSE); - status = load(channel, &error); - /* also closes file: */ - g_io_channel_unref(channel); - if (status == G_IO_STATUS_ERROR) { + try { + load(channel); + } catch (Error &e) { Error err("Error reading file \"%s\": %s", - filename, error->message); - g_error_free(error); + filename, e.description); + g_io_channel_unref(channel); throw err; } + + /* also closes file: */ + g_io_channel_unref(channel); } #if 0 @@ -471,132 +297,30 @@ make_savepoint(const gchar *filename) #endif -GIOStatus -IOView::save(GIOChannel *channel, guint position, gsize len, - gsize *bytes_written, gint &state, GError **error) -{ - const gchar *buffer; - const gchar *eol_seq; - gchar last_c; - guint i = 0; - guint block_start; - gsize block_written; - - GIOStatus status; - - enum { - SAVE_STATE_START = 0, - SAVE_STATE_WRITE_LF - }; - - buffer = (const gchar *)ssm(SCI_GETRANGEPOINTER, - position, (sptr_t)len); - - if (!(Flags::ed & Flags::ED_AUTOEOL)) - /* - * Write without EOL-translation: - * `state` is not required - */ - return g_io_channel_write_chars(channel, buffer, len, - bytes_written, 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. - */ - *bytes_written = 0; - if (state == SAVE_STATE_WRITE_LF) { - /* complete writing a CRLF sequence */ - status = g_io_channel_write_chars(channel, "\n", 1, NULL, error); - if (status != G_IO_STATUS_NORMAL) - return status; - state = SAVE_STATE_START; - (*bytes_written)++; - i++; - } - - eol_seq = get_eol_seq(ssm(SCI_GETEOLMODE)); - last_c = ssm(SCI_GETCHARAT, position-1); - - block_start = i; - while (i < 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': - status = g_io_channel_write_chars(channel, buffer+block_start, - i-block_start, &block_written, error); - *bytes_written += block_written; - if (status != G_IO_STATUS_NORMAL || - block_written < i-block_start) - return status; - - status = g_io_channel_write_chars(channel, eol_seq, - -1, &block_written, error); - if (status != G_IO_STATUS_NORMAL) - return status; - if (eol_seq[block_written]) { - /* incomplete EOL seq - we have written CR of CRLF */ - state = SAVE_STATE_WRITE_LF; - return G_IO_STATUS_NORMAL; - } - (*bytes_written)++; - - block_start = i+1; - break; - } - - last_c = buffer[i++]; - } - - /* - * Write out remaining block (i.e. line) - */ - status = g_io_channel_write_chars(channel, buffer+block_start, - len-block_start, &block_written, error); - *bytes_written += block_written; - return status; -} - -gboolean -IOView::save(GIOChannel *channel, GError **error) +void +IOView::save(GIOChannel *channel) { + EOLWriterGIO writer(channel, ssm(SCI_GETEOLMODE)); sptr_t gap; gsize size; + const gchar *buffer; gsize bytes_written; - gint state = 0; /* write part of buffer before gap */ gap = ssm(SCI_GETGAPPOSITION); if (gap > 0) { - if (save(channel, 0, gap, &bytes_written, state, error) == G_IO_STATUS_ERROR) - return FALSE; + 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) { - if (save(channel, gap, size, &bytes_written, state, error) == G_IO_STATUS_ERROR) - return FALSE; + buffer = (const gchar *)ssm(SCI_GETRANGEPOINTER, gap, (sptr_t)size); + bytes_written = writer.convert(buffer, size); g_assert(bytes_written == size); } - - return TRUE; } void @@ -636,9 +360,10 @@ IOView::save(const gchar *filename) g_io_channel_set_encoding(channel, NULL, NULL); g_io_channel_set_buffered(channel, TRUE); - if (!save(channel, &error)) { - Error err("Error writing file \"%s\": %s", filename, error->message); - g_error_free(error); + try { + save(channel); + } catch (Error &e) { + Error err("Error writing file \"%s\": %s", filename, e.description); g_io_channel_unref(channel); throw err; } diff --git a/src/ioview.h b/src/ioview.h index 83517b5..18a3be6 100644 --- a/src/ioview.h +++ b/src/ioview.h @@ -124,20 +124,10 @@ class IOView : public ViewCurrent { }; public: - static GIOStatus channel_read_with_eol(GIOChannel *channel, - gchar *buffer, gsize buffer_len, - gsize &read_len, - guint &offset, gsize &block_len, - gint &state, gint &eol_style, - gboolean &eol_style_inconsistent, - GError **error = NULL); - - GIOStatus load(GIOChannel *channel, GError **error = NULL); + void load(GIOChannel *channel); void load(const gchar *filename); - GIOStatus save(GIOChannel *channel, guint position, gsize len, - gsize *bytes_written, gint &state, GError **error = NULL); - gboolean save(GIOChannel *channel, GError **error = NULL); + void save(GIOChannel *channel); void save(const gchar *filename); }; diff --git a/src/parser.cpp b/src/parser.cpp index 941db49..a9e9213 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -2228,6 +2228,10 @@ StateECommand::custom(gchar chr) * - 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. diff --git a/src/qregisters.cpp b/src/qregisters.cpp index 195521c..b6fa472 100644 --- a/src/qregisters.cpp +++ b/src/qregisters.cpp @@ -37,6 +37,7 @@ #include "document.h" #include "ring.h" #include "ioview.h" +#include "eol.h" #include "error.h" #include "qregisters.h" @@ -509,6 +510,192 @@ QRegisterWorkingDir::undo_exchange_string(QRegisterData ®) 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 ®) +{ + gchar *own_str; + gsize own_str_len; + gchar *other_str = reg.get_string(); + gsize other_str_len = reg.get_string_size(); + + /* + * FIXME: What if `reg` is a clipboard and it changes + * between the two calls? + * QRegister::get_string() should always return the length as well. + */ + QRegisterData::set_string(other_str, other_str_len); + g_free(other_str); + own_str = QRegisterClipboard::get_string(&own_str_len); + reg.set_string(own_str, own_str_len); + g_free(own_str); +} + +void +QRegisterClipboard::undo_exchange_string(QRegisterData ®) +{ + QRegisterClipboard::undo_set_string(); + reg.undo_set_string(); +} + QRegisterTable::QRegisterTable(bool _undo) : RBTree(), must_undo(_undo) { /* general purpose registers */ diff --git a/src/qregisters.h b/src/qregisters.h index 563a774..22fa93d 100644 --- a/src/qregisters.h +++ b/src/qregisters.h @@ -135,11 +135,18 @@ public: }; class QRegister : public RBTree::RBEntry, public QRegisterData { +protected: + /** + * The default constructor for subclasses. + * This leaves the name uninitialized. + */ + QRegister(void) : name(NULL) {} + public: gchar *name; QRegister(const gchar *_name) - : QRegisterData(), name(g_strdup(_name)) {} + : name(g_strdup(_name)) {} virtual ~QRegister() { @@ -223,6 +230,76 @@ public: void undo_exchange_string(QRegisterData ®); }; +class QRegisterClipboard : public QRegister { + class UndoTokenSetClipboard : public UndoToken { + gchar *name; + gchar *str; + gsize str_len; + + public: + /** + * Construct undo token. + * + * This passes ownership of the clipboard content string + * to the undo token object. + */ + UndoTokenSetClipboard(const gchar *_name, gchar *_str, gsize _str_len) + : name(g_strdup(_name)), str(_str), str_len(_str_len) {} + ~UndoTokenSetClipboard() + { + g_free(str); + g_free(name); + } + + void run(void); + + gsize + get_size(void) const + { + return sizeof(*this) + strlen(name) + str_len; + } + }; + + /** + * Gets the clipboard name. + * Can be easily derived from the Q-Register name. + */ + inline const gchar * + get_clipboard_name(void) const + { + return name+1; + } + +public: + QRegisterClipboard(const gchar *_name = NULL) + { + name = g_strconcat("~", _name, NIL); + } + + void set_string(const gchar *str, gsize len); + void undo_set_string(void); + + /* + * FIXME: We could support that. + */ + void + append_string(const gchar *str, gsize len) + { + throw QRegOpUnsupportedError(name); + } + void undo_append_string(void) {} + + gchar *get_string(gsize *out_len); + gchar *get_string(void); + gsize get_string_size(void); + gint get_character(gint pos); + + void edit(void); + + void exchange_string(QRegisterData ®); + void undo_exchange_string(QRegisterData ®); +}; + class QRegisterTable : private RBTree { class UndoTokenRemove : public UndoTokenWithSize<UndoTokenRemove> { QRegisterTable *table; diff --git a/src/sciteco.h b/src/sciteco.h index 1163020..b7c62f2 100644 --- a/src/sciteco.h +++ b/src/sciteco.h @@ -39,10 +39,11 @@ typedef tecoInt tecoBool; namespace Flags { enum { - ED_AUTOEOL = (1 << 4), - ED_HOOKS = (1 << 5), - ED_FNKEYS = (1 << 6), - ED_SHELLEMU = (1 << 7) + ED_AUTOEOL = (1 << 4), + ED_HOOKS = (1 << 5), + ED_FNKEYS = (1 << 6), + ED_SHELLEMU = (1 << 7), + ED_XTERM_CLIPBOARD = (1 << 8) }; extern tecoInt ed; diff --git a/src/spawn.cpp b/src/spawn.cpp index 0722180..ee35a03 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -26,7 +26,7 @@ #include "undo.h" #include "expressions.h" #include "qregisters.h" -#include "ioview.h" +#include "eol.h" #include "ring.h" #include "parser.h" #include "error.h" @@ -262,6 +262,8 @@ StateExecuteCommand::~StateExecuteCommand() */ g_main_context_unref(ctx.mainctx); #endif + + delete ctx.error; } void @@ -341,6 +343,7 @@ StateExecuteCommand::done(const gchar *str) */ return &States::start; + GError *error = NULL; gchar **argv, **envp; static const gint flags = G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH | @@ -350,16 +353,29 @@ StateExecuteCommand::done(const gchar *str) 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; - /* opaque state for IOView::save() */ - ctx.stdin_state = 0; - /* opaque state for IOView::channel_read_with_eol() */ - ctx.stdout_state = 0; - /* eol style guessed from the stdout stream */ - ctx.eol_style = -1; + + 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, &ctx.error); + argv = parse_shell_command_line(str, &error); if (!argv) goto gerror; @@ -368,12 +384,12 @@ StateExecuteCommand::done(const gchar *str) g_spawn_async_with_pipes(NULL, argv, envp, (GSpawnFlags)flags, NULL, NULL, &pid, &stdin_fd, &stdout_fd, NULL, - &ctx.error); + &error); g_strfreev(envp); g_strfreev(argv); - if (ctx.error) + if (error) goto gerror; ctx.child_src = g_child_watch_source_new(pid); @@ -390,14 +406,17 @@ StateExecuteCommand::done(const gchar *str) #endif g_io_channel_set_flags(stdin_chan, G_IO_FLAG_NONBLOCK, NULL); g_io_channel_set_encoding(stdin_chan, NULL, NULL); - g_io_channel_set_buffered(stdin_chan, FALSE); - g_io_channel_set_flags(stdout_chan, G_IO_FLAG_NONBLOCK, NULL); - g_io_channel_set_encoding(stdout_chan, NULL, NULL); /* - * IOView::save() expects the channel to be buffered + * EOLWriterGIO expects the channel to be buffered * for performance reasons */ - g_io_channel_set_buffered(stdout_chan, TRUE); + 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)); @@ -425,9 +444,9 @@ StateExecuteCommand::done(const gchar *str) interface.ssm(SCI_ENDUNDOACTION); if (register_argument) { - if (ctx.eol_style >= 0) { + if (stdout_reader.eol_style >= 0) { register_argument->undo_set_eol_mode(); - register_argument->set_eol_mode(ctx.eol_style); + 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 */ @@ -448,8 +467,17 @@ StateExecuteCommand::done(const gchar *str) g_source_unref(ctx.child_src); g_spawn_close_pid(pid); - if (ctx.error) - goto gerror; + 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"); @@ -457,25 +485,17 @@ StateExecuteCommand::done(const gchar *str) if (eval_colon()) expressions.push(SUCCESS); - undo.push_var(register_argument) = NULL; - return &States::start; + goto cleanup; gerror: if (!eval_colon()) - throw GlibError(ctx.error); + throw GlibError(error); + g_error_free(error); - /* - * If possible, encode process exit code - * in return boolean. It's guaranteed to be - * a failure since it's non-negative. - */ - if (ctx.error->domain == G_SPAWN_EXIT_ERROR) - expressions.push(ABS(ctx.error->code)); - else - expressions.push(FAILURE); - undo.push_var(register_argument) = NULL; + expressions.push(ctx.rc); - g_error_free(ctx.error); +cleanup: + undo.push_var(register_argument) = NULL; return &States::start; } @@ -523,13 +543,17 @@ 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, &ctx.error); + 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); @@ -541,29 +565,28 @@ stdin_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data) StateExecuteCommand::Context &ctx = *(StateExecuteCommand::Context *)data; - /* we always read from the current view */ - IOView *view = (IOView *)interface.get_current_view(); - + const gchar *buffer; gsize bytes_written; - /* - * IOView::save() cares about automatic EOL conversion - */ - switch (view->save(chan, ctx.from, ctx.to - ctx.start, - &bytes_written, ctx.stdin_state, - ctx.error ? NULL : &ctx.error)) { - case G_IO_STATUS_ERROR: + /* we always read from the current view */ + buffer = (const gchar *)interface.ssm(SCI_GETRANGEPOINTER, + ctx.from, (sptr_t)(ctx.to - ctx.start)); + + try { + /* + * This cares about automatic EOL conversion + */ + bytes_written = ctx.stdin_writer->convert(buffer, ctx.to - ctx.start); + } catch (Error &e) { + ctx.error = new Error(e); /* do not yet quit -- we still have to reap the child */ goto remove; - case G_IO_STATUS_NORMAL: - break; - case G_IO_STATUS_EOF: - /* process closed stdin preliminarily? */ - goto remove; - case G_IO_STATUS_AGAIN: - return G_SOURCE_CONTINUE; } + if (bytes_written == 0) + /* EOF: process closed stdin preliminarily? */ + goto remove; + ctx.start += bytes_written; if (ctx.start == ctx.to) @@ -590,55 +613,44 @@ stdout_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data) StateExecuteCommand::Context &ctx = *(StateExecuteCommand::Context *)data; - GIOStatus status; - - gchar buffer[1024]; - gsize read_len = 0; - guint offset = 0; - gsize block_len = 0; - /* we're not really interested in that: */ - gboolean eol_style_inconsistent = FALSE; - for (;;) { - status = IOView::channel_read_with_eol( - chan, buffer, sizeof(buffer), - read_len, offset, block_len, - ctx.stdout_state, ctx.eol_style, - eol_style_inconsistent, - ctx.error ? NULL : &ctx.error - ); - - switch (status) { - case G_IO_STATUS_NORMAL: - break; - case G_IO_STATUS_ERROR: - case G_IO_STATUS_EOF: - if (g_source_is_destroyed(ctx.child_src)) - g_main_loop_quit(ctx.mainloop); - return G_SOURCE_REMOVE; - case G_IO_STATUS_AGAIN: - return G_SOURCE_CONTINUE; + 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 (!block_len) - continue; + if (!data_len) + return G_SOURCE_CONTINUE; if (register_argument) { if (ctx.text_added) { register_argument->undo_append_string(); - register_argument->append_string(buffer+offset, block_len); + register_argument->append_string(buffer, data_len); } else { register_argument->undo_set_string(); - register_argument->set_string(buffer+offset, block_len); + register_argument->set_string(buffer, data_len); } } else { - interface.ssm(SCI_ADDTEXT, block_len, (sptr_t)(buffer+offset)); + 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 64ee999..3e01b84 100644 --- a/src/spawn.h +++ b/src/spawn.h @@ -23,6 +23,8 @@ #include "sciteco.h" #include "parser.h" #include "qregisters.h" +#include "error.h" +#include "eol.h" namespace SciTECO { @@ -42,10 +44,12 @@ public: tecoInt from, to; tecoInt start; bool text_added; - gint stdin_state; - gint stdout_state; - gint eol_style; - GError *error; + + EOLWriterGIO *stdin_writer; + EOLReaderGIO *stdout_reader; + + Error *error; + tecoBool rc; }; private: |