Skip to content

Tab-completion in custom provider adds spurious repeated path on non-container items #24744

Open
@fsackur

Description

Prerequisites

Steps to reproduce

Here's some repro code to create a provider where:

  • the items are all integers from 0 to 9
  • even numbers are containers
  • odd numbers are non-container items
multi-file code content that defines IntProvider
  • paket.references:

    System.Management.Automation
    
  • IntProvider.csproj:

    <?xml version="1.0" encoding="utf-8"?>
    <Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>
    <Import Project=".paket\Paket.Restore.targets" />
    </Project>
    
  • IntProvider.cs:

    using System;
    using System.Linq;
    using System.Management.Automation;
    using System.Management.Automation.Provider;
    
    namespace BugRepro
    {
        public class IntItemInfo
        {
            public string Name;
            public IntItemInfo(string name) => Name = name;
        }
    
        [CmdletProvider("Int", ProviderCapabilities.None)]
        public class IntProvider : NavigationCmdletProvider
        {
            public static string[] ToChunks(string path) => path.Split("/", StringSplitOptions.RemoveEmptyEntries);
    
            protected string _ChildName(string path)
            {
                var name = ToChunks(path).LastOrDefault();
                return name ?? string.Empty;
            }
    
            protected string Normalize(string path) => string.Join("/", ToChunks(path));
    
            protected override string GetChildName(string path)
            {
                var name = _ChildName(path);
                // if (!IsItemContainer(path)) { return string.Empty; }
                return name;
            }
    
            protected override bool IsValidPath(string path) => int.TryParse(GetChildName(path), out int _);
    
            protected override bool IsItemContainer(string path)
            {
                var name = _ChildName(path);
                if (!int.TryParse(name, out int value))
                {
                    return false;
                }
                if (ToChunks(path).Count() > 3)
                {
                    return false;
                }
                return value % 2 == 0;
            }
    
            protected override bool ItemExists(string path)
            {
                foreach (var chunk in ToChunks(path))
                {
                    if (!int.TryParse(chunk, out int value))
                    {
                        return false;
                    }
                    if (value < 0 || value > 9)
                    {
                        return false;
                    }
                }
                return true;
            }
    
            protected override void GetItem(string path)
            {
                var name = GetChildName(path);
                if (!int.TryParse(name, out int _))
                {
                    return;
                }
                WriteItemObject(new IntItemInfo(name), path, IsItemContainer(path));
            }
            protected override bool HasChildItems(string path) => IsItemContainer(path);
    
            protected override void GetChildItems(string path, bool recurse)
            {
                if (!IsItemContainer(path)) { GetItem(path); return; }
    
                for (var i = 0; i <= 9; i++)
                {
                    var _path = $"{Normalize(path)}/{i}";
                    if (recurse)
                    {
                        GetChildItems(_path, recurse);
                    }
                    else
                    {
                        GetItem(_path);
                    }
                }
            }
        }
    }
    
  • IntProvider.psd1:

    @{
        RootModule = 'IntProvider.psm1'
        NestedModules = @('./IntProvider.dll')
        ModuleVersion = '0.0.1'
        FunctionsToExport = @()
        CmdletsToExport = @()
        VariablesToExport = '*'
        AliasesToExport = @()
    }
    
  • IntProvider.psm1:

    if (-not (Test-Path $PSScriptRoot/IntProvider.dll))
    {
        $CS = Get-Content -Raw $PSScriptRoot/IntProvider.cs
        Add-Type -TypeDefinition $CS -OutputAssembly $PSScriptRoot/IntProvider.dll
    }
    
  • To build:

    dotnet tool install paket --create-manifest-if-needed
    dotnet tool restore
    dotnet build
    
  • Set up PSDrive:

    ipmo IntProvider.psm1  # compile the C#
    ipmo IntProvider.psd1  # import the assembly - apparently it needs to be a nested module...?
    New-PSDrive -Name Int -PSProvider Int -Root "/"
    

Expected behavior

When tab-completing in FileSystemProvider, no completions are offered as children of non-container items. E.g.:

❯ gi ./IntProvider.csproj/<tab>

Tab-completion strips the trailing slash.

# either the child name, or an exception if the child name is suffixed with dir sep$inputScript = "gi ./IntProvider.csproj"[System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null).CompletionMatches

CompletionText       ListItemText         ResultType ToolTip
--------------       ------------         ---------- -------
./IntProvider.csproj IntProvider.csproj ProviderItem /home/freddie/gitroot/BugRepro/IntProvider.csproj$inputScript = "gi ./IntProvider.csproj/"[System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null).CompletionMatches
MethodInvocationException: Exception calling "CompleteInput" with "3" argument(s): "Could not find a part of the path '/home/freddie/gitroot/BugRepro/IntProvider.csproj'."

Actual behavior

When tab-completing in a custom provider, completions are offered as children of non-container items. E.g.:

❯ gi Int:/2/3/<tab>

completes, incorrectly, to Int:/2/3/2/3 (or to Int:/2/3/3 as pointed out by MartinGC94). Both of these are incorrect.

Tab-completion strips the trailing slash.

# this is correct:$inputScript = "gi Int:/2/3"[System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null).CompletionMatches

CompletionText ListItemText   ResultType ToolTip
-------------- ------------   ---------- -------
Int:/2/3       3            ProviderItem /2/3


# this is the wrong behaviour.
# "3" is not a container, so nothing should be appended$inputScript = "gi Int:/2/3/"                                                                                              [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null).CompletionMatches

CompletionText ListItemText   ResultType ToolTip
-------------- ------------   ---------- -------
Int:/2/3//2/3/ /2/3/        ProviderItem /2/3//2/3/

Error details

No response

Environment data

Name                           Value
----                           -----
PSVersion                      7.4.6
PSEdition                      Core
GitCommitId                    7.4.6
OS                             Fedora Linux 40 (Workstation Edition)
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

# problem also reproduces on master branch, as of c29e9140bf6d47494a3c85b9031db81583b78b20 (2025-01-06)

Visuals

No response

Metadata

Labels

In-PRIndicates that a PR is out for the issueNeeds-InvestigationThe behavior reported in the issue is unexpected and needs further investigation.Needs-TriageThe issue is new and needs to be triaged by a work group.WG-Interactive-Consolethe console experienceWG-NeedsReviewNeeds a review by the labeled Working Group

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions