[LyX features/breakrows] Use unordered maps to store inset and math rows geometry.

Jean-Marc Lasgouttes lasgouttes at lyx.org
Tue Oct 5 15:31:47 UTC 2021


The branch, breakrows, has been updated.
  discards  aa3f55a4e31f2631e8c77d10a5479979c05e7b89 (commit)
  discards  8224ebef5e34b5997cc101c0a387414d49f55e81 (commit)
  discards  94adb4facfaeba960a7da8fe1f5eac83af332730 (commit)
  discards  1d0fbca260492ac48f870ae709848c5def91ef51 (commit)
  discards  4e9c754bbb097a836220cb80cadb563c9ce48d2d (commit)
  discards  13a905b4a4519ad6aa0990798446dad39f9b82bf (commit)
  discards  4ff27953128eb2920f40e63b599fe5dd0315421f (commit)
  discards  0d6ebc9ba679d868a8ad66a24b5346ec6e918942 (commit)
  discards  22eead1c271451b09f9cf0b8d3b810385853d043 (commit)
  discards  d9a196ffde2f41639a7f24d45a83b97597b3bcdc (commit)
  discards  a7138ae74a424ec7a8565d1b3edbe3781c6c593a (commit)
  discards  cc4b822abe80598e9ac5418e857b018bb7c48169 (commit)
  discards  5e251d8e6983f024e53bd71f769955b7ad85f942 (commit)
  discards  6a195673db223be1016b01a254561b26d3312b59 (commit)
  discards  3d9988b6e65bca3368f8ab2206a6811761d4db7a (commit)
  discards  2a81a4393409340f9d2ef01d9a8e5b9e42ec7623 (commit)
  discards  52309a9097d4106494312ae3ac392a6060ca9a7b (commit)
  discards  30d23f2350d1252c5ef2d923a011cc5ff5d743b5 (commit)
  discards  729d2fac569544837ec9bb3b2885c58ee4f419a1 (commit)
  discards  dfff28800d34fa1dacdb4d90d8d704ef18c9a1d0 (commit)
  discards  ad55a82992866067ae245da73d55e63aafbfd693 (commit)
  discards  7e07630120614e2561a023c1a7ae92f447eedee0 (commit)

This update added new revisions after undoing existing revisions.  That is
to say, the old revision is not a strict subset of the new revision.  This
situation occurs when you --force push a change and generate a repository
containing something like this:

 * -- * -- B -- O -- O -- O (aa3f55a4e31f2631e8c77d10a5479979c05e7b89)
            \
             N -- N -- N (04a4d50774a6b153f8533b3929c78a2bb488e49e)

When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.

- Log -----------------------------------------------------------------

commit 04a4d50774a6b153f8533b3929c78a2bb488e49e
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Tue Oct 5 16:58:49 2021 +0200

    Use unordered maps to store inset and math rows geometry.
    
    Simply using unordered_map instead of map makes a big difference for
    documents with large text insets.
    
    Related to bug #12297.

diff --git a/src/BufferView.cpp b/src/BufferView.cpp
index 526cb33..0f36dca 100644
--- a/src/BufferView.cpp
+++ b/src/BufferView.cpp
@@ -228,7 +228,7 @@ struct BufferView::Private
 	///
 	CoordCache coord_cache_;
 	///
-	typedef map<MathData const *, MathRow> MathRows;
+	typedef unordered_map<MathData const *, MathRow> MathRows;
 	MathRows math_rows_;
 
 	/// this is used to handle XSelection events in the right manner.
diff --git a/src/CoordCache.h b/src/CoordCache.h
index d1c0fa3..2780fc3 100644
--- a/src/CoordCache.h
+++ b/src/CoordCache.h
@@ -16,7 +16,7 @@
 
 #include "Dimension.h"
 
-#include <map>
+#include <unordered_map>
 
 namespace lyx {
 
@@ -153,7 +153,7 @@ private:
 			lyxbreaker(thing, hint, data_.size());
 	}
 
-	typedef std::map<T const *, Geometry> cache_type;
+	typedef std::unordered_map<T const *, Geometry> cache_type;
 	cache_type data_;
 };
 

commit 15b0e32df7d02dfbd2807929146cadbd2ce7306e
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Tue Oct 5 15:52:31 2021 +0200

    Increase metrics cache maximal size
    
    Increase the maximal size of the breakString cache (to compute where
    to break lines) from 512kB to 10MB. This has a big impact of cache
    hits on large file like the example in #12297, which is now 99%. On
    this example the time taken by breakString decreases from 33.5us to
    2.4us.
    
    The string width cache has been increased fro 512kB to 1MB, but this
    does not make such a big difference.
    
    Additionally, comments and variable names have been improved.
    
    Related to bug #12297.

diff --git a/src/frontends/qt/GuiFontMetrics.cpp b/src/frontends/qt/GuiFontMetrics.cpp
index 3feb33e..e51d40f 100644
--- a/src/frontends/qt/GuiFontMetrics.cpp
+++ b/src/frontends/qt/GuiFontMetrics.cpp
@@ -86,25 +86,27 @@ namespace lyx {
 namespace frontend {
 
 
-/*
- * Limit (strwidth|breakstr)_cache_ size to 512kB of string data.
- * Limit qtextlayout_cache_ size to 500 elements (we do not know the
- * size of the QTextLayout objects anyway).
- * Note that all these numbers are arbitrary.
- * Also, setting size to 0 is tantamount to disabling the cache.
- */
-int cache_metrics_width_size = 1 << 19;
-int cache_metrics_breakstr_size = 1 << 19;
+namespace {
+// Maximal size/cost for various caches. See QCache documentation to
+// see what cost means.
+
+// Limit strwidth_cache_ total cost to 1MB of string data.
+int const strwidth_cache_max_cost = 1024 * 1024;
+// Limit breakat_cache_ total cost to 10MB of string data.
+// This is useful for documents with very large insets.
+int const breakstr_cache_max_cost = 10 * 1024 * 1024;
 // Qt 5.x already has its own caching of QTextLayout objects
 // but it does not seem to work well on MacOS X.
 #if (QT_VERSION < 0x050000) || defined(Q_OS_MAC)
-int cache_metrics_qtextlayout_size = 500;
+// Limit qtextlayout_cache_ size to 500 elements (we do not know the
+// size of the QTextLayout objects anyway).
+int const qtextlayout_cache_max_size = 500;
 #else
-int cache_metrics_qtextlayout_size = 0;
+// Disable the cache
+int const qtextlayout_cache_max_size = 0;
 #endif
 
 
-namespace {
 /**
  * Convert a UCS4 character into a QChar.
  * This is a hack (it does only make sense for the common part of the UCS4
@@ -128,9 +130,9 @@ inline QChar const ucs4_to_qchar(char_type const ucs4)
 
 GuiFontMetrics::GuiFontMetrics(QFont const & font)
 	: font_(font), metrics_(font, 0),
-	  strwidth_cache_(cache_metrics_width_size),
-	  breakstr_cache_(cache_metrics_breakstr_size),
-	  qtextlayout_cache_(cache_metrics_qtextlayout_size)
+	  strwidth_cache_(strwidth_cache_max_cost),
+	  breakstr_cache_(breakstr_cache_max_cost),
+	  qtextlayout_cache_(qtextlayout_cache_max_size)
 {
 	// Determine italic slope
 	double const defaultSlope = tan(qDegreesToRadians(19.0));

commit 582042450650da46699d13f5bd93f7bfd681007d
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Wed Sep 22 15:09:26 2021 +0200

    Improve row flushing
    
    Add new row flags Flush and FlushBefore to let insets indicate whether
    they cause flushing of current row (eg. newline) or of previous row
    (e.g. display insets).

diff --git a/src/RowFlags.h b/src/RowFlags.h
index f94f0c6..01c4dd2 100644
--- a/src/RowFlags.h
+++ b/src/RowFlags.h
@@ -32,22 +32,26 @@ enum RowFlags {
 	BreakBefore = 1 << 0,
 	// Avoid breaking row before this element
 	NoBreakBefore = 1 << 1,
+	// flush the row before this element (useful with BreakBefore)
+	FlushBefore = 1 << 2,
 	// force new (maybe empty) row after this element
-	AlwaysBreakAfter = 1 << 2,
+	AlwaysBreakAfter = 1 << 3,
 	// break row after this element if there are more elements
-	BreakAfter = 1 << 3,
+	BreakAfter = 1 << 4,
 	// break row whenever needed after this element
-	CanBreakAfter = 1 << 4,
+	CanBreakAfter = 1 << 5,
 	// Avoid breaking row after this element
-	NoBreakAfter = 1 << 5,
+	NoBreakAfter = 1 << 6,
 	// The contents of the row may be broken in two (e.g. string)
-	CanBreakInside = 1 << 6,
+	CanBreakInside = 1 << 7,
+	// Flush the row that ends with this element
+	Flush = 1 << 8,
 	// specify an alignment (left, right) for a display element
 	// (default is center)
-	AlignLeft = 1 << 7,
-	AlignRight = 1 << 8,
+	AlignLeft = 1 << 9,
+	AlignRight = 1 << 10,
 	// A display element breaks row at both ends
-	Display = BreakBefore | BreakAfter,
+	Display = FlushBefore | BreakBefore | BreakAfter,
 	// Flags that concern breaking after element
 	AfterFlags = AlwaysBreakAfter | BreakAfter | CanBreakAfter | NoBreakAfter
 };
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index 3d89aba..9647cad 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -1068,7 +1068,6 @@ void cleanupRow(Row & row, bool at_end)
 	}
 
 	row.endpos(row.back().endpos);
-	row.flushed(at_end);
 	// remove trailing spaces on row break
 	if (!at_end)
 		row.back().rtrim();
@@ -1118,8 +1117,11 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 		int const f2 = (fcit == end) ? (end_label ? Inline : NoBreakBefore)
 		                             : fcit->row_flags;
 		if (rows.empty() || needsRowBreak(f1, f2)) {
-			if (!rows.empty())
+			if (!rows.empty()) {
 				cleanupRow(rows.back(), false);
+				// Flush row as requested by row flags
+				rows.back().flushed((f1 & Flush) || (f2 & FlushBefore));
+			}
 			pos_type pos = rows.empty() ? 0 : rows.back().endpos();
 			rows.push_back(newRow(*this, bigrow.pit(), pos, is_rtl));
 			// the width available for the row.
diff --git a/src/insets/InsetNewline.cpp b/src/insets/InsetNewline.cpp
index f5fcd42..ecfacf4 100644
--- a/src/insets/InsetNewline.cpp
+++ b/src/insets/InsetNewline.cpp
@@ -39,6 +39,15 @@ InsetNewline::InsetNewline() : Inset(nullptr)
 {}
 
 
+int InsetNewline::rowFlags() const
+{
+	if (params_.kind == InsetNewlineParams::LINEBREAK)
+		return AlwaysBreakAfter;
+	else
+	    return AlwaysBreakAfter | Flush;
+}
+
+
 void InsetNewlineParams::write(ostream & os) const
 {
 	switch (kind) {
diff --git a/src/insets/InsetNewline.h b/src/insets/InsetNewline.h
index 1ef0ae5..c85a97d 100644
--- a/src/insets/InsetNewline.h
+++ b/src/insets/InsetNewline.h
@@ -47,7 +47,7 @@ public:
 	explicit InsetNewline(InsetNewlineParams par) : Inset(0)
 	{ params_.kind = par.kind; }
 	///
-	int rowFlags() const override { return AlwaysBreakAfter; }
+	int rowFlags() const override;
 	///
 	static void string2params(std::string const &, InsetNewlineParams &);
 	///
diff --git a/src/insets/InsetSeparator.h b/src/insets/InsetSeparator.h
index 9352bdf..0c12d95 100644
--- a/src/insets/InsetSeparator.h
+++ b/src/insets/InsetSeparator.h
@@ -65,7 +65,7 @@ public:
 		return docstring();
 	}
 	///
-	int rowFlags() const override { return BreakAfter; }
+	int rowFlags() const override { return BreakAfter | Flush; }
 private:
 	///
 	InsetCode lyxCode() const override { return SEPARATOR_CODE; }

commit e179c73934c0c6e11007d2787b7fc2f4d41570f7
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Wed Sep 22 13:17:46 2021 +0200

    Simplify setting of RTL in rows
    
    Set RTL status at row creation, which allows to remove a parameter from
    cleanupRow.

diff --git a/src/Row.cpp b/src/Row.cpp
index 6b0faa3..4b9836e 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -624,7 +624,7 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 }
 
 
-void Row::reverseRTL(bool const rtl_par)
+void Row::reverseRTL()
 {
 	pos_type i = 0;
 	pos_type const end = elements_.size();
@@ -636,14 +636,13 @@ void Row::reverseRTL(bool const rtl_par)
 			++j;
 		// if the direction is not the same as the paragraph
 		// direction, the sequence has to be reverted.
-		if (rtl != rtl_par)
+		if (rtl != rtl_)
 			reverse(elements_.begin() + i, elements_.begin() + j);
 		i = j;
 	}
 	// If the paragraph itself is RTL, reverse everything
-	if (rtl_par)
+	if (rtl_)
 		reverse(elements_.begin(), elements_.end());
-	rtl_ = rtl_par;
 }
 
 Row::const_iterator const
diff --git a/src/Row.h b/src/Row.h
index bf49eb1..2c60638 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -301,10 +301,12 @@ public:
 	 * Find sequences of right-to-left elements and reverse them.
 	 * This should be called once the row is completely built.
 	 */
-	void reverseRTL(bool rtl_par);
+	void reverseRTL();
 	///
 	bool isRTL() const { return rtl_; }
 	///
+	void setRTL(bool rtl) { rtl_ = rtl; }
+	///
 	bool needsChangeBar() const { return changebar_; }
 	///
 	void needsChangeBar(bool ncb) { changebar_ = ncb; }
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index caa802c..3d89aba 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -1050,6 +1050,7 @@ Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
 	nrow.pos(pos);
 	nrow.left_margin = tm.leftMargin(pit, pos);
 	nrow.right_margin = tm.rightMargin(pit);
+	nrow.setRTL(is_rtl);
 	if (is_rtl)
 		swap(nrow.left_margin, nrow.right_margin);
 	// Remember that the row width takes into account the left_margin
@@ -1059,7 +1060,7 @@ Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
 }
 
 
-void cleanupRow(Row & row, bool at_end, bool is_rtl)
+void cleanupRow(Row & row, bool at_end)
 {
 	if (row.empty()) {
 		row.endpos(0);
@@ -1074,7 +1075,7 @@ void cleanupRow(Row & row, bool at_end, bool is_rtl)
 	// boundary exists when there was no space at the end of row
 	row.right_boundary(!at_end && row.back().endpos == row.endpos());
 	// make sure that the RTL elements are in reverse ordering
-	row.reverseRTL(is_rtl);
+	row.reverseRTL();
 }
 
 
@@ -1118,7 +1119,7 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 		                             : fcit->row_flags;
 		if (rows.empty() || needsRowBreak(f1, f2)) {
 			if (!rows.empty())
-				cleanupRow(rows.back(), false, is_rtl);
+				cleanupRow(rows.back(), false);
 			pos_type pos = rows.empty() ? 0 : rows.back().endpos();
 			rows.push_back(newRow(*this, bigrow.pit(), pos, is_rtl));
 			// the width available for the row.
@@ -1152,7 +1153,7 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 	}
 
 	if (!rows.empty()) {
-		cleanupRow(rows.back(), true, is_rtl);
+		cleanupRow(rows.back(), true);
 		// Last row in paragraph is flushed
 		rows.back().flushed(true);
 	}

commit 041df8a835df8c55382114cba0d7e2f461fb92cc
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Mon Sep 6 14:52:42 2021 +0200

    Break multi-row strings in one pass
    
    Replace FontMetrics::breakAt, which returned the next break point,
    with FontMetrics::breakString, which returns a vector of break points.
    To this end, an additional parameter gives the available width for
    next rows.
    
    Rename various variables and methods accordingly. Factor the code in
    breakString_helper to be more manageable.
    
    Adapt Row::Element::splitAt to return a bool on sucess and provide
    remaining row elements in a vector. The width noted above has been
    added as parameters.
    
    Rename the helper function splitFrom to moveElements and rewrite the
    code to be more efficient.
    
    Remove type of row element INVALID, which is not needed anymore.
    
    The code in TextMetrics::breakParagraph is now much simpler.
    
    In Row::finalize, remove the code that computed inconditionnally the
    current element size, and make sure that this width will be computed
    in all code paths of Row::Element::splitAt.

diff --git a/src/Row.cpp b/src/Row.cpp
index b7e4c07..6b0faa3 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -123,41 +123,81 @@ pos_type Row::Element::x2pos(int &x) const
 			x = 0;
 			i = isRTL();
 		}
-		break;
-	case INVALID:
-		LYXERR0("x2pos: INVALID row element !");
 	}
 	//lyxerr << "=> p=" << pos + i << " x=" << x << endl;
 	return pos + i;
 }
 
 
-Row::Element Row::Element::splitAt(int w, bool force)
+bool Row::Element::splitAt(int const width, int next_width, bool force,
+                           Row::Elements & tail)
 {
-	if (type != STRING || !(row_flags & CanBreakInside))
-		return Element();
+	// Not a string or already OK.
+	if (type != STRING || (dim.wid > 0 && dim.wid < width))
+		return false;
 
 	FontMetrics const & fm = theFontMetrics(font);
-	dim.wid = w;
-	int const i = fm.breakAt(str, dim.wid, isRTL(), force);
-	if (i != -1) {
-		//Create a second row element to return
-		Element ret(STRING, pos + i, font, change);
-		ret.str = str.substr(i);
-		ret.endpos = ret.pos + ret.str.length();
-		// Copy the after flags of the original element to the second one.
-		ret.row_flags = row_flags & (CanBreakInside | AfterFlags);
-
-		// Now update ourselves
-		str.erase(i);
-		endpos = pos + i;
-		// Row should be broken after the original element
-		row_flags = (row_flags & ~AfterFlags) | BreakAfter;
-		//LYXERR0("breakAt(" << w << ")  Row element Broken at " << w << "(w(str)=" << fm.width(str) << "): e=" << *this);
-		return ret;
+
+	// A a string that is not breakable
+	if (!(row_flags & CanBreakInside)) {
+		// has width been computed yet?
+		if (dim.wid == 0)
+			dim.wid = fm.width(str);
+		return false;
+	}
+
+	bool const wrap_any = !font.language()->wordWrap();
+	FontMetrics::Breaks breaks = fm.breakString(str, width, next_width,
+                                                isRTL(), wrap_any | force);
+
+	// if breaking did not really work, give up
+	if (!force && breaks.front().wid > width) {
+		if (dim.wid == 0)
+			dim.wid = fm.width(str);
+		return false;
+	}
+
+	Element first_e(STRING, pos, font, change);
+	// should next element eventually replace *this?
+	bool first = true;
+	docstring::size_type i = 0;
+	for (FontMetrics::Break const & brk : breaks) {
+		Element e(STRING, pos + i, font, change);
+		e.str = str.substr(i, brk.len);
+		e.endpos = e.pos + brk.len;
+		e.dim.wid = brk.wid;
+		e.row_flags = CanBreakInside | BreakAfter;
+		if (first) {
+			// this element eventually goes to *this
+			e.row_flags |= row_flags & ~AfterFlags;
+			first_e = e;
+			first = false;
+		} else
+			tail.push_back(e);
+		i += brk.len;
 	}
 
-	return Element();
+	if (!tail.empty()) {
+		// Avoid having a last empty element. This happens when
+		// breaking at the trailing space of string
+		if (tail.back().str.empty())
+			tail.pop_back();
+		else {
+			// Copy the after flags of the original element to the last one.
+			tail.back().row_flags &= ~BreakAfter;
+			tail.back().row_flags |= row_flags & AfterFlags;
+		}
+		// first_e row should be broken after the original element
+		first_e.row_flags |= BreakAfter;
+	} else {
+		// Restore the after flags of the original element.
+		first_e.row_flags &= ~BreakAfter;
+		first_e.row_flags |= row_flags & AfterFlags;
+	}
+
+	// update ourselves
+	swap(first_e, *this);
+	return true;
 }
 
 
@@ -265,10 +305,6 @@ ostream & operator<<(ostream & os, Row::Element const & e)
 		break;
 	case Row::SPACE:
 		os << "SPACE: ";
-		break;
-	case Row::INVALID:
-		os << "INVALID: ";
-		break;
 	}
 	os << "width=" << e.full_width() << ", row_flags=" << e.row_flags;
 	return os;
@@ -393,11 +429,6 @@ void Row::finalizeLast()
 	elt.final = true;
 	if (elt.change.changed())
 		changebar_ = true;
-
-	if (elt.type == STRING && elt.dim.wid == 0) {
-		elt.dim.wid = theFontMetrics(elt.font).width(elt.str);
-		dim_.wid += elt.dim.wid;
-	}
 }
 
 
@@ -474,19 +505,14 @@ void Row::pop_back()
 
 namespace {
 
-// Remove stuff after \c it from \c elts, and return it.
-// if \c init is provided, it will prepended to the rest
-Row::Elements splitFrom(Row::Elements & elts, Row::Elements::iterator const & it,
-                        Row::Element const & init = Row::Element())
+// Move stuff after \c it from \c from and the end of \c to.
+void moveElements(Row::Elements & from, Row::Elements::iterator const & it,
+                  Row::Elements & to)
 {
-	Row::Elements ret;
-	if (init.isValid())
-		ret.push_back(init);
-	ret.insert(ret.end(), it, elts.end());
-	elts.erase(it, elts.end());
-	if (!elts.empty())
-		elts.back().row_flags = (elts.back().row_flags & ~AfterFlags) | BreakAfter;
-	return ret;
+	to.insert(to.end(), it, from.end());
+	from.erase(it, from.end());
+	if (!from.empty())
+		from.back().row_flags = (from.back().row_flags & ~AfterFlags) | BreakAfter;
 }
 
 }
@@ -522,6 +548,7 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 	Elements::iterator cit_brk = cit;
 	int wid_brk = wid + cit_brk->dim.wid;
 	++cit_brk;
+	Elements tail;
 	while (cit_brk != beg) {
 		--cit_brk;
 		// make a copy of the element to work on it.
@@ -533,28 +560,18 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 		if (wid_brk <= w && brk.row_flags & CanBreakAfter) {
 			end_ = brk.endpos;
 			dim_.wid = wid_brk;
-			return splitFrom(elements_, cit_brk + 1);
+			moveElements(elements_, cit_brk + 1, tail);
+			return tail;
 		}
 		// assume now that the current element is not there
 		wid_brk -= brk.dim.wid;
-		/*
-		 * Some Asian languages split lines anywhere (no notion of
-		 * word). It seems that QTextLayout is not aware of this fact.
-		 * See for reference:
-		 *    https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages
-		 *
-		 * FIXME: Something shall be done about characters which are
-		 * not allowed at the beginning or end of line.
-		*/
-		bool const word_wrap = brk.font.language()->wordWrap();
 		/* We have found a suitable separable element. This is the common case.
-		 * Try to break it cleanly (at word boundary) at a length that is both
+		 * Try to break it cleanly at a length that is both
 		 * - less than the available space on the row
 		 * - shorter than the natural width of the element, in order to enforce
 		 *   break-up.
 		 */
-		Element remainder = brk.splitAt(min(w - wid_brk, brk.dim.wid - 2), !word_wrap);
-		if (brk.row_flags & BreakAfter) {
+		if (brk.splitAt(min(w - wid_brk, brk.dim.wid - 2), next_width, false, tail)) {
 			/* if this element originally did not cause a row overflow
 			 * in itself, and the remainder of the row would still be
 			 * too large after breaking, then we will have issues in
@@ -568,12 +585,10 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 			*cit_brk = brk;
 			dim_.wid = wid_brk + brk.dim.wid;
 			// If there are other elements, they should be removed.
-			// remainder can be empty when splitting at trailing space
-			if (remainder.str.empty())
-				return splitFrom(elements_, next(cit_brk, 1));
-			else
-				return splitFrom(elements_, next(cit_brk, 1), remainder);
+			moveElements(elements_, cit_brk + 1, tail);
+			return tail;
 		}
+		LATTEST(tail.empty());
 	}
 
 	if (cit != beg && cit->row_flags & NoBreakBefore) {
@@ -588,20 +603,23 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 		// been added. We can cut right here.
 		end_ = cit->pos;
 		dim_.wid = wid;
-		return splitFrom(elements_, cit);
+		moveElements(elements_, cit, tail);
+		return tail;
 	}
 
 	/* If we are here, it means that we have not found a separator to
-	 * shorten the row. Let's try to break it again, but not at word
-	 * boundary this time.
+	 * shorten the row. Let's try to break it again, but force
+	 * splitting this time.
 	 */
-	Element remainder = cit->splitAt(w - wid, true);
-	if (cit->row_flags & BreakAfter) {
+	if (cit->splitAt(w - wid, next_width, true, tail)) {
+		LYXERR0(*cit);
 		end_ = cit->endpos;
 		dim_.wid = wid + cit->dim.wid;
 		// If there are other elements, they should be removed.
-		return splitFrom(elements_, next(cit, 1), remainder);
+		moveElements(elements_, cit + 1, tail);
+		return tail;
 	}
+
 	return Elements();
 }
 
diff --git a/src/Row.h b/src/Row.h
index 1272a0f..bf49eb1 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -50,9 +50,7 @@ public:
 		// An inset
 		INSET,
 		// Some spacing described by its width, not a string
-		SPACE,
-		// Something that should not happen (for error handling)
-		INVALID
+		SPACE
 	};
 
 /**
@@ -61,8 +59,6 @@ public:
  */
 	struct Element {
 		//
-		Element() = default;
-		//
 		Element(Type const t, pos_type p, Font const & f, Change const & ch)
 			: type(t), pos(p), endpos(p + 1), font(f), change(ch) {}
 
@@ -94,13 +90,16 @@ public:
 		pos_type x2pos(int &x) const;
 		/** Break the element in two if possible, so that its width is less
 		 * than \param w.
-		 * \return an element containing the remainder of the text, or
-		 *   an invalid element if nothing happened.
-		 * \param w: the desired maximum width
-		 * \param force: if true, the string is cut at any place, otherwise it
-		 *   respects the row breaking rules of characters.
+		 * \return a vector of elements containing the remainder of
+		 *   the text (empty if nothing happened).
+		 * \param width maximum width of the row.
+		 * \param next_width available width on next row.
+		 * \param force: if true, cut string at any place, even for
+		 *   languages that wrap at word delimiters; if false, do not
+		 *   break at all if first element would larger than \c width.
 		 */
-		Element splitAt(int w, bool force);
+		// FIXME: ideally last parameter should be Elements&, but it is not possible.
+		bool splitAt(int width, int next_width, bool force, std::vector<Element> & tail);
 		// remove trailing spaces (useful for end of row)
 		void rtrim();
 
@@ -108,8 +107,6 @@ public:
 		bool isRTL() const { return font.isVisibleRightToLeft(); }
 		// This is true for virtual elements.
 		bool isVirtual() const { return type == VIRTUAL; }
-		// Invalid element, for error handling
-		bool isValid() const { return type !=INVALID; }
 
 		// Returns the position on left side of the element.
 		pos_type left_pos() const { return isRTL() ? endpos : pos; };
@@ -117,11 +114,11 @@ public:
 		pos_type right_pos() const { return isRTL() ? pos : endpos; };
 
 		// The kind of row element
-		Type type = INVALID;
+		Type type;
 		// position of the element in the paragraph
-		pos_type pos = 0;
+		pos_type pos;
 		// first position after the element in the paragraph
-		pos_type endpos = 0;
+		pos_type endpos;
 		// The dimension of the chunk (does not contains the
 		// separator correction)
 		Dimension dim;
@@ -289,8 +286,8 @@ public:
 	 * separator and update endpos if necessary. If all that
 	 * remains is a large word, cut it to \param width.
 	 * \param width maximum width of the row.
-	 * \param available width on next row.
-	 * \return true if the row has been shortened.
+	 * \param next_width available width on next row.
+	 * \return list of elements remaining after breaking.
 	 */
 	Elements shortenIfNeeded(int const width, int const next_width);
 
diff --git a/src/RowPainter.cpp b/src/RowPainter.cpp
index 656f89a..400b7b6 100644
--- a/src/RowPainter.cpp
+++ b/src/RowPainter.cpp
@@ -565,10 +565,6 @@ void RowPainter::paintText()
 
 		case Row::SPACE:
 			paintTextDecoration(e);
-			break;
-
-		case Row::INVALID:
-			LYXERR0("Trying to paint INVALID row element.");
 		}
 
 		// The markings of foreign languages
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index 689addc..caa802c 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -1059,7 +1059,7 @@ Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
 }
 
 
-void cleanupRow(Row & row, pos_type real_endpos, bool is_rtl)
+void cleanupRow(Row & row, bool at_end, bool is_rtl)
 {
 	if (row.empty()) {
 		row.endpos(0);
@@ -1067,11 +1067,12 @@ void cleanupRow(Row & row, pos_type real_endpos, bool is_rtl)
 	}
 
 	row.endpos(row.back().endpos);
+	row.flushed(at_end);
 	// remove trailing spaces on row break
-	if (row.endpos() < real_endpos)
+	if (!at_end)
 		row.back().rtrim();
 	// boundary exists when there was no space at the end of row
-	row.right_boundary(row.endpos() < real_endpos && row.back().endpos == row.endpos());
+	row.right_boundary(!at_end && row.back().endpos == row.endpos());
 	// make sure that the RTL elements are in reverse ordering
 	row.reverseRTL(is_rtl);
 }
@@ -1098,6 +1099,8 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 	RowList rows;
 	bool const is_rtl = text_->isRTL(bigrow.pit());
 	bool const end_label = text_->getEndLabel(bigrow.pit()) != END_LABEL_NO_LABEL;
+	int const next_width = max_width_ - leftMargin(bigrow.pit(), bigrow.endpos())
+		- rightMargin(bigrow.pit());
 
 	int width = 0;
 	flexible_const_iterator<Row> fcit = flexible_begin(bigrow);
@@ -1115,7 +1118,7 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 		                             : fcit->row_flags;
 		if (rows.empty() || needsRowBreak(f1, f2)) {
 			if (!rows.empty())
-				cleanupRow(rows.back(), bigrow.endpos(), is_rtl);
+				cleanupRow(rows.back(), false, is_rtl);
 			pos_type pos = rows.empty() ? 0 : rows.back().endpos();
 			rows.push_back(newRow(*this, bigrow.pit(), pos, is_rtl));
 			// the width available for the row.
@@ -1130,45 +1133,26 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 		// Next element to consider is either the top of the temporary
 		// pile, or the place when we were in main row
 		Row::Element elt = *fcit;
-		Row::Element next_elt = elt.splitAt(width - rows.back().width(),
-		                                    !elt.font.language()->wordWrap());
-		if (elt.dim.wid > width - rows.back().width()) {
-			Row & rb = rows.back();
-			rb.push_back(*fcit);
-			// if the row is too large, try to cut at last separator. In case
-			// of success, reset indication that the row was broken abruptly.
-			int const next_width = max_width_ - leftMargin(rb.pit(), rb.endpos())
-				- rightMargin(rb.pit());
-
-			Row::Elements next_elts = rb.shortenIfNeeded(width, next_width);
-
-			// Go to next element
-			++fcit;
-
-			// Handle later the elements returned by shortenIfNeeded.
-			if (!next_elts.empty()) {
-				rb.flushed(false);
-				fcit.put(next_elts);
-			}
-		} else {
-			// a new element in the row
-			rows.back().push_back(elt);
-			rows.back().finalizeLast();
-
-			// Go to next element
-			++fcit;
-
-			// Add a new next element on the pile
-			if (next_elt.isValid()) {
-				// do as if we inserted this element in the original row
-				if (!next_elt.str.empty())
-					fcit.put(next_elt);
-			}
+		Row::Elements tail;
+		elt.splitAt(width - rows.back().width(), next_width, false, tail);
+		Row & rb = rows.back();
+		rb.push_back(elt);
+		rb.finalizeLast();
+		if (rb.width() > width) {
+			LATTEST(tail.empty());
+			// if the row is too large, try to cut at last separator.
+			tail = rb.shortenIfNeeded(width, next_width);
 		}
+
+		// Go to next element
+		++fcit;
+
+		// Handle later the elements returned by splitAt or shortenIfNeeded.
+		fcit.put(tail);
 	}
 
 	if (!rows.empty()) {
-		cleanupRow(rows.back(), bigrow.endpos(), is_rtl);
+		cleanupRow(rows.back(), true, is_rtl);
 		// Last row in paragraph is flushed
 		rows.back().flushed(true);
 	}
diff --git a/src/frontends/FontMetrics.h b/src/frontends/FontMetrics.h
index b562ebf..b78bc2d 100644
--- a/src/frontends/FontMetrics.h
+++ b/src/frontends/FontMetrics.h
@@ -16,6 +16,8 @@
 
 #include "support/strfwd.h"
 
+#include <vector>
+
 /**
  * A class holding helper functions for determining
  * the screen dimensions of fonts.
@@ -121,15 +123,27 @@ public:
 	 * \param ws is the amount of extra inter-word space applied text justification.
 	 */
 	virtual int x2pos(docstring const & s, int & x, bool rtl, double ws) const = 0;
+
+	// The places where to break a string and the width of the resulting lines.
+	struct Break {
+		Break(int l, int w) : len(l), wid(w) {}
+		int len = 0;
+		int wid = 0;
+	};
+	typedef std::vector<Break> Breaks;
 	/**
-	 * Break string s at width at most x.
-	 * \return break position (-1 if not successful)
-	 * \param position x is updated to real width
-	 * \param rtl is true for right-to-left layout
+	 * Break a string in multiple fragments according to width limits.
+	 * \return a sequence of Break elements.
+	 * \param s is the string to break.
+	 * \param first_wid is the available width for first line.
+	 * \param wid is the available width for the next lines.
+	 * \param rtl is true for right-to-left layout.
 	 * \param force is false for breaking at word separator, true for
 	 *   arbitrary position.
 	 */
-	virtual int breakAt(docstring const & s, int & x, bool rtl, bool force) const = 0;
+	virtual Breaks
+	breakString(docstring const & s, int first_wid, int wid, bool rtl, bool force) const = 0;
+
 	/// return char dimension for the font.
 	virtual Dimension const dimension(char_type c) const = 0;
 	/**
diff --git a/src/frontends/qt/GuiFontMetrics.cpp b/src/frontends/qt/GuiFontMetrics.cpp
index 47537ae..3feb33e 100644
--- a/src/frontends/qt/GuiFontMetrics.cpp
+++ b/src/frontends/qt/GuiFontMetrics.cpp
@@ -19,6 +19,7 @@
 
 #include "support/convert.h"
 #include "support/lassert.h"
+#include "support/lstrings.h"
 #include "support/lyxlib.h"
 #include "support/debug.h"
 
@@ -86,14 +87,14 @@ namespace frontend {
 
 
 /*
- * Limit (strwidth|breakat)_cache_ size to 512kB of string data.
+ * Limit (strwidth|breakstr)_cache_ size to 512kB of string data.
  * Limit qtextlayout_cache_ size to 500 elements (we do not know the
  * size of the QTextLayout objects anyway).
  * Note that all these numbers are arbitrary.
  * Also, setting size to 0 is tantamount to disabling the cache.
  */
 int cache_metrics_width_size = 1 << 19;
-int cache_metrics_breakat_size = 1 << 19;
+int cache_metrics_breakstr_size = 1 << 19;
 // Qt 5.x already has its own caching of QTextLayout objects
 // but it does not seem to work well on MacOS X.
 #if (QT_VERSION < 0x050000) || defined(Q_OS_MAC)
@@ -128,7 +129,7 @@ inline QChar const ucs4_to_qchar(char_type const ucs4)
 GuiFontMetrics::GuiFontMetrics(QFont const & font)
 	: font_(font), metrics_(font, 0),
 	  strwidth_cache_(cache_metrics_width_size),
-	  breakat_cache_(cache_metrics_breakat_size),
+	  breakstr_cache_(cache_metrics_breakstr_size),
 	  qtextlayout_cache_(cache_metrics_qtextlayout_size)
 {
 	// Determine italic slope
@@ -485,11 +486,13 @@ int GuiFontMetrics::countExpanders(docstring const & str) const
 }
 
 
-pair<int, int>
-GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
-                               bool const rtl, bool const force) const
+namespace {
+
+const int brkStrOffset = 1 + BIDI_OFFSET;
+
+
+QString createBreakableString(docstring const & s, bool rtl, QTextLayout & tl)
 {
-	QTextLayout tl;
 	/* Qt will not break at a leading or trailing space, and we need
 	 * that sometimes, see http://www.lyx.org/trac/ticket/9921.
 	 *
@@ -518,34 +521,23 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
 		// Left-to-right override: forces to draw text left-to-right
 		qs =  QChar(0x202D) + qs;
 #endif
-	int const offset = 1 + BIDI_OFFSET;
+	return qs;
+}
 
-	tl.setText(qs);
-	tl.setFont(font_);
-	QTextOption to;
-	to.setWrapMode(force ? QTextOption::WrapAtWordBoundaryOrAnywhere
-	                     : QTextOption::WordWrap);
-	// Let QTextLine::naturalTextWidth() account for trailing spaces
-	// (horizontalAdvance() still does not).
-	to.setFlags(QTextOption::IncludeTrailingSpaces);
-	tl.setTextOption(to);
-	tl.beginLayout();
-	QTextLine line = tl.createLine();
-	line.setLineWidth(x);
-	tl.createLine();
-	tl.endLayout();
-	int line_wid = iround(line.horizontalAdvance());
-	if ((force && line.textLength() == offset) || line_wid > x)
-		return {-1, line_wid};
+
+docstring::size_type brkstr2str_pos(QString brkstr, docstring const & str, int pos)
+{
 	/* Since QString is UTF-16 and docstring is UCS-4, the offsets may
 	 * not be the same when there are high-plan unicode characters
 	 * (bug #10443).
 	 */
-	// The variable `offset' is here to account for the extra leading characters.
+	// The variable `brkStrOffset' is here to account for the extra leading characters.
 	// The ending character zerow_nbsp has to be ignored if the line is complete.
-	int const qlen = line.textLength() - offset - (line.textLength() == qs.length());
+	int const qlen = pos - brkStrOffset - (pos == brkstr.length());
 #if QT_VERSION < 0x040801 || QT_VERSION >= 0x050100
-	int len = qstring_to_ucs4(qs.mid(offset, qlen)).length();
+	auto const len = qstring_to_ucs4(brkstr.mid(brkStrOffset, qlen)).length();
+	// Avoid warning
+	(void)str;
 #else
 	/* Due to QTBUG-25536 in 4.8.1 <= Qt < 5.1.0, the string returned
 	 * by QString::toUcs4 (used by qstring_to_ucs4) may have wrong
@@ -555,52 +547,108 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
 	 * worthwhile to implement a dichotomy search if this shows up
 	 * under a profiler.
 	 */
-	int len = min(qlen, static_cast<int>(s.length()));
-	while (len >= 0 && toqstr(s.substr(0, len)).length() != qlen)
+	int len = min(qlen, static_cast<int>(str.length()));
+	while (len >= 0 && toqstr(str.substr(0, len)).length() != qlen)
 		--len;
 	LASSERT(len > 0 || qlen == 0, /**/);
 #endif
-	// Do not cut is the string is already short enough. We rely on
-	// naturalTextWidth() to catch the case where we cut at the trailing
-	// space.
-	if (len == static_cast<int>(s.length())
-		&& line.naturalTextWidth() <= x) {
-		len = -1;
-#if QT_VERSION < 0x050000
+	return len;
+}
+
+}
+
+FontMetrics::Breaks
+GuiFontMetrics::breakString_helper(docstring const & s, int first_wid, int wid,
+                                   bool rtl, bool force) const
+{
+	QTextLayout tl;
+	QString qs = createBreakableString(s, rtl, tl);
+	tl.setText(qs);
+	tl.setFont(font_);
+	QTextOption to;
+	/*
+	 * Some Asian languages split lines anywhere (no notion of
+	 * word). It seems that QTextLayout is not aware of this fact.
+	 * See for reference:
+	 *    https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages
+	 *
+	 * FIXME: Something shall be done about characters which are
+	 * not allowed at the beginning or end of line.
+	 */
+	to.setWrapMode(force ? QTextOption::WrapAtWordBoundaryOrAnywhere
+	                     : QTextOption::WordWrap);
+	// Let QTextLine::naturalTextWidth() account for trailing spaces
+	// (horizontalAdvance() still does not).
+	to.setFlags(QTextOption::IncludeTrailingSpaces);
+	tl.setTextOption(to);
+
+	bool first = true;
+	tl.beginLayout();
+	while(true) {
+		QTextLine line = tl.createLine();
+		if (!line.isValid())
+			break;
+		line.setLineWidth(first ? first_wid : wid);
+		tl.createLine();
+		first = false;
+	}
+	tl.endLayout();
+
+	Breaks breaks;
+	int pos = 0;
+	for (int i = 0 ; i < tl.lineCount() ; ++i) {
+		QTextLine const & line = tl.lineAt(i);
+		int const epos = brkstr2str_pos(qs, s, line.textStart() + line.textLength());
+#if QT_VERSION >= 0x050000
+		int const wid = i + 1 < tl.lineCount() ? iround(line.horizontalAdvance())
+		                                       : iround(line.naturalTextWidth());
+#else
 		// With some monospace fonts, the value of horizontalAdvance()
 		// can be wrong with Qt4. One hypothesis is that the invisible
 		// characters that we use are given a non-null width.
-		line_wid = width(s);
+		// FIXME: this is slower than it could be but we'll get rid of Qt4 anyway
+		int const wid = i + 1 < tl.lineCount() ? width(rtrim(s.substr(pos, epos - pos)))
+		                                       : width(s.substr(pos, epos - pos));
+#endif
+		breaks.emplace_back(epos - pos, wid);
+		pos = epos;
+#if 0
+		// FIXME: should it be kept in some form?
+		if ((force && line.textLength() == brkStrOffset) || line_wid > x)
+			return {-1, line_wid};
 #endif
+
 	}
-	return {len, line_wid};
+
+	return breaks;
 }
 
 
-uint qHash(BreakAtKey const & key)
+uint qHash(BreakStringKey const & key)
 {
-	int params = key.force + 2 * key.rtl + 4 * key.x;
+	// assume widths are less than 10000. This fits in 32 bits.
+	uint params = key.force + 2 * key.rtl + 4 * key.first_wid + 10000 * key.wid;
 	return std::qHash(key.s) ^ ::qHash(params);
 }
 
 
-int GuiFontMetrics::breakAt(docstring const & s, int & x, bool const rtl, bool const force) const
+FontMetrics::Breaks GuiFontMetrics::breakString(docstring const & s, int first_wid, int wid,
+                                                bool rtl, bool force) const
 {
-	PROFILE_THIS_BLOCK(breakAt);
+	PROFILE_THIS_BLOCK(breakString);
 	if (s.empty())
-		return false;
+		return Breaks();
 
-	BreakAtKey key{s, x, rtl, force};
-	pair<int, int> pp;
-	if (auto * pp_ptr = breakat_cache_.object_ptr(key))
-		pp = *pp_ptr;
+	BreakStringKey key{s, first_wid, wid, rtl, force};
+	Breaks brks;
+	if (auto * brks_ptr = breakstr_cache_.object_ptr(key))
+		brks = *brks_ptr;
 	else {
-		PROFILE_CACHE_MISS(breakAt);
-		pp = breakAt_helper(s, x, rtl, force);
-		breakat_cache_.insert(key, pp, sizeof(key) + s.size() * sizeof(char_type));
+		PROFILE_CACHE_MISS(breakString);
+		brks = breakString_helper(s, first_wid, wid, rtl, force);
+		breakstr_cache_.insert(key, brks, sizeof(key) + s.size() * sizeof(char_type));
 	}
-	x = pp.second;
-	return pp.first;
+	return brks;
 }
 
 
diff --git a/src/frontends/qt/GuiFontMetrics.h b/src/frontends/qt/GuiFontMetrics.h
index ef8588a..9501eb8 100644
--- a/src/frontends/qt/GuiFontMetrics.h
+++ b/src/frontends/qt/GuiFontMetrics.h
@@ -27,14 +27,16 @@
 namespace lyx {
 namespace frontend {
 
-struct BreakAtKey
+struct BreakStringKey
 {
-	bool operator==(BreakAtKey const & key) const {
-		return key.s == s && key.x == x && key.rtl == rtl && key.force == force;
+	bool operator==(BreakStringKey const & key) const {
+		return key.s == s && key.first_wid == first_wid && key.wid == wid
+			&& key.rtl == rtl && key.force == force;
 	}
 
 	docstring s;
-	int x;
+	int first_wid;
+	int wid;
 	bool rtl;
 	bool force;
 };
@@ -77,7 +79,7 @@ public:
 	int signedWidth(docstring const & s) const override;
 	int pos2x(docstring const & s, int pos, bool rtl, double ws) const override;
 	int x2pos(docstring const & s, int & x, bool rtl, double ws) const override;
-	int breakAt(docstring const & s, int & x, bool rtl, bool force) const override;
+	Breaks breakString(docstring const & s, int first_wid, int wid, bool rtl, bool force) const override;
 	Dimension const dimension(char_type c) const override;
 
 	void rectText(docstring const & str,
@@ -101,8 +103,8 @@ public:
 
 private:
 
-	std::pair<int, int> breakAt_helper(docstring const & s, int const x,
-	                                   bool const rtl, bool const force) const;
+	Breaks breakString_helper(docstring const & s, int first_wid, int wid,
+	                          bool rtl, bool force) const;
 
 	/// The font
 	QFont font_;
@@ -117,8 +119,8 @@ private:
 	mutable QHash<char_type, int> width_cache_;
 	/// Cache of string widths
 	mutable Cache<docstring, int> strwidth_cache_;
-	/// Cache for breakAt
-	mutable Cache<BreakAtKey, std::pair<int, int>> breakat_cache_;
+	/// Cache for breakString
+	mutable Cache<BreakStringKey, Breaks> breakstr_cache_;
 	/// Cache for QTextLayout
 	mutable Cache<TextLayoutKey, std::shared_ptr<QTextLayout>> qtextlayout_cache_;
 

commit 7976614d0696b894ed05addf448f1ae1fc34723e
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Mon Sep 20 17:32:18 2021 +0200

    Add operator<< for Row::Elements
    
    This is useful for debugging.

diff --git a/src/Row.cpp b/src/Row.cpp
index 8233a96..b7e4c07 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -275,6 +275,17 @@ ostream & operator<<(ostream & os, Row::Element const & e)
 }
 
 
+ostream & operator<<(ostream & os, Row::Elements const & elts)
+{
+	double x = 0;
+	for (Row::Element const & e : elts) {
+		os << "x=" << x << " => " << e << endl;
+		x += e.full_width();
+	}
+	return os;
+}
+
+
 ostream & operator<<(ostream & os, Row const & row)
 {
 	os << " pos: " << row.pos_ << " end: " << row.end_
@@ -286,11 +297,11 @@ ostream & operator<<(ostream & os, Row const & row)
 	   << " separator: " << row.separator
 	   << " label_hfill: " << row.label_hfill
 	   << " row_boundary: " << row.right_boundary() << "\n";
+	// We cannot use the operator above, unfortunately
 	double x = row.left_margin;
-	Row::Elements::const_iterator it = row.elements_.begin();
-	for ( ; it != row.elements_.end() ; ++it) {
-		os << "x=" << x << " => " << *it << endl;
-		x += it->full_width();
+	for (Row::Element const & e : row.elements_) {
+		os << "x=" << x << " => " << e << endl;
+		x += e.full_width();
 	}
 	return os;
 }
diff --git a/src/Row.h b/src/Row.h
index 7466797..1272a0f 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -378,6 +378,8 @@ private:
 	bool changebar_ = false;
 };
 
+std::ostream & operator<<(std::ostream & os, Row::Elements const & elts);
+
 
 /**
  * Each paragraph is broken up into a number of rows on the screen.

commit f626095ac713c33442a79117b09f70317bf8f222
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Thu Sep 2 15:16:28 2021 +0200

    Fix setting of row pos/endpos (overlapping rows)
    
    In TextMetrics::breakParagraph, get rid of the fragile `pos' local
    variable, which was not correctly updated. Rely on the endpos of the
    last element in row instead.
    
    Rewrite cleanupRow to rely on the endpos of last the row element to
    set row endpos, instead of a `pos' parameter.

diff --git a/src/Row.cpp b/src/Row.cpp
index 7b74694..8233a96 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -463,8 +463,8 @@ void Row::pop_back()
 
 namespace {
 
-// Remove stuff after it from elts, and return it.
-// if init is provided, it will be in front of the rest
+// Remove stuff after \c it from \c elts, and return it.
+// if \c init is provided, it will prepended to the rest
 Row::Elements splitFrom(Row::Elements & elts, Row::Elements::iterator const & it,
                         Row::Element const & init = Row::Element())
 {
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index d96141c..689addc 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -1059,15 +1059,19 @@ Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
 }
 
 
-void cleanupRow(Row & row, pos_type pos, pos_type real_endpos, bool is_rtl)
+void cleanupRow(Row & row, pos_type real_endpos, bool is_rtl)
 {
-	row.endpos(pos);
+	if (row.empty()) {
+		row.endpos(0);
+		return;
+	}
+
+	row.endpos(row.back().endpos);
 	// remove trailing spaces on row break
-	if (pos < real_endpos && !row.empty())
+	if (row.endpos() < real_endpos)
 		row.back().rtrim();
 	// boundary exists when there was no space at the end of row
-	row.right_boundary(!row.empty() && pos < real_endpos
-	                   && row.back().endpos == pos);
+	row.right_boundary(row.endpos() < real_endpos && row.back().endpos == row.endpos());
 	// make sure that the RTL elements are in reverse ordering
 	row.reverseRTL(is_rtl);
 }
@@ -1095,7 +1099,6 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 	bool const is_rtl = text_->isRTL(bigrow.pit());
 	bool const end_label = text_->getEndLabel(bigrow.pit()) != END_LABEL_NO_LABEL;
 
-	pos_type pos = 0;
 	int width = 0;
 	flexible_const_iterator<Row> fcit = flexible_begin(bigrow);
 	flexible_const_iterator<Row> const end = flexible_end(bigrow);
@@ -1112,7 +1115,8 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 		                             : fcit->row_flags;
 		if (rows.empty() || needsRowBreak(f1, f2)) {
 			if (!rows.empty())
-				cleanupRow(rows.back(), pos, bigrow.endpos(), is_rtl);
+				cleanupRow(rows.back(), bigrow.endpos(), is_rtl);
+			pos_type pos = rows.empty() ? 0 : rows.back().endpos();
 			rows.push_back(newRow(*this, bigrow.pit(), pos, is_rtl));
 			// the width available for the row.
 			width = max_width_ - rows.back().right_margin;
@@ -1150,7 +1154,6 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 			// a new element in the row
 			rows.back().push_back(elt);
 			rows.back().finalizeLast();
-			pos = elt.endpos;
 
 			// Go to next element
 			++fcit;
@@ -1165,7 +1168,7 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 	}
 
 	if (!rows.empty()) {
-		cleanupRow(rows.back(), pos, bigrow.endpos(), is_rtl);
+		cleanupRow(rows.back(), bigrow.endpos(), is_rtl);
 		// Last row in paragraph is flushed
 		rows.back().flushed(true);
 	}

commit 8cee51ece772f4c5cabb3280573d162bc455ef0e
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Wed Sep 1 16:54:28 2021 +0200

    Get rid of need_new_row boolean in breakParagraph
    
    Instead of having breakParagraph decide when breaking a row is
    necessary, let Row::shortenIfNeeded set the row_flag of the last
    element to request a row break. This was already done in splitAt.
    
    This is in preparation of splitAt splitting in more than two elements.

diff --git a/src/Row.cpp b/src/Row.cpp
index d3529a3..7b74694 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -473,6 +473,8 @@ Row::Elements splitFrom(Row::Elements & elts, Row::Elements::iterator const & it
 		ret.push_back(init);
 	ret.insert(ret.end(), it, elts.end());
 	elts.erase(it, elts.end());
+	if (!elts.empty())
+		elts.back().row_flags = (elts.back().row_flags & ~AfterFlags) | BreakAfter;
 	return ret;
 }
 
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index 0ffd8d8..d96141c 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -1042,6 +1042,7 @@ bool operator==(flexible_const_iterator<T> const & t1,
 	return t1.cit_ == t2.cit_ && t1.pile_.empty() && t2.pile_.empty();
 }
 
+
 Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
 {
 	Row nrow;
@@ -1071,6 +1072,7 @@ void cleanupRow(Row & row, pos_type pos, pos_type real_endpos, bool is_rtl)
 	row.reverseRTL(is_rtl);
 }
 
+
 // Implement the priorities described in RowFlags.h.
 bool needsRowBreak(int f1, int f2)
 {
@@ -1093,14 +1095,12 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 	bool const is_rtl = text_->isRTL(bigrow.pit());
 	bool const end_label = text_->getEndLabel(bigrow.pit()) != END_LABEL_NO_LABEL;
 
-	bool need_new_row = true;
 	pos_type pos = 0;
 	int width = 0;
 	flexible_const_iterator<Row> fcit = flexible_begin(bigrow);
 	flexible_const_iterator<Row> const end = flexible_end(bigrow);
 	while (true) {
-		bool const has_row = !rows.empty();
-		bool const row_empty = !has_row || rows.back().empty();
+		bool const row_empty = rows.empty() || rows.back().empty();
 		// The row flags of previous element, if there is one.
 		// Otherwise we use NoBreakAfter to avoid an empty row before
 		// e.g. a displayed equation.
@@ -1110,14 +1110,12 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 		// paragraph has an end label (for which an empty row is OK).
 		int const f2 = (fcit == end) ? (end_label ? Inline : NoBreakBefore)
 		                             : fcit->row_flags;
-		need_new_row |= needsRowBreak(f1, f2);
-		if (need_new_row) {
+		if (rows.empty() || needsRowBreak(f1, f2)) {
 			if (!rows.empty())
 				cleanupRow(rows.back(), pos, bigrow.endpos(), is_rtl);
 			rows.push_back(newRow(*this, bigrow.pit(), pos, is_rtl));
 			// the width available for the row.
 			width = max_width_ - rows.back().right_margin;
-			need_new_row = false;
 		}
 
 		// The stopping condition is here because we may need a new
@@ -1147,7 +1145,6 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 			if (!next_elts.empty()) {
 				rb.flushed(false);
 				fcit.put(next_elts);
-				need_new_row = true;
 			}
 		} else {
 			// a new element in the row
@@ -1163,7 +1160,6 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 				// do as if we inserted this element in the original row
 				if (!next_elt.str.empty())
 					fcit.put(next_elt);
-				need_new_row = true;
 			}
 		}
 	}

commit ba3460042f2f1352f51c852fe5e692d266ae1d4d
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Tue Aug 31 19:23:55 2021 +0200

    Centralize the code that removes trailing spaces from end row element.
    
    Move to Row::Element::rtrim the code in Row::shortenIfNeeded that
    removes trailing spaces from last element in row, so that it can be
    called when actually breaking a row.
    
    Fixes bug found by Kornel.

diff --git a/src/Row.cpp b/src/Row.cpp
index 38dbb85..d3529a3 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -35,7 +35,6 @@ using namespace std;
 
 namespace lyx {
 
-using support::rtrim;
 using frontend::FontMetrics;
 
 
@@ -162,6 +161,20 @@ Row::Element Row::Element::splitAt(int w, bool force)
 }
 
 
+void Row::Element::rtrim()
+{
+	if (type != STRING)
+		return;
+	/* This is intended for strings that have been created by splitAt.
+	 * They may have trailing spaces, but they are not counted in the
+	 * string length (QTextLayout feature, actually). We remove them,
+	 * and decrease endpos, since spaces at row break are invisible.
+	 */
+	str = support::rtrim(str);
+	endpos = pos + str.length();
+}
+
+
 bool Row::isMarginSelected(bool left, DocIterator const & beg,
 		DocIterator const & end) const
 {
@@ -539,14 +552,6 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 				break;
 			}
 			end_ = brk.endpos;
-			/* after breakAt, there may be spaces at the end of the
-			 * string, but they are not counted in the string length
-			 * (QTextLayout feature, actually). We remove them, but do
-			 * not change the end of the row, since spaces at row
-			 * break are invisible.
-			 */
-			brk.str = rtrim(brk.str);
-			brk.endpos = brk.pos + brk.str.length();
 			*cit_brk = brk;
 			dim_.wid = wid_brk + brk.dim.wid;
 			// If there are other elements, they should be removed.
@@ -578,11 +583,8 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 	 * boundary this time.
 	 */
 	Element remainder = cit->splitAt(w - wid, true);
-	if (remainder.isValid()) {
+	if (cit->row_flags & BreakAfter) {
 		end_ = cit->endpos;
-		// See comment above.
-		cit->str = rtrim(cit->str);
-		cit->endpos = cit->pos + cit->str.length();
 		dim_.wid = wid + cit->dim.wid;
 		// If there are other elements, they should be removed.
 		return splitFrom(elements_, next(cit, 1), remainder);
diff --git a/src/Row.h b/src/Row.h
index b0b1755..7466797 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -101,6 +101,8 @@ public:
 		 *   respects the row breaking rules of characters.
 		 */
 		Element splitAt(int w, bool force);
+		// remove trailing spaces (useful for end of row)
+		void rtrim();
 
 		//
 		bool isRTL() const { return font.isVisibleRightToLeft(); }
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index 3dcacb9..0ffd8d8 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -1061,6 +1061,10 @@ Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
 void cleanupRow(Row & row, pos_type pos, pos_type real_endpos, bool is_rtl)
 {
 	row.endpos(pos);
+	// remove trailing spaces on row break
+	if (pos < real_endpos && !row.empty())
+		row.back().rtrim();
+	// boundary exists when there was no space at the end of row
 	row.right_boundary(!row.empty() && pos < real_endpos
 	                   && row.back().endpos == pos);
 	// make sure that the RTL elements are in reverse ordering

commit 7b96df198b2c11ee09f90d201cde549d548d3a7d
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Tue Aug 31 15:58:56 2021 +0200

    Handle the case where breakAt cuts after trailing space
    
    In this case, the extra element returned should empty but valid. The
    row flag BreakAfter is set to indicate that we have a break there
    (this principle will be used more generally in a forthcoming commit).
    
    To detect that we cut at the trailing space, it is necessary to rely
    on the difference between QTextLine::horizontalAdvance() and
    QTextLine::naturalTextWidth() when the flag
    QTextOption::IncludeTrailingSpaces is used: the trailing space is
    taken into account in the later, but not in the former.
    
    Somme comments have been added to make code intent clearer.

diff --git a/src/Row.cpp b/src/Row.cpp
index 6e975a5..38dbb85 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -142,13 +142,19 @@ Row::Element Row::Element::splitAt(int w, bool force)
 	dim.wid = w;
 	int const i = fm.breakAt(str, dim.wid, isRTL(), force);
 	if (i != -1) {
+		//Create a second row element to return
 		Element ret(STRING, pos + i, font, change);
 		ret.str = str.substr(i);
 		ret.endpos = ret.pos + ret.str.length();
+		// Copy the after flags of the original element to the second one.
 		ret.row_flags = row_flags & (CanBreakInside | AfterFlags);
+
+		// Now update ourselves
 		str.erase(i);
 		endpos = pos + i;
-		//lyxerr << "breakAt(" << w << ")  Row element Broken at " << x << "(w(str)=" << fm.width(str) << "): e=" << *this << endl;
+		// Row should be broken after the original element
+		row_flags = (row_flags & ~AfterFlags) | BreakAfter;
+		//LYXERR0("breakAt(" << w << ")  Row element Broken at " << w << "(w(str)=" << fm.width(str) << "): e=" << *this);
 		return ret;
 	}
 
@@ -251,7 +257,7 @@ ostream & operator<<(ostream & os, Row::Element const & e)
 		os << "INVALID: ";
 		break;
 	}
-	os << "width=" << e.full_width();
+	os << "width=" << e.full_width() << ", row_flags=" << e.row_flags;
 	return os;
 }
 
@@ -522,7 +528,7 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 		 *   break-up.
 		 */
 		Element remainder = brk.splitAt(min(w - wid_brk, brk.dim.wid - 2), !word_wrap);
-		if (remainder.isValid()) {
+		if (brk.row_flags & BreakAfter) {
 			/* if this element originally did not cause a row overflow
 			 * in itself, and the remainder of the row would still be
 			 * too large after breaking, then we will have issues in
@@ -544,7 +550,11 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 			*cit_brk = brk;
 			dim_.wid = wid_brk + brk.dim.wid;
 			// If there are other elements, they should be removed.
-			return splitFrom(elements_, next(cit_brk, 1), remainder);
+			// remainder can be empty when splitting at trailing space
+			if (remainder.str.empty())
+				return splitFrom(elements_, next(cit_brk, 1));
+			else
+				return splitFrom(elements_, next(cit_brk, 1), remainder);
 		}
 	}
 
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index 187d0ed..3dcacb9 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -1157,7 +1157,8 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 			// Add a new next element on the pile
 			if (next_elt.isValid()) {
 				// do as if we inserted this element in the original row
-				fcit.put(next_elt);
+				if (!next_elt.str.empty())
+					fcit.put(next_elt);
 				need_new_row = true;
 			}
 		}
diff --git a/src/frontends/qt/GuiFontMetrics.cpp b/src/frontends/qt/GuiFontMetrics.cpp
index 56ed676..47537ae 100644
--- a/src/frontends/qt/GuiFontMetrics.cpp
+++ b/src/frontends/qt/GuiFontMetrics.cpp
@@ -525,6 +525,9 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
 	QTextOption to;
 	to.setWrapMode(force ? QTextOption::WrapAtWordBoundaryOrAnywhere
 	                     : QTextOption::WordWrap);
+	// Let QTextLine::naturalTextWidth() account for trailing spaces
+	// (horizontalAdvance() still does not).
+	to.setFlags(QTextOption::IncludeTrailingSpaces);
 	tl.setTextOption(to);
 	tl.beginLayout();
 	QTextLine line = tl.createLine();
@@ -557,8 +560,11 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
 		--len;
 	LASSERT(len > 0 || qlen == 0, /**/);
 #endif
-	// Do not cut is the string is already short enough
-	if (len == static_cast<int>(s.length())) {
+	// Do not cut is the string is already short enough. We rely on
+	// naturalTextWidth() to catch the case where we cut at the trailing
+	// space.
+	if (len == static_cast<int>(s.length())
+		&& line.naturalTextWidth() <= x) {
 		len = -1;
 #if QT_VERSION < 0x050000
 		// With some monospace fonts, the value of horizontalAdvance()

commit 4bd4ff1ed4c1d2f1c364bd542ef46cd4b7665dda
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Mon Aug 30 15:48:44 2021 +0200

    Workaround for Qt 4
    
    At least with Qt 4.8.7 on Ubuntu 16.04, QTextLine::lineWidth() can
    return a bogus value, at least with Courier font. One hypothesis is
    that the invisible characters that we use in breakAt_helper are given
    a non-null width.
    
    Work around it, although the exact bug has not been pinpointed.

diff --git a/src/frontends/qt/GuiFontMetrics.cpp b/src/frontends/qt/GuiFontMetrics.cpp
index 25bf7a4..56ed676 100644
--- a/src/frontends/qt/GuiFontMetrics.cpp
+++ b/src/frontends/qt/GuiFontMetrics.cpp
@@ -531,7 +531,7 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
 	line.setLineWidth(x);
 	tl.createLine();
 	tl.endLayout();
-	int const line_wid = iround(line.horizontalAdvance());
+	int line_wid = iround(line.horizontalAdvance());
 	if ((force && line.textLength() == offset) || line_wid > x)
 		return {-1, line_wid};
 	/* Since QString is UTF-16 and docstring is UCS-4, the offsets may
@@ -557,9 +557,16 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
 		--len;
 	LASSERT(len > 0 || qlen == 0, /**/);
 #endif
-	// si la chaîne est déjà trop courte, on ne coupe pas
-	if (len == static_cast<int>(s.length()))
+	// Do not cut is the string is already short enough
+	if (len == static_cast<int>(s.length())) {
 		len = -1;
+#if QT_VERSION < 0x050000
+		// With some monospace fonts, the value of horizontalAdvance()
+		// can be wrong with Qt4. One hypothesis is that the invisible
+		// characters that we use are given a non-null width.
+		line_wid = width(s);
+#endif
+	}
 	return {len, line_wid};
 }
 

commit 2ebc7ee4ab7b79dcc5d2ccb466675680c0575790
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Tue Jul 20 00:07:13 2021 +0200

    Last step of transition: use sortenIfNeeded again.
    
    Change semantics of Row::shortenIfNeeded: instead of breaking the row
    and returning a boolean, it returns the list of row elements that have
    been removed (or broken) from the row. The logic of the method remains
    the same.
    
    Use shortenIfNeeded in breakParagraph. This was the last missing block.
    
    Remove Row::breakAt and the old breakRow. Only bugs remain now :)

diff --git a/src/Row.cpp b/src/Row.cpp
index 3bc8ef0..6e975a5 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -156,12 +156,6 @@ Row::Element Row::Element::splitAt(int w, bool force)
 }
 
 
-bool Row::Element::breakAt(int w, bool force)
-{
-	return splitAt(w, force).isValid();
-}
-
-
 bool Row::isMarginSelected(bool left, DocIterator const & beg,
 		DocIterator const & end) const
 {
@@ -448,10 +442,31 @@ void Row::pop_back()
 }
 
 
-bool Row::shortenIfNeeded(int const w, int const next_width)
+namespace {
+
+// Remove stuff after it from elts, and return it.
+// if init is provided, it will be in front of the rest
+Row::Elements splitFrom(Row::Elements & elts, Row::Elements::iterator const & it,
+                        Row::Element const & init = Row::Element())
+{
+	Row::Elements ret;
+	if (init.isValid())
+		ret.push_back(init);
+	ret.insert(ret.end(), it, elts.end());
+	elts.erase(it, elts.end());
+	return ret;
+}
+
+}
+
+
+Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
 {
+	// FIXME: performance: if the last element is a string, we would
+	// like to avoid computing its length.
+	finalizeLast();
 	if (empty() || width() <= w)
-		return false;
+		return Elements();
 
 	Elements::iterator const beg = elements_.begin();
 	Elements::iterator const end = elements_.end();
@@ -467,8 +482,8 @@ bool Row::shortenIfNeeded(int const w, int const next_width)
 
 	if (cit == end) {
 		// This should not happen since the row is too long.
-		LYXERR0("Something is wrong cannot shorten row: " << *this);
-		return false;
+		LYXERR0("Something is wrong, cannot shorten row: " << *this);
+		return Elements();
 	}
 
 	// Iterate backwards over breakable elements and try to break them
@@ -486,8 +501,7 @@ bool Row::shortenIfNeeded(int const w, int const next_width)
 		if (wid_brk <= w && brk.row_flags & CanBreakAfter) {
 			end_ = brk.endpos;
 			dim_.wid = wid_brk;
-			elements_.erase(cit_brk + 1, end);
-			return true;
+			return splitFrom(elements_, cit_brk + 1);
 		}
 		// assume now that the current element is not there
 		wid_brk -= brk.dim.wid;
@@ -507,7 +521,8 @@ bool Row::shortenIfNeeded(int const w, int const next_width)
 		 * - shorter than the natural width of the element, in order to enforce
 		 *   break-up.
 		 */
-		if (brk.breakAt(min(w - wid_brk, brk.dim.wid - 2), !word_wrap)) {
+		Element remainder = brk.splitAt(min(w - wid_brk, brk.dim.wid - 2), !word_wrap);
+		if (remainder.isValid()) {
 			/* if this element originally did not cause a row overflow
 			 * in itself, and the remainder of the row would still be
 			 * too large after breaking, then we will have issues in
@@ -529,14 +544,13 @@ bool Row::shortenIfNeeded(int const w, int const next_width)
 			*cit_brk = brk;
 			dim_.wid = wid_brk + brk.dim.wid;
 			// If there are other elements, they should be removed.
-			elements_.erase(cit_brk + 1, end);
-			return true;
+			return splitFrom(elements_, next(cit_brk, 1), remainder);
 		}
 	}
 
-	if (cit != beg && cit->type == VIRTUAL) {
-		// It is not possible to separate a virtual element from the
-		// previous one.
+	if (cit != beg && cit->row_flags & NoBreakBefore) {
+		// It is not possible to separate this element from the
+		// previous one. (e.g. VIRTUAL)
 		--cit;
 		wid -= cit->dim.wid;
 	}
@@ -546,25 +560,24 @@ bool Row::shortenIfNeeded(int const w, int const next_width)
 		// been added. We can cut right here.
 		end_ = cit->pos;
 		dim_.wid = wid;
-		elements_.erase(cit, end);
-		return true;
+		return splitFrom(elements_, cit);
 	}
 
 	/* If we are here, it means that we have not found a separator to
 	 * shorten the row. Let's try to break it again, but not at word
 	 * boundary this time.
 	 */
-	if (cit->breakAt(w - wid, true)) {
+	Element remainder = cit->splitAt(w - wid, true);
+	if (remainder.isValid()) {
 		end_ = cit->endpos;
 		// See comment above.
 		cit->str = rtrim(cit->str);
 		cit->endpos = cit->pos + cit->str.length();
 		dim_.wid = wid + cit->dim.wid;
 		// If there are other elements, they should be removed.
-		elements_.erase(next(cit, 1), end);
-		return true;
+		return splitFrom(elements_, next(cit, 1), remainder);
 	}
-	return false;
+	return Elements();
 }
 
 
diff --git a/src/Row.h b/src/Row.h
index 72d7a86..b0b1755 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -101,12 +101,6 @@ public:
 		 *   respects the row breaking rules of characters.
 		 */
 		Element splitAt(int w, bool force);
-		/** Break the element if possible, so that its width is less
-		 * than \param w. Returns true on success. When \param force
-		 * is true, the string is cut at any place, otherwise it
-		 * respects the row breaking rules of characters.
-		 */
-		bool breakAt(int w, bool force);
 
 		//
 		bool isRTL() const { return font.isVisibleRightToLeft(); }
@@ -296,7 +290,7 @@ public:
 	 * \param available width on next row.
 	 * \return true if the row has been shortened.
 	 */
-	bool shortenIfNeeded(int const width, int const next_width);
+	Elements shortenIfNeeded(int const width, int const next_width);
 
 	/**
 	 * If last element of the row is a string, compute its width
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index c829d94..187d0ed 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -491,7 +491,7 @@ bool TextMetrics::redoParagraph(pit_type const pit, bool const align_rows)
 
 		// If there is an end of paragraph marker, its size should be
 		// substracted to the available width. The logic here is
-		// almost the same as in breakRow, remember keep them in sync.
+		// almost the same as in tokenizeParagraph, remember keep them in sync.
 		int eop = 0;
 		if (e.pos + 1 == par.size()
 		      && (lyxrc.paragraph_markers || par.lookupChange(par.size()).changed())
@@ -1006,6 +1006,11 @@ public:
 
 	void put(value_type const & e) { pile_.push_back(e); }
 
+	// Put a sequence of elements on the pile (in reverse order!)
+	void put(vector<value_type> const & elts) {
+		pile_.insert(pile_.end(), elts.rbegin(), elts.rend());
+	}
+
 // This should be private, but declaring the friend functions is too much work
 //private:
 	typename T::const_iterator cit_;
@@ -1121,19 +1126,40 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 		Row::Element elt = *fcit;
 		Row::Element next_elt = elt.splitAt(width - rows.back().width(),
 		                                    !elt.font.language()->wordWrap());
-		// a new element in the row
-		rows.back().push_back(elt);
-		rows.back().finalizeLast();
-		pos = elt.endpos;
-
-		// Go to next element
-		++fcit;
-
-		// Add a new next element on the pile
-		if (next_elt.isValid()) {
-			// do as if we inserted this element in the original row
-			fcit.put(next_elt);
-			need_new_row = true;
+		if (elt.dim.wid > width - rows.back().width()) {
+			Row & rb = rows.back();
+			rb.push_back(*fcit);
+			// if the row is too large, try to cut at last separator. In case
+			// of success, reset indication that the row was broken abruptly.
+			int const next_width = max_width_ - leftMargin(rb.pit(), rb.endpos())
+				- rightMargin(rb.pit());
+
+			Row::Elements next_elts = rb.shortenIfNeeded(width, next_width);
+
+			// Go to next element
+			++fcit;
+
+			// Handle later the elements returned by shortenIfNeeded.
+			if (!next_elts.empty()) {
+				rb.flushed(false);
+				fcit.put(next_elts);
+				need_new_row = true;
+			}
+		} else {
+			// a new element in the row
+			rows.back().push_back(elt);
+			rows.back().finalizeLast();
+			pos = elt.endpos;
+
+			// Go to next element
+			++fcit;
+
+			// Add a new next element on the pile
+			if (next_elt.isValid()) {
+				// do as if we inserted this element in the original row
+				fcit.put(next_elt);
+				need_new_row = true;
+			}
 		}
 	}
 
@@ -1146,185 +1172,6 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 	return rows;
 }
 
-/** This is the function where the hard work is done. The code here is
- * very sensitive to small changes :) Note that part of the
- * intelligence is also in Row::shortenIfNeeded.
- */
-bool TextMetrics::breakRow(Row & row, int const right_margin) const
-{
-	LATTEST(row.empty());//
-	Paragraph const & par = text_->getPar(row.pit());//
-	Buffer const & buf = text_->inset().buffer();//
-	BookmarksSection::BookmarkPosList bpl =//
-		theSession().bookmarks().bookmarksInPar(buf.fileName(), par.id());//
-
-	pos_type const end = par.size();//
-	pos_type const pos = row.pos();//
-	pos_type const body_pos = par.beginOfBody();//
-	bool const is_rtl = text_->isRTL(row.pit());//
-	bool need_new_row = false;//
-
-	row.left_margin = leftMargin(row.pit(), pos);//
-	row.right_margin = right_margin;//
-	if (is_rtl)//
-		swap(row.left_margin, row.right_margin);//
-	// Remember that the row width takes into account the left_margin
-	// but not the right_margin.
-	row.dim().wid = row.left_margin;//
-	// the width available for the row.
-	int const width = max_width_ - row.right_margin;//
-
-	// check for possible inline completion
-	DocIterator const & ic_it = bv_->inlineCompletionPos();//
-	pos_type ic_pos = -1;//
-	if (ic_it.inTexted() && ic_it.text() == text_ && ic_it.pit() == row.pit())//
-		ic_pos = ic_it.pos();//
-
-	// Now we iterate through until we reach the right margin
-	// or the end of the par, then build a representation of the row.
-	pos_type i = pos;//---------------------------------------------------vvv
-	FontIterator fi = FontIterator(*this, par, row.pit(), pos);
-	// The real stopping condition is a few lines below.
-	while (true) {
-		// Firstly, check whether there is a bookmark here.
-		if (lyxrc.bookmarks_visibility == LyXRC::BMK_INLINE)
-			for (auto const & bp_p : bpl)
-				if (bp_p.second == i) {
-					Font f = *fi;
-					f.fontInfo().setColor(Color_bookmark);
-					// ❶ U+2776 DINGBAT NEGATIVE CIRCLED DIGIT ONE
-					char_type const ch = 0x2775 + bp_p.first;
-					row.addVirtual(i, docstring(1, ch), f, Change());
-				}
-
-		// The stopping condition is here so that the display of a
-		// bookmark can take place at paragraph start too.
-		if (i >= end || (i != pos && row.width() > width))//^width
-			break;
-
-		char_type c = par.getChar(i);
-		// The most special cases are handled first.
-		if (par.isInset(i)) {
-			Inset const * ins = par.getInset(i);
-			Dimension dim = bv_->coordCache().insets().dim(ins);
-			row.add(i, ins, dim, *fi, par.lookupChange(i));
-		} else if (c == ' ' && i + 1 == body_pos) {
-			// There is a space at i, but it should not be
-			// added as a separator, because it is just
-			// before body_pos. Instead, insert some spacing to
-			// align text
-			FontMetrics const & fm = theFontMetrics(text_->labelFont(par));
-			// this is needed to make sure that the row width is correct
-			row.finalizeLast();
-			int const add = max(fm.width(par.layout().labelsep),
-			                    labelEnd(row.pit()) - row.width());
-			row.addSpace(i, add, *fi, par.lookupChange(i));
-		} else if (c == '\t')
-			row.addSpace(i, theFontMetrics(*fi).width(from_ascii("    ")),
-				     *fi, par.lookupChange(i));
-		else if (c == 0x2028 || c == 0x2029) {
-			/**
-			 * U+2028 LINE SEPARATOR
-			 * U+2029 PARAGRAPH SEPARATOR
-
-			 * These are special unicode characters that break
-			 * lines/pragraphs. Not handling them lead to trouble wrt
-			 * Qt QTextLayout formatting. We add a visible character
-			 * on screen so that the user can see that something is
-			 * happening.
-			*/
-			row.finalizeLast();
-			// ⤶ U+2936 ARROW POINTING DOWNWARDS THEN CURVING LEFTWARDS
-			// ¶ U+00B6 PILCROW SIGN
-			char_type const screen_char = (c == 0x2028) ? 0x2936 : 0x00B6;
-			row.add(i, screen_char, *fi, par.lookupChange(i), i >= body_pos);
-		} else
-			row.add(i, c, *fi, par.lookupChange(i), i >= body_pos);
-
-		// add inline completion width
-		// draw logically behind the previous character
-		if (ic_pos == i + 1 && !bv_->inlineCompletion().empty()) {
-			docstring const comp = bv_->inlineCompletion();
-			size_t const uniqueTo =bv_->inlineCompletionUniqueChars();
-			Font f = *fi;
-
-			if (uniqueTo > 0) {
-				f.fontInfo().setColor(Color_inlinecompletion);
-				row.addVirtual(i + 1, comp.substr(0, uniqueTo), f, Change());
-			}
-			f.fontInfo().setColor(Color_nonunique_inlinecompletion);
-			row.addVirtual(i + 1, comp.substr(uniqueTo), f, Change());
-		}
-
-		// Handle some situations that abruptly terminate the row
-		// - Before an inset with BreakBefore
-		// - After an inset with BreakAfter
-		Inset const * prevInset = !row.empty() ? row.back().inset : 0;
-		Inset const * nextInset = (i + 1 < end) ? par.getInset(i + 1) : 0;
-		if ((nextInset && nextInset->rowFlags() & BreakBefore)
-		    || (prevInset && prevInset->rowFlags() & BreakAfter)) {
-			row.flushed(true);
-			// Force a row creation after this one if it is ended by
-			// an inset that either
-			// - has row flag RowAfter that enforces that;
-			// - or (1) did force the row breaking, (2) is at end of
-			//   paragraph and (3) the said paragraph has an end label.
-			need_new_row = prevInset &&
-				(prevInset->rowFlags() & AlwaysBreakAfter
-				 || (prevInset->rowFlags() & BreakAfter && i + 1 == end
-				     && text_->getEndLabel(row.pit()) != END_LABEL_NO_LABEL));
-			++i;
-			break;
-		}
-
-		++i;
-		++fi;
-	}
-	row.finalizeLast();
-	row.endpos(i);
-
-	// End of paragraph marker. The logic here is almost the
-	// same as in redoParagraph, remember keep them in sync.
-	ParagraphList const & pars = text_->paragraphs();
-	Change const & change = par.lookupChange(i);
-	if ((lyxrc.paragraph_markers || change.changed())
-	    && !need_new_row // not this
-	    && i == end && size_type(row.pit() + 1) < pars.size()) {
-		// add a virtual element for the end-of-paragraph
-		// marker; it is shown on screen, but does not exist
-		// in the paragraph.
-		Font f(text_->layoutFont(row.pit()));
-		f.fontInfo().setColor(Color_paragraphmarker);
-		f.setLanguage(par.getParLanguage(buf.params()));
-		// ¶ U+00B6 PILCROW SIGN
-		row.addVirtual(end, docstring(1, char_type(0x00B6)), f, change);
-	}
-
-	// Is there a end-of-paragaph change?
-	if (i == end && par.lookupChange(end).changed() && !need_new_row)
-		row.needsChangeBar(true);
-    //--------------------------------------------------------------------^^^
-	// FIXME : nothing below this
-
-	// if the row is too large, try to cut at last separator. In case
-	// of success, reset indication that the row was broken abruptly.
-	int const next_width = max_width_ - leftMargin(row.pit(), row.endpos())
-		- rightMargin(row.pit());
-
-	if (row.shortenIfNeeded(width, next_width))
-		row.flushed(false);
-	row.right_boundary(!row.empty() && row.endpos() < end//
-	                   && row.back().endpos == row.endpos());//
-	// Last row in paragraph is flushed
-	if (row.endpos() == end)//
-		row.flushed(true);//
-
-	// make sure that the RTL elements are in reverse ordering
-	row.reverseRTL(is_rtl);//
-	//LYXERR0("breakrow: row is " << row);
-
-	return need_new_row;
-}
 
 int TextMetrics::parTopSpacing(pit_type const pit) const
 {
diff --git a/src/TextMetrics.h b/src/TextMetrics.h
index 3ff1161..e38ba71 100644
--- a/src/TextMetrics.h
+++ b/src/TextMetrics.h
@@ -151,15 +151,12 @@ private:
 	/// FIXME??
 	int labelEnd(pit_type const pit) const;
 
+	// Turn paragraph oh index \c pit into a single row
 	Row tokenizeParagraph(pit_type pit) const;
 
+	// Break the row produced by tokenizeParagraph() into a list of rows.
 	RowList breakParagraph(Row const & row) const;
 
-	/// sets row.end to the pos value *after* which a row should break.
-	/// for example, the pos after which isNewLine(pos) == true
-	/// \return true when another row is required (after a newline)
-	bool breakRow(Row & row, int right_margin) const;
-
 	// Expands the alignment of row \param row in paragraph \param par
 	LyXAlignment getAlign(Paragraph const & par, Row const & row) const;
 	/// Aligns properly the row contents (computes spaces and fills)

commit 1f14678e954149847f71aa1cf038172bdf1e9c0c
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Sun Jul 18 01:09:33 2021 +0200

    Implement handling of row_flags for row breaking
    
    To this end, add the helper function needsRowBreak which computes the
    effect of two consecutive row flags. This function implements the
    priorities described in RowFlags.h.
    
    This function is called with the relevant flags, or NoBreak* when at
    boundaries and updates need_new_row.
    
    Some common code is factored in a new cleanupRow() helper.

diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index 051d050..c829d94 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -980,22 +980,6 @@ Row TextMetrics::tokenizeParagraph(pit_type const pit) const
 
 namespace {
 
-Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
-{
-	Row nrow;
-	nrow.pit(pit);
-	nrow.pos(pos);
-	nrow.left_margin = tm.leftMargin(pit, pos);
-	nrow.right_margin = tm.rightMargin(pit);
-	if (is_rtl)
-		swap(nrow.left_margin, nrow.right_margin);
-	// Remember that the row width takes into account the left_margin
-	// but not the right_margin.
-	nrow.dim().wid = nrow.left_margin;
-	return nrow;
-}
-
-
 /** Helper template flexible_const_iterator<T>
  * A way to iterate over a const container, but insert fake elements in it.
  * In the case of a row, we will have to break some elements, which
@@ -1018,6 +1002,8 @@ public:
 
 	value_type operator*() const { return pile_.empty() ? *cit_ : pile_.back(); }
 
+	value_type const * operator->() const { return pile_.empty() ? &*cit_ : &pile_.back(); }
+
 	void put(value_type const & e) { pile_.push_back(e); }
 
 // This should be private, but declaring the friend functions is too much work
@@ -1051,6 +1037,44 @@ bool operator==(flexible_const_iterator<T> const & t1,
 	return t1.cit_ == t2.cit_ && t1.pile_.empty() && t2.pile_.empty();
 }
 
+Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
+{
+	Row nrow;
+	nrow.pit(pit);
+	nrow.pos(pos);
+	nrow.left_margin = tm.leftMargin(pit, pos);
+	nrow.right_margin = tm.rightMargin(pit);
+	if (is_rtl)
+		swap(nrow.left_margin, nrow.right_margin);
+	// Remember that the row width takes into account the left_margin
+	// but not the right_margin.
+	nrow.dim().wid = nrow.left_margin;
+	return nrow;
+}
+
+
+void cleanupRow(Row & row, pos_type pos, pos_type real_endpos, bool is_rtl)
+{
+	row.endpos(pos);
+	row.right_boundary(!row.empty() && pos < real_endpos
+	                   && row.back().endpos == pos);
+	// make sure that the RTL elements are in reverse ordering
+	row.reverseRTL(is_rtl);
+}
+
+// Implement the priorities described in RowFlags.h.
+bool needsRowBreak(int f1, int f2)
+{
+	if (f1 & AlwaysBreakAfter /*|| f2 & AlwaysBreakBefore*/)
+		return true;
+	if (f1 & NoBreakAfter || f2 & NoBreakBefore)
+		return false;
+	if (f1 & BreakAfter || f2 & BreakBefore)
+		return true;
+	return false;
+}
+
+
 }
 
 
@@ -1058,6 +1082,7 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 {
 	RowList rows;
 	bool const is_rtl = text_->isRTL(bigrow.pit());
+	bool const end_label = text_->getEndLabel(bigrow.pit()) != END_LABEL_NO_LABEL;
 
 	bool need_new_row = true;
 	pos_type pos = 0;
@@ -1065,15 +1090,21 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 	flexible_const_iterator<Row> fcit = flexible_begin(bigrow);
 	flexible_const_iterator<Row> const end = flexible_end(bigrow);
 	while (true) {
+		bool const has_row = !rows.empty();
+		bool const row_empty = !has_row || rows.back().empty();
+		// The row flags of previous element, if there is one.
+		// Otherwise we use NoBreakAfter to avoid an empty row before
+		// e.g. a displayed equation.
+		int const f1 = row_empty ? NoBreakAfter : rows.back().back().row_flags;
+		// The row flags of next element, if there is one.
+		// Otherwise we use NoBreakBefore (see above), unless the
+		// paragraph has an end label (for which an empty row is OK).
+		int const f2 = (fcit == end) ? (end_label ? Inline : NoBreakBefore)
+		                             : fcit->row_flags;
+		need_new_row |= needsRowBreak(f1, f2);
 		if (need_new_row) {
-			if (!rows.empty()) {
-				Row & rb = rows.back();
-				rb.endpos(pos);
-				rb.right_boundary(!rb.empty() && rb.endpos() < bigrow.endpos()
-								   && rb.back().endpos == rb.endpos());
-				// make sure that the RTL elements are in reverse ordering
-				rb.reverseRTL(is_rtl);
-			}
+			if (!rows.empty())
+				cleanupRow(rows.back(), pos, bigrow.endpos(), is_rtl);
 			rows.push_back(newRow(*this, bigrow.pit(), pos, is_rtl));
 			// the width available for the row.
 			width = max_width_ - rows.back().right_margin;
@@ -1107,13 +1138,9 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 	}
 
 	if (!rows.empty()) {
-		Row & rb = rows.back();
+		cleanupRow(rows.back(), pos, bigrow.endpos(), is_rtl);
 		// Last row in paragraph is flushed
-		rb.flushed(true);
-		rb.endpos(bigrow.endpos());
-		rb.right_boundary(false);
-		// make sure that the RTL elements are in reverse ordering
-		rb.reverseRTL(is_rtl);
+		rows.back().flushed(true);
 	}
 
 	return rows;
@@ -1227,9 +1254,8 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 			}
 			f.fontInfo().setColor(Color_nonunique_inlinecompletion);
 			row.addVirtual(i + 1, comp.substr(uniqueTo), f, Change());
-		}//---------------------------------------------------------------^^^
+		}
 
-		// FIXME: Handle when breaking the rows
 		// Handle some situations that abruptly terminate the row
 		// - Before an inset with BreakBefore
 		// - After an inset with BreakAfter
@@ -1254,7 +1280,6 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 		++i;
 		++fi;
 	}
-	//--------------------------------------------------------------------vvv
 	row.finalizeLast();
 	row.endpos(i);
 

commit 33a89f8419a5bd773a9c1c068c22fb9de0efd34c
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Sat Jul 17 23:16:15 2021 +0200

    Change the way the element's width is updated.
    
    Remove the code that computed the width every 30 characters (yay!).
    Make sure that finalizeLast() is called after inserting a row element in
    a row in breakParagraph.

diff --git a/src/Row.cpp b/src/Row.cpp
index 705a147..3bc8ef0 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -370,8 +370,7 @@ void Row::finalizeLast()
 	if (elt.change.changed())
 		changebar_ = true;
 
-	if (elt.type == STRING) {
-		dim_.wid -= elt.dim.wid;
+	if (elt.type == STRING && elt.dim.wid == 0) {
 		elt.dim.wid = theFontMetrics(elt.font).width(elt.str);
 		dim_.wid += elt.dim.wid;
 	}
@@ -401,16 +400,8 @@ void Row::add(pos_type const pos, char_type const c,
 		e.row_flags = can_break ? CanBreakInside : Inline;
 		elements_.push_back(e);
 	}
-	if (back().str.length() % 30 == 0) {
-		dim_.wid -= back().dim.wid;
-		back().str += c;
-		back().endpos = pos + 1;
-		back().dim.wid = theFontMetrics(back().font).width(back().str);
-		dim_.wid += back().dim.wid;
-	} else {
-		back().str += c;
-		back().endpos = pos + 1;
-	}
+	back().str += c;
+	back().endpos = pos + 1;
 }
 
 
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index eaaafee..051d050 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -1092,6 +1092,7 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 		                                    !elt.font.language()->wordWrap());
 		// a new element in the row
 		rows.back().push_back(elt);
+		rows.back().finalizeLast();
 		pos = elt.endpos;
 
 		// Go to next element

commit cef786c5f6745df020499b395679aef6d5f7b14f
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Sat Jul 17 02:31:49 2021 +0200

    Introduce helper template to simplify breakParagraph code
    
    This is a semi-generic iterator for iterating over a container and
    pretend that we add elements to it along the way.

diff --git a/src/Row.h b/src/Row.h
index 4fdcee4..72d7a86 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -150,6 +150,8 @@ public:
 		friend std::ostream & operator<<(std::ostream & os, Element const & row);
 	};
 
+	///
+	typedef Element value_type;
 
 	///
 	Row() {}
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index 3734520..eaaafee 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -995,6 +995,62 @@ Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
 	return nrow;
 }
 
+
+/** Helper template flexible_const_iterator<T>
+ * A way to iterate over a const container, but insert fake elements in it.
+ * In the case of a row, we will have to break some elements, which
+ * create new ones. This class allows to abstract this.
+ * Only the required parts are implemented for now.
+ */
+template<class T>
+class flexible_const_iterator {
+	typedef typename T::value_type value_type;
+public:
+
+	//
+	flexible_const_iterator operator++() {
+		if (pile_.empty())
+			++cit_;
+		else
+			pile_.pop_back();
+		return *this;
+	}
+
+	value_type operator*() const { return pile_.empty() ? *cit_ : pile_.back(); }
+
+	void put(value_type const & e) { pile_.push_back(e); }
+
+// This should be private, but declaring the friend functions is too much work
+//private:
+	typename T::const_iterator cit_;
+	// A vector that is used as like a pile to store the elements to
+	// consider before incrementing the underlying iterator.
+	vector<value_type> pile_;
+};
+
+
+template<class T>
+flexible_const_iterator<T> flexible_begin(T const & t)
+{
+	return { t.begin(), vector<typename T::value_type>() };
+}
+
+
+template<class T>
+flexible_const_iterator<T> flexible_end(T const & t)
+{
+	return { t.end(), vector<typename T::value_type>() };
+}
+
+
+// Equality is only possible if respective piles are empty
+template<class T>
+bool operator==(flexible_const_iterator<T> const & t1,
+                flexible_const_iterator<T> const & t2)
+{
+	return t1.cit_ == t2.cit_ && t1.pile_.empty() && t2.pile_.empty();
+}
+
 }
 
 
@@ -1006,11 +1062,8 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 	bool need_new_row = true;
 	pos_type pos = 0;
 	int width = 0;
-	Row::const_iterator cit = bigrow.begin();
-	Row::const_iterator const end = bigrow.end();
-	// This is a vector, but we use it like a pile putting and taking
-	// stuff at the back.
-	Row::Elements pile;
+	flexible_const_iterator<Row> fcit = flexible_begin(bigrow);
+	flexible_const_iterator<Row> const end = flexible_end(bigrow);
 	while (true) {
 		if (need_new_row) {
 			if (!rows.empty()) {
@@ -1029,27 +1082,25 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
 
 		// The stopping condition is here because we may need a new
 		// empty row at the end.
-		if (cit == end && pile.empty())
+		if (fcit == end)
 			break;
 
 		// Next element to consider is either the top of the temporary
 		// pile, or the place when we were in main row
-		Row::Element elt = pile.empty() ? *cit : pile.back();
-		//LYXERR0("elt=" << elt);
+		Row::Element elt = *fcit;
 		Row::Element next_elt = elt.splitAt(width - rows.back().width(),
 		                                    !elt.font.language()->wordWrap());
-		//LYXERR0("next_elt=" << next_elt);
 		// a new element in the row
 		rows.back().push_back(elt);
 		pos = elt.endpos;
+
 		// Go to next element
-		if (pile.empty())
-			++cit;
-		else
-			pile.pop_back();
+		++fcit;
+
 		// Add a new next element on the pile
 		if (next_elt.isValid()) {
-			pile.push_back(next_elt);
+			// do as if we inserted this element in the original row
+			fcit.put(next_elt);
 			need_new_row = true;
 		}
 	}

commit 05b3268742bcb40311f8348742a0a4f4efa4a4e9
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Fri Jul 16 00:10:25 2021 +0200

    A set of easy fixes and missing features
    
    * show changebar when end of paragraph is changed.
    
    * when row is finished, set endpos and right_boundary
    
    * handle bidi.

diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index aed277c..3734520 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -955,12 +955,15 @@ Row TextMetrics::tokenizeParagraph(pit_type const pit) const
 	row.finalizeLast();
 	row.endpos(end);
 
-	// End of paragraph marker. The logic here is almost the
+	// End of paragraph marker, either if LyXRc requires it, or there
+	// is an end of paragraph change. The logic here is almost the
 	// same as in redoParagraph, remember keep them in sync.
 	ParagraphList const & pars = text_->paragraphs();
-	Change const & change = par.lookupChange(i);
-	if ((lyxrc.paragraph_markers || change.changed())
-	    && i == end && size_type(pit + 1) < pars.size()) {
+	Change const & endchange = par.lookupChange(end);
+	if (endchange.changed())
+		row.needsChangeBar(true);
+	if ((lyxrc.paragraph_markers || endchange.changed())
+	    && size_type(pit + 1) < pars.size()) {
 		// add a virtual element for the end-of-paragraph
 		// marker; it is shown on screen, but does not exist
 		// in the paragraph.
@@ -968,7 +971,7 @@ Row TextMetrics::tokenizeParagraph(pit_type const pit) const
 		f.fontInfo().setColor(Color_paragraphmarker);
 		f.setLanguage(par.getParLanguage(buf.params()));
 		// ¶ U+00B6 PILCROW SIGN
-		row.addVirtual(end, docstring(1, char_type(0x00B6)), f, change);
+		row.addVirtual(end, docstring(1, char_type(0x00B6)), f, endchange);
 	}
 
 	return row;
@@ -995,24 +998,30 @@ Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
 }
 
 
-RowList TextMetrics::breakParagraph(Row const & row) const
+RowList TextMetrics::breakParagraph(Row const & bigrow) const
 {
 	RowList rows;
-	bool const is_rtl = text_->isRTL(row.pit());
+	bool const is_rtl = text_->isRTL(bigrow.pit());
 
 	bool need_new_row = true;
 	pos_type pos = 0;
 	int width = 0;
-	Row::const_iterator cit = row.begin();
-	Row::const_iterator const end = row.end();
+	Row::const_iterator cit = bigrow.begin();
+	Row::const_iterator const end = bigrow.end();
 	// This is a vector, but we use it like a pile putting and taking
 	// stuff at the back.
 	Row::Elements pile;
 	while (true) {
 		if (need_new_row) {
-			if (!rows.empty())
-				rows.back().endpos(pos);
-			rows.push_back(newRow(*this, row.pit(), pos, is_rtl));
+			if (!rows.empty()) {
+				Row & rb = rows.back();
+				rb.endpos(pos);
+				rb.right_boundary(!rb.empty() && rb.endpos() < bigrow.endpos()
+								   && rb.back().endpos == rb.endpos());
+				// make sure that the RTL elements are in reverse ordering
+				rb.reverseRTL(is_rtl);
+			}
+			rows.push_back(newRow(*this, bigrow.pit(), pos, is_rtl));
 			// the width available for the row.
 			width = max_width_ - rows.back().right_margin;
 			need_new_row = false;
@@ -1045,6 +1054,16 @@ RowList TextMetrics::breakParagraph(Row const & row) const
 		}
 	}
 
+	if (!rows.empty()) {
+		Row & rb = rows.back();
+		// Last row in paragraph is flushed
+		rb.flushed(true);
+		rb.endpos(bigrow.endpos());
+		rb.right_boundary(false);
+		// make sure that the RTL elements are in reverse ordering
+		rb.reverseRTL(is_rtl);
+	}
+
 	return rows;
 }
 
@@ -1217,14 +1236,14 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 
 	if (row.shortenIfNeeded(width, next_width))
 		row.flushed(false);
-	row.right_boundary(!row.empty() && row.endpos() < end
-	                   && row.back().endpos == row.endpos());
+	row.right_boundary(!row.empty() && row.endpos() < end//
+	                   && row.back().endpos == row.endpos());//
 	// Last row in paragraph is flushed
-	if (row.endpos() == end)
-		row.flushed(true);
+	if (row.endpos() == end)//
+		row.flushed(true);//
 
 	// make sure that the RTL elements are in reverse ordering
-	row.reverseRTL(is_rtl);
+	row.reverseRTL(is_rtl);//
 	//LYXERR0("breakrow: row is " << row);
 
 	return need_new_row;

commit 4aca8bb267ba34575ade2914d96fc56543aa9746
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Wed Jul 14 00:48:03 2021 +0200

    Use the new tokenizing and breaking code instead of breakRow.

diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index 0578913..aed277c 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -516,43 +516,28 @@ bool TextMetrics::redoParagraph(pit_type const pit, bool const align_rows)
 		}
 	}
 
-	pos_type first = 0;
-	size_t row_index = 0;
-	bool need_new_row = false;
-	// maximum pixel width of a row
-	do {
-		if (row_index == pm.rows().size())
-			pm.rows().push_back(Row());
-		else
-			pm.rows()[row_index] = Row();
-		Row & row = pm.rows()[row_index];
-		row.pit(pit);
-		row.pos(first);
-		need_new_row = breakRow(row, right_margin);
+	// Transform the paragraph into a single row containing all the elements.
+	Row const bigrow = tokenizeParagraph(pit);
+	// Split the row in several rows fitting in available width
+	pm.rows() = breakParagraph(bigrow);
+
+	/* If there is more than one row, expand the text to the full
+	 * allowable width. This setting here is needed for the
+	 * setRowAlignment() below. We do nothing when tight insets are
+	 * requested.
+	 */
+	if (pm.rows().size() > 1 && !tight_ && dim_.wid < max_width_)
+			dim_.wid = max_width_;
+
+	// Compute height and alignment of the rows.
+	for (Row & row : pm.rows()) {
 		setRowHeight(row);
-		row.changed(true);
-		if ((row_index || row.endpos() < par.size() || row.right_boundary())
-		    && !tight_) {
-			/* If there is more than one row or the row has been
-			 * broken by a display inset or a newline, expand the text
-			 * to the full allowable width. This setting here is
-			 * needed for the setRowAlignment() below.
-			 * We do nothing when tight insets are requested.
-			 */
-			if (dim_.wid < max_width_)
-				dim_.wid = max_width_;
-		}
 		if (align_rows)
 			setRowAlignment(row, max(dim_.wid, row.width()));
-		first = row.endpos();
-		++row_index;
 
 		pm.dim().wid = max(pm.dim().wid, row.width() + row.right_margin);
 		pm.dim().des += row.height();
-	} while (first < par.size() || need_new_row);
-
-	if (row_index < pm.rows().size())
-		pm.rows().resize(row_index);
+	}
 
 	// This type of margin can only be handled at the global paragraph level
 	if (par.layout().margintype == MARGIN_RIGHT_ADDRESS_BOX) {

commit 118debd64a009ec686d2d6e2b22c516c5dfde3ee
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Wed Jul 14 00:47:42 2021 +0200

    Break the paragraph's big row according to margins
    
    Still many features missing:
    - handle insets that break rows (display math, newline, ...)
    - handle rows that are too long by replacing the single call to
      breakAt with a call to a reworked Row::shortenIfNeeded.
    - some easy things at the end of breakRow (bidi text, etc.).

diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index ffd3df4..0578913 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -990,6 +990,79 @@ Row TextMetrics::tokenizeParagraph(pit_type const pit) const
 }
 
 
+namespace {
+
+Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
+{
+	Row nrow;
+	nrow.pit(pit);
+	nrow.pos(pos);
+	nrow.left_margin = tm.leftMargin(pit, pos);
+	nrow.right_margin = tm.rightMargin(pit);
+	if (is_rtl)
+		swap(nrow.left_margin, nrow.right_margin);
+	// Remember that the row width takes into account the left_margin
+	// but not the right_margin.
+	nrow.dim().wid = nrow.left_margin;
+	return nrow;
+}
+
+}
+
+
+RowList TextMetrics::breakParagraph(Row const & row) const
+{
+	RowList rows;
+	bool const is_rtl = text_->isRTL(row.pit());
+
+	bool need_new_row = true;
+	pos_type pos = 0;
+	int width = 0;
+	Row::const_iterator cit = row.begin();
+	Row::const_iterator const end = row.end();
+	// This is a vector, but we use it like a pile putting and taking
+	// stuff at the back.
+	Row::Elements pile;
+	while (true) {
+		if (need_new_row) {
+			if (!rows.empty())
+				rows.back().endpos(pos);
+			rows.push_back(newRow(*this, row.pit(), pos, is_rtl));
+			// the width available for the row.
+			width = max_width_ - rows.back().right_margin;
+			need_new_row = false;
+		}
+
+		// The stopping condition is here because we may need a new
+		// empty row at the end.
+		if (cit == end && pile.empty())
+			break;
+
+		// Next element to consider is either the top of the temporary
+		// pile, or the place when we were in main row
+		Row::Element elt = pile.empty() ? *cit : pile.back();
+		//LYXERR0("elt=" << elt);
+		Row::Element next_elt = elt.splitAt(width - rows.back().width(),
+		                                    !elt.font.language()->wordWrap());
+		//LYXERR0("next_elt=" << next_elt);
+		// a new element in the row
+		rows.back().push_back(elt);
+		pos = elt.endpos;
+		// Go to next element
+		if (pile.empty())
+			++cit;
+		else
+			pile.pop_back();
+		// Add a new next element on the pile
+		if (next_elt.isValid()) {
+			pile.push_back(next_elt);
+			need_new_row = true;
+		}
+	}
+
+	return rows;
+}
+
 /** This is the function where the hard work is done. The code here is
  * very sensitive to small changes :) Note that part of the
  * intelligence is also in Row::shortenIfNeeded.
@@ -1003,20 +1076,20 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 		theSession().bookmarks().bookmarksInPar(buf.fileName(), par.id());//
 
 	pos_type const end = par.size();//
-	pos_type const pos = row.pos();
+	pos_type const pos = row.pos();//
 	pos_type const body_pos = par.beginOfBody();//
-	bool const is_rtl = text_->isRTL(row.pit());
-	bool need_new_row = false;
+	bool const is_rtl = text_->isRTL(row.pit());//
+	bool need_new_row = false;//
 
-	row.left_margin = leftMargin(row.pit(), pos);
-	row.right_margin = right_margin;
-	if (is_rtl)
-		swap(row.left_margin, row.right_margin);
+	row.left_margin = leftMargin(row.pit(), pos);//
+	row.right_margin = right_margin;//
+	if (is_rtl)//
+		swap(row.left_margin, row.right_margin);//
 	// Remember that the row width takes into account the left_margin
 	// but not the right_margin.
-	row.dim().wid = row.left_margin;
+	row.dim().wid = row.left_margin;//
 	// the width available for the row.
-	int const width = max_width_ - row.right_margin;
+	int const width = max_width_ - row.right_margin;//
 
 	// check for possible inline completion
 	DocIterator const & ic_it = bv_->inlineCompletionPos();//
diff --git a/src/TextMetrics.h b/src/TextMetrics.h
index 1666cd0..3ff1161 100644
--- a/src/TextMetrics.h
+++ b/src/TextMetrics.h
@@ -153,6 +153,8 @@ private:
 
 	Row tokenizeParagraph(pit_type pit) const;
 
+	RowList breakParagraph(Row const & row) const;
+
 	/// sets row.end to the pos value *after* which a row should break.
 	/// for example, the pos after which isNewLine(pos) == true
 	/// \return true when another row is required (after a newline)

commit c63702dc0a5033dd174e073e6dae1a59f66d43c3
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Mon Jul 12 00:07:59 2021 +0200

    Implement Row::Element::row_flags
    
    Move the enum definition RowFlags in its own include file, to avoid
    loading Inset.h. Document it more thoroughly.
    
    Rename RowAfter to AlwaysBreakAfter.
    
    Add CanBreakInside (rows that can be themselves broken). This allow to
    differentiate elements before bodyPos() and allows to remove a
    parameter to shortenIfNeeded().
    
    Make the Inset::rowFlags() method return int instead of RowFlags, as
    should be done for all the bitwise flags. Remove the hand-made bitwise
    operators.
    
    Set R::E::row_flags when creating elements.
    * INSET elements use the inset's rowFLags();
    * virtual element forbid breaking before them, and inherit the *After
      flags from the previous element of the row;
    * STRING elements usr CanBreakInside, except before bodyPos.
    
    More stuff may be added later.

diff --git a/src/Makefile.am b/src/Makefile.am
index 31701f2..99a0f98 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -273,6 +273,7 @@ HEADERFILESCORE = \
 	ParIterator.h \
 	PDFOptions.h \
 	Row.h \
+	RowFlags.h \
 	RowPainter.h \
 	Server.h \
 	ServerSocket.h \
diff --git a/src/Row.cpp b/src/Row.cpp
index da2d885..705a147 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -135,7 +135,7 @@ pos_type Row::Element::x2pos(int &x) const
 
 Row::Element Row::Element::splitAt(int w, bool force)
 {
-	if (type != STRING)
+	if (type != STRING || !(row_flags & CanBreakInside))
 		return Element();
 
 	FontMetrics const & fm = theFontMetrics(font);
@@ -145,6 +145,7 @@ Row::Element Row::Element::splitAt(int w, bool force)
 		Element ret(STRING, pos + i, font, change);
 		ret.str = str.substr(i);
 		ret.endpos = ret.pos + ret.str.length();
+		ret.row_flags = row_flags & (CanBreakInside | AfterFlags);
 		str.erase(i);
 		endpos = pos + i;
 		//lyxerr << "breakAt(" << w << ")  Row element Broken at " << x << "(w(str)=" << fm.width(str) << "): e=" << *this << endl;
@@ -378,12 +379,13 @@ void Row::finalizeLast()
 
 
 void Row::add(pos_type const pos, Inset const * ins, Dimension const & dim,
-	      Font const & f, Change const & ch)
+              Font const & f, Change const & ch)
 {
 	finalizeLast();
 	Element e(INSET, pos, f, ch);
 	e.inset = ins;
 	e.dim = dim;
+	e.row_flags = ins->rowFlags();
 	elements_.push_back(e);
 	dim_.wid += dim.wid;
 	changebar_ |= ins->isChanged();
@@ -391,11 +393,12 @@ void Row::add(pos_type const pos, Inset const * ins, Dimension const & dim,
 
 
 void Row::add(pos_type const pos, char_type const c,
-	      Font const & f, Change const & ch)
+              Font const & f, Change const & ch, bool can_break)
 {
 	if (!sameString(f, ch)) {
 		finalizeLast();
 		Element e(STRING, pos, f, ch);
+		e.row_flags = can_break ? CanBreakInside : Inline;
 		elements_.push_back(e);
 	}
 	if (back().str.length() % 30 == 0) {
@@ -420,6 +423,10 @@ void Row::addVirtual(pos_type const pos, docstring const & s,
 	e.dim.wid = theFontMetrics(f).width(s);
 	dim_.wid += e.dim.wid;
 	e.endpos = pos;
+	// Copy after* flags from previous elements, forbid break before element
+	int const prev_row_flags = elements_.empty() ? Inline : elements_.back().row_flags;
+	int const can_inherit = AfterFlags & ~AlwaysBreakAfter;
+	e.row_flags = (prev_row_flags & can_inherit) | NoBreakBefore;
 	elements_.push_back(e);
 	finalizeLast();
 }
@@ -450,7 +457,7 @@ void Row::pop_back()
 }
 
 
-bool Row::shortenIfNeeded(pos_type const keep, int const w, int const next_width)
+bool Row::shortenIfNeeded(int const w, int const next_width)
 {
 	if (empty() || width() <= w)
 		return false;
@@ -482,11 +489,10 @@ bool Row::shortenIfNeeded(pos_type const keep, int const w, int const next_width
 		// make a copy of the element to work on it.
 		Element brk = *cit_brk;
 		/* If the current element is an inset that allows breaking row
-		 * after itself, and it the row is already short enough after
+		 * after itself, and if the row is already short enough after
 		 * this inset, then cut right after this element.
 		 */
-		if (wid_brk <= w && brk.type == INSET
-		    && brk.inset->rowFlags() & Inset::CanBreakAfter) {
+		if (wid_brk <= w && brk.row_flags & CanBreakAfter) {
 			end_ = brk.endpos;
 			dim_.wid = wid_brk;
 			elements_.erase(cit_brk + 1, end);
@@ -504,10 +510,6 @@ bool Row::shortenIfNeeded(pos_type const keep, int const w, int const next_width
 		 * not allowed at the beginning or end of line.
 		*/
 		bool const word_wrap = brk.font.language()->wordWrap();
-		// When there is text before the body part (think description
-		// environment), do not try to break.
-		if (brk.pos < keep)
-			continue;
 		/* We have found a suitable separable element. This is the common case.
 		 * Try to break it cleanly (at word boundary) at a length that is both
 		 * - less than the available space on the row
diff --git a/src/Row.h b/src/Row.h
index 3048cf1..4fdcee4 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -18,6 +18,7 @@
 #include "Changes.h"
 #include "Dimension.h"
 #include "Font.h"
+#include "RowFlags.h"
 
 #include "support/docstring.h"
 #include "support/types.h"
@@ -143,6 +144,8 @@ public:
 		Change change;
 		// is it possible to add contents to this element?
 		bool final = false;
+		// properties with respect to row breaking (made of RowFlag enums)
+		int row_flags = Inline;
 
 		friend std::ostream & operator<<(std::ostream & os, Element const & row);
 	};
@@ -247,7 +250,7 @@ public:
 		 Font const & f, Change const & ch);
 	///
 	void add(pos_type pos, char_type const c,
-		 Font const & f, Change const & ch);
+	         Font const & f, Change const & ch, bool can_break);
 	///
 	void addVirtual(pos_type pos, docstring const & s,
 			Font const & f, Change const & ch);
@@ -287,12 +290,11 @@ public:
 	 * if row width is too large, remove all elements after last
 	 * separator and update endpos if necessary. If all that
 	 * remains is a large word, cut it to \param width.
-	 * \param body_pos minimum amount of text to keep.
 	 * \param width maximum width of the row.
 	 * \param available width on next row.
 	 * \return true if the row has been shortened.
 	 */
-	bool shortenIfNeeded(pos_type const body_pos, int const width, int const next_width);
+	bool shortenIfNeeded(int const width, int const next_width);
 
 	/**
 	 * If last element of the row is a string, compute its width
diff --git a/src/RowFlags.h b/src/RowFlags.h
new file mode 100644
index 0000000..f94f0c6
--- /dev/null
+++ b/src/RowFlags.h
@@ -0,0 +1,57 @@
+// -*- C++ -*-
+/**
+ * \file RowFlags.h
+ * This file is part of LyX, the document processor.
+ * Licence details can be found in the file COPYING.
+ *
+ * \author Jean-Marc Lasgouttes
+ *
+ * Full author contact details are available in file CREDITS.
+ */
+
+#ifndef ROWFLAGS_H
+#define ROWFLAGS_H
+
+// Do not include anything here
+
+namespace lyx {
+
+/* The list of possible flags, that can be combined.
+ * Some flags that should logically be here (e.g.,
+ * CanBreakBefore), do not exist. This is because the need has not
+ * been identitfied yet.
+ *
+ * Priorities when before/after disagree:
+ *      AlwaysBreak* > NoBreak* > Break* or CanBreak*.
+ */
+enum RowFlags {
+	// Do not break before or after this element, except if really
+	// needed (between NoBreak* and CanBreak*).
+	Inline = 0,
+	// break row before this element if the row is not empty
+	BreakBefore = 1 << 0,
+	// Avoid breaking row before this element
+	NoBreakBefore = 1 << 1,
+	// force new (maybe empty) row after this element
+	AlwaysBreakAfter = 1 << 2,
+	// break row after this element if there are more elements
+	BreakAfter = 1 << 3,
+	// break row whenever needed after this element
+	CanBreakAfter = 1 << 4,
+	// Avoid breaking row after this element
+	NoBreakAfter = 1 << 5,
+	// The contents of the row may be broken in two (e.g. string)
+	CanBreakInside = 1 << 6,
+	// specify an alignment (left, right) for a display element
+	// (default is center)
+	AlignLeft = 1 << 7,
+	AlignRight = 1 << 8,
+	// A display element breaks row at both ends
+	Display = BreakBefore | BreakAfter,
+	// Flags that concern breaking after element
+	AfterFlags = AlwaysBreakAfter | BreakAfter | CanBreakAfter | NoBreakAfter
+};
+
+} // namespace lyx
+
+#endif
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index 1036066..ffd3df4 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -634,10 +634,10 @@ LyXAlignment TextMetrics::getAlign(Paragraph const & par, Row const & row) const
 
 	// Display-style insets should always be on a centered row
 	if (Inset const * inset = par.getInset(row.pos())) {
-		if (inset->rowFlags() & Inset::Display) {
-			if (inset->rowFlags() & Inset::AlignLeft)
+		if (inset->rowFlags() & Display) {
+			if (inset->rowFlags() & AlignLeft)
 				align = LYX_ALIGN_BLOCK;
-			else if (inset->rowFlags() & Inset::AlignRight)
+			else if (inset->rowFlags() & AlignRight)
 				align = LYX_ALIGN_RIGHT;
 			else
 				align = LYX_ALIGN_CENTER;
@@ -928,7 +928,7 @@ Row TextMetrics::tokenizeParagraph(pit_type const pit) const
 			row.addSpace(i, add, *fi, par.lookupChange(i));
 		} else if (c == '\t')
 			row.addSpace(i, theFontMetrics(*fi).width(from_ascii("    ")),
-				     *fi, par.lookupChange(i));
+			             *fi, par.lookupChange(i));
 		else if (c == 0x2028 || c == 0x2029) {
 			/**
 			 * U+2028 LINE SEPARATOR
@@ -944,9 +944,10 @@ Row TextMetrics::tokenizeParagraph(pit_type const pit) const
 			// ⤶ U+2936 ARROW POINTING DOWNWARDS THEN CURVING LEFTWARDS
 			// ¶ U+00B6 PILCROW SIGN
 			char_type const screen_char = (c == 0x2028) ? 0x2936 : 0x00B6;
-			row.add(i, screen_char, *fi, par.lookupChange(i));
+			row.add(i, screen_char, *fi, par.lookupChange(i), i >= body_pos);
 		} else
-			row.add(i, c, *fi, par.lookupChange(i));
+			// row elements before body are unbreakable
+			row.add(i, c, *fi, par.lookupChange(i), i >= body_pos);
 
 		// add inline completion width
 		// draw logically behind the previous character
@@ -1080,9 +1081,9 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 			// ⤶ U+2936 ARROW POINTING DOWNWARDS THEN CURVING LEFTWARDS
 			// ¶ U+00B6 PILCROW SIGN
 			char_type const screen_char = (c == 0x2028) ? 0x2936 : 0x00B6;
-			row.add(i, screen_char, *fi, par.lookupChange(i));
+			row.add(i, screen_char, *fi, par.lookupChange(i), i >= body_pos);
 		} else
-			row.add(i, c, *fi, par.lookupChange(i));
+			row.add(i, c, *fi, par.lookupChange(i), i >= body_pos);
 
 		// add inline completion width
 		// draw logically behind the previous character
@@ -1105,8 +1106,8 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 		// - After an inset with BreakAfter
 		Inset const * prevInset = !row.empty() ? row.back().inset : 0;
 		Inset const * nextInset = (i + 1 < end) ? par.getInset(i + 1) : 0;
-		if ((nextInset && nextInset->rowFlags() & Inset::BreakBefore)
-		    || (prevInset && prevInset->rowFlags() & Inset::BreakAfter)) {
+		if ((nextInset && nextInset->rowFlags() & BreakBefore)
+		    || (prevInset && prevInset->rowFlags() & BreakAfter)) {
 			row.flushed(true);
 			// Force a row creation after this one if it is ended by
 			// an inset that either
@@ -1114,8 +1115,8 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 			// - or (1) did force the row breaking, (2) is at end of
 			//   paragraph and (3) the said paragraph has an end label.
 			need_new_row = prevInset &&
-				(prevInset->rowFlags() & Inset::RowAfter
-				 || (prevInset->rowFlags() & Inset::BreakAfter && i + 1 == end
+				(prevInset->rowFlags() & AlwaysBreakAfter
+				 || (prevInset->rowFlags() & BreakAfter && i + 1 == end
 				     && text_->getEndLabel(row.pit()) != END_LABEL_NO_LABEL));
 			++i;
 			break;
@@ -1156,7 +1157,7 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 	int const next_width = max_width_ - leftMargin(row.pit(), row.endpos())
 		- rightMargin(row.pit());
 
-	if (row.shortenIfNeeded(body_pos, width, next_width))
+	if (row.shortenIfNeeded(width, next_width))
 		row.flushed(false);
 	row.right_boundary(!row.empty() && row.endpos() < end
 	                   && row.back().endpos == row.endpos());
@@ -1900,7 +1901,7 @@ int TextMetrics::leftMargin(pit_type const pit, pos_type const pos) const
 	    // display style insets do not need indentation
 	    && !(!par.empty()
 	         && par.isInset(0)
-	         && par.getInset(0)->rowFlags() & Inset::Display)
+	         && par.getInset(0)->rowFlags() & Display)
 	    && (!(tclass.isDefaultLayout(par.layout())
 	        || tclass.isPlainLayout(par.layout()))
 	        || buffer.params().paragraph_separation
diff --git a/src/insets/Inset.h b/src/insets/Inset.h
index af24423..372b95a 100644
--- a/src/insets/Inset.h
+++ b/src/insets/Inset.h
@@ -20,6 +20,7 @@
 #include "LayoutEnums.h"
 #include "OutputEnums.h"
 #include "OutputParams.h"
+#include "RowFlags.h"
 
 #include "support/docstring.h"
 #include "support/strfwd.h"
@@ -478,26 +479,8 @@ public:
 
 	virtual CtObject getCtObject(OutputParams const &) const;
 
-	enum RowFlags {
-		Inline = 0,
-		// break row before this inset
-		BreakBefore = 1 << 0,
-		// break row after this inset
-		BreakAfter = 1 << 1,
-		// it is possible to break after this inset
-		CanBreakAfter = 1 << 2,
-		// force new (maybe empty) row after this inset
-		RowAfter = 1 << 3,
-		// specify an alignment (left, right) for a display inset
-		// (default is center)
-		AlignLeft = 1 << 4,
-		AlignRight = 1 << 5,
-		// A display inset breaks row at both ends
-		Display = BreakBefore | BreakAfter
-	};
-
-	/// How should this inset be displayed in its row?
-	virtual RowFlags rowFlags() const { return Inline; }
+	// properties with respect to row breaking (made of RowFLag enums)
+	virtual int rowFlags() const { return Inline; }
 	/// indentation before this inset (only needed for displayed hull insets with fleqn option)
 	virtual int indent(BufferView const &) const { return 0; }
 	///
@@ -655,20 +638,6 @@ protected:
 };
 
 
-inline Inset::RowFlags operator|(Inset::RowFlags const d1,
-                                    Inset::RowFlags const d2)
-{
-	return static_cast<Inset::RowFlags>(int(d1) | int(d2));
-}
-
-
-inline Inset::RowFlags operator&(Inset::RowFlags const d1,
-                                    Inset::RowFlags const d2)
-{
-	return static_cast<Inset::RowFlags>(int(d1) & int(d2));
-}
-
-
 } // namespace lyx
 
 #endif
diff --git a/src/insets/InsetBibtex.h b/src/insets/InsetBibtex.h
index be7659f..55451f5 100644
--- a/src/insets/InsetBibtex.h
+++ b/src/insets/InsetBibtex.h
@@ -47,7 +47,7 @@ public:
 	///
 	InsetCode lyxCode() const override { return BIBTEX_CODE; }
 	///
-	RowFlags rowFlags() const override { return Display; }
+	int rowFlags() const override { return Display; }
 	///
 	void latex(otexstream &, OutputParams const &) const override;
 	///
diff --git a/src/insets/InsetCaption.h b/src/insets/InsetCaption.h
index ed6dbbb..c1bcd17 100644
--- a/src/insets/InsetCaption.h
+++ b/src/insets/InsetCaption.h
@@ -40,7 +40,7 @@ private:
 	///
 	void write(std::ostream & os) const override;
 	///
-	RowFlags rowFlags() const override { return Display; }
+	int rowFlags() const override { return Display; }
 	///
 	bool neverIndent() const override { return true; }
 	///
diff --git a/src/insets/InsetFloatList.h b/src/insets/InsetFloatList.h
index 489b0fe..ce6caa5 100644
--- a/src/insets/InsetFloatList.h
+++ b/src/insets/InsetFloatList.h
@@ -32,7 +32,7 @@ public:
 	///
 	InsetCode lyxCode() const override { return FLOAT_LIST_CODE; }
 	///
-	RowFlags rowFlags() const override { return Display; }
+	int rowFlags() const override { return Display; }
 	///
 	void write(std::ostream &) const override;
 	///
diff --git a/src/insets/InsetInclude.cpp b/src/insets/InsetInclude.cpp
index aeae2fb..10ea52b 100644
--- a/src/insets/InsetInclude.cpp
+++ b/src/insets/InsetInclude.cpp
@@ -1251,7 +1251,7 @@ string InsetInclude::contextMenuName() const
 }
 
 
-Inset::RowFlags InsetInclude::rowFlags() const
+int InsetInclude::rowFlags() const
 {
 	return type(params()) == INPUT ? Inline : Display;
 }
diff --git a/src/insets/InsetInclude.h b/src/insets/InsetInclude.h
index 8585222..d62c751 100644
--- a/src/insets/InsetInclude.h
+++ b/src/insets/InsetInclude.h
@@ -75,7 +75,7 @@ public:
 	///
 	void draw(PainterInfo & pi, int x, int y) const override;
 	///
-	RowFlags rowFlags() const override;
+	int rowFlags() const override;
 	///
 	InsetCode lyxCode() const override { return INCLUDE_CODE; }
 	///
diff --git a/src/insets/InsetIndex.h b/src/insets/InsetIndex.h
index bddb8ba..b064cc7 100644
--- a/src/insets/InsetIndex.h
+++ b/src/insets/InsetIndex.h
@@ -119,7 +119,7 @@ public:
 	///
 	bool hasSettings() const override;
 	///
-	RowFlags rowFlags() const override { return Display; }
+	int rowFlags() const override { return Display; }
 	//@}
 
 	/// \name Static public methods obligated for InsetCommand derived classes
diff --git a/src/insets/InsetListings.cpp b/src/insets/InsetListings.cpp
index e8fe8b1..57df06e 100644
--- a/src/insets/InsetListings.cpp
+++ b/src/insets/InsetListings.cpp
@@ -64,7 +64,7 @@ InsetListings::~InsetListings()
 }
 
 
-Inset::RowFlags InsetListings::rowFlags() const
+int InsetListings::rowFlags() const
 {
 	return params().isInline() || params().isFloat() ? Inline : Display | AlignLeft;
 }
diff --git a/src/insets/InsetListings.h b/src/insets/InsetListings.h
index 41be439..9d4eeb1 100644
--- a/src/insets/InsetListings.h
+++ b/src/insets/InsetListings.h
@@ -46,7 +46,7 @@ private:
 	///
 	InsetCode lyxCode() const override { return LISTINGS_CODE; }
 	/// lstinline is inlined, normal listing is displayed
-	RowFlags rowFlags() const override;
+	int rowFlags() const override;
 	///
 	docstring layoutName() const override;
 	///
diff --git a/src/insets/InsetNewline.h b/src/insets/InsetNewline.h
index 3d540a8..1ef0ae5 100644
--- a/src/insets/InsetNewline.h
+++ b/src/insets/InsetNewline.h
@@ -47,7 +47,7 @@ public:
 	explicit InsetNewline(InsetNewlineParams par) : Inset(0)
 	{ params_.kind = par.kind; }
 	///
-	RowFlags rowFlags() const override { return BreakAfter | RowAfter; }
+	int rowFlags() const override { return AlwaysBreakAfter; }
 	///
 	static void string2params(std::string const &, InsetNewlineParams &);
 	///
diff --git a/src/insets/InsetNewpage.h b/src/insets/InsetNewpage.h
index f020488..d086276 100644
--- a/src/insets/InsetNewpage.h
+++ b/src/insets/InsetNewpage.h
@@ -76,7 +76,7 @@ private:
 	///
 	void write(std::ostream & os) const override;
 	///
-	RowFlags rowFlags() const override { return (params_.kind == InsetNewpageParams::NOPAGEBREAK) ? Inline : Display; }
+	int rowFlags() const override { return (params_.kind == InsetNewpageParams::NOPAGEBREAK) ? Inline : Display; }
 	///
 	docstring insetLabel() const;
 	///
diff --git a/src/insets/InsetNomencl.h b/src/insets/InsetNomencl.h
index 1778e01..362cd46 100644
--- a/src/insets/InsetNomencl.h
+++ b/src/insets/InsetNomencl.h
@@ -94,7 +94,7 @@ public:
 	///
 	bool hasSettings() const override { return true; }
 	///
-	RowFlags rowFlags() const override { return Display; }
+	int rowFlags() const override { return Display; }
 	///
 	void latex(otexstream &, OutputParams const &) const override;
 	///
diff --git a/src/insets/InsetSeparator.h b/src/insets/InsetSeparator.h
index f7e0ab9..9352bdf 100644
--- a/src/insets/InsetSeparator.h
+++ b/src/insets/InsetSeparator.h
@@ -65,7 +65,7 @@ public:
 		return docstring();
 	}
 	///
-	RowFlags rowFlags() const override { return BreakAfter; }
+	int rowFlags() const override { return BreakAfter; }
 private:
 	///
 	InsetCode lyxCode() const override { return SEPARATOR_CODE; }
diff --git a/src/insets/InsetSpace.cpp b/src/insets/InsetSpace.cpp
index 9585596..1a2eb09 100644
--- a/src/insets/InsetSpace.cpp
+++ b/src/insets/InsetSpace.cpp
@@ -192,7 +192,7 @@ bool InsetSpace::getStatus(Cursor & cur, FuncRequest const & cmd,
 }
 
 
-Inset::RowFlags InsetSpace::rowFlags() const
+int InsetSpace::rowFlags() const
 {
 	switch (params_.kind) {
 		case InsetSpaceParams::PROTECTED:
diff --git a/src/insets/InsetSpace.h b/src/insets/InsetSpace.h
index 5cf7aa7..e401d6d 100644
--- a/src/insets/InsetSpace.h
+++ b/src/insets/InsetSpace.h
@@ -115,7 +115,7 @@ public:
 	///
 	docstring toolTip(BufferView const & bv, int x, int y) const override;
 	/// unprotected spaces allow line breaking after them
-	RowFlags rowFlags() const override;
+	int rowFlags() const override;
 	///
 	void metrics(MetricsInfo &, Dimension &) const override;
 	///
diff --git a/src/insets/InsetSpecialChar.cpp b/src/insets/InsetSpecialChar.cpp
index 3ecdaf1..88af653 100644
--- a/src/insets/InsetSpecialChar.cpp
+++ b/src/insets/InsetSpecialChar.cpp
@@ -83,7 +83,7 @@ docstring InsetSpecialChar::toolTip(BufferView const &, int, int) const
 }
 
 
-Inset::RowFlags InsetSpecialChar::rowFlags() const
+int InsetSpecialChar::rowFlags() const
 {
 	switch (kind_) {
 	case ALLOWBREAK:
diff --git a/src/insets/InsetSpecialChar.h b/src/insets/InsetSpecialChar.h
index 3056b10..0c8cc36 100644
--- a/src/insets/InsetSpecialChar.h
+++ b/src/insets/InsetSpecialChar.h
@@ -63,7 +63,7 @@ public:
 	///
 	docstring toolTip(BufferView const & bv, int x, int y) const override;
 	/// some special chars allow line breaking after them
-	RowFlags rowFlags() const override;
+	int rowFlags() const override;
 	///
 	void metrics(MetricsInfo &, Dimension &) const override;
 	///
diff --git a/src/insets/InsetTOC.h b/src/insets/InsetTOC.h
index 045ae07..ca3f463 100644
--- a/src/insets/InsetTOC.h
+++ b/src/insets/InsetTOC.h
@@ -37,7 +37,7 @@ public:
 	///
 	docstring layoutName() const override;
 	///
-	RowFlags rowFlags() const override { return Display; }
+	int rowFlags() const override { return Display; }
 	///
 	void validate(LaTeXFeatures &) const override;
 	///
diff --git a/src/insets/InsetTabular.cpp b/src/insets/InsetTabular.cpp
index 765388a..ac4e8e8 100644
--- a/src/insets/InsetTabular.cpp
+++ b/src/insets/InsetTabular.cpp
@@ -6139,21 +6139,21 @@ bool InsetTabular::getStatus(Cursor & cur, FuncRequest const & cmd,
 }
 
 
-Inset::RowFlags InsetTabular::rowFlags() const
-{
-		if (tabular.is_long_tabular) {
-			switch (tabular.longtabular_alignment) {
-			case Tabular::LYX_LONGTABULAR_ALIGN_LEFT:
-				return Display | AlignLeft;
-			case Tabular::LYX_LONGTABULAR_ALIGN_CENTER:
-				return Display;
-			case Tabular::LYX_LONGTABULAR_ALIGN_RIGHT:
-				return Display | AlignRight;
-			default:
-				return Display;
-			}
-		} else
-			return Inline;
+int InsetTabular::rowFlags() const
+{
+	if (tabular.is_long_tabular) {
+		switch (tabular.longtabular_alignment) {
+		case Tabular::LYX_LONGTABULAR_ALIGN_LEFT:
+			return Display | AlignLeft;
+		case Tabular::LYX_LONGTABULAR_ALIGN_CENTER:
+			return Display;
+		case Tabular::LYX_LONGTABULAR_ALIGN_RIGHT:
+			return Display | AlignRight;
+		default:
+			return Display;
+		}
+	} else
+		return Inline;
 }
 
 
diff --git a/src/insets/InsetTabular.h b/src/insets/InsetTabular.h
index 8d1be1b..0d4e7af 100644
--- a/src/insets/InsetTabular.h
+++ b/src/insets/InsetTabular.h
@@ -989,7 +989,7 @@ public:
 	//
 	bool isTable() const override { return true; }
 	///
-	RowFlags rowFlags() const override;
+	int rowFlags() const override;
 	///
 	void latex(otexstream &, OutputParams const &) const override;
 	///
diff --git a/src/insets/InsetVSpace.h b/src/insets/InsetVSpace.h
index 9b95f00..51c2b5b 100644
--- a/src/insets/InsetVSpace.h
+++ b/src/insets/InsetVSpace.h
@@ -62,7 +62,7 @@ private:
 	///
 	void write(std::ostream & os) const override;
 	///
-	RowFlags rowFlags() const override { return Display; }
+	int rowFlags() const override { return Display; }
 	///
 	void doDispatch(Cursor & cur, FuncRequest & cmd) override;
 	///
diff --git a/src/mathed/InsetMathHull.cpp b/src/mathed/InsetMathHull.cpp
index 97d95da..26ac40b 100644
--- a/src/mathed/InsetMathHull.cpp
+++ b/src/mathed/InsetMathHull.cpp
@@ -988,7 +988,7 @@ bool InsetMathHull::outerDisplay() const
 }
 
 
-Inset::RowFlags InsetMathHull::rowFlags() const
+int InsetMathHull::rowFlags() const
 {
 	switch (type_) {
 	case hullUnknown:
diff --git a/src/mathed/InsetMathHull.h b/src/mathed/InsetMathHull.h
index 0b865be..b019188 100644
--- a/src/mathed/InsetMathHull.h
+++ b/src/mathed/InsetMathHull.h
@@ -288,7 +288,7 @@ public:
 	///
 	Inset * editXY(Cursor & cur, int x, int y) override;
 	///
-	RowFlags rowFlags() const override;
+	int rowFlags() const override;
 	/// helper function
 	bool display() const { return rowFlags() & Display; }
 

commit aac01a89617703f3bec4dd2e7d2ca86b1b3551e5
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Sun Jul 11 15:19:37 2021 +0200

    Create new method TM::tokenizeParagraph
    
    This contains large parts of breakRow, but creates a unique row for the paragraph.
    
    The parts taken or not in redoParagraph are annotated.
    
    The new method is not used yet.

diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index 57c7715..1036066 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -868,21 +868,142 @@ private:
 
 } // namespace
 
+
+Row TextMetrics::tokenizeParagraph(pit_type const pit) const
+{
+	Row row;
+	row.pit(pit);
+	Paragraph const & par = text_->getPar(pit);
+	Buffer const & buf = text_->inset().buffer();
+	BookmarksSection::BookmarkPosList bpl =
+		theSession().bookmarks().bookmarksInPar(buf.fileName(), par.id());
+
+	pos_type const end = par.size();
+	pos_type const body_pos = par.beginOfBody();
+
+	// check for possible inline completion
+	DocIterator const & ic_it = bv_->inlineCompletionPos();
+	pos_type ic_pos = -1;
+	if (ic_it.inTexted() && ic_it.text() == text_ && ic_it.pit() == pit)
+		ic_pos = ic_it.pos();
+
+	// Now we iterate through until we reach the right margin
+	// or the end of the par, then build a representation of the row.
+	pos_type i = 0;
+	FontIterator fi = FontIterator(*this, par, pit, 0);
+	// The real stopping condition is a few lines below.
+	while (true) {
+		// Firstly, check whether there is a bookmark here.
+		if (lyxrc.bookmarks_visibility == LyXRC::BMK_INLINE)
+			for (auto const & bp_p : bpl)
+				if (bp_p.second == i) {
+					Font f = *fi;
+					f.fontInfo().setColor(Color_bookmark);
+					// ❶ U+2776 DINGBAT NEGATIVE CIRCLED DIGIT ONE
+					char_type const ch = 0x2775 + bp_p.first;
+					row.addVirtual(i, docstring(1, ch), f, Change());
+				}
+
+		// The stopping condition is here so that the display of a
+		// bookmark can take place at paragraph start too.
+		if (i >= end)
+			break;
+
+		char_type c = par.getChar(i);
+		// The most special cases are handled first.
+		if (par.isInset(i)) {
+			Inset const * ins = par.getInset(i);
+			Dimension dim = bv_->coordCache().insets().dim(ins);
+			row.add(i, ins, dim, *fi, par.lookupChange(i));
+		} else if (c == ' ' && i + 1 == body_pos) {
+			// There is a space at i, but it should not be
+			// added as a separator, because it is just
+			// before body_pos. Instead, insert some spacing to
+			// align text
+			FontMetrics const & fm = theFontMetrics(text_->labelFont(par));
+			// this is needed to make sure that the row width is correct
+			row.finalizeLast();
+			int const add = max(fm.width(par.layout().labelsep),
+			                    labelEnd(pit) - row.width());
+			row.addSpace(i, add, *fi, par.lookupChange(i));
+		} else if (c == '\t')
+			row.addSpace(i, theFontMetrics(*fi).width(from_ascii("    ")),
+				     *fi, par.lookupChange(i));
+		else if (c == 0x2028 || c == 0x2029) {
+			/**
+			 * U+2028 LINE SEPARATOR
+			 * U+2029 PARAGRAPH SEPARATOR
+
+			 * These are special unicode characters that break
+			 * lines/pragraphs. Not handling them lead to trouble wrt
+			 * Qt QTextLayout formatting. We add a visible character
+			 * on screen so that the user can see that something is
+			 * happening.
+			*/
+			row.finalizeLast();
+			// ⤶ U+2936 ARROW POINTING DOWNWARDS THEN CURVING LEFTWARDS
+			// ¶ U+00B6 PILCROW SIGN
+			char_type const screen_char = (c == 0x2028) ? 0x2936 : 0x00B6;
+			row.add(i, screen_char, *fi, par.lookupChange(i));
+		} else
+			row.add(i, c, *fi, par.lookupChange(i));
+
+		// add inline completion width
+		// draw logically behind the previous character
+		if (ic_pos == i + 1 && !bv_->inlineCompletion().empty()) {
+			docstring const comp = bv_->inlineCompletion();
+			size_t const uniqueTo =bv_->inlineCompletionUniqueChars();
+			Font f = *fi;
+
+			if (uniqueTo > 0) {
+				f.fontInfo().setColor(Color_inlinecompletion);
+				row.addVirtual(i + 1, comp.substr(0, uniqueTo), f, Change());
+			}
+			f.fontInfo().setColor(Color_nonunique_inlinecompletion);
+			row.addVirtual(i + 1, comp.substr(uniqueTo), f, Change());
+		}
+
+		++i;
+		++fi;
+	}
+	row.finalizeLast();
+	row.endpos(end);
+
+	// End of paragraph marker. The logic here is almost the
+	// same as in redoParagraph, remember keep them in sync.
+	ParagraphList const & pars = text_->paragraphs();
+	Change const & change = par.lookupChange(i);
+	if ((lyxrc.paragraph_markers || change.changed())
+	    && i == end && size_type(pit + 1) < pars.size()) {
+		// add a virtual element for the end-of-paragraph
+		// marker; it is shown on screen, but does not exist
+		// in the paragraph.
+		Font f(text_->layoutFont(pit));
+		f.fontInfo().setColor(Color_paragraphmarker);
+		f.setLanguage(par.getParLanguage(buf.params()));
+		// ¶ U+00B6 PILCROW SIGN
+		row.addVirtual(end, docstring(1, char_type(0x00B6)), f, change);
+	}
+
+	return row;
+}
+
+
 /** This is the function where the hard work is done. The code here is
  * very sensitive to small changes :) Note that part of the
  * intelligence is also in Row::shortenIfNeeded.
  */
 bool TextMetrics::breakRow(Row & row, int const right_margin) const
 {
-	LATTEST(row.empty());
-	Paragraph const & par = text_->getPar(row.pit());
-	Buffer const & buf = text_->inset().buffer();
-	BookmarksSection::BookmarkPosList bpl =
-		theSession().bookmarks().bookmarksInPar(buf.fileName(), par.id());
+	LATTEST(row.empty());//
+	Paragraph const & par = text_->getPar(row.pit());//
+	Buffer const & buf = text_->inset().buffer();//
+	BookmarksSection::BookmarkPosList bpl =//
+		theSession().bookmarks().bookmarksInPar(buf.fileName(), par.id());//
 
-	pos_type const end = par.size();
+	pos_type const end = par.size();//
 	pos_type const pos = row.pos();
-	pos_type const body_pos = par.beginOfBody();
+	pos_type const body_pos = par.beginOfBody();//
 	bool const is_rtl = text_->isRTL(row.pit());
 	bool need_new_row = false;
 
@@ -897,14 +1018,14 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 	int const width = max_width_ - row.right_margin;
 
 	// check for possible inline completion
-	DocIterator const & ic_it = bv_->inlineCompletionPos();
-	pos_type ic_pos = -1;
-	if (ic_it.inTexted() && ic_it.text() == text_ && ic_it.pit() == row.pit())
-		ic_pos = ic_it.pos();
+	DocIterator const & ic_it = bv_->inlineCompletionPos();//
+	pos_type ic_pos = -1;//
+	if (ic_it.inTexted() && ic_it.text() == text_ && ic_it.pit() == row.pit())//
+		ic_pos = ic_it.pos();//
 
 	// Now we iterate through until we reach the right margin
 	// or the end of the par, then build a representation of the row.
-	pos_type i = pos;
+	pos_type i = pos;//---------------------------------------------------vvv
 	FontIterator fi = FontIterator(*this, par, row.pit(), pos);
 	// The real stopping condition is a few lines below.
 	while (true) {
@@ -921,7 +1042,7 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 
 		// The stopping condition is here so that the display of a
 		// bookmark can take place at paragraph start too.
-		if (i >= end || (i != pos && row.width() > width))
+		if (i >= end || (i != pos && row.width() > width))//^width
 			break;
 
 		char_type c = par.getChar(i);
@@ -976,8 +1097,9 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 			}
 			f.fontInfo().setColor(Color_nonunique_inlinecompletion);
 			row.addVirtual(i + 1, comp.substr(uniqueTo), f, Change());
-		}
+		}//---------------------------------------------------------------^^^
 
+		// FIXME: Handle when breaking the rows
 		// Handle some situations that abruptly terminate the row
 		// - Before an inset with BreakBefore
 		// - After an inset with BreakAfter
@@ -1002,6 +1124,7 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 		++i;
 		++fi;
 	}
+	//--------------------------------------------------------------------vvv
 	row.finalizeLast();
 	row.endpos(i);
 
@@ -1010,7 +1133,7 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 	ParagraphList const & pars = text_->paragraphs();
 	Change const & change = par.lookupChange(i);
 	if ((lyxrc.paragraph_markers || change.changed())
-	    && !need_new_row
+	    && !need_new_row // not this
 	    && i == end && size_type(row.pit() + 1) < pars.size()) {
 		// add a virtual element for the end-of-paragraph
 		// marker; it is shown on screen, but does not exist
@@ -1025,6 +1148,8 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const
 	// Is there a end-of-paragaph change?
 	if (i == end && par.lookupChange(end).changed() && !need_new_row)
 		row.needsChangeBar(true);
+    //--------------------------------------------------------------------^^^
+	// FIXME : nothing below this
 
 	// if the row is too large, try to cut at last separator. In case
 	// of success, reset indication that the row was broken abruptly.
diff --git a/src/TextMetrics.h b/src/TextMetrics.h
index 1501250..1666cd0 100644
--- a/src/TextMetrics.h
+++ b/src/TextMetrics.h
@@ -151,6 +151,8 @@ private:
 	/// FIXME??
 	int labelEnd(pit_type const pit) const;
 
+	Row tokenizeParagraph(pit_type pit) const;
+
 	/// sets row.end to the pos value *after* which a row should break.
 	/// for example, the pos after which isNewLine(pos) == true
 	/// \return true when another row is required (after a newline)

commit 07a453fb45536cf414f59984ba402296eb1d0578
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Sun Jul 11 15:33:33 2021 +0200

    Small Row cleanups
    
    Move declaration of RowList to Row.h
    
    Move initialization of POD members of Row and Row::Element to declaration.
    
    Make method isVirtual() depend on type.
    
    Add new row element type INVALID and method isValid()
    
    Make methods R::E::left/right_pos inline.
    
    Add method R::E::splitAt() that returns an element containing the
    remaining stuff, or an invalid element if nothing was split. breakAt
    is now a simple wrapper around this function.
    
    Add method R::push_back().

diff --git a/src/ParagraphMetrics.h b/src/ParagraphMetrics.h
index 9889473..1d690aa 100644
--- a/src/ParagraphMetrics.h
+++ b/src/ParagraphMetrics.h
@@ -20,17 +20,8 @@
 #include "Dimension.h"
 #include "Row.h"
 
-#include <vector>
-
 namespace lyx {
 
-/**
- * Each paragraph is broken up into a number of rows on the screen.
- * This is a list of such on-screen rows, ordered from the top row
- * downwards.
- */
-typedef std::vector<Row> RowList;
-
 class BufferView;
 class Paragraph;
 
diff --git a/src/Row.cpp b/src/Row.cpp
index 2f6db3d..da2d885 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -124,53 +124,43 @@ pos_type Row::Element::x2pos(int &x) const
 			x = 0;
 			i = isRTL();
 		}
+		break;
+	case INVALID:
+		LYXERR0("x2pos: INVALID row element !");
 	}
 	//lyxerr << "=> p=" << pos + i << " x=" << x << endl;
 	return pos + i;
 }
 
 
-bool Row::Element::breakAt(int w, bool force)
+Row::Element Row::Element::splitAt(int w, bool force)
 {
 	if (type != STRING)
-		return false;
+		return Element();
 
 	FontMetrics const & fm = theFontMetrics(font);
 	dim.wid = w;
 	int const i = fm.breakAt(str, dim.wid, isRTL(), force);
 	if (i != -1) {
+		Element ret(STRING, pos + i, font, change);
+		ret.str = str.substr(i);
+		ret.endpos = ret.pos + ret.str.length();
 		str.erase(i);
 		endpos = pos + i;
 		//lyxerr << "breakAt(" << w << ")  Row element Broken at " << x << "(w(str)=" << fm.width(str) << "): e=" << *this << endl;
+		return ret;
 	}
 
-	return i != - 1;
+	return Element();
 }
 
 
-pos_type Row::Element::left_pos() const
-{
-	return isRTL() ? endpos : pos;
-}
-
-
-pos_type Row::Element::right_pos() const
+bool Row::Element::breakAt(int w, bool force)
 {
-	return isRTL() ? pos : endpos;
+	return splitAt(w, force).isValid();
 }
 
 
-Row::Row()
-	: separator(0), label_hfill(0), left_margin(0), right_margin(0),
-	  sel_beg(-1), sel_end(-1),
-	  begin_margin_sel(false), end_margin_sel(false),
-	  changed_(true),
-	  pit_(0), pos_(0), end_(0),
-	  right_boundary_(false), flushed_(false), rtl_(false),
-	  changebar_(false)
-{}
-
-
 bool Row::isMarginSelected(bool left, DocIterator const & beg,
 		DocIterator const & end) const
 {
@@ -262,6 +252,9 @@ ostream & operator<<(ostream & os, Row::Element const & e)
 	case Row::SPACE:
 		os << "SPACE: ";
 		break;
+	case Row::INVALID:
+		os << "INVALID: ";
+		break;
 	}
 	os << "width=" << e.full_width();
 	return os;
@@ -443,6 +436,13 @@ void Row::addSpace(pos_type const pos, int const width,
 }
 
 
+void Row::push_back(Row::Element const & e)
+{
+	dim_.wid += e.dim.wid;
+	elements_.push_back(e);
+}
+
+
 void Row::pop_back()
 {
 	dim_.wid -= elements_.back().dim.wid;
diff --git a/src/Row.h b/src/Row.h
index b54a233..3048cf1 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -49,7 +49,9 @@ public:
 		// An inset
 		INSET,
 		// Some spacing described by its width, not a string
-		SPACE
+		SPACE,
+		// Something that should not happen (for error handling)
+		INVALID
 	};
 
 /**
@@ -57,9 +59,12 @@ public:
  * by other methods that need to parse the Row contents.
  */
 	struct Element {
+		//
+		Element() = default;
+		//
 		Element(Type const t, pos_type p, Font const & f, Change const & ch)
-			: type(t), pos(p), endpos(p + 1), inset(0),
-			  extra(0), font(f), change(ch), final(false) {}
+			: type(t), pos(p), endpos(p + 1), font(f), change(ch) {}
+
 
 		// Return the number of separator in the element (only STRING type)
 		int countSeparators() const;
@@ -86,40 +91,49 @@ public:
 		 *  adjusted to the actual pixel position.
 		*/
 		pos_type x2pos(int &x) const;
+		/** Break the element in two if possible, so that its width is less
+		 * than \param w.
+		 * \return an element containing the remainder of the text, or
+		 *   an invalid element if nothing happened.
+		 * \param w: the desired maximum width
+		 * \param force: if true, the string is cut at any place, otherwise it
+		 *   respects the row breaking rules of characters.
+		 */
+		Element splitAt(int w, bool force);
 		/** Break the element if possible, so that its width is less
 		 * than \param w. Returns true on success. When \param force
-		 * is true, the string is cut at any place, other wise it
+		 * is true, the string is cut at any place, otherwise it
 		 * respects the row breaking rules of characters.
 		 */
 		bool breakAt(int w, bool force);
 
-		// Returns the position on left side of the element.
-		pos_type left_pos() const;
-		// Returns the position on right side of the element.
-		pos_type right_pos() const;
-
 		//
 		bool isRTL() const { return font.isVisibleRightToLeft(); }
 		// This is true for virtual elements.
-		// Note that we do not use the type here. The two definitions
-		// should be equivalent
-		bool isVirtual() const { return pos == endpos; }
+		bool isVirtual() const { return type == VIRTUAL; }
+		// Invalid element, for error handling
+		bool isValid() const { return type !=INVALID; }
+
+		// Returns the position on left side of the element.
+		pos_type left_pos() const { return isRTL() ? endpos : pos; };
+		// Returns the position on right side of the element.
+		pos_type right_pos() const { return isRTL() ? pos : endpos; };
 
 		// The kind of row element
-		Type type;
+		Type type = INVALID;
 		// position of the element in the paragraph
-		pos_type pos;
+		pos_type pos = 0;
 		// first position after the element in the paragraph
-		pos_type endpos;
+		pos_type endpos = 0;
 		// The dimension of the chunk (does not contains the
 		// separator correction)
 		Dimension dim;
 
 		// Non-zero only if element is an inset
-		Inset const * inset;
+		Inset const * inset = nullptr;
 
 		// Only non-null for justified rows
-		double extra;
+		double extra = 0;
 
 		// Non-empty if element is a string or is virtual
 		docstring str;
@@ -128,14 +142,15 @@ public:
 		//
 		Change change;
 		// is it possible to add contents to this element?
-		bool final;
+		bool final = false;
 
 		friend std::ostream & operator<<(std::ostream & os, Element const & row);
 	};
 
 
 	///
-	Row();
+	Row() {}
+
 	/**
 	 * Helper function: set variable \c var to value \c val, and mark
 	 * row as changed is the values were different. This is intended
@@ -264,7 +279,9 @@ public:
 	Element & back() { return elements_.back(); }
 	///
 	Element const & back() const { return elements_.back(); }
-	/// remove last element
+	/// add element at the end and update width
+	void push_back(Element const &);
+	/// remove last element and update width
 	void pop_back();
 	/**
 	 * if row width is too large, remove all elements after last
@@ -301,21 +318,21 @@ public:
 	friend std::ostream & operator<<(std::ostream & os, Row const & row);
 
 	/// additional width for separators in justified rows (i.e. space)
-	double separator;
+	double separator = 0;
 	/// width of hfills in the label
-	double label_hfill;
+	double label_hfill = 0;
 	/// the left margin position of the row
-	int left_margin;
+	int left_margin = 0;
 	/// the right margin of the row
-	int right_margin;
+	int right_margin = 0;
 	///
-	mutable pos_type sel_beg;
+	mutable pos_type sel_beg = -1;
 	///
-	mutable pos_type sel_end;
+	mutable pos_type sel_end = -1;
 	///
-	mutable bool begin_margin_sel;
+	mutable bool begin_margin_sel = false;
 	///
-	mutable bool end_margin_sel;
+	mutable bool end_margin_sel = false;
 
 private:
 	/// Decides whether the margin is selected.
@@ -340,28 +357,35 @@ private:
 	Elements elements_;
 
 	/// has the Row appearance changed since last drawing?
-	mutable bool changed_;
+	mutable bool changed_ = true;
 	/// Index of the paragraph that contains this row
-	pit_type pit_;
+	pit_type pit_ = 0;
 	/// first pos covered by this row
-	pos_type pos_;
+	pos_type pos_ = 0;
 	/// one behind last pos covered by this row
-	pos_type end_;
+	pos_type end_ = 0;
 	// Is there a boundary at the end of the row (display inset...)
-	bool right_boundary_;
+	bool right_boundary_ = false;
 	// Shall the row be flushed when it is supposed to be justified?
-	bool flushed_;
+	bool flushed_ = false;
 	/// Row dimension.
 	Dimension dim_;
 	/// Row contents dimension. Does not contain the space above/below row.
 	Dimension contents_dim_;
 	/// true when this row lives in a right-to-left paragraph
-	bool rtl_;
+	bool rtl_ = false;
 	/// true when a changebar should be drawn in the margin
-	bool changebar_;
+	bool changebar_ = false;
 };
 
 
+/**
+ * Each paragraph is broken up into a number of rows on the screen.
+ * This is a list of such on-screen rows, ordered from the top row
+ * downwards.
+ */
+typedef std::vector<Row> RowList;
+
 } // namespace lyx
 
 #endif
diff --git a/src/RowPainter.cpp b/src/RowPainter.cpp
index 400b7b6..656f89a 100644
--- a/src/RowPainter.cpp
+++ b/src/RowPainter.cpp
@@ -565,6 +565,10 @@ void RowPainter::paintText()
 
 		case Row::SPACE:
 			paintTextDecoration(e);
+			break;
+
+		case Row::INVALID:
+			LYXERR0("Trying to paint INVALID row element.");
 		}
 
 		// The markings of foreign languages

commit 8344f7967b7d3ce53d4ebac4c5af13f4261b5087
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Sat Jul 10 23:21:27 2021 +0200

    Change FontMetrics::breakAt to return a position
    
    Since we intend to break the row element in two, it is not good to
    truncate the string too early.
    
    Moreover, the row element width is now set at this point, even if no
    breaking occurs.

diff --git a/src/Row.cpp b/src/Row.cpp
index 16ae9ae..2f6db3d 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -132,19 +132,19 @@ pos_type Row::Element::x2pos(int &x) const
 
 bool Row::Element::breakAt(int w, bool force)
 {
-	if (type != STRING || dim.wid <= w)
+	if (type != STRING)
 		return false;
 
 	FontMetrics const & fm = theFontMetrics(font);
-	int x = w;
-	if(fm.breakAt(str, x, isRTL(), force)) {
-		dim.wid = x;
-		endpos = pos + str.length();
+	dim.wid = w;
+	int const i = fm.breakAt(str, dim.wid, isRTL(), force);
+	if (i != -1) {
+		str.erase(i);
+		endpos = pos + i;
 		//lyxerr << "breakAt(" << w << ")  Row element Broken at " << x << "(w(str)=" << fm.width(str) << "): e=" << *this << endl;
-		return true;
 	}
 
-	return false;
+	return i != - 1;
 }
 
 
diff --git a/src/frontends/FontMetrics.h b/src/frontends/FontMetrics.h
index 2a6ffea..b562ebf 100644
--- a/src/frontends/FontMetrics.h
+++ b/src/frontends/FontMetrics.h
@@ -122,13 +122,14 @@ public:
 	 */
 	virtual int x2pos(docstring const & s, int & x, bool rtl, double ws) const = 0;
 	/**
-	 * Break string at width at most x.
-	 * \return true if successful
+	 * Break string s at width at most x.
+	 * \return break position (-1 if not successful)
+	 * \param position x is updated to real width
 	 * \param rtl is true for right-to-left layout
 	 * \param force is false for breaking at word separator, true for
 	 *   arbitrary position.
 	 */
-	virtual bool breakAt(docstring & s, int & x, bool rtl, bool force) const = 0;
+	virtual int breakAt(docstring const & s, int & x, bool rtl, bool force) const = 0;
 	/// return char dimension for the font.
 	virtual Dimension const dimension(char_type c) const = 0;
 	/**
diff --git a/src/frontends/qt/GuiFontMetrics.cpp b/src/frontends/qt/GuiFontMetrics.cpp
index 5e8f535..25bf7a4 100644
--- a/src/frontends/qt/GuiFontMetrics.cpp
+++ b/src/frontends/qt/GuiFontMetrics.cpp
@@ -533,7 +533,7 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
 	tl.endLayout();
 	int const line_wid = iround(line.horizontalAdvance());
 	if ((force && line.textLength() == offset) || line_wid > x)
-		return {-1, -1};
+		return {-1, line_wid};
 	/* Since QString is UTF-16 and docstring is UCS-4, the offsets may
 	 * not be the same when there are high-plan unicode characters
 	 * (bug #10443).
@@ -557,6 +557,9 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
 		--len;
 	LASSERT(len > 0 || qlen == 0, /**/);
 #endif
+	// si la chaîne est déjà trop courte, on ne coupe pas
+	if (len == static_cast<int>(s.length()))
+		len = -1;
 	return {len, line_wid};
 }
 
@@ -568,7 +571,7 @@ uint qHash(BreakAtKey const & key)
 }
 
 
-bool GuiFontMetrics::breakAt(docstring & s, int & x, bool const rtl, bool const force) const
+int GuiFontMetrics::breakAt(docstring const & s, int & x, bool const rtl, bool const force) const
 {
 	PROFILE_THIS_BLOCK(breakAt);
 	if (s.empty())
@@ -583,11 +586,8 @@ bool GuiFontMetrics::breakAt(docstring & s, int & x, bool const rtl, bool const
 		pp = breakAt_helper(s, x, rtl, force);
 		breakat_cache_.insert(key, pp, sizeof(key) + s.size() * sizeof(char_type));
 	}
-	if (pp.first == -1)
-		return false;
-	s = s.substr(0, pp.first);
 	x = pp.second;
-	return true;
+	return pp.first;
 }
 
 
diff --git a/src/frontends/qt/GuiFontMetrics.h b/src/frontends/qt/GuiFontMetrics.h
index 9c7ce89..ef8588a 100644
--- a/src/frontends/qt/GuiFontMetrics.h
+++ b/src/frontends/qt/GuiFontMetrics.h
@@ -77,7 +77,7 @@ public:
 	int signedWidth(docstring const & s) const override;
 	int pos2x(docstring const & s, int pos, bool rtl, double ws) const override;
 	int x2pos(docstring const & s, int & x, bool rtl, double ws) const override;
-	bool breakAt(docstring & s, int & x, bool rtl, bool force) const override;
+	int breakAt(docstring const & s, int & x, bool rtl, bool force) const override;
 	Dimension const dimension(char_type c) const override;
 
 	void rectText(docstring const & str,

commit 9a4a6ca079f24b0ecdd7e33e1b42a5360980cca3
Author: Juergen Spitzmueller <spitz at lyx.org>
Date:   Fri Oct 1 12:42:06 2021 +0200

    Fix \cline calculation when last column has decimal alignment

diff --git a/src/insets/InsetTabular.cpp b/src/insets/InsetTabular.cpp
index 15f1ca7..765388a 100644
--- a/src/insets/InsetTabular.cpp
+++ b/src/insets/InsetTabular.cpp
@@ -2605,7 +2605,7 @@ void Tabular::TeXTopHLine(otexstream & os, row_type row, list<col_type> columns,
 						break;
 				}
 
-				for (col_type j = cstart ; j < c ; ++j)
+				for (col_type j = cstart ; j <= c ; ++j)
 					if (column_info[j].alignment == LYX_ALIGN_DECIMAL)
 						++offset;
 				col_type lastcol = (*it1 == *it2) ? c + 1 + offset : columns.size() - c + offset;
@@ -2741,7 +2741,7 @@ void Tabular::TeXBottomHLine(otexstream & os, row_type row, list<col_type> colum
 						break;
 				}
 
-				for (col_type j = cstart ; j < c ; ++j)
+				for (col_type j = cstart ; j <= c ; ++j)
 					if (column_info[j].alignment == LYX_ALIGN_DECIMAL)
 						++offset;
 				col_type lastcol = (*it1 == *it2) ? c + 1 + offset : columns.size() - c + offset;

commit 4658781e565455383ed26adb13fcbefdf6a85c28
Author: Scott Kostyshak <skostysh at lyx.org>
Date:   Thu Sep 30 14:22:49 2021 -0400

    Add regression test for font switch before comment
    
    This is a tricky issue that was fixed at 9eab66eb.

diff --git a/autotests/export/latex/lyxbugs-resolved/font-switch-before-comment.lyx b/autotests/export/latex/lyxbugs-resolved/font-switch-before-comment.lyx
new file mode 100644
index 0000000..b3377fb
--- /dev/null
+++ b/autotests/export/latex/lyxbugs-resolved/font-switch-before-comment.lyx
@@ -0,0 +1,99 @@
+#LyX 2.3 created this file. For more info see http://www.lyx.org/
+\lyxformat 544
+\begin_document
+\begin_header
+\save_transient_properties true
+\origin unavailable
+\textclass article
+\use_default_options true
+\maintain_unincluded_children false
+\language english
+\language_package default
+\inputencoding auto
+\fontencoding global
+\font_roman "default" "default"
+\font_sans "default" "default"
+\font_typewriter "default" "default"
+\font_math "auto" "auto"
+\font_default_family default
+\use_non_tex_fonts false
+\font_sc false
+\font_osf false
+\font_sf_scale 100 100
+\font_tt_scale 100 100
+\use_microtype false
+\use_dash_ligatures true
+\graphics default
+\default_output_format pdf2
+\output_sync 1
+\bibtex_command default
+\index_command default
+\paperfontsize default
+\spacing single
+\use_hyperref false
+\papersize default
+\use_geometry false
+\use_package amsmath 1
+\use_package amssymb 1
+\use_package cancel 1
+\use_package esint 1
+\use_package mathdots 1
+\use_package mathtools 1
+\use_package mhchem 1
+\use_package stackrel 1
+\use_package stmaryrd 1
+\use_package undertilde 1
+\cite_engine basic
+\cite_engine_type default
+\biblio_style plain
+\use_bibtopic false
+\use_indices false
+\paperorientation portrait
+\suppress_date false
+\justification true
+\use_refstyle 1
+\use_minted 0
+\index Index
+\shortcut idx
+\color #008000
+\end_index
+\secnumdepth 3
+\tocdepth 3
+\paragraph_separation indent
+\paragraph_indentation default
+\is_math_indent 0
+\math_numbering_side default
+\quotes_style english
+\dynamic_quotes 0
+\papercolumns 1
+\papersides 1
+\paperpagestyle default
+\tracking_changes false
+\output_changes false
+\html_math_output 0
+\html_css_as_file 0
+\html_be_strict false
+\end_header
+
+\begin_body
+
+\begin_layout Standard
+Hello, 
+\emph on
+regardless
+\begin_inset Note Comment
+status open
+
+\begin_layout Plain Layout
+inside comment
+\end_layout
+
+\end_inset
+
+
+\emph default
+ whether...
+\end_layout
+
+\end_body
+\end_document

commit 9eab66ebb4e2aacfdec6ca472733fdeee9205bea
Author: Juergen Spitzmueller <spitz at lyx.org>
Date:   Thu Sep 30 12:53:41 2021 +0200

    Close font switches before comments

diff --git a/src/Paragraph.cpp b/src/Paragraph.cpp
index 6248d8e..e437b65 100644
--- a/src/Paragraph.cpp
+++ b/src/Paragraph.cpp
@@ -2694,7 +2694,12 @@ void Paragraph::latex(BufferParams const & bparams,
 				&& getInset(i)
 				&& getInset(i)->allowMultiPar()
 				&& getInset(i)->lyxCode() != ERT_CODE
-				&& getInset(i)->producesOutput();
+				&& (getInset(i)->producesOutput()
+				    // FIXME Something more general?
+				    // Comments do not "produce output" but are still
+				    // part of the TeX source and require font switches
+				    // to be closed (otherwise LaTeX fails).
+				    || getInset(i)->layoutName() == "Note:Comment");
 
 		bool closeLanguage = false;
 		bool lang_switched_at_inset = false;

commit d3c335a5d524e2edeb73ae1a891fcc58ba5bfd1a
Author: Yuriy Skalko <yuriy.skalko at gmail.com>
Date:   Wed Sep 29 12:49:21 2021 +0300

    Remove useless casts reported by GCC with -Wuseless-cast option

diff --git a/src/BiblioInfo.cpp b/src/BiblioInfo.cpp
index 85c3e88..638579a 100644
--- a/src/BiblioInfo.cpp
+++ b/src/BiblioInfo.cpp
@@ -1402,7 +1402,7 @@ docstring const BiblioInfo::getInfo(docstring const & key,
 {
 	BiblioInfo::const_iterator it = find(key);
 	if (it == end())
-		return docstring(_("Bibliography entry not found!"));
+		return _("Bibliography entry not found!");
 	BibTeXInfo const & data = it->second;
 	BibTeXInfoList xrefptrs;
 	for (docstring const & xref : getXRefs(data)) {
diff --git a/src/Buffer.cpp b/src/Buffer.cpp
index 457aa43..a713f8d 100644
--- a/src/Buffer.cpp
+++ b/src/Buffer.cpp
@@ -3827,7 +3827,7 @@ void Buffer::Impl::updateMacros(DocIterator & it, DocIterator & scope)
 			// FIXME (Abdel), I don't understand why we pass 'it' here
 			// instead of 'macroTemplate' defined above... is this correct?
 			macros[macroTemplate.name()][it] =
-				Impl::ScopeMacro(scope, MacroData(const_cast<Buffer *>(owner_), it));
+				Impl::ScopeMacro(scope, MacroData(owner_, it));
 		}
 
 		// next paragraph
diff --git a/src/Converter.cpp b/src/Converter.cpp
index b4340c8..4e9a2ad 100644
--- a/src/Converter.cpp
+++ b/src/Converter.cpp
@@ -853,7 +853,7 @@ Converters::RetVal Converters::runLaTeX(Buffer const & buffer, string const & co
 
 	// do the LaTeX run(s)
 	string const name = buffer.latexName();
-	LaTeX latex(command, runparams, FileName(makeAbsPath(name)),
+	LaTeX latex(command, runparams, makeAbsPath(name),
 	            buffer.filePath(), buffer.layoutPos(),
 	            buffer.isClone(), buffer.freshStartRequired());
 	TeXErrors terr;
diff --git a/src/LaTeX.cpp b/src/LaTeX.cpp
index 6e7fe92..a8d14b1 100644
--- a/src/LaTeX.cpp
+++ b/src/LaTeX.cpp
@@ -795,7 +795,7 @@ int LaTeX::scanLogFile(TeXErrors & terr)
 	string tmp =
 		onlyFileName(changeExtension(file.absFileName(), ".log"));
 	LYXERR(Debug::LATEX, "Log file: " << tmp);
-	FileName const fn = FileName(makeAbsPath(tmp));
+	FileName const fn = makeAbsPath(tmp);
 	// FIXME we should use an ifdocstream here and a docstring for token
 	// below. The encoding of the log file depends on the _output_ (font)
 	// encoding of the TeX file (T1, TU etc.). See #10728.
diff --git a/src/ParIterator.cpp b/src/ParIterator.cpp
index 7289889..10de5f3 100644
--- a/src/ParIterator.cpp
+++ b/src/ParIterator.cpp
@@ -69,7 +69,7 @@ ParIterator & ParIterator::operator--()
 
 Paragraph & ParIterator::operator*() const
 {
-	return const_cast<Paragraph&>(text()->getPar(pit()));
+	return text()->getPar(pit());
 }
 
 
@@ -81,7 +81,7 @@ pit_type ParIterator::pit() const
 
 Paragraph * ParIterator::operator->() const
 {
-	return const_cast<Paragraph*>(&text()->getPar(pit()));
+	return &text()->getPar(pit());
 }
 
 
@@ -93,7 +93,7 @@ pit_type ParIterator::outerPar() const
 
 ParagraphList & ParIterator::plist() const
 {
-	return const_cast<ParagraphList&>(text()->paragraphs());
+	return text()->paragraphs();
 }
 
 
diff --git a/src/frontends/qt/ColorCache.cpp b/src/frontends/qt/ColorCache.cpp
index 822f44a..821494e 100644
--- a/src/frontends/qt/ColorCache.cpp
+++ b/src/frontends/qt/ColorCache.cpp
@@ -23,7 +23,7 @@ namespace{
 
 QPalette::ColorRole role(ColorCode col)
 {
-	switch (ColorCode(col)) {
+	switch (col) {
 	case Color_background:
 	case Color_commentbg:
 	case Color_greyedoutbg:
diff --git a/src/frontends/qt/GuiBox.cpp b/src/frontends/qt/GuiBox.cpp
index 0d22aa7..ddd5775 100644
--- a/src/frontends/qt/GuiBox.cpp
+++ b/src/frontends/qt/GuiBox.cpp
@@ -172,7 +172,7 @@ void GuiBox::fillComboColor(QComboBox * combo, bool const is_none)
 	for (; cit != color_codes_.end(); ++cit) {
 		QString const latexname = toqstr(lcolor.getLaTeXName(*cit));
 		QString const guiname = toqstr(translateIfPossible(lcolor.getGUIName(*cit)));
-		color = QColor(guiApp->colorCache().get(*cit, false));
+		color = guiApp->colorCache().get(*cit, false);
 		coloritem.fill(color);
 		combo->addItem(QIcon(coloritem), guiname, latexname);
 	}
diff --git a/src/frontends/qt/GuiDocument.cpp b/src/frontends/qt/GuiDocument.cpp
index 40d7bc0..9389272 100644
--- a/src/frontends/qt/GuiDocument.cpp
+++ b/src/frontends/qt/GuiDocument.cpp
@@ -1029,7 +1029,7 @@ GuiDocument::GuiDocument(GuiView & lv)
 		if (encvar.unsafe() ||encvar.guiName().empty()
 		    || utf8_base_encodings.contains(toqstr(encvar.name())))
 			continue;
-		if (std::string(encvar.name()).find("utf8") == 0)
+		if (encvar.name().find("utf8") == 0)
 			encodingmap_utf8.insert(qt_(encvar.guiName()), toqstr(encvar.name()));
 		else
 			encodingmap.insert(qt_(encvar.guiName()), toqstr(encvar.name()));
@@ -5165,7 +5165,7 @@ GuiDocument::modInfoStruct GuiDocument::modInfo(LyXModule const & mod)
 	QString const guiname = toqstr(translateIfPossible(from_utf8(mod.getName())));
 	m.missingreqs = !isModuleAvailable(mod.getID());
 	if (m.missingreqs) {
-		m.name = QString(qt_("%1 (missing req.)")).arg(guiname);
+		m.name = qt_("%1 (missing req.)").arg(guiname);
 	} else
 		m.name = guiname;
 	m.category = mod.category().empty() ? qt_("Miscellaneous")
@@ -5178,7 +5178,7 @@ GuiDocument::modInfoStruct GuiDocument::modInfo(LyXModule const & mod)
 		desc.truncate(pos);
 	m.local = mod.isLocal();
 	QString const mtype = m.local ? qt_("personal module") : qt_("distributed module");
-	QString modulename = QString(qt_("<b>Module name:</b> <i>%1</i> (%2)")).arg(toqstr(m.id)).arg(mtype);
+	QString modulename = qt_("<b>Module name:</b> <i>%1</i> (%2)").arg(toqstr(m.id)).arg(mtype);
 	// Tooltip is the desc followed by the module name and the type
 	m.description = QString("%1%2")
 		.arg(desc.isEmpty() ? QString() : QString("<p>%1</p>").arg(desc),
diff --git a/src/frontends/qt/GuiExternal.cpp b/src/frontends/qt/GuiExternal.cpp
index 62c56c9..65fb861 100644
--- a/src/frontends/qt/GuiExternal.cpp
+++ b/src/frontends/qt/GuiExternal.cpp
@@ -444,7 +444,7 @@ static void getSize(external::ResizeData & data,
 		data.scale = widgetToDoubleStr(&widthED);
 		data.width = Length();
 	} else {
-		data.width = Length(widgetsToLength(&widthED, &widthUnitCO));
+		data.width = widgetsToLength(&widthED, &widthUnitCO);
 		data.scale = string();
 	}
 	data.height = Length(widgetsToLength(&heightED, &heightUnitCO));
diff --git a/src/frontends/qt/GuiMathMatrix.cpp b/src/frontends/qt/GuiMathMatrix.cpp
index d339e2c..db06278 100644
--- a/src/frontends/qt/GuiMathMatrix.cpp
+++ b/src/frontends/qt/GuiMathMatrix.cpp
@@ -101,7 +101,7 @@ GuiMathMatrix::GuiMathMatrix(GuiView & lv)
 
 void GuiMathMatrix::columnsChanged(int)
 {
-	int const nx = int(columnsSB->value());
+	int const nx = columnsSB->value();
 	halignED->setText(QString(nx, 'c'));
 }
 
@@ -159,7 +159,7 @@ void GuiMathMatrix::slotOK()
 		// otherwise create just a standard AMS matrix
 		if (sh.contains('l') || sh.contains('r') || sh.contains('|')) {
 			string const str_ams = fromqstr(
-				QString("%1 %2 %3").arg(int(1)).arg(int(1)).arg(deco_name));
+				QString("%1 %2 %3").arg(1).arg(1).arg(deco_name));
 			dispatch(FuncRequest(LFUN_MATH_AMS_MATRIX, str_ams));
 		} else {
 			string const str_ams = fromqstr(
diff --git a/src/frontends/qt/GuiPrefs.cpp b/src/frontends/qt/GuiPrefs.cpp
index 7d88085..7c7a969 100644
--- a/src/frontends/qt/GuiPrefs.cpp
+++ b/src/frontends/qt/GuiPrefs.cpp
@@ -1153,7 +1153,7 @@ void PrefColors::applyRC(LyXRC & rc) const
 void PrefColors::updateRC(LyXRC const & rc)
 {
 	for (size_type i = 0; i < lcolors_.size(); ++i) {
-		QColor color = QColor(guiApp->colorCache().get(lcolors_[i], false));
+		QColor color = guiApp->colorCache().get(lcolors_[i], false);
 		QPixmap coloritem(32, 32);
 		coloritem.fill(color);
 		lyxObjectsLW->item(int(i))->setIcon(QIcon(coloritem));
diff --git a/src/frontends/qt/GuiSendto.cpp b/src/frontends/qt/GuiSendto.cpp
index ec40ff3..1f56178 100644
--- a/src/frontends/qt/GuiSendto.cpp
+++ b/src/frontends/qt/GuiSendto.cpp
@@ -112,7 +112,7 @@ bool GuiSendTo::isValid()
 {
 	int const line = formatLW->currentRow();
 
-	if (line < 0 || (line > int(formatLW->count())))
+	if (line < 0 || (line > formatLW->count()))
 		return false;
 
 	return (!formatLW->selectedItems().empty()
diff --git a/src/frontends/qt/GuiView.cpp b/src/frontends/qt/GuiView.cpp
index 9f7531c..6e433e1 100644
--- a/src/frontends/qt/GuiView.cpp
+++ b/src/frontends/qt/GuiView.cpp
@@ -1182,7 +1182,7 @@ bool GuiView::prepareAllBuffersForLogout()
 	// We cannot use a for loop as the buffer list cycles.
 	Buffer * b = first;
 	do {
-		if (!saveBufferIfNeeded(const_cast<Buffer &>(*b), false))
+		if (!saveBufferIfNeeded(*b, false))
 			return false;
 		b = theBufferList().next(b);
 	} while (b != first);
@@ -3171,7 +3171,7 @@ bool GuiView::exportBufferAs(Buffer & b, docstring const & iformat)
 		return false;
 
 	// fname is now the new Buffer location.
-	if (FileName(fname).exists()) {
+	if (fname.exists()) {
 		docstring const file = makeDisplayPath(fname.absFileName(), 30);
 		docstring text = bformat(_("The document %1$s already "
 					   "exists.\n\nDo you want to "
diff --git a/src/frontends/qt/Menus.cpp b/src/frontends/qt/Menus.cpp
index 8f6464f..2fbac3f 100644
--- a/src/frontends/qt/Menus.cpp
+++ b/src/frontends/qt/Menus.cpp
@@ -1302,8 +1302,7 @@ void MenuDefinition::expandToc2(Toc const & toc_list,
 						label += QString::number(++shortcut_count);
 				}
 			}
-			add(MenuItem(MenuItem::Command, label,
-					    FuncRequest(toc_list[i].action())));
+			add(MenuItem(MenuItem::Command, label, toc_list[i].action()));
 			// separator after the menu heading
 			if (toc_list[i].depth() < depth)
 				add(MenuItem(MenuItem::Separator));
@@ -1331,8 +1330,7 @@ void MenuDefinition::expandToc2(Toc const & toc_list,
 				break;
 			}
 			if (new_pos == pos + 1) {
-				add(MenuItem(MenuItem::Command,
-						    label, FuncRequest(toc_list[pos].action())));
+				add(MenuItem(MenuItem::Command, label, toc_list[pos].action()));
 			} else {
 				MenuDefinition sub;
 				sub.expandToc2(toc_list, pos, new_pos, depth + 1, toc_type);
diff --git a/src/insets/InsetGraphics.cpp b/src/insets/InsetGraphics.cpp
index 7daa40d..ef03b6f 100644
--- a/src/insets/InsetGraphics.cpp
+++ b/src/insets/InsetGraphics.cpp
@@ -742,7 +742,7 @@ string InsetGraphics::prepareFile(OutputParams const & runparams) const
 
 	if (from == to) {
 		// source and destination formats are the same
-		if (!runparams.nice && !FileName(temp_file).hasExtension(ext)) {
+		if (!runparams.nice && !temp_file.hasExtension(ext)) {
 			// The LaTeX compiler will not be able to determine
 			// the file format from the extension, so we must
 			// change it.
diff --git a/src/insets/InsetIPAMacro.cpp b/src/insets/InsetIPAMacro.cpp
index 5d2035b..a529d04 100644
--- a/src/insets/InsetIPAMacro.cpp
+++ b/src/insets/InsetIPAMacro.cpp
@@ -287,7 +287,7 @@ int InsetIPADeco::plaintext(odocstringstream & os,
 			    OutputParams const & runparams, size_t max_length) const
 {
 	odocstringstream ods;
-	int h = (int)(InsetCollapsible::plaintext(ods, runparams, max_length) / 2);
+	int h = InsetCollapsible::plaintext(ods, runparams, max_length) / 2;
 	docstring result = ods.str();
 	docstring const before = result.substr(0, h);
 	docstring const after = result.substr(h, result.size());
@@ -311,7 +311,7 @@ void InsetIPADeco::docbook(XMLStream & xs, OutputParams const & runparams) const
 	// The special combining character must be put in the middle, between the two other characters.
 	// It will not work if there is anything else than two pure characters, so going back to plaintext.
 	odocstringstream ods;
-	int h = (int)(InsetText::plaintext(ods, runparams) / 2);
+	int h = InsetText::plaintext(ods, runparams) / 2;
 	docstring result = ods.str();
 	docstring const before = result.substr(0, h);
 	docstring const after = result.substr(h, result.size());
diff --git a/src/insets/InsetTabular.cpp b/src/insets/InsetTabular.cpp
index d6d9c57..15f1ca7 100644
--- a/src/insets/InsetTabular.cpp
+++ b/src/insets/InsetTabular.cpp
@@ -617,7 +617,7 @@ DocIterator separatorPos(InsetTableCell const * cell, docstring const & align_d)
 
 InsetTableCell splitCell(InsetTableCell & head, docstring const & align_d, bool & hassep)
 {
-	InsetTableCell tail = InsetTableCell(head);
+	InsetTableCell tail = head;
 	DocIterator const dit = separatorPos(&head, align_d);
 	hassep = static_cast<bool>(dit);
 	if (hassep) {
@@ -839,13 +839,13 @@ void Tabular::appendRow(row_type row)
 
 void Tabular::insertRow(row_type const row, bool copy)
 {
-	row_info.insert(row_info.begin() + row + 1, RowData(row_info[row]));
+	row_info.insert(row_info.begin() + row + 1, row_info[row]);
 	cell_info.insert(cell_info.begin() + row + 1,
 		cell_vector(0, CellData(buffer_)));
 
 	for (col_type c = 0; c < ncols(); ++c) {
 		cell_info[row + 1].insert(cell_info[row + 1].begin() + c,
-			copy ? CellData(cell_info[row][c]) : CellData(buffer_));
+			copy ? cell_info[row][c] : CellData(buffer_));
 		if (cell_info[row][c].multirow == CELL_BEGIN_OF_MULTIROW)
 			cell_info[row + 1][c].multirow = CELL_PART_OF_MULTIROW;
 	}
@@ -1002,11 +1002,11 @@ void Tabular::appendColumn(col_type col)
 void Tabular::insertColumn(col_type const col, bool copy)
 {
 	bool const ct = buffer().params().track_changes;
-	column_info.insert(column_info.begin() + col + 1, ColumnData(column_info[col]));
+	column_info.insert(column_info.begin() + col + 1, column_info[col]);
 
 	for (row_type r = 0; r < nrows(); ++r) {
 		cell_info[r].insert(cell_info[r].begin() + col + 1,
-			copy ? CellData(cell_info[r][col]) : CellData(buffer_));
+			copy ? cell_info[r][col] : CellData(buffer_));
 		if (cell_info[r][col].multicolumn == CELL_BEGIN_OF_MULTICOLUMN)
 			cell_info[r][col + 1].multicolumn = CELL_PART_OF_MULTICOLUMN;
 	}
@@ -4525,7 +4525,7 @@ void InsetTabular::metrics(MetricsInfo & mi, Dimension & dim) const
 			// determine horizontal offset because of decimal align (if necessary)
 			int decimal_width = 0;
 			if (tabular.getAlignment(cell) == LYX_ALIGN_DECIMAL) {
-				InsetTableCell tail = InsetTableCell(*tabular.cellInset(cell));
+				InsetTableCell tail = *tabular.cellInset(cell);
 				tail.setBuffer(tabular.buffer());
 				// we need to set macrocontext position everywhere
 				// otherwise we crash with nested insets (e.g. footnotes)
@@ -4778,7 +4778,7 @@ void InsetTabular::drawCellLines(PainterInfo & pi, int x, int y,
 	Color colour = Color_tabularline;
 	if (tabular.column_info[col].change.changed()
 	    || tabular.row_info[row].change.changed())
-		colour = InsetTableCell(*tabular.cellInset(cell)).paragraphs().front().lookupChange(0).color();
+		colour = tabular.cellInset(cell)->paragraphs().front().lookupChange(0).color();
 
 	// Top
 	bool drawline = tabular.topLine(cell)
@@ -6018,7 +6018,7 @@ bool InsetTabular::getStatus(Cursor & cur, FuncRequest const & cmd,
 		}
 		// check if there is already a caption
 		bool have_caption = false;
-		InsetTableCell itc = InsetTableCell(*tabular.cellInset(cur.idx()));
+		InsetTableCell itc = *tabular.cellInset(cur.idx());
 		ParagraphList::const_iterator pit = itc.paragraphs().begin();
 		ParagraphList::const_iterator pend = itc.paragraphs().end();
 		for (; pit != pend; ++pit) {
diff --git a/src/mathed/InsetMathMacroTemplate.cpp b/src/mathed/InsetMathMacroTemplate.cpp
index 3525ba4..949f161 100644
--- a/src/mathed/InsetMathMacroTemplate.cpp
+++ b/src/mathed/InsetMathMacroTemplate.cpp
@@ -678,7 +678,7 @@ int InsetMathMacroTemplate::maxArgumentInDefinition() const
 		if (it.nextInset()->lyxCode() != MATH_MACROARG_CODE)
 			continue;
 		InsetMathMacroArgument * arg = static_cast<InsetMathMacroArgument*>(it.nextInset());
-		maxArg = std::max(int(arg->number()), maxArg);
+		maxArg = std::max(arg->number(), maxArg);
 	}
 	return maxArg;
 }

commit c52049bb83d71b926e6b949362a47bde9f8d5653
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Wed Sep 29 18:01:14 2021 +0200

    Autoconf build: Fix the invalid test for '-Wno-deprecated-copy' flag
    
    (shamelessly stolen from c26db650a1, which was for cmake build)
    
    The original test was always successfull, even if the flag was invalid.
    
    But checking for '-Wdeprecated-copy' instead yields to error if the
    warning does not exist. Existent warning for 'deprecated-copy' implies
    that 'no-deprecated-copy' also exist.

diff --git a/config/lyxinclude.m4 b/config/lyxinclude.m4
index a3f89a5..0b41e21 100644
--- a/config/lyxinclude.m4
+++ b/config/lyxinclude.m4
@@ -390,8 +390,8 @@ if test x$GXX = xyes; then
       dnl Shut off warning -Wdeprecated-copy, which triggers too much
       dnl note that g++ always accepts -Wno-xxx, even when -Wxxx is an error.
       AC_LANG_PUSH(C++)
-      AX_CHECK_COMPILE_FLAG([-Wno-deprecated-copy],
-	[AM_CXXFLAGS="$AM_CXXFLAGS -Wno-deprecated-copy"], [], [-Werror])
+      AX_CHECK_COMPILE_FLAG([-Wdeprecated-copy],
+	[AM_CXXFLAGS="$AM_CXXFLAGS -Wno-deprecated-copy"])
       AC_LANG_POP(C++)
     fi
   case $gxx_version in

commit c26db650a1e93573e4c09d4612ad45ae1e219854
Author: Kornel Benko <kornel at lyx.org>
Date:   Wed Sep 29 17:53:50 2021 +0200

    Cmake build: Fix the invalid test for '-Wno-deprecated-copy' flag
    
    The original test was always successfull, even if the flag was invalid.
    But checking for '-Wdeprecated-copy' instead yields to error if the warning does not exist.
    Existent warning for 'deprecated-copy' implies that 'no-deprecated-copy' also exist.

diff --git a/CMakeLists.txt b/CMakeLists.txt
index ecebdc9..5e9c7c8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -714,7 +714,7 @@ else()
 	# check_cxx_source_compiles("..." HAVE_DEF_MAKE_UNIQUE)
 	include(CheckCXXCompilerFlag)
 	unset(CHECK_WNODEPRECATEDCOPY_FLAG CACHE)
-	CHECK_CXX_COMPILER_FLAG("-Wno-deprecated-copy" CHECK_WNODEPRECATEDCOPY_FLAG)
+	CHECK_CXX_COMPILER_FLAG("-Wdeprecated-copy" CHECK_WNODEPRECATEDCOPY_FLAG)
 	if(${CHECK_WNODEPRECATEDCOPY_FLAG})
 		set(LYX_CXX_FLAGS "-Wall -Wextra -Wno-deprecated-copy ${LYX_GCC11_MODE}${LYX_CXX_FLAGS}")
 	else()

commit 0862042b28039159656e8a4d6dea1be5a94cf8b6
Author: Daniel Ramoeller <d.lyx at web.de>
Date:   Wed Sep 29 04:25:58 2021 +0200

    SVG replacement of busy.gif
    
    Fix for bug #10384.

diff --git a/lib/Makefile.am b/lib/Makefile.am
index 71cebde..61d6f1a 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -564,7 +564,7 @@ dist_images_DATA1X = \
 	images/buffer-write-as.svgz \
 	images/buffer-write.svgz \
 	images/build-program.svgz \
-	images/busy.gif \
+	images/busy.svgz \
 	images/change-accept.svgz \
 	images/change-next.svgz \
 	images/change-reject.svgz \
diff --git a/lib/images/busy.gif b/lib/images/busy.gif
deleted file mode 100644
index 5b33f7e..0000000
Binary files a/lib/images/busy.gif and /dev/null differ
diff --git a/lib/images/busy.svgz b/lib/images/busy.svgz
new file mode 100644
index 0000000..2a07dfa
Binary files /dev/null and b/lib/images/busy.svgz differ
diff --git a/src/frontends/qt/GuiView.cpp b/src/frontends/qt/GuiView.cpp
index 3a6a04c..9f7531c 100644
--- a/src/frontends/qt/GuiView.cpp
+++ b/src/frontends/qt/GuiView.cpp
@@ -96,7 +96,6 @@
 #include <QMenu>
 #include <QMenuBar>
 #include <QMimeData>
-#include <QMovie>
 #include <QPainter>
 #include <QPixmap>
 #include <QPoint>
@@ -612,20 +611,19 @@ GuiView::GuiView(int id)
 	setAcceptDrops(true);
 
 	// add busy indicator to statusbar
-	GuiClickableLabel * busylabel = new GuiClickableLabel(statusBar());
-	statusBar()->addPermanentWidget(busylabel);
 	search_mode mode = theGuiApp()->imageSearchMode();
-	QString fn = toqstr(lyx::libFileSearch("images", "busy", "gif", mode).absFileName());
-	QMovie * busyanim = new QMovie(fn, QByteArray(), busylabel);
-	busylabel->setMovie(busyanim);
-	busyanim->start();
-	busylabel->hide();
+	QString fn = toqstr(lyx::libFileSearch("images", "busy", "svgz", mode).absFileName());
+	PressableSvgWidget * busySVG = new PressableSvgWidget(fn);
+	statusBar()->addPermanentWidget(busySVG);
+	// make busy indicator square with 5px margins
+	busySVG->setMaximumSize(busySVG->height() - 5, busySVG->height() - 5);
+	busySVG->hide();
 
 	connect(&d.processing_thread_watcher_, SIGNAL(started()),
-		busylabel, SLOT(show()));
+		busySVG, SLOT(show()));
 	connect(&d.processing_thread_watcher_, SIGNAL(finished()),
-		busylabel, SLOT(hide()));
-	connect(busylabel, SIGNAL(clicked()), this, SLOT(checkCancelBackground()));
+		busySVG, SLOT(hide()));
+	connect(busySVG, SIGNAL(pressed()), this, SLOT(checkCancelBackground()));
 
 	QFontMetrics const fm(statusBar()->fontMetrics());
 
@@ -5130,6 +5128,14 @@ SEMenu::SEMenu(QWidget * parent)
 		parent, SLOT(disableShellEscape()));
 }
 
+
+void PressableSvgWidget::mousePressEvent(QMouseEvent * event)
+{
+	if (event->button() == Qt::LeftButton) {
+        Q_EMIT pressed();
+    }
+}
+
 } // namespace frontend
 } // namespace lyx
 
diff --git a/src/frontends/qt/GuiView.h b/src/frontends/qt/GuiView.h
index 47f5e40..657634c 100644
--- a/src/frontends/qt/GuiView.h
+++ b/src/frontends/qt/GuiView.h
@@ -21,6 +21,7 @@
 
 #include <QMainWindow>
 #include <QMenu>
+#include <QSvgWidget>
 
 class QCloseEvent;
 class QDragEnterEvent;
@@ -534,6 +535,19 @@ public Q_SLOTS:
 	void showMenu(QPoint const &) { exec(QCursor::pos()); }
 };
 
+
+class PressableSvgWidget : public QSvgWidget
+{
+	Q_OBJECT
+public:
+    explicit PressableSvgWidget(const QString &file, QWidget * parent = nullptr)
+	: QSvgWidget(file, parent) {};
+protected:
+    void mousePressEvent(QMouseEvent *event) override;
+Q_SIGNALS:
+    void pressed();
+};
+
 } // namespace frontend
 } // namespace lyx
 

commit 42abb26054ed2a4aa161e97f9479a6902c92e669
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Wed Feb 17 12:30:16 2021 +0100

    Make paragraph-goto and friends set paragraph to the top
    
    Add parameter 'force' to scrollToCursor(...) to avoid the case where the
    cursor is not set to top because it is already visible on screen.
    Change screen offset in this method so that the paragraph is really at
    the top of the screen. This part may cause unforeseen issues and needs care.
    
    gotoInset: use the new force flag and do not trigger a redraw.
    Instead, return a boolean telling whether redraw is needed.
    In the code that use it, set an update flag instead of the extra redraw.
    
    In the handling of paragraph-goto, also set the update flag instead of
    triggering a repaint.
    
    Remove Bufferview::scrollToCursor(), which was equivalent to showCursor().
    
    Fixes bug #10425.

diff --git a/src/BufferView.cpp b/src/BufferView.cpp
index bff44cd..526cb33 100644
--- a/src/BufferView.cpp
+++ b/src/BufferView.cpp
@@ -171,18 +171,18 @@ bool findInset(DocIterator & dit, vector<InsetCode> const & codes,
 
 
 /// Moves cursor to the next inset with one of the given codes.
-void gotoInset(BufferView * bv, vector<InsetCode> const & codes,
+bool gotoInset(BufferView * bv, vector<InsetCode> const & codes,
 	       bool same_content)
 {
 	Cursor tmpcur = bv->cursor();
 	if (!findInset(tmpcur, codes, same_content)) {
 		bv->cursor().message(_("No more insets"));
-		return;
+		return false;
 	}
 
 	tmpcur.clearSelection();
 	bv->setCursor(tmpcur);
-	bv->showCursor();
+	return bv->scrollToCursor(bv->cursor(), false, true);
 }
 
 
@@ -541,13 +541,13 @@ void BufferView::processUpdateFlags(Update::flags flags)
 		if (needsFitCursor()) {
 			// First try to make the selection start visible
 			// (which is just the cursor when there is no selection)
-			scrollToCursor(d->cursor_.selectionBegin(), false);
+			scrollToCursor(d->cursor_.selectionBegin(), false, false);
 			// Metrics have to be recomputed (maybe again)
 			updateMetrics();
 			// Is the cursor visible? (only useful if cursor is at end of selection)
 			if (needsFitCursor()) {
 				// then try to make cursor visible instead
-				scrollToCursor(d->cursor_, false);
+				scrollToCursor(d->cursor_, false, false);
 				// Metrics have to be recomputed (maybe again)
 				updateMetrics(flags);
 			}
@@ -741,7 +741,7 @@ void BufferView::scrollDocView(int const pixels, bool update)
 	// cut off at the top
 	if (pixels <= d->scrollbarParameters_.min) {
 		DocIterator dit = doc_iterator_begin(&buffer_);
-		showCursor(dit, false, update);
+		showCursor(dit, false, false, update);
 		LYXERR(Debug::SCROLLING, "scroll to top");
 		return;
 	}
@@ -750,7 +750,7 @@ void BufferView::scrollDocView(int const pixels, bool update)
 	if (pixels >= d->scrollbarParameters_.max) {
 		DocIterator dit = doc_iterator_end(&buffer_);
 		dit.backwardPos();
-		showCursor(dit, false, update);
+		showCursor(dit, false, false, update);
 		LYXERR(Debug::SCROLLING, "scroll to bottom");
 		return;
 	}
@@ -775,7 +775,7 @@ void BufferView::scrollDocView(int const pixels, bool update)
 	DocIterator dit = doc_iterator_begin(&buffer_);
 	dit.pit() = i;
 	LYXERR(Debug::SCROLLING, "pixels = " << pixels << " -> scroll to pit " << i);
-	showCursor(dit, false, update);
+	showCursor(dit, false, false, update);
 }
 
 
@@ -961,32 +961,25 @@ int BufferView::workWidth() const
 
 void BufferView::recenter()
 {
-	showCursor(d->cursor_, true, true);
+	showCursor(d->cursor_, true, false, true);
 }
 
 
 void BufferView::showCursor()
 {
-	showCursor(d->cursor_, false, true);
+	showCursor(d->cursor_, false, false, true);
 }
 
 
 void BufferView::showCursor(DocIterator const & dit,
-	bool recenter, bool update)
-{
-	if (scrollToCursor(dit, recenter) && update)
-		processUpdateFlags(Update::Force);
-}
-
-
-void BufferView::scrollToCursor()
+	bool recenter, bool force, bool update)
 {
-	if (scrollToCursor(d->cursor_, false))
+	if (scrollToCursor(dit, recenter, force) && update)
 		processUpdateFlags(Update::Force);
 }
 
 
-bool BufferView::scrollToCursor(DocIterator const & dit, bool const recenter)
+bool BufferView::scrollToCursor(DocIterator const & dit, bool const recenter, bool force)
 {
 	// We are not properly started yet, delay until resizing is done.
 	if (height_ == 0)
@@ -1014,7 +1007,7 @@ bool BufferView::scrollToCursor(DocIterator const & dit, bool const recenter)
 	else if (bot_pit == tm.last().first + 1)
 		tm.newParMetricsDown();
 
-	if (tm.contains(bot_pit)) {
+	if (tm.contains(bot_pit) && !force) {
 		ParagraphMetrics const & pm = tm.parMetrics(bot_pit);
 		LBUFERR(!pm.rows().empty());
 		// FIXME: smooth scrolling doesn't work in mathed.
@@ -1078,9 +1071,9 @@ bool BufferView::scrollToCursor(DocIterator const & dit, bool const recenter)
 	else if (d->anchor_pit_ == max_pit)
 		d->anchor_ypos_ = height_ - offset - row_dim.descent();
 	else if (offset > height_)
-		d->anchor_ypos_ = height_ - offset - defaultRowHeight();
+		d->anchor_ypos_ = height_ - offset - row_dim.descent();
 	else
-		d->anchor_ypos_ = defaultRowHeight() * 2;
+		d->anchor_ypos_ = row_dim.ascent();
 
 	return true;
 }
@@ -1586,8 +1579,8 @@ void BufferView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
 					success = setCursorFromEntries({id, pos},
 					                               {id_end, pos_end});
 				}
-				if (success)
-					dr.screenUpdate(Update::Force | Update::FitCursor);
+				if (success && scrollToCursor(d->cursor_, false, true))
+						dr.screenUpdate(Update::Force);
 			} else {
 				// Switch to other buffer view and resend cmd
 				lyx::dispatch(FuncRequest(
@@ -1600,19 +1593,13 @@ void BufferView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
 	}
 
 	case LFUN_NOTE_NEXT:
-		gotoInset(this, { NOTE_CODE }, false);
-		// FIXME: if SinglePar is changed to act on the inner
-		// paragraph, this will not be OK anymore. The update is
-		// useful for auto-open collapsible insets.
-		dr.screenUpdate(Update::SinglePar | Update::FitCursor);
+		if (gotoInset(this, { NOTE_CODE }, false))
+			dr.screenUpdate(Update::Force);
 		break;
 
 	case LFUN_REFERENCE_NEXT: {
-		gotoInset(this, { LABEL_CODE, REF_CODE }, true);
-		// FIXME: if SinglePar is changed to act on the inner
-		// paragraph, this will not be OK anymore. The update is
-		// useful for auto-open collapsible insets.
-		dr.screenUpdate(Update::SinglePar | Update::FitCursor);
+		if (gotoInset(this, { LABEL_CODE, REF_CODE }, true))
+			dr.screenUpdate(Update::Force);
 		break;
 	}
 
diff --git a/src/BufferView.h b/src/BufferView.h
index b2fa1c0..e831e3a 100644
--- a/src/BufferView.h
+++ b/src/BufferView.h
@@ -205,13 +205,13 @@ public:
 	/// This method will automatically scroll and update the BufferView
 	/// (metrics+drawing) if needed.
 	/// \param recenter Whether the cursor should be centered on screen
-	void showCursor(DocIterator const & dit, bool recenter,
+	/// \param force If true, disregard current position
+	void showCursor(DocIterator const & dit, bool recenter, bool force,
 		bool update);
 	/// Scroll to the cursor.
-	void scrollToCursor();
-	/// Scroll to the cursor.
 	/// \param recenter Whether the cursor should be centered on screen
-	bool scrollToCursor(DocIterator const & dit, bool recenter);
+	/// \param force If true, disregard current position
+	bool scrollToCursor(DocIterator const & dit, bool recenter, bool force);
 	/// scroll down document by the given number of pixels.
 	int scrollDown(int pixels);
 	/// scroll up document by the given number of pixels.
diff --git a/src/frontends/qt/GuiWorkArea.cpp b/src/frontends/qt/GuiWorkArea.cpp
index bebd122..e08779a 100644
--- a/src/frontends/qt/GuiWorkArea.cpp
+++ b/src/frontends/qt/GuiWorkArea.cpp
@@ -464,7 +464,7 @@ void GuiWorkArea::Private::resizeBufferView()
 	bool const caret_in_view = buffer_view_->caretInView();
 	buffer_view_->resize(p->viewport()->width(), p->viewport()->height());
 	if (caret_in_view)
-		buffer_view_->scrollToCursor();
+		buffer_view_->showCursor();
 	resetCaret();
 
 	// Update scrollbars which might have changed due different

commit a9119c3fa80746bb22de91654e958ede01029cac
Author: Yuriy Skalko <yuriy.skalko at gmail.com>
Date:   Tue Sep 28 20:20:57 2021 +0300

    Remove redundant declarations reported by GCC with -Wredundant-decls option

diff --git a/src/frontends/qt/qt_helpers.h b/src/frontends/qt/qt_helpers.h
index 73f7db9..08d50fa 100644
--- a/src/frontends/qt/qt_helpers.h
+++ b/src/frontends/qt/qt_helpers.h
@@ -191,8 +191,6 @@ QString onlyFileName(QString const & str);
 QString onlyPath(QString const & str);
 QStringList fileFilters(QString const & description);
 
-QString changeExtension(QString const & oldname, QString const & extension);
-
 /// Remove the extension from \p name
 QString removeExtension(QString const & name);
 
diff --git a/src/support/ForkedCalls.cpp b/src/support/ForkedCalls.cpp
index 387f856..e6f5a8e 100644
--- a/src/support/ForkedCalls.cpp
+++ b/src/support/ForkedCalls.cpp
@@ -439,12 +439,6 @@ namespace ForkedCallQueue {
 
 /// A process in the queue
 typedef pair<string, ForkedCall::sigPtr> Process;
-/** Add a process to the queue. Processes are forked sequentially
- *  only one is running at a time.
- *  Connect to the returned signal and you'll be informed when
- *  the process has ended.
- */
-ForkedCall::sigPtr add(string const & process);
 
 /// in-progress queue
 static queue<Process> callQueue_;
@@ -459,6 +453,11 @@ void stopCaller();
 ///
 void callback(pid_t, int);
 
+/** Add a process to the queue. Processes are forked sequentially
+ *  only one is running at a time.
+ *  Connect to the returned signal and you'll be informed when
+ *  the process has ended.
+ */
 ForkedCall::sigPtr add(string const & process)
 {
 	ForkedCall::sigPtr ptr;
diff --git a/src/xml.h b/src/xml.h
index b3569ba..bf72fac 100644
--- a/src/xml.h
+++ b/src/xml.h
@@ -143,6 +143,7 @@ namespace xml {
 docstring escapeChar(char_type c, XMLStream::EscapeSettings e);
 
 /// Escape the given character, if necessary, to an entity.
+/// \param c must be ASCII
 docstring escapeChar(char c, XMLStream::EscapeSettings e);
 
 /// Escape a word instead of a single character
@@ -151,9 +152,6 @@ docstring escapeString(docstring const & raw, XMLStream::EscapeSettings e=XMLStr
 /// cleans \param str for use as an attribute by replacing all non-altnum by "_"
 docstring cleanAttr(docstring const & str);
 
-/// \p c must be ASCII
-docstring escapeChar(char c, XMLStream::EscapeSettings e);
-
 /// replaces illegal characters from ID attributes
 docstring cleanID(docstring const &orig);
 

commit 7067f48fa7afb89575abc58495d076078ce20137
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Tue Sep 28 11:25:25 2021 +0200

    typo

diff --git a/src/frontends/qt/GuiApplication.cpp b/src/frontends/qt/GuiApplication.cpp
index 4161db5..f411088 100644
--- a/src/frontends/qt/GuiApplication.cpp
+++ b/src/frontends/qt/GuiApplication.cpp
@@ -559,7 +559,7 @@ IconInfo iconInfo(FuncRequest const & f, bool unknown, bool rtl)
 	// The folders where icons are searched for
 	QStringList imagedirs;
 	imagedirs << "images/ipa/" << "images/";
-	// This is used to search for rtl version of icons which have the +rrtl suffix.
+	// This is used to search for rtl version of icons which have the +rtl suffix.
 	QStringList suffixes;
 	if (rtl)
 		suffixes << "+rtl";

commit 84655a07d846e093441ed4888eaf097c01ea9390
Author: Kornel Benko <kornel at lyx.org>
Date:   Tue Sep 28 10:32:02 2021 +0200

    Fix crash
    
    Lyx crashes on export to pdf if used with sanitizer set to 'unspecified'.
    Crash found by Scott.
    
    Given that if we export without GUI, there is some weirdness here though.
    1.) Why does lyx not crash if not using '-fsanitize' compile-option
    2.) Why is export to pdf dependent on the screen-resolution

diff --git a/src/frontends/qt/GuiApplication.cpp b/src/frontends/qt/GuiApplication.cpp
index 01a646d..4161db5 100644
--- a/src/frontends/qt/GuiApplication.cpp
+++ b/src/frontends/qt/GuiApplication.cpp
@@ -555,7 +555,7 @@ IconInfo iconInfo(FuncRequest const & f, bool unknown, bool rtl)
 	if (unknown)
 		names << "unknown";
 
-	search_mode const mode = theGuiApp()->imageSearchMode();
+	search_mode const mode = theGuiApp() ? theGuiApp()->imageSearchMode() : support::must_exist;
 	// The folders where icons are searched for
 	QStringList imagedirs;
 	imagedirs << "images/ipa/" << "images/";

commit 325c405541207e91110cc8e488c9210ddeb436f7
Author: Yuriy Skalko <yuriy.skalko at gmail.com>
Date:   Tue Sep 28 11:21:45 2021 +0300

    Remove redundant semicolons reported by GCC with -Wextra-semi option

diff --git a/src/LyXRC.h b/src/LyXRC.h
index f9b5664..74c0e4c 100644
--- a/src/LyXRC.h
+++ b/src/LyXRC.h
@@ -195,7 +195,7 @@ public:
 	///
 	LyXRC() : user_name(support::user_name()),
 	          user_email(support::user_email()) // always empty
-		{};
+		{}
 
 	/// \param check_format: whether to try to convert the file format,
 	/// if it is not current. this should only be true, really, for the
diff --git a/src/Session.h b/src/Session.h
index b7443b9..ea51754 100644
--- a/src/Session.h
+++ b/src/Session.h
@@ -361,7 +361,7 @@ class ShellEscapeSection : SessionSection
 {
 public:
 	///
-	explicit ShellEscapeSection() {};
+	explicit ShellEscapeSection() {}
 
 	///
 	void read(std::istream & is) override;
diff --git a/src/VCBackend.h b/src/VCBackend.h
index e7dc735..e0f6266 100644
--- a/src/VCBackend.h
+++ b/src/VCBackend.h
@@ -154,7 +154,7 @@ public:
 	/// get file from repo, the caller must ensure that it does not exist locally
 	static bool retrieve(support::FileName const & file);
 
-	std::string vcname() const override { return "RCS"; };
+	std::string vcname() const override { return "RCS"; }
 
 	void registrer(std::string const & msg) override;
 
@@ -247,7 +247,7 @@ public:
 	/// get file from repo, the caller must ensure that it does not exist locally
 	static bool retrieve(support::FileName const & file);
 
-	std::string vcname() const override { return "CVS"; };
+	std::string vcname() const override { return "CVS"; }
 
 	void registrer(std::string const & msg) override;
 
@@ -393,7 +393,7 @@ public:
 	/// get file from repo, the caller must ensure that it does not exist locally
 	static bool retrieve(support::FileName const & file);
 
-	std::string vcname() const override { return "SVN"; };
+	std::string vcname() const override { return "SVN"; }
 
 	void registrer(std::string const & msg) override;
 
@@ -504,7 +504,7 @@ public:
 	/// get file from repo, the caller must ensure that it does not exist locally
 	static bool retrieve(support::FileName const & file);
 
-	std::string vcname() const override { return "GIT"; };
+	std::string vcname() const override { return "GIT"; }
 
 	void registrer(std::string const & msg) override;
 
diff --git a/src/frontends/qt/DialogView.h b/src/frontends/qt/DialogView.h
index ee29471..514242e 100644
--- a/src/frontends/qt/DialogView.h
+++ b/src/frontends/qt/DialogView.h
@@ -47,7 +47,7 @@ protected:
 	void hideEvent(QHideEvent * ev) override;
 
 protected Q_SLOTS:
-	void onBufferViewChanged() override {};
+	void onBufferViewChanged() override {}
 };
 
 } // namespace frontend
diff --git a/src/insets/InsetPreview.h b/src/insets/InsetPreview.h
index ca6641a..f36ea56 100644
--- a/src/insets/InsetPreview.h
+++ b/src/insets/InsetPreview.h
@@ -69,7 +69,7 @@ public:
 
 	void edit(Cursor & cur, bool front, EntryDirection entry_from) override;
 
-	bool canPaintChange(BufferView const &) const override { return true; };
+	bool canPaintChange(BufferView const &) const override { return true; }
 	//@}
 
 protected:
diff --git a/src/lyxfind.cpp b/src/lyxfind.cpp
index 004e10b..4a8c037 100644
--- a/src/lyxfind.cpp
+++ b/src/lyxfind.cpp
@@ -935,7 +935,7 @@ public:
 	int pos_len;
 	int searched_size;
 	vector <string> result = vector <string>();
-	MatchResult(int len = 0): match_len(len),match_prefix(0),match2end(0), pos(0),leadsize(0),pos_len(-1),searched_size(0) {};
+	MatchResult(int len = 0): match_len(len),match_prefix(0),match2end(0), pos(0),leadsize(0),pos_len(-1),searched_size(0) {}
 };
 
 static MatchResult::range interpretMatch(MatchResult &oldres, MatchResult &newres)
@@ -1248,7 +1248,7 @@ class KeyInfo {
 
 class Border {
  public:
- Border(int l=0, int u=0) : low(l), upper(u) {};
+ Border(int l=0, int u=0) : low(l), upper(u) {}
   int low;
   int upper;
 };
@@ -1920,7 +1920,7 @@ class LatexInfo {
     buildKeys(isPatternString);
     entries_ = vector<KeyInfo>();
     buildEntries(isPatternString);
-  };
+  }
   int getFirstKey() {
     entidx_ = 0;
     if (entries_.empty()) {
@@ -1941,7 +1941,7 @@ class LatexInfo {
         return -1;
     }
     return 0;
-  };
+  }
   int getNextKey() {
     entidx_++;
     if (int(entries_.size()) > entidx_) {
@@ -1950,7 +1950,7 @@ class LatexInfo {
     else {
       return -1;
     }
-  };
+  }
   bool setNextKey(int idx) {
     if ((idx == entidx_) && (entidx_ >= 0)) {
       entidx_--;
@@ -1958,7 +1958,7 @@ class LatexInfo {
     }
     else
       return false;
-  };
+  }
   int find(int start, KeyInfo::KeyType keytype) const {
     if (start < 0)
       return -1;
@@ -1969,20 +1969,20 @@ class LatexInfo {
       tmpIdx++;
     }
     return -1;
-  };
+  }
   int process(ostringstream & os, KeyInfo const & actual);
   int dispatch(ostringstream & os, int previousStart, KeyInfo & actual);
-  // string show(int lastpos) { return interval.show(lastpos);};
-  int nextNotIgnored(int start) { return interval_.nextNotIgnored(start);};
+  // string show(int lastpos) { return interval.show(lastpos);}
+  int nextNotIgnored(int start) { return interval_.nextNotIgnored(start);}
   KeyInfo &getKeyInfo(int keyinfo) {
     static KeyInfo invalidInfo = KeyInfo();
     if ((keyinfo < 0) || ( keyinfo >= int(entries_.size())))
       return invalidInfo;
     else
       return entries_[keyinfo];
-  };
-  void setForDefaultLang(KeyInfo const & defLang) {interval_.setForDefaultLang(defLang);};
-  void addIntervall(int low, int up) { interval_.addIntervall(low, up); };
+  }
+  void setForDefaultLang(KeyInfo const & defLang) {interval_.setForDefaultLang(defLang);}
+  void addIntervall(int low, int up) { interval_.addIntervall(low, up); }
 };
 
 
@@ -2036,7 +2036,7 @@ class MathInfo {
     m.mathSize = m.mathEnd - m.mathStart;
     entries_.push_back(m);
   }
-  bool empty() const { return entries_.empty(); };
+  bool empty() const { return entries_.empty(); }
   size_t getEndPos() const {
     if (entries_.empty() || (actualIdx_ >= entries_.size())) {
       return 0;
@@ -2071,7 +2071,7 @@ class MathInfo {
     }
     return entries_[actualIdx_].mathSize;
   }
-  void incrEntry() { actualIdx_++; };
+  void incrEntry() { actualIdx_++; }
 };
 
 void LatexInfo::buildEntries(bool isPatternString)
diff --git a/src/mathed/InsetMathGrid.h b/src/mathed/InsetMathGrid.h
index 70e9ddb..ce8a852 100644
--- a/src/mathed/InsetMathGrid.h
+++ b/src/mathed/InsetMathGrid.h
@@ -94,7 +94,7 @@ public:
 	InsetMathGrid(Buffer * buf, col_type m, row_type n, char valign,
 		docstring const & halign);
 	///
-	marker_type marker(BufferView const *) const override { return marker_type::MARKER2; };
+	marker_type marker(BufferView const *) const override { return marker_type::MARKER2; }
 	///
 	void metrics(MetricsInfo & mi, Dimension &) const override;
 	///
diff --git a/src/mathed/InsetMathMacroArgument.h b/src/mathed/InsetMathMacroArgument.h
index 441ac32..c11a9a6 100644
--- a/src/mathed/InsetMathMacroArgument.h
+++ b/src/mathed/InsetMathMacroArgument.h
@@ -24,7 +24,7 @@ namespace lyx {
 // A # that failed to parse
 class InsetMathHash : public InsetMath {
 public:
-	explicit InsetMathHash(docstring const & str = docstring()) : str_('#' + str) {};
+	explicit InsetMathHash(docstring const & str = docstring()) : str_('#' + str) {}
 	///
 	void metrics(MetricsInfo & mi, Dimension & dim) const override;
 	///
diff --git a/src/mathed/MathStream.h b/src/mathed/MathStream.h
index d55831d..d6a35c5 100644
--- a/src/mathed/MathStream.h
+++ b/src/mathed/MathStream.h
@@ -107,7 +107,7 @@ public:
 	/// tell whether to use only ascii chars when producing latex code
 	bool asciiOnly() const { return ascii_; }
 	/// tell whether we are in a MathClass inset
-	void inMathClass(bool mathclass) { mathclass_ = mathclass; };
+	void inMathClass(bool mathclass) { mathclass_ = mathclass; }
 	/// tell whether we are in a MathClass inset
 	bool inMathClass() const { return mathclass_; }
 	/// LaTeX encoding
diff --git a/src/support/Length.h b/src/support/Length.h
index 03b4d36..cb612b6 100644
--- a/src/support/Length.h
+++ b/src/support/Length.h
@@ -69,9 +69,9 @@ public:
 	explicit Length(std::string const & data);
 
 	///
-	double value() const { return val_; };
+	double value() const { return val_; }
 	///
-	Length::UNIT unit() const { return unit_; };
+	Length::UNIT unit() const { return unit_; }
 	///
 	void value(double val) { val_ = val; }
 	///
diff --git a/src/xml.h b/src/xml.h
index 471d925..b3569ba 100644
--- a/src/xml.h
+++ b/src/xml.h
@@ -101,7 +101,7 @@ public:
 	/// Is the last tag that was added to the stream a new line (CR)? This is mostly to known
 	/// whether a new line must be added. Therefore, consider that an empty stream just had a CR,
 	/// that simplifies the logic using this code.
-	bool isLastTagCR() const { return is_last_tag_cr_; };
+	bool isLastTagCR() const { return is_last_tag_cr_; }
 	///
 	void writeError(std::string const &);
 	///

commit 05dd6614c4e3984f2c1ace3b4a59e47421e13f06
Author: Pavel Sanda <sanda at lyx.org>
Date:   Mon Sep 27 22:07:07 2021 +0200

    Include docbook_copy.py in released tarball.

diff --git a/lib/Makefile.am b/lib/Makefile.am
index 2d359e7..71cebde 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -2690,6 +2690,7 @@ dist_scripts_DATA += \
 	scripts/convert_pdf.py \
 	scripts/convertDefault.py \
 	scripts/csv2lyx.py \
+	scripts/docbook_copy.py \
 	scripts/ext_copy.py \
 	scripts/fen2ascii.py \
 	scripts/fig2pdftex.py \

commit 3cdfb42cced175c108563f378589ec9aae134093
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Mon Sep 27 17:46:38 2021 +0200

    Make rectangles have pointy corners
    
    This only makes a difference in HiDpi mode.
    
    Adaptation of the patch proposed by Daniel.
    
    Fix for bug #12336.

diff --git a/src/frontends/qt/GuiPainter.cpp b/src/frontends/qt/GuiPainter.cpp
index 75e9ace..cf9cd7c 100644
--- a/src/frontends/qt/GuiPainter.cpp
+++ b/src/frontends/qt/GuiPainter.cpp
@@ -57,7 +57,7 @@ GuiPainter::~GuiPainter()
 
 
 void GuiPainter::setQPainterPen(QColor const & col,
-	Painter::line_style ls, int lw)
+	Painter::line_style ls, int lw, Qt::PenJoinStyle js)
 {
 	if (col == current_color_ && ls == current_ls_ && lw == current_lw_)
 		return;
@@ -79,6 +79,8 @@ void GuiPainter::setQPainterPen(QColor const & col,
 
 	pen.setWidth(lw);
 
+	pen.setJoinStyle(js);
+
 	setPen(pen);
 }
 
@@ -210,7 +212,7 @@ void GuiPainter::rectangle(int x, int y, int w, int h,
 	line_style ls,
 	int lw)
 {
-	setQPainterPen(computeColor(col), ls, lw);
+	setQPainterPen(computeColor(col), ls, lw, Qt::MiterJoin);
 	drawRect(x, y, w, h);
 }
 
diff --git a/src/frontends/qt/GuiPainter.h b/src/frontends/qt/GuiPainter.h
index f3b521b..522b83c 100644
--- a/src/frontends/qt/GuiPainter.h
+++ b/src/frontends/qt/GuiPainter.h
@@ -185,7 +185,8 @@ private:
 
 	/// set pen parameters
 	void setQPainterPen(QColor const & col,
-		line_style ls = line_solid, int lw = thin_line);
+		line_style ls = line_solid, int lw = thin_line,
+		Qt::PenJoinStyle js = Qt::BevelJoin);
 
 	// Direction for painting text
 	enum Direction { LtR, RtL, Auto };

commit 69834f1e0da4dc09007f0947b870fd66a72501e3
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Mon Sep 27 13:56:04 2021 +0200

    Fixup 6bbd88ac: compilation fix for Qt4

diff --git a/src/frontends/qt/GuiFontMetrics.cpp b/src/frontends/qt/GuiFontMetrics.cpp
index 8273ed8..5e8f535 100644
--- a/src/frontends/qt/GuiFontMetrics.cpp
+++ b/src/frontends/qt/GuiFontMetrics.cpp
@@ -57,6 +57,15 @@ using namespace lyx::support;
 #  error "Define at least one of BIDI_USE_OVERRIDE or BIDI_USE_FLAG"
 #endif
 
+
+#if QT_VERSION < 0x050000
+inline uint qHash(double key)
+{
+	return qHash(QByteArray(reinterpret_cast<char const *>(&key), sizeof(key)));
+}
+#endif
+
+
 namespace std {
 
 /*

commit 73865ce9997f75f5b88160f973b316d657be20cf
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Mon Sep 27 11:53:46 2021 +0200

    Whitespace

diff --git a/src/mathed/InsetMathNest.cpp b/src/mathed/InsetMathNest.cpp
index 9210886..7e763b7 100644
--- a/src/mathed/InsetMathNest.cpp
+++ b/src/mathed/InsetMathNest.cpp
@@ -1665,10 +1665,10 @@ void InsetMathNest::lfunMouseRelease(Cursor & cur, FuncRequest & cmd)
 
 bool InsetMathNest::interpretChar(Cursor & cur, char_type const c)
 {
-        // try auto-correction
-        if (lyxrc.autocorrection_math && cur.pos() != 0 && !cur.selection()
-                  && math_autocorrect(cur, c))
-                return true;
+	// try auto-correction
+	if (lyxrc.autocorrection_math && cur.pos() != 0 && !cur.selection()
+	     && math_autocorrect(cur, c))
+		return true;
 
 	//lyxerr << "interpret 2: '" << c << "'" << endl;
 	docstring save_selection;

commit e9db9d36441ef892ea4540a108879e193984be24
Author: Thibaut Cuvelier <tcuvelier at lyx.org>
Date:   Mon Sep 27 01:46:01 2021 +0200

    DocBook copy: add links to bug reports.

diff --git a/lib/scripts/docbook_copy.py b/lib/scripts/docbook_copy.py
index 7cb68dc..8148b85 100644
--- a/lib/scripts/docbook_copy.py
+++ b/lib/scripts/docbook_copy.py
@@ -85,6 +85,7 @@ class DocBookCopier:
         #     looking_for_end_programlisting = False
         #     for line in f_before:
         #         # TODO: find an efficient way to distinguish those left-overs.
+        # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00040.html
 
     def call_lilypond(self):
         # LilyPond requires that its input file has the .lyxml extension (plus bugs in LilyPond).
@@ -97,7 +98,8 @@ class DocBookCopier:
             os.environ['PATH'] += os.pathsep + self.lilypond_folder
 
         # Make LilyPond believe it is working from the temporary LyX directory. Otherwise, it tries to find files
-        # starting from LyX's working directory... LilyPond bug.
+        # starting from LyX's working directory... LilyPond bug, most likely.
+        # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00041.html
         os.chdir(self.in_folder)
 
         # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.

commit 6d3be39587c83590d2d8e91b1fe93261593a2b25
Author: Thibaut Cuvelier <tcuvelier at lyx.org>
Date:   Mon Sep 27 01:15:16 2021 +0200

    DocBook copy: don't error if the file was already copied.

diff --git a/autotests/export/docbook/LilyPond_Book.xml b/autotests/export/docbook/LilyPond_Book.xml
index 109e112..43df8e1 100644
--- a/autotests/export/docbook/LilyPond_Book.xml
+++ b/autotests/export/docbook/LilyPond_Book.xml
@@ -3,11 +3,13 @@
   See https://www.lyx.org/ for more information -->
 <article xml:lang="en_US" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:m="http://www.w3.org/1998/Math/MathML" xmlns:xi="http://www.w3.org/2001/XInclude" version="5.2">
 <title>LilyPond-book and LyX</title>
-<mediaobject>
-<textobject>
-<programlisting language='lilypond' role='fragment verbatim staffsize=16 ragged-right relative=2'>
+<programlisting>
+
 \relative c'' {  g a b c}
-</programlisting>
-</textobject>
-</mediaobject>
+</programlisting><mediaobject><imageobject role="latex">
+  <imagedata fileref="ff\lily-3ed27d76.pdf" format="PDF"/>
+</imageobject>
+<imageobject role="html">
+  <imagedata fileref="ff\lily-3ed27d76.png" format="PNG"/>
+</imageobject></mediaobject>
 </article>
\ No newline at end of file
diff --git a/lib/scripts/docbook_copy.py b/lib/scripts/docbook_copy.py
index 044c3e1..7cb68dc 100644
--- a/lib/scripts/docbook_copy.py
+++ b/lib/scripts/docbook_copy.py
@@ -77,8 +77,14 @@ class DocBookCopier:
         os.unlink(self.in_file)
 
     def postprocess_output_for_lilypond(self):
-        # TODO.
         pass
+        # # Erase the <programlisting> that LilyPond left behind in the XML.
+        # in_file_before = self.in_file + '.tmp'
+        # shutil.move(self.in_file, in_file_before)
+        # with open(in_file_before, 'r', encoding='utf-8') as f_before, open(self.in_file, 'w', encoding='utf-8') as f_after:
+        #     looking_for_end_programlisting = False
+        #     for line in f_before:
+        #         # TODO: find an efficient way to distinguish those left-overs.
 
     def call_lilypond(self):
         # LilyPond requires that its input file has the .lyxml extension (plus bugs in LilyPond).
@@ -125,17 +131,19 @@ class DocBookCopier:
         if failed:
             sys.exit(1)
 
-        # Now, in_file should have the LilyPond-processed contents.
         # LilyPond has a distressing tendency to leave the raw LilyPond code in the new file.
         self.postprocess_output_for_lilypond()
 
+        # Now, in_file should have the clean LilyPond-processed contents.
+
     def copy_lilypond_generated_images(self):
         # LilyPond generates a lot of files in LyX' temporary folder, within the ff folder: source LilyPond files
         # for each snippet to render, images in several formats.
         in_generated_images_folder = os.path.join(self.in_folder, 'ff')
         out_generated_images_folder = os.path.join(self.out_folder, 'ff')
 
-        os.mkdir(out_generated_images_folder)
+        if not os.path.isdir(out_generated_images_folder):
+            os.mkdir(out_generated_images_folder)
 
         for img in os.listdir(in_generated_images_folder):
             if not img.endswith('.png') and not img.endswith('.pdf'):

commit a464915f581caa27af8041c6726c128f1f352d12
Author: Thibaut Cuvelier <tcuvelier at lyx.org>
Date:   Mon Sep 27 00:42:08 2021 +0200

    DocBook copy: large refactoring to improve readability.

diff --git a/autotests/export/docbook/ff/lily-3ed27d76.png b/autotests/export/docbook/ff/lily-3ed27d76.png
new file mode 100644
index 0000000..870394e
Binary files /dev/null and b/autotests/export/docbook/ff/lily-3ed27d76.png differ
diff --git a/lib/scripts/docbook_copy.py b/lib/scripts/docbook_copy.py
index a4cb01a..044c3e1 100644
--- a/lib/scripts/docbook_copy.py
+++ b/lib/scripts/docbook_copy.py
@@ -26,51 +26,46 @@ import shutil
 import sys
 
 
-def need_lilypond(file):
-    # Really tailored to the kind of output lilypond.module makes (in lib/layouts).
-    with open(file, 'r') as f:
-        return "language='lilypond'" in f.read()
-
-
-def copy_docbook(args):
-    if len(args) != 4:
-        print('Exactly four arguments are expected, only %s found: %s.' % (len(args), args))
-        sys.exit(1)
-
-    # Parse the command line.
-    lilypond_command = args[1]
-    in_file = args[2]
-    out_file = args[3]
-
-    has_lilypond = lilypond_command not in {'', 'none'}
-    in_folder = os.path.split(in_file)[0]
-
-    # Guess the path for LilyPond.
-    lilypond_folder = os.path.split(lilypond_command)[0] if has_lilypond else ''
-
-    # Help debugging.
-    print('>> Given arguments:')
-    print('>> LilyPond: ' + ('present' if has_lilypond else 'not found') + '.')
-    print('>> LilyPond callable as: ' + lilypond_command + '.')
-    print('>> LilyPond path: ' + lilypond_folder + '.')
-    print('>> Input file: ' + in_file + '.')
-    print('>> Input folder: ' + in_folder + '.')
-    print('>> Output file: ' + out_file + '.')
-
-    # Apply LilyPond to the original file if available and needed.
-    if has_lilypond and need_lilypond(in_file):
-        in_lily_file = in_file.replace(".xml", ".lyxml")
-        print('>> The input file needs a LilyPond pass and LilyPond is available.')
-        print('>> Rewriting ' + in_file)
-        print('>> as ' + in_lily_file + '.')
-
+class DocBookCopier:
+    def __init__(self, args):
+
+        # Parse the command line.
+        self.lilypond_command = args[1]
+        self.in_file = args[2]
+        self.out_file = args[3]
+
+        # Compute a few things from the raw parameters.
+        self.in_folder = os.path.split(self.in_file)[0]
+        self.out_folder = os.path.split(self.out_file)[0]
+
+        self.in_lily_file = self.in_file.replace('.xml', '.lyxml')
+        self.has_lilypond = self.lilypond_command not in {'', 'none'}
+        self.lilypond_folder = os.path.split(self.lilypond_command)[0] if self.has_lilypond else ''
+        self.do_lilypond_processing = self.has_lilypond and self.in_file_needs_lilypond()
+
+        # Help debugging.
+        print('>> Given arguments:')
+        print('>> LilyPond: ' + ('present' if self.has_lilypond else 'not found') + '.')
+        print('>> LilyPond callable as: ' + self.lilypond_command + '.')
+        print('>> LilyPond path: ' + self.lilypond_folder + '.')
+        print('>> Input file: ' + self.in_file + '.')
+        print('>> Output file: ' + self.out_file + '.')
+        print('>> Input folder: ' + self.in_folder + '.')
+        print('>> Output folder: ' + self.out_folder + '.')
+
+    def in_file_needs_lilypond(self):
+        # Really tailored to the kind of output lilypond.module makes (in lib/layouts).
+        with open(self.in_file, 'r') as f:
+            return "language='lilypond'" in f.read()
+
+    def preprocess_input_for_lilypond(self):
         # LilyPond requires that its input file has the .lyxml extension. Due to a bug in LilyPond,
         # use " instead of ' to encode XML attributes.
         # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00039.html
         # Typical transformation:
         #     FROM:  language='lilypond' role='fragment verbatim staffsize=16 ragged-right relative=2'
         #     TO:    language="lilypond" role="fragment verbatim staffsize=16 ragged-right relative=2"
-        with open(in_file, 'r', encoding='utf-8') as f, open(in_lily_file, 'w', encoding='utf-8') as f_lily:
+        with open(self.in_file, 'r', encoding='utf-8') as f, open(self.in_lily_file, 'w', encoding='utf-8') as f_lily:
             for line in f:
                 if "language='lilypond'" in line:
                     line = re.sub(
@@ -79,50 +74,94 @@ def copy_docbook(args):
                         line
                     )
                 f_lily.write(line)
-        os.unlink(in_file)
+        os.unlink(self.in_file)
+
+    def postprocess_output_for_lilypond(self):
+        # TODO.
+        pass
+
+    def call_lilypond(self):
+        # LilyPond requires that its input file has the .lyxml extension (plus bugs in LilyPond).
+        print('>> Rewriting ' + self.in_file)
+        print('>> as ' + self.in_lily_file + '.')
+        self.preprocess_input_for_lilypond()
 
         # Add LilyPond to the PATH. lilypond-book uses a direct call to lilypond from the PATH.
-        if os.path.isdir(lilypond_folder):
-            os.environ['PATH'] += os.pathsep + lilypond_folder
+        if os.path.isdir(self.lilypond_folder):
+            os.environ['PATH'] += os.pathsep + self.lilypond_folder
 
         # Make LilyPond believe it is working from the temporary LyX directory. Otherwise, it tries to find files
-        # starting from LyX's working directory...
-        os.chdir(in_folder)
+        # starting from LyX's working directory... LilyPond bug.
+        os.chdir(self.in_folder)
 
         # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.
-        command_args = ['--format=docbook', '--output=' + in_folder, in_lily_file]
-        command_raw = [lilypond_command] + command_args
-        command_python = ['python', lilypond_command] + command_args
+        command_args = ['--format=docbook', '--output=' + self.in_folder, self.in_lily_file]
+        command_raw = [self.lilypond_command] + command_args
+        command_python = ['python', self.lilypond_command] + command_args
 
         print('>> Running LilyPond.')
         sys.stdout.flush()  # So that the LilyPond output is at the right place in the logs.
 
-        failed = False
-        try:
-            subprocess.check_call(command_raw, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
-            print('>> Success running LilyPond with ')
-            print('>> ' + str(command_raw))
-        except (subprocess.CalledProcessError, OSError) as e1:
+        failed = True
+        exceptions = []
+        for cmd in [command_raw, command_python]:
             try:
-                subprocess.check_call(command_python, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
+                subprocess.check_call(cmd, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
                 print('>> Success running LilyPond with ')
-                print('>> ' + str(command_python) + '.')
-            except (subprocess.CalledProcessError, OSError) as e2:
-                print('>> Error from LilyPond. The successive calls were:')
-                print('>> (1) Error from trying ' + str(command_raw) + ':')
-                print('>> (1) ' + str(e1))
-                print('>> (2) Error from trying ' + str(command_python) + ':')
-                print('>> (2) ' + str(e2))
-                failed = True
+                print('>> ' + str(cmd))
+                failed = False
+            except (subprocess.CalledProcessError, OSError) as e:
+                exceptions.append((cmd, e))
+
+        if failed:
+            print('>> Error from LilyPond. The successive calls were:')
+            for (i, pair) in enumerate(exceptions):
+                exc = pair[0]
+                cmd = pair[1]
+
+                print('>> (' + i + ') Error from trying ' + str(cmd) + ':')
+                print('>> (' + i + ') ' + str(exc))
 
         if failed:
             sys.exit(1)
 
         # Now, in_file should have the LilyPond-processed contents.
+        # LilyPond has a distressing tendency to leave the raw LilyPond code in the new file.
+        self.postprocess_output_for_lilypond()
+
+    def copy_lilypond_generated_images(self):
+        # LilyPond generates a lot of files in LyX' temporary folder, within the ff folder: source LilyPond files
+        # for each snippet to render, images in several formats.
+        in_generated_images_folder = os.path.join(self.in_folder, 'ff')
+        out_generated_images_folder = os.path.join(self.out_folder, 'ff')
+
+        os.mkdir(out_generated_images_folder)
+
+        for img in os.listdir(in_generated_images_folder):
+            if not img.endswith('.png') and not img.endswith('.pdf'):
+                continue
 
-    # Perform the final copy.
-    shutil.copyfile(in_file, out_file, follow_symlinks=False)
+            shutil.copyfile(
+                os.path.join(in_generated_images_folder, img),
+                os.path.join(out_generated_images_folder, img),
+                follow_symlinks=False,
+            )
+
+    def copy(self):
+        # Apply LilyPond to the original file if available and needed.
+        if self.do_lilypond_processing:
+            print('>> The input file needs a LilyPond pass and LilyPond is available.')
+            self.call_lilypond()
+
+        # Perform the actual copy: both the modified XML file and the generated images, if LilyPond is used.
+        shutil.copyfile(self.in_file, self.out_file, follow_symlinks=False)
+        if self.do_lilypond_processing:
+            self.copy_lilypond_generated_images()
 
 
 if __name__ == '__main__':
-    copy_docbook(sys.argv)
+    if len(sys.argv) != 4:
+        print('Exactly four arguments are expected, only %s found: %s.' % (len(sys.argv), sys.argv))
+        sys.exit(1)
+
+    DocBookCopier(sys.argv).copy()

commit e22f52e73120ef7a8c9d8427dbf1b741b2f03b71
Author: Thibaut Cuvelier <tcuvelier at lyx.org>
Date:   Mon Sep 27 00:39:10 2021 +0200

    DocBook: make LilyPond work more reliably.

diff --git a/lib/scripts/docbook_copy.py b/lib/scripts/docbook_copy.py
index 1346a90..a4cb01a 100644
--- a/lib/scripts/docbook_copy.py
+++ b/lib/scripts/docbook_copy.py
@@ -33,7 +33,6 @@ def need_lilypond(file):
 
 
 def copy_docbook(args):
-    print(args)
     if len(args) != 4:
         print('Exactly four arguments are expected, only %s found: %s.' % (len(args), args))
         sys.exit(1)
@@ -43,23 +42,27 @@ def copy_docbook(args):
     in_file = args[2]
     out_file = args[3]
 
-    has_lilypond = lilypond_command != "" and lilypond_command != "none"
+    has_lilypond = lilypond_command not in {'', 'none'}
+    in_folder = os.path.split(in_file)[0]
 
     # Guess the path for LilyPond.
     lilypond_folder = os.path.split(lilypond_command)[0] if has_lilypond else ''
 
     # Help debugging.
-    print(">> Given arguments:")
-    print(">> LilyPond: " + ("present" if has_lilypond else "not found") + " " + lilypond_command)
-    print(">> LilyPond path: " + lilypond_folder)
-    print(">> Input file: " + in_file)
-    print(">> Output file: " + out_file)
+    print('>> Given arguments:')
+    print('>> LilyPond: ' + ('present' if has_lilypond else 'not found') + '.')
+    print('>> LilyPond callable as: ' + lilypond_command + '.')
+    print('>> LilyPond path: ' + lilypond_folder + '.')
+    print('>> Input file: ' + in_file + '.')
+    print('>> Input folder: ' + in_folder + '.')
+    print('>> Output file: ' + out_file + '.')
 
     # Apply LilyPond to the original file if available and needed.
     if has_lilypond and need_lilypond(in_file):
         in_lily_file = in_file.replace(".xml", ".lyxml")
-        print(">> The input file needs a LilyPond pass and LilyPond is available.")
-        print(">> Rewriting " + in_file + " as " + in_lily_file)
+        print('>> The input file needs a LilyPond pass and LilyPond is available.')
+        print('>> Rewriting ' + in_file)
+        print('>> as ' + in_lily_file + '.')
 
         # LilyPond requires that its input file has the .lyxml extension. Due to a bug in LilyPond,
         # use " instead of ' to encode XML attributes.
@@ -78,28 +81,38 @@ def copy_docbook(args):
                 f_lily.write(line)
         os.unlink(in_file)
 
-        # Add LilyPond to the PATH.
+        # Add LilyPond to the PATH. lilypond-book uses a direct call to lilypond from the PATH.
         if os.path.isdir(lilypond_folder):
             os.environ['PATH'] += os.pathsep + lilypond_folder
 
+        # Make LilyPond believe it is working from the temporary LyX directory. Otherwise, it tries to find files
+        # starting from LyX's working directory...
+        os.chdir(in_folder)
+
         # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.
-        command_raw = [lilypond_command, '--format=docbook', in_lily_file]
-        command_python = ['python', lilypond_command, '--format=docbook', in_lily_file]
+        command_args = ['--format=docbook', '--output=' + in_folder, in_lily_file]
+        command_raw = [lilypond_command] + command_args
+        command_python = ['python', lilypond_command] + command_args
+
+        print('>> Running LilyPond.')
+        sys.stdout.flush()  # So that the LilyPond output is at the right place in the logs.
 
         failed = False
         try:
             subprocess.check_call(command_raw, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
-            print(">> Success running LilyPond with " + str(command_raw))
+            print('>> Success running LilyPond with ')
+            print('>> ' + str(command_raw))
         except (subprocess.CalledProcessError, OSError) as e1:
             try:
                 subprocess.check_call(command_python, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
-                print(">> Success running LilyPond with " + str(command_python))
+                print('>> Success running LilyPond with ')
+                print('>> ' + str(command_python) + '.')
             except (subprocess.CalledProcessError, OSError) as e2:
-                print('>> Error from LilyPond')
-                print('>> Error from trying ' + str(command_raw) + ':')
-                print(e1)
-                print('>> Error from trying ' + str(command_python) + ':')
-                print(e2)
+                print('>> Error from LilyPond. The successive calls were:')
+                print('>> (1) Error from trying ' + str(command_raw) + ':')
+                print('>> (1) ' + str(e1))
+                print('>> (2) Error from trying ' + str(command_python) + ':')
+                print('>> (2) ' + str(e2))
                 failed = True
 
         if failed:

commit 508badc78a9898210febbec6580768e316e53dc0
Author: Thibaut Cuvelier <tcuvelier at lyx.org>
Date:   Sun Sep 26 21:13:17 2021 +0200

    DocBook: redirect LilyPond output to main LyX output to ease debugging.

diff --git a/lib/scripts/docbook_copy.py b/lib/scripts/docbook_copy.py
index 034d504..1346a90 100644
--- a/lib/scripts/docbook_copy.py
+++ b/lib/scripts/docbook_copy.py
@@ -18,6 +18,7 @@
 # /!\ The original file may be modified by this script!
 
 
+import subprocess
 import os
 import os.path
 import re
@@ -48,17 +49,17 @@ def copy_docbook(args):
     lilypond_folder = os.path.split(lilypond_command)[0] if has_lilypond else ''
 
     # Help debugging.
-    print("Given arguments:")
-    print("LilyPond: " + ("present" if has_lilypond else "not found") + " " + lilypond_command)
-    print("LilyPond path: " + lilypond_folder)
-    print("Input file: " + in_file)
-    print("Output file: " + out_file)
+    print(">> Given arguments:")
+    print(">> LilyPond: " + ("present" if has_lilypond else "not found") + " " + lilypond_command)
+    print(">> LilyPond path: " + lilypond_folder)
+    print(">> Input file: " + in_file)
+    print(">> Output file: " + out_file)
 
     # Apply LilyPond to the original file if available and needed.
     if has_lilypond and need_lilypond(in_file):
         in_lily_file = in_file.replace(".xml", ".lyxml")
-        print("The input file needs a LilyPond pass and LilyPond is available.")
-        print("Rewriting " + in_file + " as " + in_lily_file)
+        print(">> The input file needs a LilyPond pass and LilyPond is available.")
+        print(">> Rewriting " + in_file + " as " + in_lily_file)
 
         # LilyPond requires that its input file has the .lyxml extension. Due to a bug in LilyPond,
         # use " instead of ' to encode XML attributes.
@@ -69,36 +70,40 @@ def copy_docbook(args):
         with open(in_file, 'r', encoding='utf-8') as f, open(in_lily_file, 'w', encoding='utf-8') as f_lily:
             for line in f:
                 if "language='lilypond'" in line:
-                    # print(line)
-                    # print(re.match('<programlisting\\s+language=\'lilypond\'.*?(role=\'(?P<options>.*?)\')?>', line))
                     line = re.sub(
                         '<programlisting\\s+language=\'lilypond\'.*?(role=\'(?P<options>.*?)\')?>',
                         '<programlisting language="lilypond" role="\\g<options>">',
                         line
                     )
-                    # print(line)
                 f_lily.write(line)
         os.unlink(in_file)
-        # shutil.move(in_file, in_lily_file)
 
         # Add LilyPond to the PATH.
         if os.path.isdir(lilypond_folder):
             os.environ['PATH'] += os.pathsep + lilypond_folder
 
         # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.
-        command_raw = lilypond_command + ' --format=docbook ' + in_lily_file
-        command_python = 'python -tt "' + lilypond_command + '" --format=docbook ' + in_lily_file
-
-        if os.system(command_raw) == 0:
-            print("Success running LilyPond:")
-            print(command_raw)
-        else:
-            if os.system(command_python) == 0:
-                print("Success running LilyPond:")
-                print(command_python)
-            else:
-                print('Error from LilyPond')
-                sys.exit(1)
+        command_raw = [lilypond_command, '--format=docbook', in_lily_file]
+        command_python = ['python', lilypond_command, '--format=docbook', in_lily_file]
+
+        failed = False
+        try:
+            subprocess.check_call(command_raw, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
+            print(">> Success running LilyPond with " + str(command_raw))
+        except (subprocess.CalledProcessError, OSError) as e1:
+            try:
+                subprocess.check_call(command_python, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
+                print(">> Success running LilyPond with " + str(command_python))
+            except (subprocess.CalledProcessError, OSError) as e2:
+                print('>> Error from LilyPond')
+                print('>> Error from trying ' + str(command_raw) + ':')
+                print(e1)
+                print('>> Error from trying ' + str(command_python) + ':')
+                print(e2)
+                failed = True
+
+        if failed:
+            sys.exit(1)
 
         # Now, in_file should have the LilyPond-processed contents.
 

commit e983676f6c9b53c94c0c94c2805073a8fa6832c8
Author: Thibaut Cuvelier <tcuvelier at lyx.org>
Date:   Sun Sep 26 21:00:59 2021 +0200

    DocBook: work around bug in LilyPond.
    
    https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00039.html

diff --git a/lib/scripts/docbook_copy.py b/lib/scripts/docbook_copy.py
index 8e00db7..034d504 100644
--- a/lib/scripts/docbook_copy.py
+++ b/lib/scripts/docbook_copy.py
@@ -19,6 +19,8 @@
 
 
 import os
+import os.path
+import re
 import shutil
 import sys
 
@@ -42,21 +44,59 @@ def copy_docbook(args):
 
     has_lilypond = lilypond_command != "" and lilypond_command != "none"
 
+    # Guess the path for LilyPond.
+    lilypond_folder = os.path.split(lilypond_command)[0] if has_lilypond else ''
+
+    # Help debugging.
+    print("Given arguments:")
+    print("LilyPond: " + ("present" if has_lilypond else "not found") + " " + lilypond_command)
+    print("LilyPond path: " + lilypond_folder)
+    print("Input file: " + in_file)
+    print("Output file: " + out_file)
+
     # Apply LilyPond to the original file if available and needed.
     if has_lilypond and need_lilypond(in_file):
-        # LilyPond requires that its input file has the .lyxml extension.
-        # Move the file, so that LilyPond doesn't have to erase the contents of the original file before
-        # writing the converted output.
         in_lily_file = in_file.replace(".xml", ".lyxml")
-        shutil.move(in_file, in_lily_file)
+        print("The input file needs a LilyPond pass and LilyPond is available.")
+        print("Rewriting " + in_file + " as " + in_lily_file)
+
+        # LilyPond requires that its input file has the .lyxml extension. Due to a bug in LilyPond,
+        # use " instead of ' to encode XML attributes.
+        # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00039.html
+        # Typical transformation:
+        #     FROM:  language='lilypond' role='fragment verbatim staffsize=16 ragged-right relative=2'
+        #     TO:    language="lilypond" role="fragment verbatim staffsize=16 ragged-right relative=2"
+        with open(in_file, 'r', encoding='utf-8') as f, open(in_lily_file, 'w', encoding='utf-8') as f_lily:
+            for line in f:
+                if "language='lilypond'" in line:
+                    # print(line)
+                    # print(re.match('<programlisting\\s+language=\'lilypond\'.*?(role=\'(?P<options>.*?)\')?>', line))
+                    line = re.sub(
+                        '<programlisting\\s+language=\'lilypond\'.*?(role=\'(?P<options>.*?)\')?>',
+                        '<programlisting language="lilypond" role="\\g<options>">',
+                        line
+                    )
+                    # print(line)
+                f_lily.write(line)
+        os.unlink(in_file)
+        # shutil.move(in_file, in_lily_file)
+
+        # Add LilyPond to the PATH.
+        if os.path.isdir(lilypond_folder):
+            os.environ['PATH'] += os.pathsep + lilypond_folder
 
         # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.
-        command = lilypond_command + ' --format=docbook ' + in_lily_file
-        print(command)
-        if os.system(command) != 0:
-            command = 'python -tt "' + lilypond_command + '" --format=docbook ' + in_lily_file
-            print(command)
-            if os.system(command) != 0:
+        command_raw = lilypond_command + ' --format=docbook ' + in_lily_file
+        command_python = 'python -tt "' + lilypond_command + '" --format=docbook ' + in_lily_file
+
+        if os.system(command_raw) == 0:
+            print("Success running LilyPond:")
+            print(command_raw)
+        else:
+            if os.system(command_python) == 0:
+                print("Success running LilyPond:")
+                print(command_python)
+            else:
                 print('Error from LilyPond')
                 sys.exit(1)
 

commit 7c1d4fd3c41a7a99d3cb2eb90b1e7c1eb7e1456f
Author: Thibaut Cuvelier <tcuvelier at lyx.org>
Date:   Sun Sep 26 19:10:22 2021 +0200

    DocBook: missing multicol test.

diff --git a/autotests/export/docbook/multicol_doc_ru_additional.lyx b/autotests/export/docbook/multicol_doc_ru_additional.lyx
new file mode 100644
index 0000000..4de72e3
--- /dev/null
+++ b/autotests/export/docbook/multicol_doc_ru_additional.lyx
@@ -0,0 +1,1507 @@
+#LyX 2.4 created this file. For more info see https://www.lyx.org/
+\lyxformat 599
+\begin_document
+\begin_header
+\save_transient_properties true
+\origin unavailable
+\textclass scrbook
+\begin_preamble
+% DO NOT ALTER THIS PREAMBLE!!!
+%
+% This preamble is designed to ensure that the manual prints
+% out as advertised. If you mess with this preamble,
+% parts of the manual may not print out as expected.  If you
+% have problems LaTeXing this file, please contact 
+% the documentation team
+% email: lyx-docs at lists.lyx.org
+
+% the pages of the TOC are numbered roman
+% and a PDF-bookmark for the TOC is added
+\pagenumbering{roman}
+\let\myTOC\tableofcontents
+\renewcommand{\tableofcontents}{%
+  \frontmatter
+ \pdfbookmark[1]{\contentsname}{}
+ \myTOC
+ \cleardoublepage
+ \mainmatter
+ \pagenumbering{arabic}}
+
+% extra space for tables
+\newcommand{\extratablespace}[1]{\noalign{\vskip#1}}
+
+% for reduces the overfull lines
+\tolerance 1414
+\hbadness 1414
+\emergencystretch 1.5em
+\hfuzz 0.3pt
+
+% Use serif font
+\addtokomafont{disposition}{\rmfamily}
+\addtokomafont{descriptionlabel}{\rmfamily}
+\end_preamble
+\options bibliography=totoc,index=totoc,BCOR7.5mm,titlepage,captions=tableheading
+\use_default_options false
+\begin_modules
+logicalmkup
+theorems-ams
+theorems-ams-extended
+multicol
+shapepar
+\end_modules
+\maintain_unincluded_children no
+\language russian
+\language_package default
+\inputencoding utf8
+\fontencoding auto
+\font_roman "default" "default"
+\font_sans "default" "default"
+\font_typewriter "default" "default"
+\font_math "auto" "auto"
+\font_default_family default
+\use_non_tex_fonts false
+\font_sc false
+\font_roman_osf false
+\font_sans_osf false
+\font_typewriter_osf false
+\font_sf_scale 100 100
+\font_tt_scale 100 100
+\use_microtype true
+\use_dash_ligatures true
+\graphics default
+\default_output_format pdf2
+\output_sync 0
+\bibtex_command default
+\index_command makeindex
+\float_placement class
+\float_alignment class
+\paperfontsize 12
+\spacing single
+\use_hyperref true
+\pdf_title "Дополнительные возможности LyX"
+\pdf_author "Команда разработки LyX"
+\pdf_subject "Документация LyX - Дополнительные возможности"
+\pdf_keywords "LyX"
+\pdf_bookmarks true
+\pdf_bookmarksnumbered true
+\pdf_bookmarksopen false
+\pdf_bookmarksopenlevel 1
+\pdf_breaklinks false
+\pdf_pdfborder false
+\pdf_colorlinks true
+\pdf_backref false
+\pdf_pdfusetitle false
+\pdf_quoted_options "linkcolor=black, citecolor=black, urlcolor=blue, filecolor=blue, pdfpagelayout=OneColumn, pdfnewwindow=true, pdfstartview=XYZ, plainpages=false"
+\papersize a4
+\use_geometry false
+\use_package amsmath 1
+\use_package amssymb 1
+\use_package cancel 1
+\use_package esint 1
+\use_package mathdots 1
+\use_package mathtools 1
+\use_package mhchem 1
+\use_package stackrel 1
+\use_package stmaryrd 1
+\use_package undertilde 1
+\cite_engine basic
+\cite_engine_type default
+\biblio_style plain
+\use_bibtopic false
+\use_indices false
+\paperorientation portrait
+\suppress_date true
+\justification true
+\use_refstyle 1
+\use_minted 0
+\use_lineno 0
+\notefontcolor #0000ff
+\index Index
+\shortcut idx
+\color #008000
+\end_index
+\secnumdepth 3
+\tocdepth 3
+\paragraph_separation skip
+\defskip halfline
+\is_math_indent 0
+\math_numbering_side default
+\quotes_style russian
+\dynamic_quotes 0
+\papercolumns 1
+\papersides 2
+\paperpagestyle headings
+\tablestyle default
+\tracking_changes false
+\output_changes false
+\change_bars false
+\postpone_fragile_content false
+\html_math_output 0
+\html_css_as_file 0
+\html_be_strict true
+\docbook_table_output 0
+\end_header
+
+\begin_body
+
+\begin_layout Title
+Дополнительные возможности \SpecialChar LyX
+
+\end_layout
+
+\begin_layout Chapter
+Введение
+\end_layout
+
+\begin_layout Standard
+Пример:
+\end_layout
+
+\begin_layout Standard
+\noindent
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+\noindent
+
+\series bold
+\size small
+\lang english
+The Adventure of the Empty House
+\series default
+
+\begin_inset Newline newline
+\end_inset
+
+by Sir Arthur Conan Doyle
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+It was in the spring of the year 1894 that all London was interested, and
+ the fashionable world dismayed, by the murder of the Honourable Ronald
+ Adair under most unusual and inexplicable circumstances.
+ The public has already learned those particulars of the crime which came
+ out in the police investigation, but a good deal was suppressed upon that
+ occasion, since the case for the prosecution was so overwhelmingly strong
+ that it was not necessary to bring forward all the facts.
+ Only now, at the end of nearly ten years, am I allowed to supply those
+ missing links which make up the whole of that remarkable chain.
+ The crime was of interest in itself, but that interest was as nothing to
+ me compared to the inconceivable sequel, which afforded me the greatest
+ shock and surprise of any event in my adventurous life.
+ Even now, after this long interval, I find myself thrilling as I think
+ of it, and feeling once more that sudden flood of joy, amazement, and increduli
+ty which utterly submerged my mind.
+ Let me say to that public, which has shown some interest in those glimpses
+ which I have occasionally given them of the thoughts and actions of a very
+ remarkable man, that they are not to blame me if I have not shared my knowledge
+ with them, for I should have considered it my first duty to do so, had
+ I not been barred by a positive prohibition from his own lips, which was
+ only withdrawn upon the third of last month.
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+Пример с 3
+\begin_inset space ~
+\end_inset
+
+колонками:
+\end_layout
+
+\begin_layout Standard
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+
+\size footnotesize
+\begin_inset Argument 1
+status open
+
+\begin_layout Plain Layout
+
+3
+\end_layout
+
+\end_inset
+
+
+\lang english
+It can be imagined that my close intimacy with Sherlock Holmes had interested
+ me deeply in crime, and that after his disappearance I never failed to
+ read with care the various problems which came before the public.
+ And I even attempted, more than once, for my own private satisfaction,
+ to employ his methods in their solution, though with indifferent success.
+ There was none, however, which appealed to me like this tragedy of Ronald
+ Adair.
+ As I read the evidence at the inquest, which led up to a verdict of willful
+ murder against some person or persons unknown, I realized more clearly
+ than I had ever done the loss which the community had sustained by the
+ death of Sherlock Holmes.
+ There were points about this strange business which would, I was sure,
+ have specially appealed to him, and the efforts of the police would have
+ been supplemented, or more probably anticipated, by the trained observation
+ and the alert mind of the first criminal agent in Europe.
+ All day, as I drove upon my round, I turned over the case in my mind and
+ found no explanation which appeared to me to be adequate.
+ At the risk of telling a twice-told tale, I will recapitulate the facts
+ as they were known to the public at the conclusion of the inquest.
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+Можно иметь до 10 колонок, но следует учитывать степень удобства при чтении
+ такого фрагмента документа.
+\end_layout
+
+\begin_layout Standard
+\begin_inset Newpage newpage
+\end_inset
+
+
+\end_layout
+
+\begin_layout Subsection
+Колонки внутри колонок
+\end_layout
+
+\begin_layout Standard
+Также можно иметь колонки внутри колонок:
+\end_layout
+
+\begin_layout Standard
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+
+\size footnotesize
+\lang english
+The Honourable Ronald Adair was the second son of the Earl of Maynooth,
+ at that time governor of one of the Australian colonies.
+ Adair's mother had returned from Australia to undergo the operation for
+ cataract, and she, her son Ronald, and her daughter Hilda were living together
+ at 427 Park Lane.
+\end_layout
+
+\begin_layout Plain Layout
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+
+\size footnotesize
+\lang english
+The youth moved in the best society
+\begin_inset space ~
+\end_inset
+
+– had, so far as was known, no enemies and no particular vices.
+ He had been engaged to Miss Edith Woodley, of Carstairs, but the engagement
+ had been broken off by mutual consent some months before, and there was
+ no sign that it had left any very profound feeling behind it.
+ For the rest {sic} the man's life moved in a narrow and conventional circle,
+ for his habits were quiet and his nature unemotional.
+ Yet it was upon this easy-going young aristocrat that death came, in most
+ strange and unexpected form, between the hours of ten and eleven-twenty
+ on the night of March 30, 1894.
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Plain Layout
+
+\size footnotesize
+\lang english
+Ronald Adair was fond of cards
+\begin_inset space ~
+\end_inset
+
+– playing continually, but never for such stakes as would hurt him.
+ He was a member of the Baldwin, the Cavendish, and the Bagatelle card clubs.
+ It was shown that, after dinner on the day of his death, he had played
+ a rubber of whist at the latter club.
+ He had also played there in the afternoon.
+
+\size default
+ 
+\size footnotesize
+The evidence of those who had played with him
+\begin_inset space ~
+\end_inset
+
+– Mr.
+ Murray, Sir John Hardy, and Colonel Moran
+\begin_inset space ~
+\end_inset
+
+– showed that the game was whist, and that there was a fairly equal fall
+ of the cards.
+ Adair might have lost five pounds, but not more.
+ His fortune was a considerable one, and such a loss could not in any way
+ affect him.
+ He had played nearly every day at one club or other, but he was a cautious
+ player, and usually rose a winner.
+ It came out in evidence that, in partnership with Colonel Moran, he had
+ actually won as much as four hundred and twenty pounds in a sitting, some
+ weeks before, from Godfrey Milner and Lord Balmoral.
+ So much for his recent history as it came out at the inquest.
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Subsection
+Дополнительные примеры
+\end_layout
+
+\begin_layout Standard
+Примеры в этом разделе демонстрируют некоторые дополнительные особенности
+ организации мульти-колонок.
+\end_layout
+
+\begin_layout Standard
+Дополнительные возможности использования нескольких колонок см.
+ в 
+\begin_inset CommandInset href
+LatexCommand href
+name "документации"
+target "http://mirror.ctan.org/macros/latex/required/tools/multicol.pdf"
+literal "false"
+
+\end_inset
+
+ \SpecialChar LaTeX
+-пакета 
+\series bold
+multicol
+\series default
+.
+\end_layout
+
+\begin_layout Subsubsection
+Введение
+\end_layout
+
+\begin_layout Standard
+Чтобы добавить текст введения для нескольких колонок, установите курсор
+ во вставку с многоколоночностью и используйте меню 
+\family sans
+Вставка\SpecialChar menuseparator
+Введение
+\family default
+.
+ Введите текст введения во вставке.
+\begin_inset Newline newline
+\end_inset
+
+Пример с некоторым текстом введения:
+\end_layout
+
+\begin_layout Standard
+\begin_inset VSpace bigskip
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+\begin_inset Argument 2
+status open
+
+\begin_layout Plain Layout
+
+\lang english
+And the story continues and continues and continues and continues\SpecialChar ldots
+
+\end_layout
+
+\end_inset
+
+
+\size small
+\lang english
+On the evening of the crime, he returned from the club exactly at ten.
+ His mother and sister were out spending the evening with a relation.
+ The servant deposed that she heard him enter the front room on the second
+ floor, generally used as his sitting-room.
+ She had lit a fire there, and as it smoked she had opened the window.
+ No sound was heard from the room until eleven-twenty, the hour of the return
+ of Lady Maynooth and her daughter.
+ Desiring to say good-night, she attempted to enter her son's room.
+ The door was locked on the inside, and no answer could be got to their
+ cries and knocking.
+ Help was obtained, and the door forced.
+ The unfortunate young man was found lying near the table.
+ His head had been horribly mutilated by an expanding revolver bullet, but
+ no weapon of any sort was to be found in the room.
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+Вы также можете использовать заголовок раздела в качестве введения, если
+ используете команду раздела как 
+\family sans
+Код TeX
+\family default
+.
+ Например, команда
+\end_layout
+
+\begin_layout LyX-Code
+
+\backslash
+subsection{Заголовок}
+\end_layout
+
+\begin_layout Standard
+создает подраздел.
+ В этом примере, введение — это заголовок подраздела:
+\end_layout
+
+\begin_layout Standard
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+\begin_inset Argument 2
+status open
+
+\begin_layout Plain Layout
+\begin_inset ERT
+status collapsed
+
+\begin_layout Plain Layout
+
+
+\backslash
+subsubsection{
+\end_layout
+
+\end_inset
+
+Этот заголовок подраздела — введение
+\begin_inset ERT
+status collapsed
+
+\begin_layout Plain Layout
+
+}
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\end_inset
+
+
+\size small
+\lang english
+A minute examination of the circumstances served only to make the case more
+ complex.
+ In the first place, no reason could be given why the young man should have
+ fastened the door upon the inside.
+ There was the possibility that the murderer had done this, and had afterwards
+ escaped by the window.
+ The drop was at least twenty feet, however, and a bed of crocuses in full
+ bloom lay beneath.
+ Neither the flowers nor the earth showed any sign of having been disturbed,
+ nor were there any marks upon the narrow strip of grass which separated
+ the house from the road.
+ Apparently, therefore, it was the young man himself who had fastened the
+ door.
+ But how did he come by his death? No one could have climbed up to the window
+ without leaving traces.
+ Suppose a man had fired through the window, he would indeed be a remarkable
+ shot who could with a revolver inflict so deadly a wound.
+ Again, Park Lane is a frequented thoroughfare; there is a cab stand within
+ a hundred yards of the house.
+ No one had heard a shot.
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+Если вертикальное пространство меньше, чем 6 строк текста, оставшихся на
+ странице в начале мульти-колонок, разрыв страницы будет вставлен перед
+ этими колонками.
+ В зависимости от количества строк текста введения вы можете изменить размер
+ этого пространства.
+ Это делается путем установки курсора во вставку из нескольких колонок за
+ введением (если таковое имеется) и используя меню 
+\family sans
+Вставка\SpecialChar menuseparator
+Пробел перед разрывом страницы
+\family default
+.
+ Вставьте во вставку требуемую величину промежутка, например, «5cm».
+\begin_inset Newline newline
+\end_inset
+
+В следующем примере вертикальное расстояние установлено на 7 текстовых строк
+ с помощью 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+7
+\backslash
+baselineskip
+\end_layout
+
+\end_inset
+
+ (где команда 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+baselineskip
+\end_layout
+
+\end_inset
+
+ должна быть вставлена как \SpecialChar TeX
+-код):
+\end_layout
+
+\begin_layout Standard
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+\begin_inset Argument 3
+status open
+
+\begin_layout Plain Layout
+7
+\begin_inset ERT
+status collapsed
+
+\begin_layout Plain Layout
+
+
+\backslash
+baselineskip
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\end_inset
+
+
+\size small
+\lang english
+On the evening of the crime, he returned from the club exactly at ten.
+ His mother and sister were out spending the evening with a relation.
+ The servant deposed that she heard him enter the front room on the second
+ floor, generally used as his sitting-room.
+ She had lit a fire there, and as it smoked she had opened the window.
+ No sound was heard from the room until eleven-twenty, the hour of the return
+ of Lady Maynooth and her daughter.
+ Desiring to say good-night, she attempted to enter her son's room.
+ The door was locked on the inside, and no answer could be got to their
+ cries and knocking.
+ Help was obtained, and the door forced.
+ The unfortunate young man was found lying near the table.
+ His head had been horribly mutilated by an expanding revolver bullet, but
+ no weapon of any sort was to be found in the room.
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Subsubsection
+Окружающее пространство
+\end_layout
+
+\begin_layout Standard
+Размер пространства до и после нескольких колонок можно изменить с помощью
+ 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+multicolsep
+\end_layout
+
+\end_inset
+
+.
+ Например, команда
+\end_layout
+
+\begin_layout LyX-Code
+
+\backslash
+setlength{
+\backslash
+multicolsep}{3cm}
+\end_layout
+
+\begin_layout Standard
+в \SpecialChar TeX
+-коде меняет значение на 3
+\begin_inset space \thinspace{}
+\end_inset
+
+см.
+ Изменение необходимо сделать до начала колонок.
+ Предустановленное значение — 13
+\begin_inset space \thinspace{}
+\end_inset
+
+pt.
+\end_layout
+
+\begin_layout Standard
+Для этого примера 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+multicolsep
+\end_layout
+
+\end_inset
+
+ устанавливается в 2.5
+\begin_inset space \thinspace{}
+\end_inset
+
+cm:
+\end_layout
+
+\begin_layout Standard
+\begin_inset ERT
+status open
+
+\begin_layout Plain Layout
+
+
+\backslash
+setlength{
+\backslash
+multicolsep}{2.5cm}
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+All day I turned these facts over in my mind, endeavouring to hit upon some
+ theory which could reconcile them all, and to find that line of least resistanc
+e which my poor friend had declared to be the starting-point of every investigat
+ion.
+ I confess that I made little progress.
+ In the evening I strolled across the Park, and found myself about six o'clock
+ at the Oxford Street end of Park Lane.
+ A group of loafers upon the pavements, all staring up at a particular window,
+ directed me to the house which I had come to see.
+ A tall, thin man with coloured glasses, whom I strongly suspected of being
+ a plain-clothes detective, was pointing out some theory of his own, while
+ the others crowded round to listen to what he said.
+ I got as near him as I could, but his observations seemed to me to be absurd,
+ so I withdrew again in some disgust.
+ As I did so I struck against an elderly, deformed man, who had been behind
+ me, and I knocked down several books which he was carrying.
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+\begin_inset Note Greyedout
+status open
+
+\begin_layout Plain Layout
+
+\series bold
+Примечание.
+
+\series default
+ Значения, устанавливаемые с помощью 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+setlength
+\end_layout
+
+\end_inset
+
+, будут использоваться для всех последующих мульти-колонок, пока не будут
+ изменены снова.
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+\begin_inset ERT
+status collapsed
+
+\begin_layout Plain Layout
+
+
+\backslash
+setlength{
+\backslash
+multicolsep}{13pt}
+\end_layout
+
+\end_inset
+
+
+\begin_inset Note Note
+status collapsed
+
+\begin_layout Plain Layout
+возврат к значению по умолчанию
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+\begin_inset Newpage newpage
+\end_inset
+
+
+\end_layout
+
+\begin_layout Subsubsection
+Разрывы колонок
+\end_layout
+
+\begin_layout Standard
+Разрыв колонки можно принудительно выполнить, вставив команду 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+columnbreak{}
+\end_layout
+
+\end_inset
+
+ в \SpecialChar TeX
+-коде в ту позицию в тексте, где колонка должна быть разорвана.
+ Обратите внимание, что в большинстве случаев это приводит к появлению пробелов
+ в тексте.
+\begin_inset Newline newline
+\end_inset
+
+Пример:
+\end_layout
+
+\begin_layout Standard
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+You're surprised to see me, sir,
+\begin_inset Quotes erd
+\end_inset
+
+ said he, in a strange, croaking voice.
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+I acknowledged that I was.
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+Well, I've a conscience, sir, and when I chanced to see you go into this
+ house, as I came hobbling after you, I thought to myself, I'll just step
+ in and see that kind gentleman, and tell him that if I was a bit gruff
+ in my manner there was not any harm meant, and that I am much obliged to
+ him for picking up my books.
+\begin_inset Quotes erd
+\end_inset
+
+
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+You make too much of a trifle,
+\begin_inset Quotes erd
+\end_inset
+
+ said I.
+ 
+\begin_inset Quotes eld
+\end_inset
+
+May I ask how you knew who I was?
+\begin_inset Quotes erd
+\end_inset
+
+ AFTER THIS SENTENCE THE COLUMN BREAK IS FORCED.
+\begin_inset ERT
+status collapsed
+
+\begin_layout Plain Layout
+
+
+\backslash
+columnbreak{}
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+Well, sir, if it isn't too great a liberty, I am a neighbour of yours, for
+ you'll find my little bookshop at the corner of Church Street, and very
+ happy to see you, I am sure.
+ Maybe you collect yourself, sir.
+ Here's 
+\noun on
+British
+\begin_inset space ~
+\end_inset
+
+Birds
+\noun default
+, and 
+\noun on
+Catullus
+\noun default
+, and 
+\noun on
+The Holy War
+\noun default
+
+\begin_inset space ~
+\end_inset
+
+– a bargain, every one of them.
+ With five volumes you could just fill that gap on that second shelf.
+ It looks untidy, does it not, sir?
+\begin_inset Quotes erd
+\end_inset
+
+
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Subsubsection
+Разделение колонок
+\end_layout
+
+\begin_layout Standard
+Ширина колонок рассчитывается автоматически, но вы можете изменить расстояние
+ между ними.
+ Это делается с помощью команды 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+columnsep
+\end_layout
+
+\end_inset
+
+.
+ Ее предопределенное значение — 10
+\begin_inset space \thinspace{}
+\end_inset
+
+pt.
+ Пример установки значения для 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+columnsep
+\end_layout
+
+\end_inset
+
+:
+\end_layout
+
+\begin_layout Standard
+\begin_inset ERT
+status open
+
+\begin_layout Plain Layout
+
+
+\backslash
+setlength{
+\backslash
+columnsep}{3cm}
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+My observations of No.
+\begin_inset space \space{}
+\end_inset
+
+427 Park Lane did little to clear up the problem in which I was interested.
+ The house was separated from the street by a low wall and railing, the
+ whole not more than five feet high.
+ It was perfectly easy, therefore, for anyone to get into the garden, but
+ the window was entirely inaccessible, since there was no water pipe or
+ anything which could help the most active man to climb it.
+ More puzzled than ever, I retraced my steps to Kensington.
+ I had not been in my study five minutes when the maid entered to say that
+ a person desired to see me.
+ To my astonishment it was none other than my strange old book collector,
+ his sharp, wizened face peering out from a frame of white hair, and his
+ precious volumes, a dozen of them at least, wedged under his right arm.
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+\begin_inset ERT
+status collapsed
+
+\begin_layout Plain Layout
+
+
+\backslash
+setlength{
+\backslash
+columnsep}{10pt}
+\end_layout
+
+\end_inset
+
+
+\begin_inset Note Note
+status collapsed
+
+\begin_layout Plain Layout
+go back to the default
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Subsubsection
+Вертикальные линии
+\end_layout
+
+\begin_layout Standard
+Между столбцами помещается линия толщиной, задаваемой 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+columnseprule
+\end_layout
+
+\end_inset
+
+.
+ Если толщина устанавливается в 0
+\begin_inset space \thinspace{}
+\end_inset
+
+pt (это значение по умолчанию), линия не проводится.
+ В следующем примере ширина разделительной линии составляет 2
+\begin_inset space \thinspace{}
+\end_inset
+
+pt:
+\end_layout
+
+\begin_layout Standard
+\begin_inset ERT
+status open
+
+\begin_layout Plain Layout
+
+
+\backslash
+setlength{
+\backslash
+columnseprule}{2pt}
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+You're surprised to see me, sir,
+\begin_inset Quotes erd
+\end_inset
+
+ said he, in a strange, croaking voice.
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+I acknowledged that I was.
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+Well, I've a conscience, sir, and when I chanced to see you go into this
+ house, as I came hobbling after you, I thought to myself, I'll just step
+ in and see that kind gentleman, and tell him that if I was a bit gruff
+ in my manner there was not any harm meant, and that I am much obliged to
+ him for picking up my books.
+\begin_inset Quotes erd
+\end_inset
+
+
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+You make too much of a trifle,
+\begin_inset Quotes erd
+\end_inset
+
+ said I.
+ 
+\begin_inset Quotes eld
+\end_inset
+
+May I ask how you knew who I was?
+\begin_inset Quotes erd
+\end_inset
+
+
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+Well, sir, if it isn't too great a liberty, I am a neighbour of yours, for
+ you'll find my little bookshop at the corner of Church Street, and very
+ happy to see you, I am sure.
+ Maybe you collect yourself, sir.
+ Here's 
+\noun on
+British
+\begin_inset space ~
+\end_inset
+
+Birds
+\noun default
+, and 
+\noun on
+Catullus
+\noun default
+, and 
+\noun on
+The Holy War
+\noun default
+
+\begin_inset space ~
+\end_inset
+
+– a bargain, every one of them.
+ With five volumes you could just fill that gap on that second shelf.
+ It looks untidy, does it not, sir?
+\begin_inset Quotes erd
+\end_inset
+
+
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+\begin_inset VSpace defskip
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+Линию можно вывести в цвете, переопределив 
+\begin_inset Flex Code
+status collapsed
+
+\begin_layout Plain Layout
+
+\backslash
+columnseprulecolor
+\end_layout
+
+\end_inset
+
+.
+ Это делается путем вставки команды
+\end_layout
+
+\begin_layout LyX-Code
+
+\backslash
+renewcommand{
+\backslash
+columnseprulecolor}{
+\backslash
+color{red}}
+\end_layout
+
+\begin_layout Standard
+как \SpecialChar TeX
+-кода перед вставкой мульти-колонок, для получения дополнительной информации
+ о предварительно определенных и само-определенных цветах см.
+ руководство 
+\shape italic
+Встроенные объекты
+\shape default
+, раздел 
+\shape italic
+Цветные таблицы
+\shape default
+.
+ Чтобы вернуться к цвету по умолчанию, вставьте команду
+\end_layout
+
+\begin_layout LyX-Code
+
+\backslash
+renewcommand{
+\backslash
+columnseprulecolor}{
+\backslash
+normalcolor}
+\end_layout
+
+\begin_layout Standard
+Пример с линией голубого цвета и расстоянием между колонками в 1
+\begin_inset space \thinspace{}
+\end_inset
+
+см:
+\end_layout
+
+\begin_layout Standard
+\begin_inset ERT
+status open
+
+\begin_layout Plain Layout
+
+
+\backslash
+setlength{
+\backslash
+columnsep}{1cm}
+\end_layout
+
+\begin_layout Plain Layout
+
+
+\backslash
+renewcommand{
+\backslash
+columnseprulecolor}{
+\backslash
+color{cyan}}
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+\begin_inset Flex Multiple Columns
+status open
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+You're surprised to see me, sir,
+\begin_inset Quotes erd
+\end_inset
+
+ said he, in a strange, croaking voice.
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+I acknowledged that I was.
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+Well, I've a conscience, sir, and when I chanced to see you go into this
+ house, as I came hobbling after you, I thought to myself, I'll just step
+ in and see that kind gentleman, and tell him that if I was a bit gruff
+ in my manner there was not any harm meant, and that I am much obliged to
+ him for picking up my books.
+\begin_inset Quotes erd
+\end_inset
+
+
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+You make too much of a trifle,
+\begin_inset Quotes erd
+\end_inset
+
+ said I.
+ 
+\begin_inset Quotes eld
+\end_inset
+
+May I ask how you knew who I was?
+\begin_inset Quotes erd
+\end_inset
+
+
+\end_layout
+
+\begin_layout Plain Layout
+
+\size small
+\lang english
+\begin_inset Quotes eld
+\end_inset
+
+Well, sir, if it isn't too great a liberty, I am a neighbour of yours, for
+ you'll find my little bookshop at the corner of Church Street, and very
+ happy to see you, I am sure.
+ Maybe you collect yourself, sir.
+ Here's 
+\noun on
+British
+\begin_inset space ~
+\end_inset
+
+Birds
+\noun default
+, and 
+\noun on
+Catullus
+\noun default
+, and 
+\noun on
+The Holy War
+\noun default
+
+\begin_inset space ~
+\end_inset
+
+– a bargain, every one of them.
+ With five volumes you could just fill that gap on that second shelf.
+ It looks untidy, does it not, sir?
+\begin_inset Quotes erd
+\end_inset
+
+
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\begin_layout Standard
+\begin_inset ERT
+status collapsed
+
+\begin_layout Plain Layout
+
+
+\backslash
+setlength{
+\backslash
+columnseprule}{0pt}
+\end_layout
+
+\begin_layout Plain Layout
+
+
+\backslash
+renewcommand{
+\backslash
+columnseprulecolor}{
+\backslash
+normalcolor}
+\end_layout
+
+\end_inset
+
+
+\begin_inset Note Note
+status collapsed
+
+\begin_layout Plain Layout
+восстановление значения по умолчанию
+\end_layout
+
+\end_inset
+
+
+\end_layout
+
+\end_body
+\end_document
diff --git a/autotests/export/docbook/multicol_doc_ru_additional.xml b/autotests/export/docbook/multicol_doc_ru_additional.xml
new file mode 100644
index 0000000..deb9985
--- /dev/null
+++ b/autotests/export/docbook/multicol_doc_ru_additional.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This DocBook file was created by LyX 2.4.0dev
+  See http://www.lyx.org/ for more information -->
+<book xml:lang="ru_RU" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:m="http://www.w3.org/1998/Math/MathML" xmlns:xi="http://www.w3.org/2001/XInclude" version="5.2">
+<title>Дополнительные возможности LyX</title>
+<chapter>
+<title>Введение</title>
+<para>Пример:</para>
+<para>The Adventure of the Empty Houseby Sir Arthur Conan DoyleIt was in the spring of the year 1894 that all London was interested, and the fashionable world dismayed, by the murder of the Honourable Ronald Adair under most unusual and inexplicable circumstances. The public has already learned those particulars of the crime which came out in the police investigation, but a good deal was suppressed upon that occasion, since the case for the prosecution was so overwhelmingly strong that it was not necessary to bring forward all the facts. Only now, at the end of nearly ten years, am I allowed to supply those missing links which make up the whole of that remarkable chain. The crime was of interest in itself, but that interest was as nothing to me compared to the inconceivable sequel, which afforded me the greatest shock and surprise of any event in my adventurous life. Even now, after this long interval, I find myself thrilling as I think of it, and feeling once more that sudd
 en flood of joy, amazement, and incredulity which utterly submerged my mind. Let me say to that public, which has shown some interest in those glimpses which I have occasionally given them of the thoughts and actions of a very remarkable man, that they are not to blame me if I have not shared my knowledge with them, for I should have considered it my first duty to do so, had I not been barred by a positive prohibition from his own lips, which was only withdrawn upon the third of last month.</para>
+
+<para>Пример с 3&#xA0;колонками:</para>
+<para>It can be imagined that my close intimacy with Sherlock Holmes had interested me deeply in crime, and that after his disappearance I never failed to read with care the various problems which came before the public. And I even attempted, more than once, for my own private satisfaction, to employ his methods in their solution, though with indifferent success. There was none, however, which appealed to me like this tragedy of Ronald Adair. As I read the evidence at the inquest, which led up to a verdict of willful murder against some person or persons unknown, I realized more clearly than I had ever done the loss which the community had sustained by the death of Sherlock Holmes. There were points about this strange business which would, I was sure, have specially appealed to him, and the efforts of the police would have been supplemented, or more probably anticipated, by the trained observation and the alert mind of the first criminal agent in Europe. All day, as I drove 
 upon my round, I turned over the case in my mind and found no explanation which appeared to me to be adequate. At the risk of telling a twice-told tale, I will recapitulate the facts as they were known to the public at the conclusion of the inquest.</para>
+
+<para>Можно иметь до 10 колонок, но следует учитывать степень удобства при чтении такого фрагмента документа.</para>
+<section>
+<title>Колонки внутри колонок</title>
+<para>Также можно иметь колонки внутри колонок:</para>
+<para>The Honourable Ronald Adair was the second son of the Earl of Maynooth, at that time governor of one of the Australian colonies. Adair's mother had returned from Australia to undergo the operation for cataract, and she, her son Ronald, and her daughter Hilda were living together at 427 Park Lane.<para>The youth moved in the best society&#xA0;– had, so far as was known, no enemies and no particular vices. He had been engaged to Miss Edith Woodley, of Carstairs, but the engagement had been broken off by mutual consent some months before, and there was no sign that it had left any very profound feeling behind it. For the rest {sic} the man's life moved in a narrow and conventional circle, for his habits were quiet and his nature unemotional. Yet it was upon this easy-going young aristocrat that death came, in most strange and unexpected form, between the hours of ten and eleven-twenty on the night of March 30, 1894.</para>
+Ronald Adair was fond of cards&#xA0;– playing continually, but never for such stakes as would hurt him. He was a member of the Baldwin, the Cavendish, and the Bagatelle card clubs. It was shown that, after dinner on the day of his death, he had played a rubber of whist at the latter club. He had also played there in the afternoon. The evidence of those who had played with him&#xA0;– Mr. Murray, Sir John Hardy, and Colonel Moran&#xA0;– showed that the game was whist, and that there was a fairly equal fall of the cards. Adair might have lost five pounds, but not more. His fortune was a considerable one, and such a loss could not in any way affect him. He had played nearly every day at one club or other, but he was a cautious player, and usually rose a winner. It came out in evidence that, in partnership with Colonel Moran, he had actually won as much as four hundred and twenty pounds in a sitting, some weeks before, from Godfrey Milner and Lord Balmoral. So much for his 
 recent history as it came out at the inquest.</para>
+</section>
+<section>
+<title>Дополнительные примеры</title>
+<para>Примеры в этом разделе демонстрируют некоторые дополнительные особенности организации мульти-колонок.</para>
+<para>Дополнительные возможности использования нескольких колонок см. в <link xlink:href="http://mirror.ctan.org/macros/latex/required/tools/multicol.pdf">документации</link> LaTeX-пакета <emphasis role='bold'>multicol</emphasis>.</para>
+<section>
+<title>Введение</title>
+<para>Чтобы добавить текст введения для нескольких колонок, установите курсор во вставку с многоколоночностью и используйте меню <emphasis role='sans'>Вставка&#x21D2;Введение</emphasis>. Введите текст введения во вставке.</para>
+<para>Пример с некоторым текстом введения:</para>
+<para role='preface'>And the story continues and continues and continues and continues&#x2026;</para>
+<para>On the evening of the crime, he returned from the club exactly at ten. His mother and sister were out spending the evening with a relation. The servant deposed that she heard him enter the front room on the second floor, generally used as his sitting-room. She had lit a fire there, and as it smoked she had opened the window. No sound was heard from the room until eleven-twenty, the hour of the return of Lady Maynooth and her daughter. Desiring to say good-night, she attempted to enter her son's room. The door was locked on the inside, and no answer could be got to their cries and knocking. Help was obtained, and the door forced. The unfortunate young man was found lying near the table. His head had been horribly mutilated by an expanding revolver bullet, but no weapon of any sort was to be found in the room.</para>
+
+<para>Вы также можете использовать заголовок раздела в качестве введения, если используете команду раздела как <emphasis role='sans'>Код TeX</emphasis>. Например, команда</para>
+<programlisting language='lyx'>\subsection{Заголовок}</programlisting>
+<para>создает подраздел. В этом примере, введение — это заголовок подраздела:</para>
+<para role='preface'><!-- \subsubsection{ -->Этот заголовок подраздела — введение<!-- } --></para>
+<para>A minute examination of the circumstances served only to make the case more complex. In the first place, no reason could be given why the young man should have fastened the door upon the inside. There was the possibility that the murderer had done this, and had afterwards escaped by the window. The drop was at least twenty feet, however, and a bed of crocuses in full bloom lay beneath. Neither the flowers nor the earth showed any sign of having been disturbed, nor were there any marks upon the narrow strip of grass which separated the house from the road. Apparently, therefore, it was the young man himself who had fastened the door. But how did he come by his death? No one could have climbed up to the window without leaving traces. Suppose a man had fired through the window, he would indeed be a remarkable shot who could with a revolver inflict so deadly a wound. Again, Park Lane is a frequented thoroughfare; there is a cab stand within a hundred yards of the house. No
  one had heard a shot.</para>
+
+<para>Если вертикальное пространство меньше, чем 6 строк текста, оставшихся на странице в начале мульти-колонок, разрыв страницы будет вставлен перед этими колонками. В зависимости от количества строк текста введения вы можете изменить размер этого пространства. Это делается путем установки курсора во вставку из нескольких колонок за введением (если таковое имеется) и используя меню <emphasis role='sans'>Вставка&#x21D2;Пробел перед разрывом страницы</emphasis>. Вставьте во вставку требуемую величину промежутка, например, «5cm».</para>
+<para>В следующем примере вертикальное расстояние установлено на 7 текстовых строк с помощью <code>7\baselineskip</code> (где команда <code>\baselineskip</code> должна быть вставлена как TeX-код):</para>
+<para>On the evening of the crime, he returned from the club exactly at ten. His mother and sister were out spending the evening with a relation. The servant deposed that she heard him enter the front room on the second floor, generally used as his sitting-room. She had lit a fire there, and as it smoked she had opened the window. No sound was heard from the room until eleven-twenty, the hour of the return of Lady Maynooth and her daughter. Desiring to say good-night, she attempted to enter her son's room. The door was locked on the inside, and no answer could be got to their cries and knocking. Help was obtained, and the door forced. The unfortunate young man was found lying near the table. His head had been horribly mutilated by an expanding revolver bullet, but no weapon of any sort was to be found in the room.</para>
+</section>
+<section>
+<title>Окружающее пространство</title>
+<para>Размер пространства до и после нескольких колонок можно изменить с помощью <code>\multicolsep</code>. Например, команда</para>
+<programlisting language='lyx'>\setlength{\multicolsep}{3cm}</programlisting>
+<para>в TeX-коде меняет значение на 3&#x2009;см. Изменение необходимо сделать до начала колонок. Предустановленное значение — 13&#x2009;pt.</para>
+<para>Для этого примера <code>\multicolsep</code> устанавливается в 2.5&#x2009;cm:</para>
+<!-- \setlength{\multicolsep}{2.5cm} --><para>All day I turned these facts over in my mind, endeavouring to hit upon some theory which could reconcile them all, and to find that line of least resistance which my poor friend had declared to be the starting-point of every investigation. I confess that I made little progress. In the evening I strolled across the Park, and found myself about six o'clock at the Oxford Street end of Park Lane. A group of loafers upon the pavements, all staring up at a particular window, directed me to the house which I had come to see. A tall, thin man with coloured glasses, whom I strongly suspected of being a plain-clothes detective, was pointing out some theory of his own, while the others crowded round to listen to what he said. I got as near him as I could, but his observations seemed to me to be absurd, so I withdrew again in some disgust. As I did so I struck against an elderly, deformed man, who had been behind me, and I knocked down sever
 al books which he was carrying.</para>
+<!-- \setlength{\multicolsep}{13pt} --></section>
+<section>
+<title>Разрывы колонок</title>
+<para>Разрыв колонки можно принудительно выполнить, вставив команду <code>\columnbreak{}</code> в TeX-коде в ту позицию в тексте, где колонка должна быть разорвана. Обратите внимание, что в большинстве случаев это приводит к появлению пробелов в тексте.</para>
+<para>Пример:</para>
+<para>“You're surprised to see me, sir,” said he, in a strange, croaking voice.I acknowledged that I was.“Well, I've a conscience, sir, and when I chanced to see you go into this house, as I came hobbling after you, I thought to myself, I'll just step in and see that kind gentleman, and tell him that if I was a bit gruff in my manner there was not any harm meant, and that I am much obliged to him for picking up my books.”“You make too much of a trifle,” said I. “May I ask how you knew who I was?” AFTER THIS SENTENCE THE COLUMN BREAK IS FORCED.<!-- \columnbreak{} -->“Well, sir, if it isn't too great a liberty, I am a neighbour of yours, for you'll find my little bookshop at the corner of Church Street, and very happy to see you, I am sure. Maybe you collect yourself, sir. Here's British&#xA0;Birds, and Catullus, and The Holy War&#xA0;– a bargain, every one of them. With five volumes you could just fill that gap on that s
 econd shelf. It looks untidy, does it not, sir?”</para>
+</section>
+<section>
+<title>Разделение колонок</title>
+<para>Ширина колонок рассчитывается автоматически, но вы можете изменить расстояние между ними. Это делается с помощью команды <code>\columnsep</code>. Ее предопределенное значение — 10&#x2009;pt. Пример установки значения для <code>\columnsep</code>:</para>
+<!-- \setlength{\columnsep}{3cm} --><para>My observations of No. 427 Park Lane did little to clear up the problem in which I was interested. The house was separated from the street by a low wall and railing, the whole not more than five feet high. It was perfectly easy, therefore, for anyone to get into the garden, but the window was entirely inaccessible, since there was no water pipe or anything which could help the most active man to climb it. More puzzled than ever, I retraced my steps to Kensington. I had not been in my study five minutes when the maid entered to say that a person desired to see me. To my astonishment it was none other than my strange old book collector, his sharp, wizened face peering out from a frame of white hair, and his precious volumes, a dozen of them at least, wedged under his right arm.</para>
+<!-- \setlength{\columnsep}{10pt} --></section>
+<section>
+<title>Вертикальные линии</title>
+<para>Между столбцами помещается линия толщиной, задаваемой <code>\columnseprule</code>. Если толщина устанавливается в 0&#x2009;pt (это значение по умолчанию), линия не проводится. В следующем примере ширина разделительной линии составляет 2&#x2009;pt:</para>
+<!-- \setlength{\columnseprule}{2pt} --><para>“You're surprised to see me, sir,” said he, in a strange, croaking voice.I acknowledged that I was.“Well, I've a conscience, sir, and when I chanced to see you go into this house, as I came hobbling after you, I thought to myself, I'll just step in and see that kind gentleman, and tell him that if I was a bit gruff in my manner there was not any harm meant, and that I am much obliged to him for picking up my books.”“You make too much of a trifle,” said I. “May I ask how you knew who I was?”“Well, sir, if it isn't too great a liberty, I am a neighbour of yours, for you'll find my little bookshop at the corner of Church Street, and very happy to see you, I am sure. Maybe you collect yourself, sir. Here's British&#xA0;Birds, and Catullus, and The Holy War&#xA0;– a bargain, every one of them. With five volumes you could just fill that gap on that second shelf. It looks untidy, d
 oes it not, sir?”</para>
+
+<para>Линию можно вывести в цвете, переопределив <code>\columnseprulecolor</code>. Это делается путем вставки команды</para>
+<programlisting language='lyx'>\renewcommand{\columnseprulecolor}{\color{red}}</programlisting>
+<para>как TeX-кода перед вставкой мульти-колонок, для получения дополнительной информации о предварительно определенных и само-определенных цветах см. руководство <emphasis>Встроенные объекты, раздел <emphasis>Цветные таблицы. Чтобы вернуться к цвету по умолчанию, вставьте команду</emphasis></emphasis></para>
+<programlisting language='lyx'>\renewcommand{\columnseprulecolor}{\normalcolor}</programlisting>
+<para>Пример с линией голубого цвета и расстоянием между колонками в 1&#x2009;см:</para>
+<!-- \setlength{\columnsep}{1cm}
+\renewcommand{\columnseprulecolor}{\color{cyan}} --><para>“You're surprised to see me, sir,” said he, in a strange, croaking voice.I acknowledged that I was.“Well, I've a conscience, sir, and when I chanced to see you go into this house, as I came hobbling after you, I thought to myself, I'll just step in and see that kind gentleman, and tell him that if I was a bit gruff in my manner there was not any harm meant, and that I am much obliged to him for picking up my books.”“You make too much of a trifle,” said I. “May I ask how you knew who I was?”“Well, sir, if it isn't too great a liberty, I am a neighbour of yours, for you'll find my little bookshop at the corner of Church Street, and very happy to see you, I am sure. Maybe you collect yourself, sir. Here's British&#xA0;Birds, and Catullus, and The Holy War&#xA0;– a bargain, every one of them. With five volumes you could just fill that gap on that second shelf. It loo
 ks untidy, does it not, sir?”</para>
+<!-- \setlength{\columnseprule}{0pt}
+\renewcommand{\columnseprulecolor}{\normalcolor} --></section>
+</section>
+</chapter>
+</book>
\ No newline at end of file

commit 57e0b860cb0f68b9cc1dd633be4bb8b4e9328a3b
Author: Thibaut Cuvelier <tcuvelier at lyx.org>
Date:   Sun Sep 26 19:10:13 2021 +0200

    DocBook: add the new script as DocBook copier.

diff --git a/lib/configure.py b/lib/configure.py
index d13b314..68bdc09 100644
--- a/lib/configure.py
+++ b/lib/configure.py
@@ -1015,9 +1015,9 @@ def checkConverterEntries():
         xpath = 'none'
     global java
     if xsltproc != '':
-        addToRC('\\converter docbook5 epub "python $$s/scripts/docbook2epub.py none none \\"' + xsltproc + '\\" ' + xpath + ' $$i $$r $$o" ""')
+        addToRC(r'\converter docbook5 epub "python $$s/scripts/docbook2epub.py none none \"' + xsltproc + r'\" ' + xpath + ' $$i $$r $$o" ""')
     elif java != '':
-        addToRC('\\converter docbook5 epub "python $$s/scripts/docbook2epub.py \\"' + java + '\\" none none ' + xpath + ' $$i $$r $$o" ""')
+        addToRC(r'\converter docbook5 epub "python $$s/scripts/docbook2epub.py \"' + java + r'\" none none ' + xpath + ' $$i $$r $$o" ""')
     #
     checkProg('a MS Word Office Open XML converter -> LaTeX', ['pandoc -s -f docx -o $$o -t latex $$i'],
         rc_entry = [ r'\converter word2      latex      "%%"	""' ])
@@ -1311,13 +1311,24 @@ def checkConverterEntries():
                     #       even when requested with --pdf. This is a problem if a user
                     #       clicks View PDF after having done a View DVI. To circumvent
                     #       this, use different output folders for eps and pdf outputs.
-                    cmd = cmd.replace('"', '\\"')
+                    cmd = cmd.replace('"', r'\"')
                     addToRC(r'\converter lilypond-book latex     "' + cmd + ' --safe --lily-output-dir=ly-eps $$i"                                ""')
                     addToRC(r'\converter lilypond-book pdflatex  "' + cmd + ' --safe --pdf --latex-program=pdflatex --lily-output-dir=ly-pdf $$i" ""')
                     addToRC(r'\converter lilypond-book-ja platex "' + cmd + ' --safe --pdf --latex-program=platex --lily-output-dir=ly-pdf $$i" ""')
                     addToRC(r'\converter lilypond-book xetex     "' + cmd + ' --safe --pdf --latex-program=xelatex --lily-output-dir=ly-pdf $$i"  ""')
                     addToRC(r'\converter lilypond-book luatex    "' + cmd + ' --safe --pdf --latex-program=lualatex --lily-output-dir=ly-pdf $$i" ""')
                     addToRC(r'\converter lilypond-book dviluatex "' + cmd + ' --safe --latex-program=dvilualatex --lily-output-dir=ly-eps $$i" ""')
+
+                    # Also create the entry to apply LilyPond on DocBook files. However,
+                    # command must be passed as argument, and it might already have
+                    # quoted parts. LyX doesn't yet handle double-quoting of commands.
+                    # Hence, pass as argument either cmd (if it's a simple command) or
+                    # the Python file that should be called (typical on Windows).
+                    docbook_lilypond_cmd = cmd
+                    if "python" in docbook_lilypond_cmd:
+                        docbook_lilypond_cmd = '"' + path + '/lilypond-book"'
+                    addToRC(r'\copier docbook5 "python $$s/scripts/docbook_copy.py ' + docbook_lilypond_cmd.replace('"', r'\"') + r' $$i $$o"')
+
                     logger.info('+  found LilyPond-book version %s.' % version_number)
                 else:
                     logger.info('+  found LilyPond-book, but version %s is too old.' % version_number)
diff --git a/lib/scripts/docbook_copy.py b/lib/scripts/docbook_copy.py
index 69c4a62..8e00db7 100644
--- a/lib/scripts/docbook_copy.py
+++ b/lib/scripts/docbook_copy.py
@@ -13,6 +13,8 @@
 # This script copies the original DocBook file (directly produced by LyX) to the output DocBook file,
 # potentially applying a post-processing step. For now, the only implemented post-processing step is
 # LilyPond.
+# lilypond_book_command is either directly the binary to call OR the equivalent Python script that is
+# not directly executable.
 # /!\ The original file may be modified by this script!
 
 
@@ -28,6 +30,7 @@ def need_lilypond(file):
 
 
 def copy_docbook(args):
+    print(args)
     if len(args) != 4:
         print('Exactly four arguments are expected, only %s found: %s.' % (len(args), args))
         sys.exit(1)
@@ -37,7 +40,7 @@ def copy_docbook(args):
     in_file = args[2]
     out_file = args[3]
 
-    has_lilypond = lilypond_command != ""
+    has_lilypond = lilypond_command != "" and lilypond_command != "none"
 
     # Apply LilyPond to the original file if available and needed.
     if has_lilypond and need_lilypond(in_file):
@@ -47,11 +50,15 @@ def copy_docbook(args):
         in_lily_file = in_file.replace(".xml", ".lyxml")
         shutil.move(in_file, in_lily_file)
 
-        # Start LilyPond on the copied file.
+        # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.
         command = lilypond_command + ' --format=docbook ' + in_lily_file
+        print(command)
         if os.system(command) != 0:
-            print('Error from LilyPond')
-            sys.exit(1)
+            command = 'python -tt "' + lilypond_command + '" --format=docbook ' + in_lily_file
+            print(command)
+            if os.system(command) != 0:
+                print('Error from LilyPond')
+                sys.exit(1)
 
         # Now, in_file should have the LilyPond-processed contents.
 

commit f0537d72ee12331cc2a0e6fdf05991228c861056
Author: Thibaut Cuvelier <tcuvelier at lyx.org>
Date:   Sun Sep 26 18:15:09 2021 +0200

    DocBook: add script to start LilyPond on the generated file.
    
    Not yet used anywhere in the code for now (see next commit).

diff --git a/lib/scripts/docbook_copy.py b/lib/scripts/docbook_copy.py
new file mode 100644
index 0000000..69c4a62
--- /dev/null
+++ b/lib/scripts/docbook_copy.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+
+# file docbook_copy.py
+# This file is part of LyX, the document processor.
+# Licence details can be found in the file COPYING.
+#
+# \author Thibaut Cuvelier
+#
+# Full author contact details are available in file CREDITS
+
+# Usage:
+#   python docbook_copy.py lilypond_book_command in.docbook out.docbook
+# This script copies the original DocBook file (directly produced by LyX) to the output DocBook file,
+# potentially applying a post-processing step. For now, the only implemented post-processing step is
+# LilyPond.
+# /!\ The original file may be modified by this script!
+
+
+import os
+import shutil
+import sys
+
+
+def need_lilypond(file):
+    # Really tailored to the kind of output lilypond.module makes (in lib/layouts).
+    with open(file, 'r') as f:
+        return "language='lilypond'" in f.read()
+
+
+def copy_docbook(args):
+    if len(args) != 4:
+        print('Exactly four arguments are expected, only %s found: %s.' % (len(args), args))
+        sys.exit(1)
+
+    # Parse the command line.
+    lilypond_command = args[1]
+    in_file = args[2]
+    out_file = args[3]
+
+    has_lilypond = lilypond_command != ""
+
+    # Apply LilyPond to the original file if available and needed.
+    if has_lilypond and need_lilypond(in_file):
+        # LilyPond requires that its input file has the .lyxml extension.
+        # Move the file, so that LilyPond doesn't have to erase the contents of the original file before
+        # writing the converted output.
+        in_lily_file = in_file.replace(".xml", ".lyxml")
+        shutil.move(in_file, in_lily_file)
+
+        # Start LilyPond on the copied file.
+        command = lilypond_command + ' --format=docbook ' + in_lily_file
+        if os.system(command) != 0:
+            print('Error from LilyPond')
+            sys.exit(1)
+
+        # Now, in_file should have the LilyPond-processed contents.
+
+    # Perform the final copy.
+    shutil.copyfile(in_file, out_file, follow_symlinks=False)
+
+
+if __name__ == '__main__':
+    copy_docbook(sys.argv)

commit 661fcb2628e8a81cc66786b9fd66e2fa0120308c
Author: Thibaut Cuvelier <tcuvelier at lyx.org>
Date:   Sun Sep 26 17:27:41 2021 +0200

    Unused code.

diff --git a/lib/scripts/tex_copy.py b/lib/scripts/tex_copy.py
index 1ec9e92..529b13e 100644
--- a/lib/scripts/tex_copy.py
+++ b/lib/scripts/tex_copy.py
@@ -46,7 +46,6 @@ def main(argv):
         error("%s is no absolute file name.\n%s"\
               % abs_to_file, usage(argv[0]))
     to_dir, rel_to_file = os.path.split(abs_to_file)
-    to_base, to_ext = os.path.splitext(rel_to_file)
 
     # latex file name
     latex_file = argv[3]

commit b2c0604ad1078acaf410fa45e04922b6371f9332
Author: Thibaut Cuvelier <tcuvelier at lyx.org>
Date:   Sun Sep 26 17:25:28 2021 +0200

    Typo.

diff --git a/lib/scripts/tex_copy.py b/lib/scripts/tex_copy.py
index bb0cc68..1ec9e92 100644
--- a/lib/scripts/tex_copy.py
+++ b/lib/scripts/tex_copy.py
@@ -13,7 +13,7 @@
 # tex_copy.py <from file> <to file> <latex name>
 
 # This script will copy a file <from file> to <to file>.
-# <to file> is no exact copy of <from file>, but any occurence of <basename>
+# <to file> is no exact copy of <from file>, but any occurrence of <basename>
 # where <basename> is <from file> without directory and extension parts is
 # replaced by <latex name> without extension.
 

commit b3890d9eab40f996061f8ae1a7353b6819cd9774
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date:   Fri Sep 24 18:45:08 2021 +0200

    Fix warning.

diff --git a/src/frontends/qt/GuiFontMetrics.h b/src/frontends/qt/GuiFontMetrics.h
index 95b577c..9c7ce89 100644
--- a/src/frontends/qt/GuiFontMetrics.h
+++ b/src/frontends/qt/GuiFontMetrics.h
@@ -39,8 +39,6 @@ struct BreakAtKey
 	bool force;
 };
 
-static uint qHash(BreakAtKey const &);
-
 struct TextLayoutKey
 {
 	bool operator==(TextLayoutKey const & key) const {

-----------------------------------------------------------------------

Summary of changes:
 CMakeLists.txt                                     |    2 +-
 autotests/export/docbook/LilyPond_Book.xml         |   14 +-
 autotests/export/docbook/ff/lily-3ed27d76.png      |  Bin 0 -> 1798 bytes
 .../export/docbook/multicol_doc_ru_additional.lyx  | 1507 ++++++++++++++++++++
 .../export/docbook/multicol_doc_ru_additional.xml  |   77 +
 .../font-switch-before-comment.lyx                 |   47 +-
 config/lyxinclude.m4                               |    4 +-
 lib/Makefile.am                                    |    3 +-
 lib/configure.py                                   |   17 +-
 lib/images/busy.gif                                |  Bin 1849 -> 0 bytes
 lib/images/busy.svgz                               |  Bin 0 -> 327 bytes
 lib/scripts/docbook_copy.py                        |  177 +++
 lib/scripts/tex_copy.py                            |    3 +-
 src/BiblioInfo.cpp                                 |    2 +-
 src/Buffer.cpp                                     |    2 +-
 src/BufferView.cpp                                 |   57 +-
 src/BufferView.h                                   |    8 +-
 src/Converter.cpp                                  |    2 +-
 src/LaTeX.cpp                                      |    2 +-
 src/LyXRC.h                                        |    2 +-
 src/ParIterator.cpp                                |    6 +-
 src/Paragraph.cpp                                  |    7 +-
 src/Session.h                                      |    2 +-
 src/VCBackend.h                                    |    8 +-
 src/frontends/qt/ColorCache.cpp                    |    2 +-
 src/frontends/qt/DialogView.h                      |    2 +-
 src/frontends/qt/GuiApplication.cpp                |    4 +-
 src/frontends/qt/GuiBox.cpp                        |    2 +-
 src/frontends/qt/GuiDocument.cpp                   |    6 +-
 src/frontends/qt/GuiExternal.cpp                   |    2 +-
 src/frontends/qt/GuiFontMetrics.cpp                |    9 +
 src/frontends/qt/GuiMathMatrix.cpp                 |    4 +-
 src/frontends/qt/GuiPainter.cpp                    |    6 +-
 src/frontends/qt/GuiPainter.h                      |    3 +-
 src/frontends/qt/GuiPrefs.cpp                      |    2 +-
 src/frontends/qt/GuiSendto.cpp                     |    2 +-
 src/frontends/qt/GuiView.cpp                       |   32 +-
 src/frontends/qt/GuiView.h                         |   14 +
 src/frontends/qt/GuiWorkArea.cpp                   |    2 +-
 src/frontends/qt/Menus.cpp                         |    6 +-
 src/frontends/qt/qt_helpers.h                      |    2 -
 src/insets/InsetGraphics.cpp                       |    2 +-
 src/insets/InsetIPAMacro.cpp                       |    4 +-
 src/insets/InsetPreview.h                          |    2 +-
 src/insets/InsetTabular.cpp                        |   20 +-
 src/lyxfind.cpp                                    |   28 +-
 src/mathed/InsetMathGrid.h                         |    2 +-
 src/mathed/InsetMathMacroArgument.h                |    2 +-
 src/mathed/InsetMathMacroTemplate.cpp              |    2 +-
 src/mathed/InsetMathNest.cpp                       |    8 +-
 src/mathed/MathStream.h                            |    2 +-
 src/support/ForkedCalls.cpp                        |   11 +-
 src/support/Length.h                               |    4 +-
 src/xml.h                                          |    6 +-
 54 files changed, 1960 insertions(+), 182 deletions(-)
 create mode 100644 autotests/export/docbook/ff/lily-3ed27d76.png
 create mode 100644 autotests/export/docbook/multicol_doc_ru_additional.lyx
 create mode 100644 autotests/export/docbook/multicol_doc_ru_additional.xml
 copy development/tools/generate_symbols_images.lyx => autotests/export/latex/lyxbugs-resolved/font-switch-before-comment.lyx (81%)
 delete mode 100644 lib/images/busy.gif
 create mode 100644 lib/images/busy.svgz
 create mode 100644 lib/scripts/docbook_copy.py


hooks/post-receive
-- 
Repository for new features


More information about the lyx-cvs mailing list