[CONUTILS:PAGER][MORE] Implement text line caching + fix some bugs.

- Implement caching of individual (newline-separated) text lines; this
  behaviour can be enabled with a flag (enabled by MORE):
  CON_PAGER_CACHE_INCOMPLETE_LINE.
  This feature is necessary when reading a text file, whose text lines
  may span across two or more successive temporary read buffers, and is
  required for correctly determining whether the lines being read are
  blank and may be squeezed.

- When squeezing blank lines, the blank-line check must be done for each
  line segment corresponding to the screen line (and following) that
  need to be displayed. This matches the behaviour of MS MORE.COM.

- Fix the IsBlankLine() check to not consider FORM-FEEDs as being blank
  characters: This is necessary for correctly handling FORM-FEED
  expansion. Also note that MS MORE.COM only checks for spaces and TABs,
  so we are slightly overdoing these checks (considering other types of
  whitespace).

- Get rid of ConCallPagerLine() and the intermediate CON_PAGER_DONT_OUTPUT
  state flag that were used repeatedly for each and every small line
  chunks. Instead, call directly the user-specified 'PagerLine' callback
  when we are about to start treating the next line segment to be
  displayed (see comment above).

- Fix the exit return condition of ConPagerWorker(): it should return
  TRUE whenever we displayed all the required lines, and FALSE otherwise.
  Otherwise, the previous (buggy) condition on the data being read from
  the text file, may lead to the prompt not showing when a screenful of
  text has been displayed, if it happened that the current text buffer
  becomes empty at the same time (even if, overall, the text file hasn't
  been fully displayed).

- In MorePagerLine(), when we encounter for the first time a blank line
  that will be squeezed with other successive ones, display a single
  blank line. But for that, just display one space and a newline: this
  single space is especially needed in order to force line wrapping when
  the ENABLE_VIRTUAL_TERMINAL_PROCESSING or DISABLE_NEWLINE_AUTO_RETURN
  console modes are enabled. Otherwise the cursor remains at the
  previous line (without wrapping), and just outputting one newline will
  not make it move past 2 lines as one would naively expect.
This commit is contained in:
Hermès Bélusca-Maïto 2021-06-28 20:08:38 +02:00
parent aff90f530c
commit 31322f5df9
No known key found for this signature in database
GPG key ID: 3B2539C65E7B93D0
3 changed files with 363 additions and 109 deletions

View file

@ -80,6 +80,19 @@ static BOOL IsBlankLine(IN PCWCH line, IN DWORD cch)
WORD wType;
for (ich = 0; ich < cch; ++ich)
{
/*
* Explicitly exclude FORM-FEED from the check,
* so that the pager can handle it.
*/
if (line[ich] == L'\f')
return FALSE;
/*
* Otherwise do the extended blanks check.
* Note that MS MORE.COM only checks for spaces (\x20) and TABs (\x09).
* See http://archives.miloush.net/michkap/archive/2007/06/11/3230072.html
* for more information.
*/
wType = 0;
GetStringTypeW(CT_CTYPE1, &line[ich], 1, &wType);
if (!(wType & (C1_BLANK | C1_SPACE)))
@ -95,15 +108,12 @@ MorePagerLine(
IN PCWCH line,
IN DWORD cch)
{
DWORD ich;
if (s_dwFlags & FLAG_PLUSn) /* Skip lines */
{
if (Pager->lineno < s_nNextLineNo)
{
Pager->dwFlags |= CON_PAGER_DONT_OUTPUT;
s_bPrevLineIsBlank = FALSE;
return TRUE; /* Don't output */
return TRUE; /* Handled */
}
s_dwFlags &= ~FLAG_PLUSn;
}
@ -113,19 +123,26 @@ MorePagerLine(
if (IsBlankLine(line, cch))
{
if (s_bPrevLineIsBlank)
{
Pager->dwFlags |= CON_PAGER_DONT_OUTPUT;
return TRUE; /* Don't output */
}
return TRUE; /* Handled */
for (ich = 0; ich < cch; ++ich)
{
if (line[ich] == L'\n')
{
s_bPrevLineIsBlank = TRUE;
break;
}
}
/*
* Display a single blank line, independently of the actual size
* of the current line, by displaying just one space: this is
* especially needed in order to force line wrapping when the
* ENABLE_VIRTUAL_TERMINAL_PROCESSING or DISABLE_NEWLINE_AUTO_RETURN
* console modes are enabled.
* Then, reposition the cursor to the next line, first column.
*/
if (Pager->PageColumns > 0)
ConStreamWrite(Pager->Screen->Stream, TEXT(" "), 1);
ConStreamWrite(Pager->Screen->Stream, TEXT("\n"), 1);
Pager->iLine++;
Pager->iColumn = 0;
s_bPrevLineIsBlank = TRUE;
s_nNextLineNo = 0;
return TRUE; /* Handled */
}
else
{
@ -134,7 +151,8 @@ MorePagerLine(
}
s_nNextLineNo = 0;
return FALSE; /* Do output */
/* Not handled, let the pager do the default action */
return FALSE;
}
static BOOL
@ -998,7 +1016,7 @@ int wmain(int argc, WCHAR* argv[])
}
Pager.PagerLine = MorePagerLine;
Pager.dwFlags |= CON_PAGER_EXPAND_TABS;
Pager.dwFlags |= CON_PAGER_EXPAND_TABS | CON_PAGER_CACHE_INCOMPLETE_LINE;
if (s_dwFlags & FLAG_P)
Pager.dwFlags |= CON_PAGER_EXPAND_FF;
Pager.nTabWidth = s_nTabWidth;
@ -1023,7 +1041,7 @@ int wmain(int argc, WCHAR* argv[])
Encoding = ENCODING_ANSI; // ENCODING_UTF8;
/* Start paging */
bContinue = ConPutsPaging(&Pager, PagePrompt, TRUE, L"");
bContinue = ConWritePaging(&Pager, PagePrompt, TRUE, NULL, 0);
if (!bContinue)
goto Quit;
@ -1050,6 +1068,11 @@ int wmain(int argc, WCHAR* argv[])
break;
}
while (bRet && dwReadBytes > 0);
/* Flush any cached pager buffers */
if (bContinue)
bContinue = ConWritePaging(&Pager, PagePrompt, FALSE, NULL, 0);
goto Quit;
}
@ -1100,7 +1123,7 @@ int wmain(int argc, WCHAR* argv[])
dwSumReadBytes = dwSumReadChars = 0;
/* Start paging */
bContinue = ConPutsPaging(&Pager, PagePrompt, TRUE, L"");
bContinue = ConWritePaging(&Pager, PagePrompt, TRUE, NULL, 0);
if (!bContinue)
{
/* We stop displaying this file */
@ -1152,6 +1175,10 @@ int wmain(int argc, WCHAR* argv[])
}
while (bRet && dwReadBytes > 0);
/* Flush any cached pager buffers */
if (bContinue)
bContinue = ConWritePaging(&Pager, PagePrompt, FALSE, NULL, 0);
CloseHandle(hFile);
/* Check whether we should stop displaying this file */

View file

@ -57,45 +57,187 @@ GetWidthOfCharCJK(
return ret;
}
static VOID
ConCallPagerLine(
/**
* @brief Retrieves a new text line, or continue fetching the current one.
*
* @remark Manages setting Pager's CurrentLine, ichCurr, iEndLine, and the
* line cache (CachedLine, cchCachedLine). Other functions must not
* modify these values.
**/
static BOOL
GetNextLine(
IN OUT PCON_PAGER Pager,
IN PCTCH line,
IN DWORD cch)
IN PCTCH TextBuff,
IN SIZE_T cch)
{
Pager->dwFlags &= ~CON_PAGER_DONT_OUTPUT; /* Clear the flag */
SIZE_T ich = Pager->ich;
SIZE_T ichStart;
SIZE_T cchLine;
BOOL bCacheLine;
if (!Pager->PagerLine || !Pager->PagerLine(Pager, line, cch))
CON_STREAM_WRITE(Pager->Screen->Stream, line, cch);
Pager->ichCurr = 0;
Pager->iEndLine = 0;
/*
* If we already had an existing line, then we can safely start a new one
* and getting rid of any current cached line. Otherwise, we don't have
* a current line and we may be caching a new one, in which case, continue
* caching it until it becomes complete.
*/
// INVESTIGATE: Do that only if (ichStart >= iEndLine) ??
if (Pager->CurrentLine)
{
// ASSERT(Pager->CurrentLine == Pager->CachedLine);
if (Pager->CachedLine)
{
HeapFree(GetProcessHeap(), 0, (PVOID)Pager->CachedLine);
Pager->CachedLine = NULL;
Pager->cchCachedLine = 0;
}
Pager->CurrentLine = NULL;
}
/* Nothing else to read if we are past the end of the buffer */
if (ich >= cch)
{
/* If we have a pending cached line, terminate it now */
if (Pager->CachedLine)
goto TerminateLine;
/* Otherwise, bail out */
return FALSE;
}
/* Start a new line, or continue an existing one */
ichStart = ich;
/* Find where this line ends, looking for a NEWLINE character.
* (NOTE: We cannot use strchr because the buffer is not NULL-terminated) */
for (; ich < cch; ++ich)
{
if (TextBuff[ich] == TEXT('\n'))
{
++ich;
break;
}
}
Pager->ich = ich;
cchLine = (ich - ichStart);
//
// FIXME: Impose a maximum string limit when the line is cached, in order
// not to potentially grow memory indefinitely. When the limit is reached,
// terminate the line.
//
/*
* If we have stopped because we have exhausted the text buffer
* and we have not found an end-of-line character, this may mean
* that the text line spans across different text buffers. If we
* have been told so, cache this line: we will complete it during
* the next call(s) and only then, display it.
* Otherwise, consider the line to be terminated now.
*/
bCacheLine = ((Pager->dwFlags & CON_PAGER_CACHE_INCOMPLETE_LINE) &&
(ich >= cch) && (TextBuff[ich - 1] != TEXT('\n')));
/* Allocate, or re-allocate, the cached line buffer */
if (bCacheLine && !Pager->CachedLine)
{
/* We start caching, allocate the cached line buffer */
Pager->CachedLine = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
cchLine * sizeof(TCHAR));
Pager->cchCachedLine = 0;
if (!Pager->CachedLine)
{
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
return FALSE;
}
}
else if (Pager->CachedLine)
{
/* We continue caching, re-allocate the cached line buffer */
PVOID ptr = HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
(PVOID)Pager->CachedLine,
(Pager->cchCachedLine + cchLine) * sizeof(TCHAR));
if (!ptr)
{
HeapFree(GetProcessHeap(), 0, (PVOID)Pager->CachedLine);
Pager->CachedLine = NULL;
Pager->cchCachedLine = 0;
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
return FALSE;
}
Pager->CachedLine = ptr;
}
if (Pager->CachedLine)
{
/* Copy/append the text to the cached line buffer */
RtlCopyMemory((PVOID)&Pager->CachedLine[Pager->cchCachedLine],
&TextBuff[ichStart],
cchLine * sizeof(TCHAR));
Pager->cchCachedLine += cchLine;
}
if (bCacheLine)
{
/* The line is currently incomplete, don't proceed further for now */
return FALSE;
}
TerminateLine:
/* The line should be complete now. If we have an existing cached line,
* it has been completed by appending the remaining text to it. */
/* We are starting a new line */
Pager->ichCurr = 0;
if (Pager->CachedLine)
{
Pager->iEndLine = Pager->cchCachedLine;
Pager->CurrentLine = Pager->CachedLine;
}
else
{
Pager->iEndLine = cchLine;
Pager->CurrentLine = &TextBuff[ichStart];
}
/* Increase only when we have got a NEWLINE */
if ((Pager->iEndLine > 0) && (Pager->CurrentLine[Pager->iEndLine - 1] == TEXT('\n')))
Pager->lineno++;
return TRUE;
}
/**
* @brief Does the main paging work: fetching text lines and displaying them.
**/
static BOOL
ConPagerWorker(IN PCON_PAGER Pager)
ConPagerWorker(
IN PCON_PAGER Pager,
IN PCTCH TextBuff,
IN DWORD cch)
{
const DWORD PageColumns = Pager->PageColumns;
const DWORD ScrollRows = Pager->ScrollRows;
const PCTCH Line = Pager->TextBuff;
const DWORD cch = Pager->cch;
BOOL bFinitePaging = ((PageColumns > 0) && (Pager->PageRows > 0));
LONG nTabWidth = Pager->nTabWidth;
DWORD ich = Pager->ich;
PCTCH Line;
SIZE_T ich;
SIZE_T ichStart;
SIZE_T iEndLine;
DWORD iColumn = Pager->iColumn;
DWORD iLine = Pager->iLine;
DWORD ichStart = ich;
UINT nCodePage;
BOOL IsCJK;
UINT nCodePage = GetConsoleOutputCP();
BOOL IsCJK = IsCJKCodePage(nCodePage);
UINT nWidthOfChar = 1;
BOOL IsDoubleWidthCharTrailing = FALSE;
if (ich >= cch)
return FALSE;
nCodePage = GetConsoleOutputCP();
IsCJK = IsCJKCodePage(nCodePage);
/* Normalize the tab width: if negative or too large,
* cap it to the number of columns. */
if (PageColumns > 0) // if (bFinitePaging)
@ -113,38 +255,88 @@ ConPagerWorker(IN PCON_PAGER Pager)
nTabWidth = 8;
}
if (Pager->dwFlags & CON_PAGER_EXPAND_TABS)
/* Continue displaying the previous line, if any, or start a new one */
Line = Pager->CurrentLine;
ichStart = Pager->ichCurr;
iEndLine = Pager->iEndLine;
ProcessLine:
/* Stop now if we have displayed more page lines than requested */
if (bFinitePaging && (Pager->iLine >= ScrollRows))
goto End;
if (!Line || (ichStart >= iEndLine))
{
/* Start a new line */
if (!GetNextLine(Pager, TextBuff, cch))
goto End;
Line = Pager->CurrentLine;
ichStart = Pager->ichCurr;
iEndLine = Pager->iEndLine;
}
else
{
/* Continue displaying the current line */
}
// ASSERT(Line && ((ichStart < iEndLine) || (ichStart == iEndLine && iEndLine == 0)));
/* Determine whether this line segment (from the current position till the end) should be displayed */
Pager->iColumn = iColumn;
if (Pager->PagerLine && Pager->PagerLine(Pager, &Line[ichStart], iEndLine - ichStart))
{
iColumn = Pager->iColumn;
/* Done with this line; start a new one */
Pager->nSpacePending = 0; // And reset any pending space.
ichStart = iEndLine;
goto ProcessLine;
}
// else: Continue displaying the line.
/* Print out any pending TAB expansion */
if (Pager->nSpacePending > 0)
{
ExpandTab:
while (Pager->nSpacePending > 0)
{
/* Stop now if we have displayed more screen lines than requested */
if (bFinitePaging && (iLine >= ScrollRows))
break;
ConCallPagerLine(Pager, L" ", 1);
/* Print filling spaces */
CON_STREAM_WRITE(Pager->Screen->Stream, TEXT(" "), 1);
--(Pager->nSpacePending);
++iColumn;
/* Check whether we are going across the column */
if ((PageColumns > 0) && (iColumn % PageColumns == 0))
{
if (!(Pager->dwFlags & CON_PAGER_DONT_OUTPUT))
++iLine;
// Pager->nSpacePending = 0; // <-- This is the mode of most text editors...
/* Reposition the cursor to the next line, first column */
if (!bFinitePaging || (PageColumns < Pager->Screen->csbi.dwSize.X))
CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1);
Pager->iLine++;
/* Restart at the character */
// ASSERT(ichStart == ich);
goto ProcessLine;
}
}
}
ProcessLine:
/* Stop now if we have displayed more screen lines than requested */
if (bFinitePaging && (iLine >= ScrollRows))
goto End;
/* Loop over each character in the buffer */
for (; ich < cch; ++ich)
/* Find, within this line segment (starting from its
* beginning), until where we can print to the page. */
for (ich = ichStart; ich < iEndLine; ++ich)
{
/* NEWLINE character */
if (Line[ich] == TEXT('\n'))
{
/* We should stop now */
// ASSERT(ich == iEndLine - 1);
break;
}
@ -194,33 +386,34 @@ ProcessLine:
}
}
/* Output the pending text */
Pager->dwFlags &= ~CON_PAGER_DONT_OUTPUT;
/* Output the pending line segment */
if (ich - ichStart > 0)
ConCallPagerLine(Pager, &Line[ichStart], ich - ichStart);
CON_STREAM_WRITE(Pager->Screen->Stream, &Line[ichStart], ich - ichStart);
/* Have we finished the buffer? */
if (ich >= cch)
goto End;
/* Have we finished the line segment? */
if (ich >= iEndLine)
{
/* Restart at the character */
ichStart = ich;
goto ProcessLine;
}
/* Handle special characters */
/* NEWLINE character */
if (Line[ich] == TEXT('\n'))
{
/* Output the newline */
if (!(Pager->dwFlags & CON_PAGER_DONT_OUTPUT))
{
// ConCallPagerLine(Pager, L"\n", 1);
CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1);
++iLine;
}
// ASSERT(ich == iEndLine - 1);
/* Reposition the cursor to the next line, first column */
CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1);
Pager->iLine++;
iColumn = 0;
/* Done with this line; start a new one */
Pager->nSpacePending = 0; // And reset any pending space.
Pager->lineno++;
ichStart = ++ich;
ichStart = iEndLine;
goto ProcessLine;
}
@ -245,28 +438,38 @@ ProcessLine:
if (Line[ich] == TEXT('\f') &&
(Pager->dwFlags & CON_PAGER_EXPAND_FF))
{
// FIXME: Should we handle CON_PAGER_DONT_OUTPUT ?
if (bFinitePaging)
{
/* Clear until the end of the screen */
while (iLine < ScrollRows)
/* Clear until the end of the page */
while (Pager->iLine < ScrollRows)
{
ConCallPagerLine(Pager, L"\n", 1);
// CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1);
// if (!(Pager->dwFlags & CON_PAGER_DONT_OUTPUT))
++iLine;
/* Call the user paging function in order to know
* whether we need to output the blank lines. */
Pager->iColumn = iColumn;
if (Pager->PagerLine && Pager->PagerLine(Pager, TEXT("\n"), 1))
{
/* Only one blank line displayed, that counts in the line count */
Pager->iLine++;
break;
}
else
{
CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1);
Pager->iLine++;
}
}
}
else
{
/* Just output a FORM-FEED character */
ConCallPagerLine(Pager, L"\f", 1);
// CON_STREAM_WRITE(Pager->Screen->Stream, L"\f", 1);
/* Just output a FORM-FEED and a NEWLINE */
CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\f\n"), 2);
Pager->iLine++;
}
iColumn = 0;
Pager->nSpacePending = 0; // And reset any pending space.
/* Skip and restart past the character */
ichStart = ++ich;
goto ProcessLine;
}
@ -276,19 +479,18 @@ ProcessLine:
if (IsDoubleWidthCharTrailing)
{
IsDoubleWidthCharTrailing = FALSE; // Reset the flag.
if (!(Pager->dwFlags & CON_PAGER_DONT_OUTPUT))
CON_STREAM_WRITE(Pager->Screen->Stream, TEXT(" "), 1);
// ++iLine;
CON_STREAM_WRITE(Pager->Screen->Stream, TEXT(" "), 1);
/* Fall back below */
}
/* Are we wrapping the line? */
if ((PageColumns > 0) && (iColumn % PageColumns == 0))
{
if (!(Pager->dwFlags & CON_PAGER_DONT_OUTPUT))
++iLine;
/* Reposition the cursor to the next line, first column */
if (!bFinitePaging || (PageColumns < Pager->Screen->csbi.dwSize.X))
CON_STREAM_WRITE(Pager->Screen->Stream, TEXT("\n"), 1);
Pager->iLine++;
}
/* Restart at the character */
@ -297,14 +499,25 @@ ProcessLine:
End:
if (iLine >= ScrollRows)
iLine = 0; /* Reset the count of lines being printed */
/*
* We are exiting, either because we displayed all the required lines
* (iLine >= ScrollRows), or, because we don't have more data to display.
*/
Pager->ich = ich;
Pager->ichCurr = ichStart;
Pager->iColumn = iColumn;
Pager->iLine = iLine;
// INVESTIGATE: Can we get rid of CurrentLine here? // if (ichStart >= iEndLine) ...
return (ich < cch);
/* Return TRUE if we displayed all the required lines; FALSE otherwise */
if (bFinitePaging && (Pager->iLine >= ScrollRows))
{
Pager->iLine = 0; /* Reset the count of lines being printed */
return TRUE;
}
else
{
return FALSE;
}
}
@ -374,25 +587,34 @@ ConWritePaging(
/* File output, or single line: all lines are displayed at once; reset to a default value */
Pager->ScrollRows = 0;
}
}
if (StartPaging)
{
/* Reset the internal data buffer */
Pager->CachedLine = NULL;
Pager->cchCachedLine = 0;
/* Reset the paging state */
Pager->CurrentLine = NULL;
Pager->ichCurr = 0;
Pager->iEndLine = 0;
Pager->nSpacePending = 0;
Pager->iColumn = 0;
Pager->iLine = 0;
Pager->lineno = 1;
Pager->lineno = 0;
}
Pager->TextBuff = szStr;
Pager->cch = len;
/* Reset the reading index in the user-provided source buffer */
Pager->ich = 0;
if (len == 0 || szStr == NULL)
return TRUE;
/* Run the pager even when the user-provided source buffer is
* empty, in case we need to flush any remaining cached line. */
if (!Pager->CachedLine)
{
/* No cached line, bail out now */
if (len == 0 || szStr == NULL)
return TRUE;
}
while (ConPagerWorker(Pager))
while (ConPagerWorker(Pager, szStr, len))
{
/* Prompt the user only when we display to a console and the screen
* is not too small: at least one line for the actual paged text and
@ -403,7 +625,8 @@ ConWritePaging(
Pager->ScrollRows = Pager->PageRows - 1;
/* Prompt the user; give him some values for statistics */
if (!PagePrompt(Pager, Pager->ich, Pager->cch))
// FIXME: Doesn't reflect what's currently being displayed.
if (!PagePrompt(Pager, Pager->ich, len))
return FALSE;
}

View file

@ -37,9 +37,10 @@ typedef BOOL
IN DWORD cch);
/* Flags for CON_PAGER */
#define CON_PAGER_DONT_OUTPUT (1 << 0)
#define CON_PAGER_EXPAND_TABS (1 << 1)
#define CON_PAGER_EXPAND_FF (1 << 2)
#define CON_PAGER_EXPAND_TABS (1 << 0)
#define CON_PAGER_EXPAND_FF (1 << 1)
// Whether or not the pager will cache the line if it's incomplete (not NEWLINE-terminated).
#define CON_PAGER_CACHE_INCOMPLETE_LINE (1 << 2)
typedef struct _CON_PAGER
{
@ -55,12 +56,15 @@ typedef struct _CON_PAGER
DWORD ScrollRows;
/* Data buffer */
PCTCH TextBuff; /* The text buffer */
DWORD cch; /* The total number of characters */
PCTCH CachedLine; /* Cached line, HeapAlloc'ated */
SIZE_T cchCachedLine; /* Its length (number of characters) */
SIZE_T ich; /* The current index of character in TextBuff (a user-provided source buffer) */
/* Paging state */
DWORD ich; /* The current index of character */
DWORD nSpacePending; /* Pending spaces for TAB expansion */
PCTCH CurrentLine; /* Pointer to the current line (either within a user-provided source buffer, or to CachedLine) */
SIZE_T ichCurr; /* The current index of character in CurrentLine */
SIZE_T iEndLine; /* End (length) of CurrentLine */
DWORD nSpacePending; /* Pending spaces for TAB expansion */
DWORD iColumn; /* The current index of column */
DWORD iLine; /* The physical output line count of screen */
DWORD lineno; /* The logical line number */