using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using Rune = System.Rune;
namespace Terminal.Gui {
///
/// Renders an overlay on another view at a given point that allows selecting
/// from a range of 'autocomplete' options.
///
public class Autocomplete {
///
/// The maximum width of the autocomplete dropdown
///
public int MaxWidth { get; set; } = 10;
///
/// The maximum number of visible rows in the autocomplete dropdown to render
///
public int MaxHeight { get; set; } = 6;
///
/// True if the autocomplete should be considered open and visible
///
protected bool Visible { get; set; } = true;
///
/// The strings that form the current list of suggestions to render
/// based on what the user has typed so far.
///
public ReadOnlyCollection Suggestions { get; protected set; } = new ReadOnlyCollection(new string[0]);
///
/// The full set of all strings that can be suggested.
///
///
public List AllSuggestions { get; set; } = new List();
///
/// The currently selected index into that the user has highlighted
///
public int SelectedIdx { get; set; }
///
/// When more suggestions are available than can be rendered the user
/// can scroll down the dropdown list. This indicates how far down they
/// have gone
///
public int ScrollOffset {get;set;}
///
/// The colors to use to render the overlay. Accessing this property before
/// the Application has been initialised will cause an error
///
public ColorScheme ColorScheme {
get
{
if(colorScheme == null)
{
colorScheme = Colors.Menu;
}
return colorScheme;
}
set
{
colorScheme = value;
}
}
private ColorScheme colorScheme;
///
/// The key that the user must press to accept the currently selected autocomplete suggestion
///
public Key SelectionKey { get; set; } = Key.Enter;
///
/// The key that the user can press to close the currently popped autocomplete menu
///
public Key CloseKey {get;set;} = Key.Esc;
///
/// Renders the autocomplete dialog inside the given at the
/// given point.
///
/// The view the overlay should be rendered into
///
public void RenderOverlay (View view, Point renderAt)
{
if (!Visible || !view.HasFocus || Suggestions.Count == 0) {
return;
}
view.Move (renderAt.X, renderAt.Y);
// don't overspill vertically
var height = Math.Min(view.Bounds.Height - renderAt.Y,MaxHeight);
var toRender = Suggestions.Skip(ScrollOffset).Take(height).ToArray();
if(toRender.Length == 0)
{
return;
}
var width = Math.Min(MaxWidth,toRender.Max(s=>s.Length));
// don't overspill horizontally
width = Math.Min(view.Bounds.Width - renderAt.X ,width);
for(int i=0;i
/// Updates to be a valid index within
///
public void EnsureSelectedIdxIsValid()
{
SelectedIdx = Math.Max (0,Math.Min (Suggestions.Count - 1, SelectedIdx));
// if user moved selection up off top of current scroll window
if(SelectedIdx < ScrollOffset)
{
ScrollOffset = SelectedIdx;
}
// if user moved selection down past bottom of current scroll window
while(SelectedIdx >= ScrollOffset + MaxHeight ){
ScrollOffset++;
}
}
///
/// Handle key events before e.g. to make key events like
/// up/down apply to the autocomplete control instead of changing the cursor position in
/// the underlying text view.
///
///
///
///
public bool ProcessKey (TextView hostControl, KeyEvent kb)
{
if(IsWordChar((char)kb.Key))
{
Visible = true;
}
if(!Visible || Suggestions.Count == 0) {
return false;
}
if (kb.Key == Key.CursorDown) {
SelectedIdx++;
EnsureSelectedIdxIsValid();
hostControl.SetNeedsDisplay ();
return true;
}
if (kb.Key == Key.CursorUp) {
SelectedIdx--;
EnsureSelectedIdxIsValid();
hostControl.SetNeedsDisplay ();
return true;
}
if(kb.Key == SelectionKey && SelectedIdx >=0 && SelectedIdx < Suggestions.Count) {
var accepted = Suggestions [SelectedIdx];
var typedSoFar = GetCurrentWord (hostControl) ?? "";
if(typedSoFar.Length < accepted.Length) {
// delete the text
for(int i=0;i
/// Clears
///
public void ClearSuggestions ()
{
Suggestions = Enumerable.Empty ().ToList ().AsReadOnly ();
}
///
/// Populates with all strings in that
/// match with the current cursor position/text in the
///
/// The text view that you want suggestions for
public void GenerateSuggestions (TextView hostControl)
{
// if there is nothing to pick from
if(AllSuggestions.Count == 0) {
ClearSuggestions ();
return;
}
var currentWord = GetCurrentWord (hostControl);
if(string.IsNullOrWhiteSpace(currentWord)) {
ClearSuggestions ();
}
else {
Suggestions = AllSuggestions.Where (o =>
o.StartsWith (currentWord, StringComparison.CurrentCultureIgnoreCase) &&
!o.Equals(currentWord,StringComparison.CurrentCultureIgnoreCase)
).ToList ().AsReadOnly();
EnsureSelectedIdxIsValid();
}
}
private string GetCurrentWord (TextView hostControl)
{
var currentLine = hostControl.GetCurrentLine ();
var cursorPosition = Math.Min (hostControl.CurrentColumn, currentLine.Count);
return IdxToWord (currentLine, cursorPosition);
}
private string IdxToWord (List line, int idx)
{
StringBuilder sb = new StringBuilder ();
// do not generate suggestions if the cursor is positioned in the middle of a word
bool areMidWord;
if(idx == line.Count) {
// the cursor positioned at the very end of the line
areMidWord = false;
}
else {
// we are in the middle of a word if the cursor is over a letter/number
areMidWord = IsWordChar (line [idx]);
}
// if we are in the middle of a word then there is no way to autocomplete that word
if(areMidWord) {
return null;
}
// we are at the end of a word. Work out what has been typed so far
while(idx-- > 0) {
if(IsWordChar(line [idx])) {
sb.Insert(0,(char)line [idx]);
}
else {
break;
}
}
return sb.ToString ();
}
///
/// Return true if the given symbol should be considered part of a word
/// and can be contained in matches. Base behaviour is to use
///
///
///
public virtual bool IsWordChar (Rune rune)
{
return Char.IsLetterOrDigit ((char)rune);
}
}
}