[PATCH 3/3] programs/cmd: implement filename completion

Eric Pouech eric.pouech at gmail.com
Thu Mar 24 08:46:52 CDT 2022


Signed-off-by: Eric Pouech <eric.pouech at gmail.com>

---
 programs/cmd/batch.c |  279 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 273 insertions(+), 6 deletions(-)

diff --git a/programs/cmd/batch.c b/programs/cmd/batch.c
index 9a262c5fec5..934cbcc7a39 100644
--- a/programs/cmd/batch.c
+++ b/programs/cmd/batch.c
@@ -227,6 +227,211 @@ WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **start, BOOL raw,
   return WCMD_parameter_with_delims (s, n, start, raw, wholecmdline, L" \t,=;");
 }
 
+struct completion_info
+{
+    WCHAR** subfiles;
+    DWORD index_search;
+    DWORD num_results;
+};
+
+/* gets directory name out of 'path', appends 'in' and surround with quotes if needed */
+static WCHAR* dup_quoted(const WCHAR* path, const WCHAR* in)
+{
+    const WCHAR* last = wcsrchr(path, L'/');
+    size_t dirlen;
+    size_t len;
+    WCHAR* ret;
+
+    if (last)
+    {
+        WCHAR* last1 = wcschr(last + 1, L'\\');
+        if (last1) last = last1;
+    }
+    else last = wcsrchr(path, L'\\');
+    dirlen = last ? (last - path) + 1 : 0;
+    len = wcslen(in);
+    ret = malloc((dirlen + len + 1) * sizeof(WCHAR));
+    if (ret)
+    {
+        memcpy(ret, path, dirlen * sizeof(WCHAR));
+        wcscpy(ret + dirlen, in);
+        if (wcspbrk(ret, L" \t")) /* do we need to surround with quotes? */
+        {
+            WCHAR* new = realloc(ret, (1 + dirlen + len + 1 + 1) * sizeof(WCHAR));
+            if (!new)
+            {
+                free(ret);
+                return NULL;
+            }
+            ret = new;
+            memmove(ret + 1, ret, (dirlen + len) * sizeof(WCHAR));
+            ret[0] = ret[1 + dirlen + len] = L'"';
+            ret[1 + dirlen + len + 1] = L'\0';
+        }
+    }
+    return ret;
+}
+
+static BOOL init_completion(struct completion_info* ci, const WCHAR* from, DWORD len, BOOL forward)
+{
+    WCHAR* ptr;
+    HANDLE hff;
+    WIN32_FIND_DATAW fd;
+    BOOL in_quotes = FALSE;
+    DWORD i, j = 0;
+
+    ptr = malloc((len + 1 + 1) * sizeof(WCHAR));
+    if (!ptr) return FALSE;
+    for (i = 0; i < len; i++)
+    {
+        switch (from[i])
+        {
+        case L'"':
+            in_quotes = !in_quotes;
+            continue;
+        case L'\\':
+            if (!in_quotes && i + 1 < len && wcschr(L" \t\"\\", from[i + 1]) != NULL)
+                ++i;
+            break;
+        case L' ':
+        case L'\t':
+            /* shouldn't happen, as 'from' should contain a single argument */
+            if (!in_quotes)
+            {
+                free(ptr);
+                return FALSE;
+            }
+            break;
+        }
+        ptr[j++] = from[i];
+    }
+    ptr[j] = L'*';
+    ptr[j + 1] = L'\0';
+    hff = FindFirstFileW(ptr, &fd);
+    ci->num_results = 0;
+    ci->subfiles = NULL;
+    if (hff != INVALID_HANDLE_VALUE)
+    {
+        do
+        {
+            WCHAR** new;
+            if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L".."))
+                continue;
+            new = realloc(ci->subfiles, (ci->num_results + 1) * sizeof(*ci->subfiles));
+            if (!new)
+            {
+                FindClose(hff);
+                free(ptr);
+                return FALSE;
+            }
+            ci->subfiles = new;
+            if (!(ci->subfiles[ci->num_results++] = dup_quoted(ptr, fd.cFileName)))
+            {
+                FindClose(hff);
+                free(ptr);
+                return FALSE;
+            }
+        } while (FindNextFileW(hff, &fd));
+        FindClose(hff);
+    }
+    free(ptr);
+    if (!ci->num_results) return FALSE;
+    ci->index_search = forward ? 0 : ci->num_results - 1;
+    return TRUE;
+}
+
+static void dispose_completion(struct completion_info* ci)
+{
+    if (ci->subfiles)
+    {
+        unsigned i;
+        for (i = 0; i < ci->num_results; i++)
+            free(ci->subfiles[i]);
+        free(ci->subfiles);
+        ci->subfiles = NULL;
+    }
+}
+
+static WCHAR* next_completion(struct completion_info* ci, const WCHAR* from, DWORD len, BOOL forward)
+{
+    if (!ci->subfiles ||
+        memcmp(ci->subfiles[ci->index_search], from, len * sizeof(WCHAR)) ||
+        ci->subfiles[ci->index_search][len])
+    {
+        dispose_completion(ci);
+        if (!init_completion(ci, from, len, forward)) return NULL;
+    }
+    else
+    {
+        ci->index_search += forward ? 1 : (ci->num_results - 1);
+        ci->index_search %= ci->num_results;
+    }
+    return ci->subfiles[ci->index_search];
+}
+
+/* backup cursor position by 'len' (display) characters */
+static BOOL backup_cursor_position(HANDLE hout, int len)
+{
+    CONSOLE_SCREEN_BUFFER_INFO sbi;
+    if (GetConsoleScreenBufferInfo(hout, &sbi))
+    {
+        if (len > sbi.dwCursorPosition.X)
+        {
+            len -= sbi.dwCursorPosition.X + 1;
+            sbi.dwCursorPosition.X = sbi.dwSize.X - 1 - (len % sbi.dwSize.X);
+            sbi.dwCursorPosition.Y -= len / sbi.dwSize.X + 1;
+            if (sbi.dwCursorPosition.Y < 0)
+                sbi.dwCursorPosition.Y = 0;
+        }
+        else
+            sbi.dwCursorPosition.X -= len;
+        return SetConsoleCursorPosition(hout, sbi.dwCursorPosition);
+    }
+    return FALSE;
+}
+
+/* write numspaces the character ' ' from cursor position, without altering cursor position */
+static void clear_spaces(HANDLE hout, int numspaces)
+{
+    static const WCHAR spaces[] = {L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' ',L' '};
+    int ns = numspaces;
+
+    while (ns > 0)
+    {
+        DWORD nwritten;
+
+        if (!WriteConsoleW(hout, spaces, min(ns, ARRAY_SIZE(spaces)), &nwritten, NULL) || !nwritten)
+            break;
+        ns -= nwritten;
+    }
+    backup_cursor_position(hout, numspaces - ns);
+}
+
+/* number of characters to display a string (including escape for control characters */
+static DWORD get_display_length(const WCHAR* str, unsigned len)
+{
+    DWORD ret = len;
+    unsigned i;
+
+    for (i = 0; i < len; i++) if (str[i] < ' ') ++ret;
+    return ret;
+}
+
+static WCHAR *find_start_of_parameter(WCHAR *buf, WCHAR *ptr)
+{
+    int n = 0;
+    for (;; n++)
+    {
+        WCHAR* next;
+        WCHAR* pmt = WCMD_parameter(buf, n, &next, TRUE, FALSE);
+        if (!pmt[0] || next > ptr)
+            return ptr;
+        if (ptr <= next + wcslen(pmt))
+            return next;
+    }
+    return NULL; /* shouldn't happen */
+}
+
 /****************************************************************************
  * WCMD_fgets
  *
@@ -242,14 +447,19 @@ WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **start, BOOL raw,
 
 WCHAR *WCMD_fgets(WCHAR *buf, DWORD noChars, HANDLE h)
 {
+  CONSOLE_READCONSOLE_CONTROL crc;
   DWORD charsRead;
   BOOL status;
   DWORD i;
 
+  crc.nLength = sizeof(crc);
+  crc.nInitialChars = 0;
+  crc.dwCtrlWakeupMask = 1 << '\t';
+  crc.dwConsoleKeyState = 0;
   /* We can't use the native f* functions because of the filename syntax differences
      between DOS and Unix. Also need to lose the LF (or CRLF) from the line. */
 
-  if (!ReadConsoleW(h, buf, noChars, &charsRead, NULL)) {
+  if (!ReadConsoleW(h, buf, noChars - 1, &charsRead, &crc)) {
       LARGE_INTEGER filepos;
       char *bufA;
       UINT cp;
@@ -282,13 +492,70 @@ WCHAR *WCMD_fgets(WCHAR *buf, DWORD noChars, HANDLE h)
       heap_free(bufA);
   }
   else {
+      struct completion_info ci = {NULL, 0, 0};
+      HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);
+      DWORD last_display_length = 0;
+
       if (!charsRead) return NULL;
+      do
+      {
+          WCHAR* tabptr = wmemchr(buf, L'\t', charsRead);
+          if (tabptr)
+          {
+              WCHAR* start;
+              WCHAR* completion;
+              DWORD nwritten;
+              unsigned display_input_length;
+
+              /* we still have <tab> in buf, so don't include it in count */
+              display_input_length = get_display_length(buf, charsRead) - 2;
+              /* find start of word containing tabptr (if any) */
+              buf[charsRead] = '\0';
+              start = find_start_of_parameter(buf, tabptr);
+              completion = next_completion(&ci, start, tabptr - start, !(crc.dwConsoleKeyState & SHIFT_PRESSED));
+              if (completion)
+              {
+                  if (start - buf + wcslen(completion) + 1 > noChars)
+                  {
+                      memcpy(start, completion, (start - buf + noChars - 1) * sizeof(WCHAR));
+                      charsRead = noChars - 1;
+                  }
+                  else
+                  {
+                      wcscpy(start, completion);
+                      charsRead = wcslen(buf);
+                  }
+              }
+              else
+              {
+                  /* Beep() -- not loud enough, maybe user won't hear it */
+                  charsRead = tabptr - buf;
+              }
+              /* scrolling or spurious write could have happened in ReadConsole
+               * so re-compute coordinates of start of input relative to current cursor position
+               */
+              if (backup_cursor_position(hout, display_input_length))
+              {
+                  DWORD display_completion_length;
+
+                  WriteConsoleW(hout, buf, charsRead, &nwritten, NULL);
+                  display_completion_length = get_display_length(buf, charsRead);
+                  if (last_display_length > display_completion_length)
+                      clear_spaces(hout, last_display_length - display_completion_length);
+                  last_display_length = display_completion_length;
+              }
+              crc.nInitialChars = charsRead;
+          }
+          /* Find first EOL */
+          for (i = 0; i < charsRead; i++) {
+              if (buf[i] == '\n' || buf[i] == '\r')
+                  break;
+          }
+          if (i < charsRead) break;
 
-      /* Find first EOL */
-      for (i = 0; i < charsRead; i++) {
-          if (buf[i] == '\n' || buf[i] == '\r')
-              break;
-      }
+      } while (ReadConsoleW(h, buf, noChars - 1, &charsRead, &crc) && charsRead);
+
+      dispose_completion(&ci);
   }
 
   /* Truncate at EOL (or end of buffer) */




More information about the wine-devel mailing list