// Scintilla source code edit control
/** @file LexTroff.cxx
 ** Lexer for the Troff typesetting language.
 ** This should work for Groff, Heirloom Troff and Neatroff and consequently
 ** for man pages.
 **
 ** There are a number of restrictions:
 ** Escapes are not interpreted everywhere e.g. as part of command names.
 ** For the same reasons, changing control characters via `.cc` or `.c2` will
 ** not affect lexing - subsequent requests will not be styled correctly.
 ** Luckily this feature is rarely used.
 ** Line feeds cannot be escaped everywhere - this would require a state machine
 ** for all parsing.
 ** However, the C lexer has the same restriction.
 ** It is impossible to predict which macro argument is a numeric expression or where
 ** the number is actually treated as text.
 ** Also, escapes with levels of indirection (eg. `\\$1`) cannot currently
 ** be highlighted, as it is impossible to predict the context in which an
 ** expansion will be used.
 ** Indirect blocks (.ami, .ami1, .dei and .dei1) cannot be folded.
 ** No effort is done to highlight any of the preprocessors (tbl, pic, grap...).
 **/
// Copyright 2022-2024 by Robin Haberkorn <robin.haberkorn@googlemail.com>
// The License.txt file describes the conditions under which this software may be distributed.

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <assert.h>
#include <ctype.h>

#include <string>
#include <algorithm>

#include "ILexer.h"
#include "Scintilla.h"
#include "SciLexer.h"

#include "WordList.h"
#include "LexAccessor.h"
#include "Accessor.h"
#include "StyleContext.h"
#include "LexCharacterSet.h"
#include "LexerModule.h"

using namespace Lexilla;

// Can be used as the line state to detect continuations
// across calls to ColouriseTroffDoc().
enum TroffState {
	DEFAULT = 0,
	// REQUEST/COMMAND on a line means that it is a command line with continuation.
	REQUEST,
	COMMAND,
	// Flow control request with continuation
	FLOW_CONTROL,
	// Inside "ignore input" block (usually .ig)
	IGNORE
};

static Sci_PositionU TroffEscapeBracketSize(Sci_PositionU startLine, Sci_PositionU endPos,
                                            Accessor &styler);
static Sci_PositionU TroffEscapeQuoteSize(Sci_PositionU startLine, Sci_PositionU endPos,
                                            Accessor &styler);

static Sci_PositionU ColouriseTroffEscape(Sci_PositionU startLine, Sci_PositionU endPos,
                                          Accessor &styler, WordList *keywordlists[],
                                          TroffState state);
static void ColouriseTroffLine(Sci_PositionU startLine, Sci_PositionU endPos,
                               Accessor &styler, WordList *keywordlists[],
                               TroffState state);

// For all escapes that take a one character (\*x), two character (\*(xy) or
// arbitrary number of characters (\*[...]).
static Sci_PositionU TroffEscapeBracketSize(Sci_PositionU startLine, Sci_PositionU endPos,
                                            Accessor &styler) {
	if (startLine > endPos) {
		return 0;
	}

	if (styler[startLine] == '(') {
		// two character name
		return std::min((Sci_PositionU)3, endPos-startLine+1);
	}

	if (styler[startLine] != '[') {
		// one character name
		return 1;
	}
	// name with arbitrary size between [...]

	Sci_PositionU i = startLine+1;

	while (i <= endPos) {
		if (styler[i] == ']') {
			return i-startLine+1;
		}

		if (styler[i] != '\\') {
			// not an escape
			i++;
			continue;
		}

		if (++i > endPos) {
			break;
		}

		// NOTE: Other escapes are not interpreted within \x[...].
		switch (styler[i]) {
		case '*': // string register
		case '$': // macro argument
		case 'V': // environment variable
		case 'n': // numeric register
			i += 1;
			i += TroffEscapeBracketSize(i, endPos, styler);
			break;
		case 'A': // Is there a string with this name?
		case 'B': // Is there a register with this name?
		case 'R': // register increase
			i += 1;
			i += TroffEscapeQuoteSize(i, endPos, styler);
			break;
		default:
			i += TroffEscapeBracketSize(i, endPos, styler);
		}
	}

	return endPos-startLine+1;
}

// For all escapes that take an argument in quotes, as in \v'...'.
// Actually, this works with any character as a delimiter.
static Sci_PositionU TroffEscapeQuoteSize(Sci_PositionU startLine, Sci_PositionU endPos,
                                          Accessor &styler) {
	if (startLine > endPos) {
		return 0;
	}

	char delim = styler[startLine];

	Sci_PositionU i = startLine+1;

	while (i <= endPos) {
		if (styler[i] == delim) {
			return i-startLine+1;
		}

		if (styler[i] != '\\') {
			// not an escape
			i++;
			continue;
		}

		if (++i > endPos) {
			break;
		}

		// NOTE: Other escapes are not interpreted within \x'...'.
		switch (styler[i]) {
		case '*': // string register
		case '$': // macro argument
		case 'V': // environment variable
		case 'n': // numeric register
			i += 1;
			i += TroffEscapeBracketSize(i, endPos, styler);
			break;
		case 'A': // Is there a string with this name?
		case 'B': // Is there a register with this name?
		case 'R': // register increase
			i += 1;
			i += TroffEscapeQuoteSize(i, endPos, styler);
			break;
		default:
			i += TroffEscapeBracketSize(i, endPos, styler);
		}
	}
	
	return endPos-startLine+1;
}

static Sci_PositionU ColouriseTroffEscape(Sci_PositionU startLine, Sci_PositionU endPos,
                                          Accessor &styler, WordList *keywordlists[],
                                          TroffState state) {
	Sci_PositionU i = startLine;

	Sci_Position curLine = styler.GetLine(startLine);

	if (styler[i] != '\\') {
		// not an escape - still consume one character
		return 1;
	}
	styler.ColourTo(i-1, SCE_TROFF_DEFAULT);
	i++;
	if (i > endPos) {
		return i-startLine;
	}

	switch (styler[i]) {
	case '"':
		// ordinary end of line comment
		styler.ColourTo(endPos, SCE_TROFF_COMMENT);
		return endPos-startLine+1;

	case '#':
		// \# comments will also "swallow" the EOL characters similar to
		// an escape at the end of line.
		// They are usually used only to comment entire lines, but we support
		// them after command lines anyway.
		styler.ColourTo(endPos, SCE_TROFF_COMMENT);
		// Next line will be a continuation
		styler.SetLineState(curLine, state);
		return endPos-startLine+1;

	case '*':
		// String register
		i += 1;
		i += TroffEscapeBracketSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_STRING);
		break;
	case '$':
		// Macro parameter
		i += 1;
		i += TroffEscapeBracketSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_MACRO);
		break;
	case 'V':
		// Environment variable
		i += 1;
		i += TroffEscapeBracketSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_ENV);
		break;
	case 'f':
	case 'F':
		// Font change
		i += 1;
		i += TroffEscapeBracketSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_FONT);
		break;
	case 'n':
		// Number register
		i += 1;
		if (styler[i] == '+' || styler[i] == '-') {
			i += 1;
		}
		if (i > endPos) {
			break;
		}
		i += TroffEscapeBracketSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_NUMBER);
		break;
	case 'g':
	case 'k':
		i += 1;
		i += TroffEscapeBracketSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_NUMBER);
		break;
	case 'R':
		// Number register increase
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_NUMBER);
		break;
	case 'm':
	case 'M':
		// Colour change
		i += 1;
		i += TroffEscapeBracketSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_COLOUR);
		break;
	case 'O':
		// Nested suppressions
		i += 1;
		i += TroffEscapeBracketSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_SUPPRESSION);
		break;
	case 's':
		// Type size
		// This is special in supporting both a +/- prefix directly
		// after \s and both the [...] and '...' syntax.
		i += 1;
		if (i > endPos) {
			break;
		}
		if (styler[i] == '+' || styler[i] == '-') {
			i += 1;
		}
		if (styler[i] == '\'') {
			i += TroffEscapeQuoteSize(i, endPos, styler);
		} else if (styler[i] == '(') {
			// You can place a +/- even after the opening brace as in \s(+12.
			// Otherwise this could be handled by TroffEscapeBracketSize().
			i += 1;
			if (i > endPos) {
				break;
			}
			if (styler[i] == '+' || styler[i] == '-') {
				i += 1;
			}
			i += std::min((Sci_PositionU)2, endPos-i);
		} else {
			i += TroffEscapeBracketSize(i, endPos, styler);
		}
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_SIZE);
		break;
	case '{':
		// Opening code block
		i += 1;
		styler.ColourTo(i-1, SCE_TROFF_OPERATOR);
		// Groff actually supports commands immediately
		// following the opening brace as in `\{.tm`.
		// That's only why \{\ will actually treat the next line
		// as an ordinary command again.
		ColouriseTroffLine(i, endPos, styler, keywordlists, state);
		return endPos-startLine+1;
	case '}':
		// Closing code block
		// This exists only for the rare case of closing a
		// block in the middle of a line. 
		i += 1;
		styler.ColourTo(i-1, SCE_TROFF_OPERATOR);
		break;
	case '\n':
	case '\r':
		// Escape newline
		styler.ColourTo(endPos, SCE_TROFF_ESCAPE_GLYPH);
		i += 1;
		// Next line will be a continuation
		styler.SetLineState(curLine, state);
		break;
	case '!':
		// transparent line
		styler.ColourTo(endPos, SCE_TROFF_ESCAPE_TRANSPARENT);
		return endPos-startLine+1;
	case '?':
		// Transparent embed until \?
		i++;
		while (i <= endPos) {
			if (styler.Match(i, "\\?")) {
				i += 2;
				break;
			}
			i++;
		}
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_TRANSPARENT);
		break;
	case 'b':
		// pile of chars
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_GLYPH);
		break;
	case 'A':
	case 'B':
		// Valid identifier or register?
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_ISVALID);
		break;
	case 'C':
	case 'N':
		// additional glyphs
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_GLYPH);
		break;
	case 'D':
	case 'l':
	case 'L':
		// drawing commands
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_DRAW);
		break;
	case 'h':
	case 'v':
		// horizontal/vertical movement
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_MOVE);
		break;
	case 'H':
		// font height
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_HEIGHT);
		break;
	case 'o':
		// overstrike
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_OVERSTRIKE);
		break;
	case 'S':
		// slant
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_SLANT);
		break;
	case 'w':
		// width of string
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_WIDTH);
		break;
	case 'x':
		// increase vertical spacing
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_VSPACING);
		break;
	case 'X':
		// device control commands
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_DEVICE);
		break;
	case 'Y':
		// device control commands
		i += 1;
		i += TroffEscapeBracketSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_DEVICE);
		break;
	case 'Z':
		// format string without moving
		i += 1;
		i += TroffEscapeQuoteSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_NOMOVE);
		break;
	case 'z':
		// Zero-width format
		i += std::min((Sci_PositionU)2, endPos-i);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_NOMOVE);
		break;
	default:
		i += TroffEscapeBracketSize(i, endPos, styler);
		styler.ColourTo(i-1, SCE_TROFF_ESCAPE_GLYPH);
	}

	return i-startLine;
}

static Sci_PositionU ColouriseTroffNumber(Sci_PositionU startLine, Sci_PositionU endPos,
                                          Accessor &styler) {
	Sci_PositionU i = startLine;

	if (!isdigit(styler[i])) {
		// not a digit
		return 0;
	}

	styler.ColourTo(i-1, SCE_TROFF_DEFAULT);

	i++;
	while (i <= endPos && isdigit(styler[i])) {
		i++;
	}
	if (i > endPos) {
		goto done;
	}
	if (styler[i] == '.') {
		i++;
		while (i <= endPos && isdigit(styler[i])) {
			i++;
		}
		if (i > endPos) {
			goto done;
		}
	}

	// Unit is part of number
	if (strchr("ciPpmMnuvsf", styler[i])) {
		i++;
	}

done:
	styler.ColourTo(i-1, SCE_TROFF_NUMBER);
	return i-startLine;
}

static void ColouriseTroffLine(Sci_PositionU startLine, Sci_PositionU endPos,
                               Accessor &styler, WordList *keywordlists[],
                               TroffState state) {
	WordList &requests = *keywordlists[0];
	WordList &flowControlCond = *keywordlists[1];
	WordList &flowControlNocond = *keywordlists[2];
	WordList &ignoreBlocks = *keywordlists[3];
	Sci_PositionU i = startLine;

	if (startLine > endPos) {
		return;
	}

	Sci_Position curLine = styler.GetLine(startLine);
	// If the line state was not DEFAULT (i.e. the line had a continuation)
	// and the corresponding escape is erased, we must make sure that
	// the line state becomes DEFAULT again.
	styler.SetLineState(curLine, TroffState::DEFAULT);

	if (state == TroffState::IGNORE) {
		// Inside "ignore input" blocks
		if (styler[i] == '.') {
			i++;
			// Groff does not appear to allow spaces after the dot on
			// lines ending ignore blocks.
#if 0
			while (i <= endPos && isspacechar(styler[i])) {
				i++;
			}
#endif

			Sci_PositionU startCommand = i;
			while (i <= endPos && !isspacechar(styler[i])) {
				i++;
			}

			// Check whether this is the end of the block by backtracking.
			// The alternative would be to maintain a per-lexer data structure
			// of ignore blocks along with their "end-mac" parameters.
			if (i > startCommand) {
				std::string endmac = styler.GetRange(startCommand, i);
				if (endmac == ".") {
					endmac.clear();
				}
				// We styled right up to the "end-mac" argument or EOL.
				Sci_PositionU startEndmac = startLine;
				int style;
				while ((style = styler.BufferStyleAt(startEndmac-1)) != SCE_TROFF_REQUEST &&
				       style != SCE_TROFF_COMMAND) {
					startEndmac--;
				}
				Sci_PositionU endEndmac = startEndmac;
				while (!isspacechar(styler.SafeGetCharAt(endEndmac))) {
					endEndmac++;
				}

				std::string buf = endEndmac > startEndmac ?
							styler.GetRange(startEndmac, endEndmac) : "";
				if (buf == endmac) {
					// Found the end of the ignore block.
					// This line can be an ordinary request or macro,
					// so we completely restyle it.
					state = TroffState::DEFAULT;
					ColouriseTroffLine(startLine, endPos, styler, keywordlists, state);
					return;
				}
			}
		}

		styler.ColourTo(endPos, SCE_TROFF_IGNORE);
		styler.SetLineState(curLine, TroffState::IGNORE);
		return;
	}

	if (state == TroffState::FLOW_CONTROL) {
		// Allow leading spaces only in continuations of flow control requests.
		while (i <= endPos && isspacechar(styler[i])) {
			i++;
		}
	}

	// NOTE: The control characters can theoretically be changed via `.cc` and `.c2`.
	// This is seldom done and in principle it makes Roff unparseable since there is
	// no way to determine whether one of those commands has been executed without
	// executing the entire code which may run forever (halting problem).
	// So not supporting alternate control characters is simply a necessary restriction of this lexer.
	if ((state == TroffState::DEFAULT || state == TroffState::FLOW_CONTROL) &&
	    (styler[i] == '.' || styler[i] == '\'')) {
		// Control line
		// TODO: It may make sense to highlight the non-breaking control character (') in
		// a separate style.
		i++;

		while (i <= endPos && isspacechar(styler[i])) {
			i++;
		}
		Sci_PositionU startCommand = i;
		while (i <= endPos && !isspacechar(styler[i])) {
			i++;
			// FIXME: Highlight escapes in this position?
		}

		if (startCommand == i) {
			// lone dot without anything following
			styler.ColourTo(endPos, SCE_TROFF_REQUEST);
			return;
		}

		std::string buf = styler.GetRange(startCommand, i);

		if (buf == "\\\"") {
			// Handling .\" separately makes sure that the entire line including
			// the leading dot is highlighted as a comment
			styler.ColourTo(endPos, SCE_TROFF_COMMENT);
			return;
		}

		if (buf == "\\}") {
			// Handling .\} separately makes sure it is styled like
			// the opening \{ braces.
			styler.ColourTo(endPos, SCE_TROFF_OPERATOR);
			return;
		}

		// Ignore blocks
		if (ignoreBlocks.InList(buf)) {
			// It's important to style right up to the optional end-mac argument,
			// since we use the SCE_TROFF_REQUEST/COMMAND style to check for block ends (see above).
			while (i <= endPos && isspacechar(styler[i]) &&
			       styler[i] != '\n' && styler[i] != '\r') {
				i++;
			}
			styler.ColourTo(i-1, requests.InList(buf) ? SCE_TROFF_REQUEST : SCE_TROFF_COMMAND);
			styler.ColourTo(endPos, SCE_TROFF_DEFAULT);
			// Next line will be an ignore block
			styler.SetLineState(curLine, TroffState::IGNORE);
			return;
		}

		// Flow control without conditionals
		if (flowControlNocond.InList(buf)) {
			styler.ColourTo(i-1, requests.InList(buf) ? SCE_TROFF_REQUEST : SCE_TROFF_COMMAND);

			state = TroffState::FLOW_CONTROL;
			styler.ColourTo(i-1, SCE_TROFF_DEFAULT);
			ColouriseTroffLine(i, endPos, styler, keywordlists, state);
			return;
		}

		// It is tempting to treat flow control requests like ordinary requests,
		// but you cannot really predict the beginning of the next command.
		// Eg. .if 'FOO'.tm' .tm BAR
		// We therefore have to parse at least the apostrophe expressions
		// and keep track of escapes.
		if (flowControlCond.InList(buf)) {
			styler.ColourTo(i-1, requests.InList(buf) ? SCE_TROFF_REQUEST : SCE_TROFF_COMMAND);

			while (i <= endPos && isspacechar(styler[i])) {
				i++;
			}
			if (i > endPos) {
				return;
			}

			if (styler[i] == '!') {
				i++;
				styler.ColourTo(i-1, SCE_TROFF_OPERATOR);
			}
			if (i > endPos) {
				return;
			}

			if (styler[i] == '\'') {
				// string comparison: 's1's2'
				// FIXME: Should be highlighted?
				// However, none of the conditionals are currently highlighted.
				i++;
				while (i <= endPos && styler[i] != '\'') {
					i += ColouriseTroffEscape(i, endPos, styler, keywordlists, state);
				}
				if (i > endPos) {
					return;
				}
				i++;
				while (i <= endPos && styler[i] != '\'') {
					i += ColouriseTroffEscape(i, endPos, styler, keywordlists, state);
				}
				if (i > endPos) {
					return;
				}
				i++;
			} else {
				// Everything else - including numeric expressions -
				// can contain spaces only in escapes and inside balanced round braces.
				int braceLevel = 0;
				while (i <= endPos && (braceLevel > 0 || !isspacechar(styler[i]))) {
					Sci_PositionU numLen;

					switch (styler[i]) {
					case '(':
						braceLevel++;
						i++;
						break;
					case ')':
						braceLevel--;
						i++;
						break;
					default:
						numLen = ColouriseTroffNumber(i, endPos, styler);
						i += numLen;
						if (numLen > 0) {
							break;
						}

						i += ColouriseTroffEscape(i, endPos, styler, keywordlists, state);
					}
				}
			}

			state = TroffState::FLOW_CONTROL;
			styler.ColourTo(i-1, SCE_TROFF_DEFAULT);
			ColouriseTroffLine(i, endPos, styler, keywordlists, state);
			return;
		}

		// remaining non-flow-control requests and commands
		state = requests.InList(buf) ? TroffState::REQUEST : TroffState::COMMAND;
		styler.ColourTo(i-1, state == TroffState::REQUEST
					? SCE_TROFF_REQUEST : SCE_TROFF_COMMAND);
	}

	// Text line, request/command parameters including continuations
	while (i <= endPos) {
		if (state == TroffState::COMMAND && styler[i] == '"') {
			// Macro or misc command line arguments - assume that double-quoted strings
			// actually denote arguments.
			// FIXME: Allow escape linebreaks on the end of strings?
			// This would require another TroffState.
			styler.ColourTo(i-1, SCE_TROFF_DEFAULT);

			i++;
			while (i <= endPos && styler[i] != '"') {
				styler.ColourTo(i-1, SCE_TROFF_STRING);
				i += ColouriseTroffEscape(i, endPos, styler, keywordlists, state);
			}
			if (styler[i] == '"') {
				i++;
			}
			styler.ColourTo(i-1, SCE_TROFF_STRING);
			continue;
		}

		if (state == TroffState::COMMAND || state == TroffState::REQUEST) {
			// Numbers are supposed to be highlighted on every command line.
			// FIXME: Not all requests actually treat numbers specially.
			// Theoretically, we have to parse on a per-request basis.
			Sci_PositionU numLen;
			numLen = ColouriseTroffNumber(i, endPos, styler);
			i += numLen;
			if (numLen > 0) {
				continue;
			}
		}

		i += ColouriseTroffEscape(i, endPos, styler, keywordlists, state);
	}

	styler.ColourTo(endPos, SCE_TROFF_DEFAULT);
}

static void ColouriseTroffDoc(Sci_PositionU startPos, Sci_Position length, int,
                              WordList *keywordlists[], Accessor &styler) {
	styler.StartAt(startPos);
	Sci_PositionU startLine = startPos;
	Sci_Position curLine = styler.GetLine(startLine);

	styler.StartSegment(startLine);

	// NOTE: startPos will always be at the beginning of a line
	for (Sci_PositionU i = startPos; i < startPos + length; i++) {
		// NOTE: The CR in CRLF counts into the current line.
		bool atEOL = (styler[i] == '\r' && styler.SafeGetCharAt(i+1) != '\n') || styler[i] == '\n';

		if (atEOL || i == startPos + length - 1) {
			// If the previous line had a continuation, the current line
			// is parsed like an argument to the command that had that continuation.
			TroffState state = curLine > 0 ? (TroffState)styler.GetLineState(curLine-1)
			                               : TroffState::DEFAULT;

			// End of line (or of line buffer) met, colourise it
			ColouriseTroffLine(startLine, i, styler, keywordlists, state);
			startLine = i + 1;
			curLine++;
		}
	}
}

static void FoldTroffDoc(Sci_PositionU startPos, Sci_Position length, int,
                         WordList *keywordlists[], Accessor &styler) {
	WordList &endmacBlocks = *keywordlists[4];

	Sci_PositionU endPos = startPos + length;
	Sci_Position lineCurrent = styler.GetLine(startPos);
	int levelPrev = styler.LevelAt(lineCurrent) & SC_FOLDLEVELNUMBERMASK;
	int levelCurrent = levelPrev;
	char chNext = styler[startPos];
	bool foldCompact = styler.GetPropertyInt("fold.compact", 1) != 0;
	int styleNext = styler.StyleAt(startPos);
	std::string requestName;

	for (Sci_PositionU i = startPos; i < endPos; i++) {
		char ch = chNext;
		chNext = styler.SafeGetCharAt(i + 1);
		int style = styleNext;
		styleNext = styler.StyleAt(i + 1);
		bool atEOL = (ch == '\r' && chNext != '\n') || ch == '\n';

		if (style == SCE_TROFF_OPERATOR && ch == '\\') {
			if (chNext == '{') {
				levelCurrent++;
			} else if (chNext == '}') {
				levelCurrent--;
			}
		} else if (style != SCE_TROFF_IGNORE && styleNext == SCE_TROFF_IGNORE) {
			// Beginning of ignore block
			levelCurrent++;
		} else if (style == SCE_TROFF_IGNORE && styleNext != SCE_TROFF_IGNORE) {
			// End of ignore block
			// The end-mac line will not be folded, which is probably okay
			// since it could start the next ignore block or could be any command that is
			// executed.
			// This is not handled by the endmac block handling below
			// since the `.ig` syntax is different.
			// FIXME: Fold at least `..` lines.
			levelCurrent--;
		} else if ((style == SCE_TROFF_REQUEST || style == SCE_TROFF_COMMAND) &&
		           !isspacechar(ch)) {
			requestName.push_back(ch);
		}

		if (!atEOL) {
			continue;
		}

		int lev = levelPrev;

		if (requestName.length() > 1) {
			if (endmacBlocks.InList(requestName.substr(1))) {
				// beginning of block
				levelCurrent++;
			} else {
				// potential end of block
				// This parsing could be avoided if we kept a list
				// line numbers and end-mac strings.
				// The parsing here could also be simplified if
				// we highlighted the end-mac strings in ColouriseTroffLine().
				std::string endmac = requestName.substr(1);
				if (endmac == ".") {
					endmac.clear();
				}

				// find start of block
				Sci_Position startLine = lineCurrent;
				while (startLine > 0 && styler.LevelAt(startLine-1) >= levelCurrent) {
					startLine--;
				}
				if (startLine) {
					Sci_Position startEndmac = styler.LineStart(startLine);
					int reqStyle;
					// skip the request/command name
					while ((reqStyle = styler.StyleAt(startEndmac)) == SCE_TROFF_REQUEST ||
					       reqStyle == SCE_TROFF_COMMAND) {
						startEndmac++;
					}
					while (isspacechar(styler.SafeGetCharAt(startEndmac))) {
						startEndmac++;
					}
					// skip the macro name
					while (!isspacechar(styler.SafeGetCharAt(startEndmac))) {
						startEndmac++;
					}
					while (isspacechar(styler.SafeGetCharAt(startEndmac)) &&
					       styler[startEndmac] != '\n' && styler[startEndmac] != '\r') {
						startEndmac++;
					}
					Sci_Position endEndmac = startEndmac;
					while (!isspacechar(styler.SafeGetCharAt(endEndmac))) {
						endEndmac++;
					}
					std::string buf = endEndmac > startEndmac ?
								styler.GetRange(startEndmac, endEndmac) : "";
					if (buf == endmac) {
						// FIXME: Better fold the previous line unless it is `..`.
						levelCurrent--;
					}
				}
			}
		} else if (foldCompact && requestName == ".") {
			// lone dot ona line has no effect
			lev |= SC_FOLDLEVELWHITEFLAG;
		}
		if (levelCurrent > levelPrev) {
			lev |= SC_FOLDLEVELHEADERFLAG;
		}
		if (lev != styler.LevelAt(lineCurrent)) {
			styler.SetLevel(lineCurrent, lev);
		}
		lineCurrent++;
		levelPrev = levelCurrent;
		requestName.clear();
	}

	// Fill in the real level of the next line, keeping the current flags as they will be filled in later
	int flagsNext = styler.LevelAt(lineCurrent) & ~SC_FOLDLEVELNUMBERMASK;
	styler.SetLevel(lineCurrent, levelPrev | flagsNext);
}

static const char * const troffWordLists[] = {
            "Predefined requests",
            "Flow control requests/commands with conditionals",
            "Flow control requests/commands without conditionals",
            "Requests and commands, initiating ignore blocks",
            "Requests and commands with end-macros",
            NULL,
        };

extern const LexerModule lmTroff(SCLEX_TROFF, ColouriseTroffDoc, "troff", FoldTroffDoc, troffWordLists);
