src/modules/cmdpal/doc/initial-sdk-spec/drafts/PrefixSearch-draft-B.md
[!INFO]
This is a draft
.idlspec. Details are still subject to change. Overall concepts however will likely remain similar
interface ISearchUpdateArgs requires IExtendedAttributesProvider
{
String NewSearchText { get; } // The text that the user has typed into the search box.
}
interface IToken
{
Object Value { get; };
String DisplayText { get; };
String Id { get; }; // GUID?
}
interface IAddTokenArgs
{
IToken Value { get; };
Int64 Position { get; }; // default -1 for "end"
}
interface IPrefixProvider requires IListPage // this definitely needs at least IPage
{
IDictionary<String, String> PrefixCommands { get; }
// event Windows.Foundation.TypedEventHandler<Object, IAddTokenArgs> RequestAddToken;
void UpdateSearch(ISearchUpdateArgs args);
}
Instead, here's the IPrefixProvider implemented on one page, separate from the command provider.
class MyPrefixCommandProvider : ICommandProvider
{
private PeopleDynamicListPage _peopleCommand; // com.contoso.people
private CommandsListPage _commandsCommand; // com.contoso.commands
private AddFileCommand _addFileCommand; // com.contoso.addFile
private PrefixSearchPage _prefixSearchPage;
public MyPrefixCommandProvider()
{
_prefixSearchPage = new PrefixSearchPage();
}
public ICommandItem[] GetTopLevelCommands()
{
return new ICommandItem[] { _prefixSearchPage };
}
public ICommand GetCommand(String id)
{
if (id == "com.contoso.people")
{
return _peopleCommand;
}
else if (id == "com.contoso.commands")
{
return _commandsCommand;
}
else if (id == "com.contoso.addFile")
{
return _addFileCommand;
}
return null;
}
}
class PrefixSearchPage : ListPage, IPrefixProvider
{
public IDictionary<String, String> PrefixCommands => new Dictionary<String, String>
{
{ "@", "com.contoso.people" },
{ "/", "com.contoso.commands" },
{ "+", "com.contoso.addFile" },
};
public event Windows.Foundation.TypedEventHandler<Object, ITokenPickedEventArgs> TokenAdded;
public PrefixSearchPage()
{
// Initialize the page...
}
public void OnTokenPicked(ITokenPickedEventArgs args)
{
// Raise the event to notify CmdPal of the picked token
TokenAdded?.Invoke(this, args);
}
public void SendQuery(ISearchUpdateArgs args)
{
// Handle the search update, possibly updating the list of items based on the new search text
var searchText = args.NewSearchText;
var properties = args.GetProperties();
var tokens = properties.TryLookup<object>("tokens") as ITokenPositions[];
// Here you could use these tokens to update the commands in our own search results
// Or just save them, and plumb them into the InvokableCommand the user eventually picks
}
// Other ListPage members...
}
sequenceDiagram
autonumber
box host
participant U as User
participant H as Host (DevPal)
end
box extension
participant PP as PrefixSearchPage
(IPrefixProvider)
participant PCP as MyPrefixCommandProvider
(ICommandProvider)
participant EC as MyDynamicCommand
(IListItem)
end
loop setup
H-->>PCP: GetTopLevelCommands()
note left of H: "navigate" to
PrefixSearchPage
activate PP
note over H, PP: InitializeProperties from PrefixSearchPage
H->>PP: GetProperties()
note left of H: cache prefix map
H->>PP: GetItems()
end
loop Typing normal characters
U->>H: Key press (char)
H-->>H: UI updates basic text
H->>PP: SendQuery(ISearchUpdateArgs)
PP->>EC: Update text or
otherwise handle query
end
loop Handling prefix char
U->>H: Key press '@'
H-->>H: UI updates basic text
H-->>H: See that '@' is a prefix matching
'com.contoso.people'
H->>PCP: GetCommand(com.contoso.people)
create participant PDP as PeopleDynamicListPage
(IDynamicListPage)
PCP-->PDP: new PeopleDynamicListPage()
deactivate PP
activate PDP
note left of H: "navigate" to
PeopleDynamicListPage
note over H, PDP: InitializeProperties from PeopleDynamicListPage
H->>PDP: GetItems()
create participant PC as PeopleCommand
PDP-->PC: new PeopleCommand()
note left of H: Items from
PeopleDynamicListPage
are now displayed
loop People Page search
U->>H: User types
note right of PDP: Normal Dynamic List page
flow here
H-->>H: UI updates basic text
H->>PDP: SearchText.set
PDP->>H: RaiseItemsChanged
H->>PDP: GetItems()
note left of H: User invokes list item
H->>PC: Invoke()
PC-->PCP: TokenPicked()
PCP->>H: RequestAddToken
end
deactivate PDP
activate PP
end
Should we be having the PrefixSearchPage handle telling us what the current tokens are? Or should CmdPal be tracking that itself?
Like, when the token is picked, should the API be AddToken or SearchTextChanged with the new text and the list of tokens?
I guess the second form makes it easier for an extension to determine what the contents of the search box are. So they'd raise a
interface ITokenPositions
{
IToken Token { get; };
Int64 Position { get; }; // default -1 for "end"
}
interface ISearchTextChangedArgs
{
String NewSearchText { get; } // The text that the user has typed into the search box.
ITokenPositions[] CurrentTokens { get; } // The tokens that are currently in the search box.
}
But, that's not really what we want either. How would we know where tokens are within the NewSearchText? The extension could set NewSearchText to just garbage that isn't related to the tokens at all.
ITextRange is in Microsoft.UI.Text. Don't want to have to rely on and WASDK types.
So that kind of necessitates that we have
interface ISearchRun
{
}
interface IInputTextRun : ISearchRun
{
String Text { get; };
}
interface ITokenRun : ISearchRun
{
IToken Token { get; };
}
interface IReadOnlyTextRun : ISearchRun
{
String Text { get; };
}
interface ISearchTextChangedArgs
{
ISearchRun[] SearchRuns { get; }
}
interface IPrefixProvider requires IListPage // this definitely needs at least IPage
{
IDictionary<String, String> PrefixCommands { get; }
void UpdateSearch(ISearchTextChangedArgs args);
}
// And just use a PropChanged(Page.SearchRuns) to notify of changes -> host
Then, when the user picks a suggestion, the page itself would have to:
public ISearchRun[] SearchRuns => _currentRuns.ToArray();
private IList<ISearchRun> _currentRuns = new List<ISearchRun>();
private void OnTokenPicked(MyTokenPickedEventArgs args)
{
var token = args.Value;
_currentRuns.Add(new TokenRun { Token = token });
_currentRuns.Add(new TextRun(" ")); // add a space after the token
// Raise the event to notify CmdPal of the picked token
RaisePropertyChanged(nameof(SearchRuns));
}
This is just that thing where all life evolves to be crabs. It's the token search spec again.
TODO! this current design requires that the picked suggestion raise an event up through the page hosting the suggestion, into the prefix page. That necessitates a lot of plumbing. Because the prefix page doesn't actually have a reference to the suggestion pages. So we can't wire the events from command -> suggestion page -> prefix page.
we need to wire
command -> suggestion page -> command provider -> prefix page
which is gross.
How could we make that easier? So that the things that are suggestions can just raise an event straight to the prefix page?
We could just have the prefix page own the suggestion pages, and have the command provider expose the other commands in GetCommand as members of the prefix page.
public class MyPrefixCommandProvider : ICommandProvider
{
private PrefixSearchPage _prefixSearchPage;
public MyPrefixCommandProvider()
{
_prefixSearchPage = new PrefixSearchPage();
}
public ICommandItem[] GetTopLevelCommands()
{
return new ICommandItem[] { _prefixSearchPage };
}
public ICommand GetCommand(String id)
{
return _prefixSearchPage.GetCommand(id);
}
}
public class PrefixSearchPage : ListPage, IPrefixProvider
{
private PeopleDynamicListPage _peopleCommand; // com.contoso.people
private CommandsListPage _commandsCommand; // com.contoso.commands
private AddFileCommand _addFileCommand; // com.contoso.addFile
public IDictionary<String, String> PrefixCommands => new Dictionary<String, String>
{
{ "@", _peopleCommand.Id },
{ "/", _commandsCommand.Id },
{ "+", _addFileCommand.Id },
};
public event Windows.Foundation.TypedEventHandler<Object, ITokenPickedEventArgs> TokenAdded;
public ISearchRun[] SearchRuns => _currentRuns.ToArray();
private IList<ISearchRun> _currentRuns = new List<ISearchRun>();
public PrefixSearchPage()
{
_peopleCommand = new PeopleDynamicListPage(HandleTokenPicked);
_commandsCommand = new CommandsListPage(HandleTokenPicked);
_addFileCommand = new AddFileCommand(HandleTokenPicked);
// Initialize the page...
}
public ICommand GetCommand(String id)
{
if (id == _peopleCommand.Id) // com.contoso.people
{
return _peopleCommand;
}
else if (id == _commandsCommand.Id) // com.contoso.commands
{
return _commandsCommand;
}
else if (id == _addFileCommand.Id) // com.contoso.addFile
{
return _addFileCommand;
}
return null;
}
public void HandleTokenPicked(ITokenPickedEventArgs args)
{
var token = args.Token;
_currentRuns.Add(new TokenRun { Token = token });
_currentRuns.Add(new TextRun(" ")); // add a space after the token
// Raise the event to notify CmdPal of the picked token
RaisePropertyChanged(nameof(SearchRuns));
}
public void UpdateSearch(ISearchTextChangedArgs args)
{
var searchTokens = args.CurrentTokens;
_currentRuns = searchTokens.ToList();
// For example: you could grab all the text from the runs like this
var newText = "";
foreach (var run in args.SearchRuns)
{
if (run is IInputTextRun inputRun)
{
newText += inputRun.Text;
}
else if (run is ITokenRun tokenRun)
{
newText += tokenRun.Token.DisplayText;
}
else if (run is IReadOnlyTextRun readOnlyRun)
{
newText += readOnlyRun.Text;
}
}
}
// Other ListPage members...
}
Then the prefix page can own the suggestion pages, and wire their events directly to itself.
Does this mean that the ISearchTextChangedArgs coming into the extension are something instantiated by the host? yea. That would force the extension to safely deal with the args. Unless we definitely had OSS MarshalByValue always, we can't be sure that what the extension sees is safe to handle.
It would be a weird mishmash where the set of tokens is half owned by the extension (tokens) and half owned by the host (text). And I don't know how having a token roundtrip across the ABI both ways would translate. Probably fine.
page.UpdateSearch(new SearchTextChangedArgs(){SearchRuns=[new InputTextRun("h")]});
page.UpdateSearch(new SearchTextChangedArgs(){SearchRuns=[new InputTextRun("he")]});
page.UpdateSearch(new SearchTextChangedArgs(){SearchRuns=[new InputTextRun("hel")]});
page.UpdateSearch(new SearchTextChangedArgs(){SearchRuns=[new InputTextRun("hell")]});
page.UpdateSearch(new SearchTextChangedArgs(){SearchRuns=[new InputTextRun("hello")]});
page.UpdateSearch(new SearchTextChangedArgs(){SearchRuns=[new InputTextRun("hello ")]});
// user types '@'
// user picks "Alice" from people page
// page itself raises a PropChanged(SearchRuns)
page.UpdateSearch(new SearchTextChangedArgs(){SearchRuns=[
new InputTextRun("hello "), // created by host
new TokenRun(Alice), // created by page
new InputTextRun(" "), // created by page
]});
var args = new CmdPalSearchUpdateArgs();
// assume that our CmdPal-side implementation has a SetProperty(string, object) to add an extended attribute
args.SetProperty("tokens", new TokenPositions[]{ new(pos: 0, token: t.Value), ... });
args.SetProperty("correlation", myWhateverValue);
prefixProvider.SendQuery(args);
This is what it looks like if we have the IPrefixProvider implemented in our command provider.
class MyPrefixCommandProvider : ICommandProvider, IPrefixProvider
{
private PeopleDynamicListPage _peopleCommand;
private CommandsListPage _commandsCommand;
private AddFileCommand _addFileCommand;
public IDictionary<String, String> PrefixCommands => new Dictionary<String, String>
{
{ "@", "com.contoso.people" },
{ "/", "com.contoso.commands" },
{ "+", "com.contoso.addFile" },
};
public MyPrefixCommandProvider()
{
_peopleCommand = new PeopleDynamicListPage() { TokenPicked = OnTokenPicked };
_commandsCommand = new CommandsListPage() { TokenPicked = OnTokenPicked };
_addFileCommand = new AddFileCommand() { TokenPicked = OnTokenPicked };
}
public event Windows.Foundation.TypedEventHandler<Object, ITokenPickedEventArgs> TokenAdded;
public void OnTokenPicked(ITokenPickedEventArgs args)
{
// Raise the event to notify CmdPal of the picked token
TokenAdded?.Invoke(this, args);
}
// Other ICommandProvider members...
public ICommand GetCommand(String id)
{
if (id == "com.contoso.people")
{
return _peopleCommand;
}
else if (id == "com.contoso.commands")
{
return _commandsCommand;
}
else if (id == "com.contoso.addFile")
{
return _addFileCommand;
}
return null;
}
}
This is a little gross in my opinion. The GetCommand implementation is close to the list of prefixes, which is nice. But picking the tokens doesn't make sense on the command provider. It should be on the Page.
I don't know if this compiles: This DOESN'T compile, a struct field can't be a pointer type.
struct TokenRun
{
String Text;
Boolean IsReadOnly;
Object Value;
}
That boxes us into those three properties and never more, but would ensure that we'd always cleanly transit the ABI