[vte/wip/egmont/bidi: 4/8] bidi: Find the BiDi mapping for the RingView's contents



commit 44a30d6a001540f1cb02a084ff3fbf1784f02077
Author: Egmont Koblinger <egmont gmail com>
Date:   Sat Jun 1 15:34:13 2019 +0200

    bidi: Find the BiDi mapping for the RingView's contents
    
    Find the mapping between logical and visual positions for the rows
    of the RingView, according to the paragraph's BiDi parameters.
    
    For implicit paragraphs, the Unicode Bidirectional Algorithm is run
    via the FriBidi library.
    
    Basic Arabic shaping is also performed using presentational form
    characters.
    
    The computed information is not yet used.

 meson.build       |   9 +
 meson_options.txt |   7 +
 src/bidi.cc       | 742 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/bidi.hh       | 116 +++++++++
 src/meson.build   |   3 +
 src/ringview.cc   |  68 ++++-
 src/ringview.hh   |  12 +
 src/vtegtk.cc     |   6 +
 src/vteunistr.cc  |  33 +++
 src/vteunistr.h   |  23 ++
 10 files changed, 1014 insertions(+), 5 deletions(-)
---
diff --git a/meson.build b/meson.build
index 3760b3f8..451195b9 100644
--- a/meson.build
+++ b/meson.build
@@ -34,6 +34,7 @@ project(
 gtk3_req_version          = '3.8.0'
 gtk4_req_version          = '4.0.0'
 
+fribidi_req_version       = '1.0.0'
 gio_req_version           = '2.40.0'
 glib_req_version          = '2.40.0'
 gnutls_req_version        = '3.2.7'
@@ -109,6 +110,7 @@ config_h = configuration_data()
 config_h.set_quoted('GETTEXT_PACKAGE', vte_gettext_domain)
 config_h.set_quoted('VERSION', vte_version)
 config_h.set('VTE_DEBUG', enable_debug)
+config_h.set('WITH_FRIBIDI', get_option('fribidi'))
 config_h.set('WITH_GNUTLS', get_option('gnutls'))
 config_h.set('WITH_ICONV', get_option('iconv'))
 
@@ -373,6 +375,12 @@ pcre2_dep    = dependency('libpcre2-8', version: '>=' + pcre2_req_version)
 pthreads_dep = dependency('threads')
 zlib_dep     = dependency('zlib')
 
+if get_option('fribidi')
+  fribidi_dep = dependency('fribidi', version: '>=' + fribidi_req_version)
+else
+  fribidi_dep = dependency('', required: false)
+endif
+
 if get_option('gnutls')
   gnutls_dep = dependency('gnutls', version: '>=' + gnutls_req_version)
 else
@@ -443,6 +451,7 @@ output += '\n'
 output += '  Coverage:     ' + get_option('b_coverage').to_string() + '\n'
 output += '  Debug:        ' + enable_debug.to_string() + '\n'
 output += '  Docs:         ' + get_option('docs').to_string() + '\n'
+output += '  FRIBIDI:      ' + get_option('fribidi').to_string() + '\n'
 output += '  GNUTLS:       ' + get_option('gnutls').to_string() + '\n'
 output += '  GTK+ 3.0:     ' + get_option('gtk3').to_string() + '\n'
 output += '  GTK+ 4.0:     ' + get_option('gtk4').to_string() + '\n'
diff --git a/meson_options.txt b/meson_options.txt
index b8a11a84..d29c66a3 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -34,6 +34,13 @@ option(
   description: 'Enable GObject Introspection',
 )
 
+option(
+  'fribidi',
+  type: 'boolean',
+  value: true,
+  description: 'Enable FriBidi support',
+)
+
 option(
   'gnutls',
   type: 'boolean',
diff --git a/src/bidi.cc b/src/bidi.cc
new file mode 100644
index 00000000..165f258a
--- /dev/null
+++ b/src/bidi.cc
@@ -0,0 +1,742 @@
+/*
+ * Copyright © 2018–2019 Egmont Koblinger
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+/*
+ * This class implements mapping the logical letters to their visual positions
+ * according to the modes tracked for each paragraph.
+ *
+ * BiDi is implemented according to Terminal-wg/bidi v0.2:
+ * https://terminal-wg.pages.freedesktop.org/bidi/
+ */
+
+#include <config.h>
+
+#ifdef WITH_FRIBIDI
+#include <fribidi.h>
+#endif
+
+#include "bidi.hh"
+#include "debug.h"
+#include "vtedefines.hh"
+#include "vteinternal.hh"
+
+#ifdef WITH_FRIBIDI
+static_assert (sizeof (gunichar) == sizeof (FriBidiChar), "Whoooo");
+#endif
+
+using namespace vte::base;
+
+BidiRow::BidiRow()
+{
+        /* The value of 0 is a valid representation of the trivial LTR mapping. */
+        m_width = 0;
+
+        /* These will be initialized / allocated on demand, when some RTL is encountered. */
+        m_width_alloc = 0;
+        m_log2vis = nullptr;
+        m_vis2log = nullptr;
+        m_vis_rtl = nullptr;
+        m_vis_shaped_char = nullptr;
+}
+
+BidiRow::~BidiRow()
+{
+        g_free (m_log2vis);
+        g_free (m_vis2log);
+        g_free (m_vis_rtl);
+        g_free (m_vis_shaped_char);
+}
+
+void BidiRow::set_width(vte::grid::column_t width)
+{
+        if (G_UNLIKELY (width > m_width_alloc)) {
+                if (m_width_alloc == 0) {
+                        m_width_alloc = 128;
+                }
+                while (width > m_width_alloc) {
+                        m_width_alloc *= 2;
+                }
+                m_log2vis = (vte::grid::column_t *) g_realloc (m_log2vis, sizeof (vte::grid::column_t) * 
m_width_alloc);
+                m_vis2log = (vte::grid::column_t *) g_realloc (m_vis2log, sizeof (vte::grid::column_t) * 
m_width_alloc);
+                m_vis_rtl = (guint8 *) g_realloc (m_vis_rtl, sizeof (guint8) * m_width_alloc);
+                m_vis_shaped_char = (gunichar *) g_realloc (m_vis_shaped_char, sizeof (gunichar) * 
m_width_alloc);
+        }
+
+        m_width = width;
+}
+
+/* Converts from logical to visual column. Offscreen columns are mirrored
+ * for RTL lines, e.g. (assuming 80 columns) -1 <=> 80, -2 <=> 81 etc. */
+vte::grid::column_t BidiRow::log2vis(vte::grid::column_t col) const
+{
+        if (col >= 0 && col < m_width) {
+                return m_log2vis[col];
+        } else {
+                return m_base_rtl ? m_width - 1 - col : col;
+        }
+}
+
+/* Converts from visual to logical column. Offscreen columns are mirrored
+ * for RTL lines, e.g. (assuming 80 columns) -1 <=> 80, -2 <=> 81 etc. */
+vte::grid::column_t BidiRow::vis2log(vte::grid::column_t col) const
+{
+        if (col >= 0 && col < m_width) {
+                return m_vis2log[col];
+        } else {
+                return m_base_rtl ? m_width - 1 - col : col;
+        }
+}
+
+/* Whether the cell at the given visual position has RTL directionality.
+ * For offscreen columns the line's base direction is returned. */
+bool BidiRow::vis_is_rtl(vte::grid::column_t col) const
+{
+        if (col >= 0 && col < m_width) {
+                return m_vis_rtl[col];
+        } else {
+                return m_base_rtl;
+        }
+}
+
+/* Whether the cell at the given logical position has RTL directionality.
+ * For offscreen columns the line's base direction is returned. */
+bool BidiRow::log_is_rtl(vte::grid::column_t col) const
+{
+        if (col >= 0 && col < m_width) {
+                col = m_log2vis[col];
+                return m_vis_rtl[col];
+        } else {
+                return m_base_rtl;
+        }
+}
+
+/* Get the shaped character (vteunistr) for the given visual position.
+ *
+ * The unshaped character (vteunistr) needs to be passed to this method because
+ * the BiDi component may not store it if no shaping was required, and does not
+ * store combining accents. This method takes care of preserving combining accents.
+ */
+vteunistr
+BidiRow::vis_get_shaped_char(vte::grid::column_t col, vteunistr s) const
+{
+        if (col >= m_width || m_vis_shaped_char[col] == 0)
+                return s;
+
+        return _vte_unistr_replace_base(s, m_vis_shaped_char[col]);
+}
+
+/* Whether the line's base direction is RTL. */
+bool BidiRow::base_is_rtl() const
+{
+        return m_base_rtl;
+}
+
+/* Whether the implicit paragraph contains a foreign directionality character.
+ * This is used in the cursor, showing the character's directionality. */
+bool BidiRow::has_foreign() const
+{
+        return m_has_foreign;
+}
+
+
+BidiRunner::BidiRunner(RingView *ringview)
+{
+        m_ringview = ringview;
+}
+
+BidiRunner::~BidiRunner() {}
+
+#ifdef WITH_FRIBIDI
+bool BidiRunner::is_arabic(gunichar c)
+{
+        return FRIBIDI_IS_ARABIC (fribidi_get_bidi_type (c));
+}
+#endif /* WITH_FRIBIDI */
+
+/* Returns whether there's an Arabic character in the row so that shaping might be required. */
+bool BidiRunner::needs_shaping(vte::grid::row_t row)
+{
+#ifdef WITH_FRIBIDI
+        const VteRowData *row_data = m_ringview->get_row(row);
+
+        for (int i = 0; i < row_data->len; i++) {
+                const VteCell *cell = _vte_row_data_get(row_data, i);
+                if (cell != nullptr) {
+                        gunichar c = _vte_unistr_get_base(cell->c);
+                        if (G_UNLIKELY (is_arabic(c)))
+                                return true;
+                }
+        }
+#endif /* WITH_FRIBIDI */
+        return false;
+}
+
+#ifdef WITH_FRIBIDI
+/* Perform Arabic shaping on an explicit line (which could be explicit LTR or explicit RTL),
+ * using presentational form characters.
+ *
+ * Don't do shaping across lines. (I'm unsure about this design decision.
+ * Shaping across soft linebreaks would require an even much more complex code.)
+ *
+ * The FriBiDi API doesn't have a method for shaping a visual string, so we need to extract
+ * Arabic words ourselves, by walking in the visual order from right to left. It's painful.
+ *
+ * This whole shaping business with presentational form characters should be replaced by HarfBuzz.
+ */
+void BidiRunner::explicit_line_shape(vte::grid::row_t row)
+{
+        VteRowData *row_data = m_ringview->get_row(row);
+        if (G_UNLIKELY (row_data == nullptr))
+                return;
+
+        BidiRow *bidirow = m_ringview->get_bidirow_writable(row);
+
+        auto width = m_ringview->get_width();
+
+        GArray *fribidi_chars_array = nullptr;
+
+        FriBidiParType pbase_dir = FRIBIDI_PAR_RTL;
+        FriBidiLevel level;
+        FriBidiChar *fribidi_chars;
+        FriBidiCharType *fribidi_chartypes;
+        FriBidiBracketType *fribidi_brackettypes;
+        FriBidiJoiningType *fribidi_joiningtypes;
+        FriBidiLevel *fribidi_levels;
+
+        int count;
+
+        const VteCell *cell;
+        gunichar c;
+        gunichar base;
+        int i, j;  /* visual columns */
+
+        fribidi_chars_array = g_array_new (FALSE, FALSE, sizeof (FriBidiChar));
+
+        /* Walk in visual order from right to left. */
+        i = width - 1;
+        while (i >= 0) {
+                cell = _vte_row_data_get(row_data, bidirow->m_vis2log[i]);
+                c = cell ? cell->c : 0;
+                base = _vte_unistr_get_base(c);
+                if (!is_arabic(base)) {
+                        i--;
+                        continue;
+                }
+
+                /* Found an Arabic character. Keep walking to the left, extracting the word. */
+                g_array_set_size(fribidi_chars_array, 0);
+                j = i;
+                do {
+                        auto prev_len = fribidi_chars_array->len;
+                        _vte_unistr_append_to_gunichars (cell->c, fribidi_chars_array);
+                        g_assert_cmpint (fribidi_chars_array->len, >, prev_len);
+
+                        j--;
+                        if (j >= 0) {
+                                cell = _vte_row_data_get(row_data, bidirow->m_vis2log[j]);
+                                c = cell ? cell->c : 0;
+                                base = _vte_unistr_get_base(c);
+                        } else {
+                                /* Pretend that visual column -1 contains a stop char. */
+                                base = 0;
+                        }
+                } while (is_arabic(base));
+
+                /* Extracted the Arabic run. Do the BiDi. */
+
+                /* Convenience stuff, we no longer need the auto-growing GArray wrapper. */
+                count = fribidi_chars_array->len;
+                fribidi_chars = (FriBidiChar *) fribidi_chars_array->data;
+
+                /* Run the BiDi algorithm on the paragraph to get the embedding levels. */
+                fribidi_chartypes = g_newa (FriBidiCharType, count);
+                fribidi_brackettypes = g_newa (FriBidiBracketType, count);
+                fribidi_joiningtypes = g_newa (FriBidiJoiningType, count);
+                fribidi_levels = g_newa (FriBidiLevel, count);
+
+                fribidi_get_bidi_types (fribidi_chars, count, fribidi_chartypes);
+                fribidi_get_bracket_types (fribidi_chars, count, fribidi_chartypes, fribidi_brackettypes);
+                fribidi_get_joining_types (fribidi_chars, count, fribidi_joiningtypes);
+                level = fribidi_get_par_embedding_levels_ex (fribidi_chartypes, fribidi_brackettypes, count, 
&pbase_dir, fribidi_levels);
+                if (level == 0) {
+                        /* Error. Skip shaping this word. */
+                        i = j - 1;
+                        continue;
+                }
+
+                /* Shaping. */
+                fribidi_join_arabic (fribidi_chartypes, count, fribidi_levels, fribidi_joiningtypes);
+                fribidi_shape_arabic (FRIBIDI_FLAGS_ARABIC, fribidi_levels, count, fribidi_joiningtypes, 
fribidi_chars);
+
+                /* Walk through the Arabic word again. */
+                j = i;
+                while (count > 0) {
+                        g_assert_cmpint (j, >=, 0);
+                        cell = _vte_row_data_get(row_data, bidirow->m_vis2log[j]);
+                        c = cell->c;
+                        base = _vte_unistr_get_base(c);
+                        if (*fribidi_chars != base) {
+                                /* Shaping changed the codepoint. Apply combining accents and store. */
+                                bidirow->m_vis_shaped_char[j] = _vte_unistr_replace_base(c, *fribidi_chars);
+                        }
+                        int len = _vte_unistr_strlen(c);
+                        fribidi_chars += len;
+                        count -= len;
+                        j--;
+                }
+
+                /* Ready to look for the next word. Skip the stop char which isn't Arabic. */
+                i = j - 1;
+        }
+
+        g_array_free (fribidi_chars_array, TRUE);
+}
+#endif /* WITH_FRIBIDI */
+
+/* Set up the mapping according to explicit mode for a given line.
+ *
+ * If @shape then perform Arabic shaping on the visual string, independently
+ * from the paragraph direction (the @rtl parameter). This is done using
+ * presentation form characters, until we have something better (e.g. HarfBuzz)
+ * in place.
+ */
+void BidiRunner::explicit_line(vte::grid::row_t row, bool rtl, bool shape)
+{
+        int i;
+
+        BidiRow *bidirow = m_ringview->get_bidirow_writable(row);
+        if (G_UNLIKELY (bidirow == nullptr))
+                return;
+        bidirow->m_base_rtl = rtl;
+        bidirow->m_has_foreign = false;
+
+        auto width = m_ringview->get_width();
+
+        if (G_LIKELY (!rtl)) {
+                if (!shape || !needs_shaping(row)) {
+                        /* Shortcut notation: a width of 0 means the trivial LTR mapping. */
+                        bidirow->set_width(0);
+                        return;
+                }
+                bidirow->set_width(width);
+                for (i = 0; i < width; i++) {
+                        bidirow->m_log2vis[i] = bidirow->m_vis2log[i] = i;
+                        bidirow->m_vis_rtl[i] = false;
+                        bidirow->m_vis_shaped_char[i] = 0;
+                }
+        } else {
+                bidirow->set_width(width);
+                for (i = 0; i < width; i++) {
+                        bidirow->m_log2vis[i] = bidirow->m_vis2log[i] = width - 1 - i;
+                        bidirow->m_vis_rtl[i] = true;
+                        bidirow->m_vis_shaped_char[i] = 0;
+                }
+                if (!shape || !needs_shaping(row)) {
+                        return;
+                }
+        }
+
+#ifdef WITH_FRIBIDI
+        explicit_line_shape(row);
+#endif
+}
+
+/* Figure out the mapping for the paragraph between the given rows. */
+void BidiRunner::paragraph(vte::grid::row_t start, vte::grid::row_t end)
+{
+        const VteRowData *row_data = m_ringview->get_row(start);
+
+#ifdef WITH_FRIBIDI
+        /* Have a consistent limit on the number of rows in a paragraph
+         * that can get implicit BiDi treatment, which is independent from
+         * the current scroll position. */
+        if ((row_data->attr.bidi_flags & VTE_BIDI_FLAG_IMPLICIT) &&
+            end - start <= VTE_RINGVIEW_PARAGRAPH_LENGTH_MAX) {
+                if (implicit_paragraph(start, end))
+                        return;
+        }
+#endif
+
+        explicit_paragraph(start, end, row_data->attr.bidi_flags & VTE_BIDI_FLAG_RTL, true);
+}
+
+/* Set up the mapping according to explicit mode, for all the lines
+ * of a paragraph between the given lines. */
+void BidiRunner::explicit_paragraph(vte::grid::row_t start, vte::grid::row_t end, bool rtl, bool shape)
+{
+        for (; start < end; start++) {
+                explicit_line(start, rtl, shape);
+        }
+}
+
+#ifdef WITH_FRIBIDI
+/* Figure out the mapping for the implicit paragraph between the given rows.
+ * Returns success. */
+bool BidiRunner::implicit_paragraph(vte::grid::row_t start, vte::grid::row_t end)
+{
+        const VteCell *cell;
+        const VteRowData *row_data;
+        bool rtl;
+        bool autodir;
+        vte::grid::row_t row;
+        FriBidiParType pbase_dir;
+        FriBidiLevel level;
+        FriBidiChar *fribidi_chars;
+        FriBidiCharType *fribidi_chartypes;
+        FriBidiBracketType *fribidi_brackettypes;
+        FriBidiJoiningType *fribidi_joiningtypes;
+        FriBidiLevel *fribidi_levels;
+        FriBidiStrIndex *fribidi_map;
+        FriBidiStrIndex *fribidi_to_term;
+        BidiRow *bidirow;
+
+        auto width = m_ringview->get_width();
+
+        row_data = m_ringview->get_row(start);
+        rtl = row_data->attr.bidi_flags & VTE_BIDI_FLAG_RTL;
+        autodir = row_data->attr.bidi_flags & VTE_BIDI_FLAG_AUTO;
+
+        int lines[VTE_RINGVIEW_PARAGRAPH_LENGTH_MAX + 1];  /* offsets to the beginning of lines */
+        lines[0] = 0;
+        int line = 0;   /* line number within the paragraph */
+        int count;      /* total character count */
+        int tl, tv;     /* terminal logical and visual */
+        int fl, fv;     /* fribidi logical and visual */
+        unsigned int col;
+
+        GArray *fribidi_chars_array   = g_array_new (FALSE, FALSE, sizeof (FriBidiChar));
+        GArray *fribidi_map_array     = g_array_new (FALSE, FALSE, sizeof (FriBidiStrIndex));
+        GArray *fribidi_to_term_array = g_array_new (FALSE, FALSE, sizeof (FriBidiStrIndex));
+
+        /* Extract the paragraph's contents, omitting unused and fragment cells. */
+
+        /* Example of what is going on, showing the most important steps:
+         *
+         * Let's take the string produced by this command:
+         *   echo -e "\u0041\u05e9\u05b8\u05c1\u05dc\u05d5\u05b9\u05dd\u0031\u0032\uff1c\u05d0"
+         *
+         * This string consists of:
+         * - English letter A
+         * - Hebrew word Shalom:
+         *     - Letter Shin: ש
+         *         - Combining accent Qamats
+         *         - Combining accent Shin Dot
+         *     - Letter Lamed: ל
+         *     - Letter Vav: ו
+         *         - Combining accent Holam
+         *     - Letter Final Mem: ם
+         * - Digits One and Two
+         * - Full-width less-than sign U+ff1c: <
+         * - Hebrew letter Alef: א
+         *
+         * Features of this example:
+         * - Overall LTR direction for convenience (set up by the leading English letter)
+         * - Combining accents within RTL
+         * - Double width character with RTL resolved direction
+         * - A mapping that is not its own inverse (due to the digits being LTR inside RTL inside LTR),
+         *   to help catch if we'd look up something in the wrong direction
+         *
+         * Not demonstrated in this example:
+         * - Wrapping a paragraph to lines
+         * - Spacing marks
+         *
+         * Pre-BiDi (logical) order, using approximating glyphs ("Shalom" is "w7io", Alef is "x"):
+         *   Aw7io12<x
+         *
+         * Post-BiDi (visual) order, using approximating glyphs ("Shalom" is "oi7w", note the mirrored 
less-than):
+         *   Ax>12oi7w
+         *
+         * Terminal's logical cells:
+         *                 [0]       [1]       [2]      [3]     [4]   [5]   [6]    [7]      [8]         [9]
+         *     row_data:    A   Shin+qam+dot   Lam    Vav+hol   Mem   One   Two   Less   Less (cont)   Alef
+         *
+         * Extracted to pass to FriBidi (combining accents get -1, double wides' continuation cells are 
skipped):
+         *                        [0]    [1]   [2]   [3]   [4]   [5]   [6]   [7]   [8]   [9]   [10]   [11]
+         *     fribidi_chars:      A    Shin   qam   dot   Lam   Vav   hol   Mem   One   Two   Less   Alef
+         *     fribidi_map:        0      1    -1    -1     4     5    -1     7     8     9     10     11
+         *     fribidi_to_term:    0      1    -1    -1     2     3    -1     4     5     6      7      9
+         *
+         * Embedding levels and other properties (shaping etc.) are looked up:
+         *                        [0]    [1]   [2]   [3]   [4]   [5]   [6]   [7]   [8]   [9]   [10]   [11]
+         *     fribidi_levels:     0      1     1     1     1     1     1     1     2     2      1      1
+         *
+         * The steps above were per-paragraph. The steps below are per-line.
+         *
+         * After fribidi_reorder_line (only this array gets shuffled):
+         *                        [0]    [1]   [2]   [3]   [4]   [5]   [6]   [7]   [8]   [9]   [10]   [11]
+         *     fribidi_map:        0     11    10     8     9     7     5    -1     4     1     -1     -1
+         *
+         * To get the visual order: walk in the new fribidi_map, and for each real entry look up the
+         * logical terminal column using fribidi_to_term:
+         * - map[0] is 0, to_term[0] is 0, hence visual column 0 belongs to logical column 0 (A)
+         * - map[1] is 11, to_term[11] is 9, hence visual column 1 belongs to logical column 9 (Alef)
+         * - map[2] is 10, to_term[10] is 7, row_data[7] is the "<" sign
+         *     - this is a double wide character, we need to map the next two visual cells to two logical 
cells
+         *     - due to levels[10] being odd, this character has a resolved RTL direction
+         *     - thus we map in reverse order: visual 2 <=> logical 8, visual 3 <=> logical 7
+         *     - the glyph is also mirrorable, it'll be displayed accordingly
+         * - [3] -> 8 -> 5, so visual 4 <=> logical 5 (One)
+         * - [4] -> 9 -> 6, so visual 5 <=> logical 6 (Two)
+         * - [5] -> 7 -> 4, so visual 6 <=> logical 4 (Mem, the last, leftmost letter of Shalom)
+         * - [6] -> 5 -> 3, so visual 7 <=> logical 3 (Vav+hol)
+         * - [7] -> -1, skipped
+         * - [8] -> 4 -> 2, so visual 8 <=> logical 2 (Lam)
+         * - [9] -> 1 -> 1, so visual 9 <=> logical 1 (Shin+qam+dot, the first, rightmost letter of Shalom)
+         * - [10] -> -1, skipped
+         * - [11] -> -1, skipped
+         *
+         * Silly FriBidi API almost allows us to skip one level of indirection, by placing the to_term values
+         * in the map to be shuffled. However, we can't get the embedding levels then.
+         * TODO: File an issue for a better API.
+         */
+        for (row = start; row < end; row++) {
+                row_data = m_ringview->get_row(row);
+
+                /* A row_data might be longer, in case rewrapping is disabled and the window was narrowed.
+                 * Truncate the logical data before applying BiDi. */
+                // FIXME what the heck to do if this truncation cuts a TAB or CJK in half???
+                for (tl = 0; tl < width && tl < row_data->len; tl++) {
+                        auto prev_len = fribidi_chars_array->len;
+                        FriBidiStrIndex val;
+
+                        cell = _vte_row_data_get (row_data, tl);
+                        if (cell->attr.fragment())
+                                continue;
+
+                        /* Extract the base character and combining accents.
+                         * Convert mid-line erased cells to spaces.
+                         * Note: see the static assert at the top of this file. */
+                        _vte_unistr_append_to_gunichars (cell->c ? cell->c : ' ', fribidi_chars_array);
+                        /* Make sure at least one character was produced. */
+                        g_assert_cmpint (fribidi_chars_array->len, >, prev_len);
+
+                        /* Track the base character, assign to it its current index in fribidi_chars.
+                         * Don't track combining accents, assign -1's to them. */
+                        val = prev_len;
+                        g_array_append_val (fribidi_map_array, val);
+                        val = tl;
+                        g_array_append_val (fribidi_to_term_array, val);
+                        prev_len++;
+                        val = -1;
+                        while (prev_len++ < fribidi_chars_array->len) {
+                                g_array_append_val (fribidi_map_array, val);
+                                g_array_append_val (fribidi_to_term_array, val);
+                        }
+                }
+
+                lines[++line] = fribidi_chars_array->len;
+        }
+
+        /* Convenience stuff, we no longer need the auto-growing GArray wrapper. */
+        count = fribidi_chars_array->len;
+        fribidi_chars = (FriBidiChar *) fribidi_chars_array->data;
+        fribidi_map = (FriBidiStrIndex *) fribidi_map_array->data;
+        fribidi_to_term = (FriBidiStrIndex *) fribidi_to_term_array->data;
+
+        /* Run the BiDi algorithm on the paragraph to get the embedding levels. */
+        fribidi_chartypes = g_newa (FriBidiCharType, count);
+        fribidi_brackettypes = g_newa (FriBidiBracketType, count);
+        fribidi_joiningtypes = g_newa (FriBidiJoiningType, count);
+        fribidi_levels = g_newa (FriBidiLevel, count);
+
+        pbase_dir = autodir ? (rtl ? FRIBIDI_PAR_WRTL : FRIBIDI_PAR_WLTR)
+                            : (rtl ? FRIBIDI_PAR_RTL  : FRIBIDI_PAR_LTR );
+
+        fribidi_get_bidi_types (fribidi_chars, count, fribidi_chartypes);
+        fribidi_get_bracket_types (fribidi_chars, count, fribidi_chartypes, fribidi_brackettypes);
+        fribidi_get_joining_types (fribidi_chars, count, fribidi_joiningtypes);
+        level = fribidi_get_par_embedding_levels_ex (fribidi_chartypes, fribidi_brackettypes, count, 
&pbase_dir, fribidi_levels);
+
+        if (level == 0) {
+                /* error */
+                g_array_free (fribidi_chars_array, TRUE);
+                g_array_free (fribidi_map_array, TRUE);
+                g_array_free (fribidi_to_term_array, TRUE);
+                return false;
+        }
+
+        /* Arabic shaping (on the entire paragraph in a single run). */
+        fribidi_join_arabic (fribidi_chartypes, count, fribidi_levels, fribidi_joiningtypes);
+        fribidi_shape_arabic (FRIBIDI_FLAGS_ARABIC, fribidi_levels, count, fribidi_joiningtypes, 
fribidi_chars);
+        g_assert_cmpint (pbase_dir, !=, FRIBIDI_PAR_ON);
+
+        /* For convenience, from now on this variable contains the resolved (i.e. possibly autodetected) 
value. */
+        rtl = (pbase_dir == FRIBIDI_PAR_RTL || pbase_dir == FRIBIDI_PAR_WRTL);
+
+        if (!rtl && level == 1) {
+                /* Fast shortcut for LTR-only paragraphs. */
+                g_array_free (fribidi_chars_array, TRUE);
+                g_array_free (fribidi_map_array, TRUE);
+                g_array_free (fribidi_to_term_array, TRUE);
+                explicit_paragraph (start, end, false, false);
+                return true;
+        }
+
+        /* Reshuffle line by line. */
+        for (row = start, line = 0; row < end; row++, line++) {
+                bidirow = m_ringview->get_bidirow_writable(row);
+                if (bidirow == nullptr)
+                        continue;
+
+                bidirow->m_base_rtl = rtl;
+                bidirow->m_has_foreign = true;
+                bidirow->set_width(width);
+
+                row_data = m_ringview->get_row(row);
+
+                level = fribidi_reorder_line (FRIBIDI_FLAGS_DEFAULT,
+                                              fribidi_chartypes,
+                                              lines[line + 1] - lines[line],
+                                              lines[line],
+                                              pbase_dir,
+                                              fribidi_levels,
+                                              NULL,
+                                              fribidi_map);
+
+                if (level == 0) {
+                        /* error, what should we do? */
+                        explicit_line (row, rtl, true);
+                        bidirow->m_has_foreign = true;
+                        continue;
+                }
+
+                if (!rtl && level == 1) {
+                        /* Fast shortcut for LTR-only lines. */
+                        explicit_line (row, false, false);
+                        bidirow->m_has_foreign = true;
+                        continue;
+                }
+
+                /* Copy to our realm. Proceed in visual order.*/
+                tv = 0;
+                if (rtl) {
+                        /* Unused cells on the left for RTL paragraphs */
+                        int unused = MAX(width - row_data->len, 0);
+                        for (; tv < unused; tv++) {
+                                bidirow->m_vis2log[tv] = width - 1 - tv;
+                                bidirow->m_vis_rtl[tv] = true;
+                                bidirow->m_vis_shaped_char[tv] = 0;
+                        }
+                }
+                for (fv = lines[line]; fv < lines[line + 1]; fv++) {
+                        /* Inflate fribidi's result by inserting fragments. */
+                        fl = fribidi_map[fv];
+                        if (fl == -1)
+                                continue;
+                        tl = fribidi_to_term[fl];
+                        cell = _vte_row_data_get (row_data, tl);
+                        g_assert (!cell->attr.fragment());
+                        g_assert (cell->attr.columns() > 0);
+                        if (FRIBIDI_LEVEL_IS_RTL(fribidi_levels[fl])) {
+                                /* RTL character directionality. Map fragments in reverse order. */
+                                for (col = 0; col < cell->attr.columns(); col++) {
+                                        bidirow->m_vis2log[tv + col] = tl + cell->attr.columns() - 1 - col;
+                                        bidirow->m_vis_rtl[tv + col] = true;
+                                        bidirow->m_vis_shaped_char[tv + col] = fribidi_chars[fl];
+                                }
+                                tv += cell->attr.columns();
+                                tl += cell->attr.columns();
+                        } else {
+                                /* LTR character directionality. */
+                                for (col = 0; col < cell->attr.columns(); col++) {
+                                        bidirow->m_vis2log[tv] = tl;
+                                        bidirow->m_vis_rtl[tv] = false;
+                                        bidirow->m_vis_shaped_char[tv] = fribidi_chars[fl];
+                                        tv++;
+                                        tl++;
+                                }
+                        }
+                }
+                if (!rtl) {
+                        /* Unused cells on the right for LTR paragraphs */
+                        g_assert_cmpint (tv, ==, MIN (row_data->len, width));
+                        for (; tv < width; tv++) {
+                                bidirow->m_vis2log[tv] = tv;
+                                bidirow->m_vis_rtl[tv] = false;
+                                bidirow->m_vis_shaped_char[tv] = 0;
+                        }
+                }
+                g_assert_cmpint (tv, ==, width);
+
+                /* From vis2log create the log2vis mapping too.
+                 * In debug mode assert that we have a bijective mapping. */
+                if (_vte_debug_on (VTE_DEBUG_BIDI)) {
+                        for (tl = 0; tl < width; tl++) {
+                                bidirow->m_log2vis[tl] = -1;
+                        }
+                }
+
+                for (tv = 0; tv < width; tv++) {
+                        bidirow->m_log2vis[bidirow->m_vis2log[tv]] = tv;
+                }
+
+                if (_vte_debug_on (VTE_DEBUG_BIDI)) {
+                        for (tl = 0; tl < width; tl++) {
+                                g_assert_cmpint (bidirow->m_log2vis[tl], !=, -1);
+                        }
+                }
+        }
+
+        g_array_free (fribidi_chars_array, TRUE);
+        g_array_free (fribidi_map_array, TRUE);
+        g_array_free (fribidi_to_term_array, TRUE);
+        return true;
+}
+#endif /* WITH_FRIBIDI */
+
+
+/* Find the mirrored counterpart of a codepoint, just like
+ * fribidi_get_mirror_char() or g_unichar_get_mirror_char() does.
+ * Two additions:
+ * - works with vteunistr, that is, preserves combining accents;
+ * - optionally mirrors box drawing characters.
+ */
+gboolean vte_bidi_get_mirror_char (vteunistr unistr, gboolean mirror_box_drawing, vteunistr *out)
+{
+        static const unsigned char mirrored_2500[0x80] = {
+                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x11, 0x12, 
0x13,
+                0x0c, 0x0d, 0x0e, 0x0f, 0x18, 0x19, 0x1a, 0x1b, 0x14, 0x15, 0x16, 0x17, 0x24, 0x25, 0x26, 
0x27,
+                0x28, 0x29, 0x2a, 0x2b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x2c, 0x2e, 0x2d, 
0x2f,
+                0x30, 0x32, 0x31, 0x33, 0x34, 0x36, 0x35, 0x37, 0x38, 0x3a, 0x39, 0x3b, 0x3c, 0x3e, 0x3d, 
0x3f,
+                0x40, 0x41, 0x42, 0x44, 0x43, 0x46, 0x45, 0x47, 0x48, 0x4a, 0x49, 0x4b, 0x4c, 0x4d, 0x4e, 
0x4f,
+                0x50, 0x51, 0x55, 0x56, 0x57, 0x52, 0x53, 0x54, 0x5b, 0x5c, 0x5d, 0x58, 0x59, 0x5a, 0x61, 
0x62,
+                0x63, 0x5e, 0x5f, 0x60, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6e, 0x6d, 
0x70,
+                0x6f, 0x72, 0x71, 0x73, 0x76, 0x75, 0x74, 0x77, 0x7a, 0x79, 0x78, 0x7b, 0x7e, 0x7d, 0x7c, 
0x7f };
+
+        gunichar base_ch = _vte_unistr_get_base (unistr);
+        gunichar base_ch_mirrored = base_ch;
+
+        if (G_UNLIKELY (base_ch >= 0x2500 && base_ch < 0x2580)) {
+                if (G_UNLIKELY (mirror_box_drawing))
+                        base_ch_mirrored = 0x2500 + mirrored_2500[base_ch - 0x2500];
+        } else {
+#ifdef WITH_FRIBIDI
+                /* Prefer the FriBidi variant as that's more likely to be in sync with the rest of our BiDi 
stuff. */
+                fribidi_get_mirror_char (base_ch, &base_ch_mirrored);
+#else
+                /* Fall back to glib, so that we still get mirrored characters in explicit RTL mode without 
BiDi support. */
+                g_unichar_get_mirror_char (base_ch, &base_ch_mirrored);
+#endif
+        }
+
+        vteunistr unistr_mirrored = _vte_unistr_replace_base (unistr, base_ch_mirrored);
+
+        if (out)
+                *out = unistr_mirrored;
+        return unistr_mirrored == unistr;
+}
diff --git a/src/bidi.hh b/src/bidi.hh
new file mode 100644
index 00000000..8cddd621
--- /dev/null
+++ b/src/bidi.hh
@@ -0,0 +1,116 @@
+/*
+ * Copyright © 2018–2019 Egmont Koblinger
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#pragma once
+
+#include <glib.h>
+
+#include "ring.hh"
+#include "ringview.hh"
+#include "vterowdata.hh"
+#include "vtetypes.hh"
+#include "vteunistr.h"
+
+namespace vte {
+
+namespace base {  // FIXME ???
+
+class RingView;
+
+/* BidiRow contains the BiDi transformation of a single row. */
+class BidiRow {
+        friend class RingView;  // is this needed?
+        friend class BidiRunner;
+
+public:
+        BidiRow();
+        ~BidiRow();
+
+        // prevent accidents
+        BidiRow(BidiRow& o) = delete;
+        BidiRow(BidiRow const& o) = delete;
+        BidiRow(BidiRow&& o) = delete;
+        BidiRow& operator= (BidiRow& o) = delete;
+        BidiRow& operator= (BidiRow const& o) = delete;
+        BidiRow& operator= (BidiRow&& o) = delete;
+
+        vte::grid::column_t log2vis(vte::grid::column_t col) const;
+        vte::grid::column_t vis2log(vte::grid::column_t col) const;
+        bool log_is_rtl(vte::grid::column_t col) const;
+        bool vis_is_rtl(vte::grid::column_t col) const;
+        vteunistr vis_get_shaped_char(vte::grid::column_t col, vteunistr s) const;
+        bool base_is_rtl() const;
+        bool has_foreign() const;
+
+private:
+        void set_width(vte::grid::column_t width);
+
+        vte::grid::column_t m_width;
+        vte::grid::column_t m_width_alloc;
+
+        vte::grid::column_t *m_log2vis;
+        vte::grid::column_t *m_vis2log;
+        guint8 *m_vis_rtl;
+        gunichar *m_vis_shaped_char;
+
+        guint8 m_base_rtl: 1;
+        guint8 m_has_foreign: 1;
+};
+
+
+/* BidiRunner is not a "real" class, rather the collection of methods that run the BiDi algorithm. */
+class BidiRunner {
+public:
+        BidiRunner(RingView *ringview);
+        ~BidiRunner();
+
+        // prevent accidents
+        BidiRunner(BidiRunner& o) = delete;
+        BidiRunner(BidiRunner const& o) = delete;
+        BidiRunner(BidiRunner&& o) = delete;
+        BidiRunner& operator= (BidiRunner& o) = delete;
+        BidiRunner& operator= (BidiRunner const& o) = delete;
+        BidiRunner& operator= (BidiRunner&& o) = delete;
+
+        void paragraph(vte::grid::row_t start, vte::grid::row_t end);
+
+private:
+        RingView *m_ringview;
+
+        bool needs_shaping(vte::grid::row_t row);
+#ifdef WITH_FRIBIDI
+        static bool is_arabic(gunichar c);
+        void explicit_line_shape(vte::grid::row_t row);
+#endif
+
+        void explicit_line(vte::grid::row_t row, bool rtl, bool shape);
+        void explicit_paragraph(vte::grid::row_t start, vte::grid::row_t end, bool rtl, bool shape);
+#ifdef WITH_FRIBIDI
+        bool implicit_paragraph(vte::grid::row_t start, vte::grid::row_t end);
+#endif
+};
+
+}; /* namespace base */
+
+}; /* namespace vte */
+
+G_BEGIN_DECLS
+
+gboolean vte_bidi_get_mirror_char (vteunistr unistr, gboolean mirror_box_drawing, vteunistr 
*unistr_mirrored);
+
+G_END_DECLS
diff --git a/src/meson.build b/src/meson.build
index 6b97aaec..1481c089 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -56,6 +56,8 @@ utf8_sources = files(
 
 libvte_common_sources = debug_sources + modes_sources + parser_sources + utf8_sources + files(
   'attr.hh',
+  'bidi.cc',
+  'bidi.hh',
   'buffer.h',
   'caps.hh',
   'cell.hh',
@@ -146,6 +148,7 @@ libvte_common_public_deps = [
 ]
 
 libvte_common_deps = libvte_common_public_deps + [
+  fribidi_dep,
   gnutls_dep,
   pcre2_dep,
   libm_dep,
diff --git a/src/ringview.cc b/src/ringview.cc
index dfb1e2f5..0dd30fd2 100644
--- a/src/ringview.cc
+++ b/src/ringview.cc
@@ -18,6 +18,7 @@
 
 #include <config.h>
 
+#include "bidi.hh"
 #include "debug.h"
 #include "vtedefines.hh"
 #include "vteinternal.hh"
@@ -28,6 +29,9 @@ RingView::RingView()
 {
         m_ring = nullptr;
         m_start = m_len = m_width = 0;
+        m_rows_alloc_len = m_bidirows_alloc_len = 0;
+
+        m_bidirunner = new BidiRunner(this);
 
         m_invalid = true;
         m_paused = true;
@@ -36,6 +40,7 @@ RingView::RingView()
 RingView::~RingView()
 {
         pause();
+        delete m_bidirunner;
 }
 
 /* Pausing a RingView frees up pretty much all of its memory.
@@ -53,8 +58,8 @@ void RingView::pause()
         if (m_paused)
                 return;
 
-        _vte_debug_print (VTE_DEBUG_RINGVIEW, "Ringview: pause, freeing %d rows.\n",
-                                              m_rows_alloc_len);
+        _vte_debug_print (VTE_DEBUG_RINGVIEW, "Ringview: pause, freeing %d rows, %d bidirows.\n",
+                                              m_rows_alloc_len, m_bidirows_alloc_len);
 
         for (i = 0; i < m_rows_alloc_len; i++) {
                 _vte_row_data_fini(m_rows[i]);
@@ -63,6 +68,12 @@ void RingView::pause()
         g_free (m_rows);
         m_rows_alloc_len = 0;
 
+        for (i = 0; i < m_bidirows_alloc_len; i++) {
+                delete m_bidirows[i];
+        }
+        g_free (m_bidirows);
+        m_bidirows_alloc_len = 0;
+
         m_invalid = true;
         m_paused = true;
 }
@@ -81,8 +92,19 @@ void RingView::resume()
                 _vte_row_data_init (m_rows[i]);
         }
 
-        _vte_debug_print (VTE_DEBUG_RINGVIEW, "Ringview: resume, allocating %d rows\n",
-                                              m_rows_alloc_len);
+        /* +2: Likely prevent a quickly following realloc.
+         * The number of lines of interest keeps jumping up and down by one
+         * due to per-pixel scrolling, and by another one due sometimes having
+         * to reshuffle another line below the bottom for the overflowing bits
+         * of the outline rectangle cursor. */
+        m_bidirows_alloc_len = m_len + 2;
+        m_bidirows = (BidiRow **) g_malloc (sizeof (BidiRow *) * m_bidirows_alloc_len);
+        for (int i = 0; i < m_bidirows_alloc_len; i++) {
+                m_bidirows[i] = new BidiRow();
+        }
+
+        _vte_debug_print (VTE_DEBUG_RINGVIEW, "Ringview: resume, allocating %d rows, %d bidirows\n",
+                                              m_rows_alloc_len, m_bidirows_alloc_len);
 
         m_paused = false;
 }
@@ -120,6 +142,21 @@ void RingView::set_rows(vte::grid::row_t start, vte::grid::row_t len)
 
         /* m_rows is expanded on demand in update() */
 
+        /* m_bidirows needs exactly this many lines */
+        if (G_UNLIKELY (!m_paused && len > m_bidirows_alloc_len)) {
+                int i = m_bidirows_alloc_len;
+                while (len > m_bidirows_alloc_len) {
+                        /* Don't realloc too aggressively. */
+                        m_bidirows_alloc_len = MAX (m_bidirows_alloc_len + 1, m_bidirows_alloc_len * 1.25 /* 
whatever */);
+                }
+                _vte_debug_print (VTE_DEBUG_RINGVIEW, "Ringview: reallocate to %d bidirows\n",
+                                                      m_bidirows_alloc_len);
+                m_bidirows = (BidiRow **) g_realloc (m_bidirows, sizeof (BidiRow *) * m_bidirows_alloc_len);
+                for (; i < m_bidirows_alloc_len; i++) {
+                        m_bidirows[i] = new BidiRow();
+                }
+        }
+
         m_start = start;
         m_len = len;
         m_invalid = true;
@@ -207,8 +244,9 @@ void RingView::update()
                 row_data = m_rows[row - m_top];
                 if (!row_data->attr.soft_wrapped || row == m_top + m_rows_len - 1) {
                         /* Found a paragraph from @top to @row, inclusive. */
+                        m_bidirunner->paragraph(top, row + 1);
 
-                        /* Doing BiDi, syntax highlighting etc. come here in the future. */
+                        /* Doing syntax highlighting etc. come here in the future. */
 
                         top = row + 1;
                 }
@@ -217,3 +255,23 @@ void RingView::update()
 
         m_invalid = false;
 }
+
+BidiRow const* RingView::get_bidirow(vte::grid::row_t row) const
+{
+        g_assert_cmpint (row, >=, m_start);
+        g_assert_cmpint (row, <, m_start + m_len);
+        g_assert_false (m_invalid);
+        g_assert_false (m_paused);
+
+        return m_bidirows[row - m_start];
+}
+
+/* For internal use by BidiRunner. Get where the BiDi mapping for the given row
+ * needs to be stored, of nullptr if it's a context row. */
+BidiRow* RingView::get_bidirow_writable(vte::grid::row_t row) const
+{
+        if (row < m_start || row >= m_start + m_len)
+                return nullptr;
+
+        return m_bidirows[row - m_start];
+}
diff --git a/src/ringview.hh b/src/ringview.hh
index fe54ea2d..fbe2c218 100644
--- a/src/ringview.hh
+++ b/src/ringview.hh
@@ -20,6 +20,7 @@
 
 #include <glib.h>
 
+#include "bidi.hh"
 #include "ring.hh"
 #include "vterowdata.hh"
 #include "vtetypes.hh"
@@ -29,6 +30,9 @@ namespace vte {
 
 namespace base {  // FIXME ???
 
+class BidiRow;
+class BidiRunner;
+
 /*
  * RingView provides a "view" to a continuous segment of the Ring (or stream),
  * typically the user visible area.
@@ -72,6 +76,9 @@ public:
 
         VteRowData *get_row(vte::grid::row_t row);
 
+        BidiRow const* get_bidirow(vte::grid::row_t row) const;
+        BidiRow* get_bidirow_writable(vte::grid::row_t row) const;
+
 private:
         Ring *m_ring;
 
@@ -79,6 +86,11 @@ private:
         int m_rows_len;
         int m_rows_alloc_len;
 
+        BidiRow **m_bidirows;
+        int m_bidirows_alloc_len;
+
+        BidiRunner *m_bidirunner;
+
         vte::grid::row_t m_top;  /* the row of the Ring corresponding to m_rows[0] */
 
         vte::grid::row_t m_start;
diff --git a/src/vtegtk.cc b/src/vtegtk.cc
index f8f296b9..f5d92d0f 100644
--- a/src/vtegtk.cc
+++ b/src/vtegtk.cc
@@ -1787,6 +1787,12 @@ const char *
 vte_get_features (void)
 {
         return
+#ifdef WITH_FRIBIDI
+                "+BIDI"
+#else
+                "-BIDI"
+#endif
+                " "
 #ifdef WITH_GNUTLS
                 "+GNUTLS"
 #else
diff --git a/src/vteunistr.cc b/src/vteunistr.cc
index 69c47fe0..319bd6bf 100644
--- a/src/vteunistr.cc
+++ b/src/vteunistr.cc
@@ -162,6 +162,39 @@ _vte_unistr_get_base (vteunistr s)
        return (gunichar) s;
 }
 
+void
+_vte_unistr_append_to_gunichars (vteunistr s, GArray *a)
+{
+        if (G_UNLIKELY (s >= VTE_UNISTR_START)) {
+                struct VteUnistrDecomp *decomp;
+                decomp = &DECOMP_FROM_UNISTR (s);
+                _vte_unistr_append_to_gunichars (decomp->prefix, a);
+                s = decomp->suffix;
+        }
+        gunichar val = (gunichar) s;
+        g_array_append_val (a, val);
+}
+
+vteunistr
+_vte_unistr_replace_base (vteunistr s, gunichar c)
+{
+        g_return_val_if_fail (s < unistr_next, s);
+
+        if (G_LIKELY (_vte_unistr_get_base(s) == c))
+                return s;
+
+        GArray *a = g_array_new (FALSE, FALSE, sizeof (gunichar));
+        _vte_unistr_append_to_gunichars (s, a);
+        g_assert_cmpint(a->len, >=, 1);
+
+        s = c;
+        for (glong i = 1; i < a->len; i++)
+                s = _vte_unistr_append_unichar (s, g_array_index (a, gunichar, i));
+
+        g_array_free (a, TRUE);
+        return s;
+}
+
 void
 _vte_unistr_append_to_string (vteunistr s, GString *gs)
 {
diff --git a/src/vteunistr.h b/src/vteunistr.h
index 9bde6b2c..84cb0562 100644
--- a/src/vteunistr.h
+++ b/src/vteunistr.h
@@ -73,6 +73,19 @@ _vte_unistr_append_unistr (vteunistr s, vteunistr t);
 gunichar
 _vte_unistr_get_base (vteunistr s);
 
+/**
+ * _vte_unistr_append_to_string:
+ * @s: a #vteunistr
+ * @c: Unicode character to replace the base character of @s.
+ *
+ * Creates a vteunistr value where the base character from @s is
+ * replaced by @c, while the combining characters from @s are carried over.
+ *
+ * Returns: the new #vteunistr value
+ */
+vteunistr
+_vte_unistr_replace_base (vteunistr s, gunichar c);
+
 /**
  * _vte_unistr_append_to_string:
  * @s: a #vteunistr
@@ -84,6 +97,16 @@ _vte_unistr_get_base (vteunistr s);
 void
 _vte_unistr_append_to_string (vteunistr s, GString *gs);
 
+/**
+ * _vte_unistr_append_to_gunichars:
+ * @s: a #vteunistr
+ * @a: a #GArray of #gunichar items to append @s to
+ *
+ * Appends @s to @a.
+ **/
+void
+_vte_unistr_append_to_gunichars (vteunistr s, GArray *a);
+
 /**
  * _vte_unistr_strlen:
  * @s: a #vteunistr


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]