Skip to content

File wsh_shell_autocomplete.c

File List > src > wsh_shell_autocomplete.c

Go to the documentation of this file

#include "wsh_shell_autocomplete.h"

#if WSH_SHELL_AUTOCOMPLETE

static WshShell_Size_t WshShellAutocomplete_CommonPrefixLen(const WshShell_Char_t* pFirst,
                                                            WshShell_Size_t stride,
                                                            WshShell_Size_t count,
                                                            WshShell_Size_t startLen,
                                                            WshShell_Size_t maxLen) {
    WshShell_Size_t prefixLen = startLen;
    while (prefixLen < maxLen) {
        WshShell_Char_t nextChar = pFirst[prefixLen];
        WshShell_Bool_t match    = true;
        for (WshShell_Size_t i = 1; i < count; i++) {
            if ((pFirst + i * stride)[prefixLen] != nextChar) {
                match = false;
                break;
            }
        }
        if (!match)
            break;
        prefixLen++;
    }
    return prefixLen;
}

WshShell_Bool_t WshShellAutocomplete_Try(WshShell_Char_t* pInBuff, WshShell_Size_t inBuffLen,
                                         WshShellCmd_Table_t* pShellCommands) {
    WSH_SHELL_ASSERT(pInBuff && pShellCommands);
    if (!pInBuff || !pShellCommands)
        return false;

    WshShell_Char_t inputCopy[WSH_SHELL_INTR_BUFF_LEN] = {0};
    WSH_SHELL_STRNCPY(inputCopy, pInBuff, inBuffLen);

    const WshShell_Char_t* pcInputCopyTrimmed =
        WshShellStr_TrimString(inputCopy, WSH_SHELL_STRLEN(inputCopy));
    WshShell_Size_t inputCopyTrimmedLen = WSH_SHELL_STRLEN(pcInputCopyTrimmed);

    const WshShellCmd_t* pcDefCmd = WshShellDefCmd_GetPtr();

    WshShell_Size_t cmdNum = WshShellCmd_GetCmdNum(pShellCommands);

    // Check if number of commands exceeds safe stack allocation limit
    if (cmdNum + 1 > WSH_SHELL_AUTOCOMPLETE_MAX_CANDIDATES) {
        WSH_SHELL_PRINT_WARN("Too many commands for autocomplete (%d > %d)\r\n", cmdNum + 1,
                             WSH_SHELL_AUTOCOMPLETE_MAX_CANDIDATES);
        return false;
    }

    WshShell_Char_t candidates[cmdNum + 1][WSH_SHELL_CMD_NAME_LEN];  // + one more for default cmd
    WshShell_Size_t matchCount      = 0;
    const WshShellCmd_t* pcCmdMatch = NULL;

    // Check default command first
    if (WSH_SHELL_STRNCMP(pcInputCopyTrimmed, pcDefCmd->Name, inputCopyTrimmedLen) == 0) {
        WSH_SHELL_STRNCPY(candidates[matchCount], pcDefCmd->Name, WSH_SHELL_CMD_NAME_LEN - 1);
        candidates[matchCount][WSH_SHELL_CMD_NAME_LEN - 1] = '\0';  // safety null-term
        pcCmdMatch                                         = pcDefCmd;
        matchCount++;
    }

    // Collect other matching commands
    for (WshShell_Size_t cmdIdx = 0; cmdIdx < cmdNum; cmdIdx++) {
        const WshShellCmd_t* pcCmd = WshShellCmd_GetCmdByIndex(pShellCommands, cmdIdx);
        if (WSH_SHELL_STRNCMP(pcInputCopyTrimmed, pcCmd->Name, inputCopyTrimmedLen) == 0) {
            WSH_SHELL_STRNCPY(candidates[matchCount], pcCmd->Name, WSH_SHELL_CMD_NAME_LEN);
            pcCmdMatch = pcCmd;
            matchCount++;
        }
    }

    // Create padding buff for pretty message print
    WshShell_Char_t sysMsgShift[WSH_SHELL_AUTOCOMPLETE_PAD_LEN + 1];
    WshShell_Size_t padLen = (inBuffLen < WSH_SHELL_AUTOCOMPLETE_PAD_LEN)
                                 ? (WSH_SHELL_AUTOCOMPLETE_PAD_LEN - inBuffLen)
                                 : 0;
    WSH_SHELL_MEMSET((void*)sysMsgShift, WSH_SHELL_AUTOCOMPLETE_PAD_SYM, padLen);
    sysMsgShift[0]      = ' ';
    sysMsgShift[padLen] = '\0';

    // If input contains a space — try flag autocomplete for the last typed token
    WshShell_Size_t firstSpaceIdx = inputCopyTrimmedLen;
    WshShell_Size_t lastSpaceIdx  = inputCopyTrimmedLen;
    for (WshShell_Size_t i = 0; i < inputCopyTrimmedLen; i++) {
        if (pcInputCopyTrimmed[i] == ' ') {
            if (firstSpaceIdx == inputCopyTrimmedLen)
                firstSpaceIdx = i;
            lastSpaceIdx = i;
        }
    }

    if (firstSpaceIdx < inputCopyTrimmedLen) {
        WshShell_Size_t cmdPartLen        = firstSpaceIdx;
        const WshShell_Char_t* pcFlagPart = pcInputCopyTrimmed + lastSpaceIdx + 1;
        WshShell_Size_t flagPartLen       = WSH_SHELL_STRLEN(pcFlagPart);

        // Find command matching first token (exact)
        const WshShellCmd_t* pcTargetCmd = NULL;
        if (WSH_SHELL_STRLEN(pcDefCmd->Name) == cmdPartLen &&
            WSH_SHELL_STRNCMP(pcInputCopyTrimmed, pcDefCmd->Name, cmdPartLen) == 0) {
            pcTargetCmd = pcDefCmd;
        }
        for (WshShell_Size_t i = 0; !pcTargetCmd && i < cmdNum; i++) {
            const WshShellCmd_t* pcCmd = WshShellCmd_GetCmdByIndex(pShellCommands, i);
            if (WSH_SHELL_STRLEN(pcCmd->Name) == cmdPartLen &&
                WSH_SHELL_STRNCMP(pcInputCopyTrimmed, pcCmd->Name, cmdPartLen) == 0) {
                pcTargetCmd = pcCmd;
            }
        }

        if (!pcTargetCmd) {
            WSH_SHELL_PRINT_SYS("%s /autocomplete: unknown command\r\n", sysMsgShift);
            return false;
        }

        // Collect flags matching partial flag token
        WshShell_Char_t flagCandidates[WSH_SHELL_CMD_OPTIONS_MAX_NUM][WSH_SHELL_OPTION_LONG_NAME_LEN];
        WshShell_Size_t flagMatchCount = 0;

        const WshShellOption_t* pOpt = pcTargetCmd->Options;
        for (; pOpt->Type != WSH_SHELL_OPTION_END && flagMatchCount < WSH_SHELL_CMD_OPTIONS_MAX_NUM;
             pOpt++) {
            if (pOpt->Type == WSH_SHELL_OPTION_NO || pOpt->Type == WSH_SHELL_OPTION_WAITS_INPUT)
                continue;

            const WshShell_Char_t* pcName = NULL;
            if (pOpt->LongName[0] &&
                WSH_SHELL_STRNCMP(pcFlagPart, pOpt->LongName, flagPartLen) == 0) {
                pcName = pOpt->LongName;
            } else if (flagPartLen <= WSH_SHELL_OPTION_SHORT_NAME_LEN && pOpt->ShortName[0] &&
                       WSH_SHELL_STRNCMP(pcFlagPart, pOpt->ShortName, flagPartLen) == 0) {
                pcName = pOpt->ShortName;
            }

            if (pcName) {
                WSH_SHELL_STRNCPY(flagCandidates[flagMatchCount], pcName,
                                  WSH_SHELL_OPTION_LONG_NAME_LEN - 1);
                flagCandidates[flagMatchCount][WSH_SHELL_OPTION_LONG_NAME_LEN - 1] = '\0';
                flagMatchCount++;
            }
        }

        if (flagMatchCount == 0) {
            WSH_SHELL_PRINT_SYS("%s /autocomplete: no matches\r\n", sysMsgShift);
            return false;
        }

        if (flagMatchCount == 1) {
            WshShell_Size_t flagLen = WSH_SHELL_STRLEN(flagCandidates[0]);
            WshShell_Size_t newLen  = cmdPartLen + 1 + flagLen;
            if (newLen + 2 > WSH_SHELL_INTR_BUFF_LEN) {
                WSH_SHELL_PRINT_WARN("Flag name too long for buffer\r\n");
                return false;
            }
            WSH_SHELL_STRNCPY(pInBuff, pcInputCopyTrimmed, cmdPartLen);
            pInBuff[cmdPartLen] = ' ';
            WSH_SHELL_MEMCPY(pInBuff + cmdPartLen + 1, flagCandidates[0], flagLen);
            pInBuff[cmdPartLen + 1 + flagLen]     = ' ';
            pInBuff[cmdPartLen + 1 + flagLen + 1] = '\0';
            return true;
        }

        // Find longest common prefix among flag candidates
        WshShell_Size_t flagPrefixLen = WshShellAutocomplete_CommonPrefixLen(
            flagCandidates[0], WSH_SHELL_OPTION_LONG_NAME_LEN, flagMatchCount, flagPartLen,
            WSH_SHELL_OPTION_LONG_NAME_LEN);

        if (flagPrefixLen > flagPartLen) {
            WshShell_Size_t newLen = cmdPartLen + 1 + flagPrefixLen;
            if (newLen + 1 > WSH_SHELL_INTR_BUFF_LEN) {
                WSH_SHELL_PRINT_WARN("Flag prefix too long for buffer\r\n");
                return false;
            }
            WSH_SHELL_STRNCPY(pInBuff, pcInputCopyTrimmed, cmdPartLen);
            pInBuff[cmdPartLen] = ' ';
            WSH_SHELL_MEMCPY(pInBuff + cmdPartLen + 1, flagCandidates[0], flagPrefixLen);
            pInBuff[newLen] = '\0';
            return true;
        }

        // Print all matching flag candidates
        WSH_SHELL_PRINT_SYS("%s /autocomplete flags found: ", sysMsgShift);
        for (WshShell_Size_t cnt = 0; cnt < flagMatchCount; cnt++)
            WSH_SHELL_PRINT("[%s] ", flagCandidates[cnt]);
        WSH_SHELL_PRINT("\r\n");
        return false;
    }

    // No matches found
    if (matchCount == 0) {
        WSH_SHELL_PRINT_SYS("%s /autocomplete: no matches\r\n", sysMsgShift);
        return false;
    }

    // Exactly one match found — autocomplete to full command
    if (matchCount == 1) {
        //Fetch options if command already autocomplited
        if (inputCopyTrimmedLen == WSH_SHELL_STRLEN(candidates[0]) &&
            WSH_SHELL_MEMCMP(pcInputCopyTrimmed, candidates[0], inputCopyTrimmedLen) == 0) {
            WSH_SHELL_PRINT_SYS("%s /autocomplete flags found: ", sysMsgShift);

            const WshShellOption_t* pOpt = pcCmdMatch->Options;
            WshShell_Bool_t optFound     = false;
            if (pOpt != NULL) {
                for (; pOpt->Type != WSH_SHELL_OPTION_END; pOpt++) {
                    if (pOpt->Type == WSH_SHELL_OPTION_NO ||
                        pOpt->Type == WSH_SHELL_OPTION_WAITS_INPUT)
                        continue;

                    optFound = true;
                    WSH_SHELL_PRINT("[%s/%s] ", pOpt->ShortName, pOpt->LongName);
                }
            }

            WSH_SHELL_PRINT("%s\r\n", optFound ? "" : "none");
            return false;
        }

        WshShell_Size_t candLen = WSH_SHELL_STRLEN(candidates[0]);

        // Check if we have space for command + space + null terminator
        if (candLen + 2 > WSH_SHELL_INTR_BUFF_LEN) {
            WSH_SHELL_PRINT_WARN("Command name too long for buffer\r\n");
            return false;
        }

        candidates[0][candLen++] = ' ';  //add extra space if command found
        WSH_SHELL_STRNCPY(pInBuff, candidates[0], candLen);
        pInBuff[candLen] = '\0';
        return true;
    }

    // Determine longest common prefix among all matches
    WshShell_Size_t prefixLen = WshShellAutocomplete_CommonPrefixLen(
        candidates[0], WSH_SHELL_CMD_NAME_LEN, matchCount, inBuffLen, WSH_SHELL_CMD_NAME_LEN);

    // If common prefix is longer than input, extend input
    if (prefixLen > inBuffLen) {
        // Check if prefix fits in buffer
        if (prefixLen + 1 > WSH_SHELL_INTR_BUFF_LEN) {
            WSH_SHELL_PRINT_WARN("Common prefix too long for buffer\r\n");
            return false;
        }
        WSH_SHELL_MEMCPY((void*)&pInBuff[inBuffLen], (void*)&candidates[0][inBuffLen],
                         prefixLen - inBuffLen);
        pInBuff[prefixLen] = '\0';
        return true;
    }

    // Print all candidates as options
    WSH_SHELL_PRINT_SYS("%s /autocomplete cmd found: ", sysMsgShift);
    for (WshShell_Size_t cnt = 0; cnt < matchCount; cnt++)
        WSH_SHELL_PRINT("%s ", candidates[cnt]);

    WSH_SHELL_PRINT("\r\n");

    return false;
}

#else /* WSH_SHELL_AUTOCOMPLETE */

WshShell_Bool_t WshShellAutocomplete_Try(WshShell_Char_t* pInBuff, WshShell_Size_t inBuffLen,
                                         WshShellCmd_Table_t* pShellCommands) {
    WSH_SHELL_ASSERT(pInBuff && pShellCommands);
    if (!pInBuff || !pShellCommands)
        return false;

    WshShell_Char_t sysMsgShift[WSH_SHELL_AUTOCOMPLETE_PAD_LEN + 1];
    WshShell_Size_t padLen = (inBuffLen < WSH_SHELL_AUTOCOMPLETE_PAD_LEN)
                                 ? (WSH_SHELL_AUTOCOMPLETE_PAD_LEN - inBuffLen)
                                 : 0;
    WSH_SHELL_MEMSET((void*)sysMsgShift, WSH_SHELL_AUTOCOMPLETE_PAD_SYM, padLen);
    sysMsgShift[0]      = ' ';
    sysMsgShift[padLen] = '\0';

    WSH_SHELL_PRINT_WARN("%s /autocomplete disabled", sysMsgShift);
    WSH_SHELL_PRINT("\r\n");

    return false;
}

#endif /* WSH_SHELL_AUTOCOMPLETE */