Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions src/Eto.Designer/Completion/Completion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class CompletionPathNode
public string Name
{
get {
return (Prefix ?? "") + LocalName;
return string.IsNullOrEmpty(Prefix) ? LocalName : Prefix + ":" + LocalName;
}
}

Expand Down Expand Up @@ -97,11 +97,11 @@ public virtual bool HandlesPrefix(string prefix)
public const string EtoFormsNamespace = "http://schema.picoe.ca/eto.forms";
public const string XamlNamespace2006 = "http://schemas.microsoft.com/winfx/2006/xaml";

public static IEnumerable<CompletionItem> GetCompletionItems(IEnumerable<CompletionNamespace> namespaces, CompletionMode mode, IEnumerable<string> path, CompletionPathNode context, CompletionFormat format = CompletionFormat.Xaml)
public static IEnumerable<CompletionItem> GetCompletionItems(IEnumerable<CompletionNamespace> namespaces, CompletionMode mode, IEnumerable<string> path, CompletionPathNode context, CompletionFormat format = CompletionFormat.Xaml, IList<Assembly> projectAssemblies = null)
{
if (mode == CompletionMode.None)
return Enumerable.Empty<CompletionItem>();
var completions = GetCompletions(namespaces, format).ToList();
var completions = GetCompletions(namespaces, format, projectAssemblies).ToList();
IEnumerable<CompletionItem> items;
if (mode == CompletionMode.Property && context != null)
{
Expand Down Expand Up @@ -158,21 +158,32 @@ static IEnumerable<CompletionNamespace> GetEffectiveNamespaces(IEnumerable<Compl
return effective;
}

public static IEnumerable<Completion> GetCompletions(IEnumerable<CompletionNamespace> namespaces, CompletionFormat format = CompletionFormat.Xaml)
/// <param name="projectAssemblies">Built assemblies of the project being edited, its own first.</param>
public static IEnumerable<Completion> GetCompletions(IEnumerable<CompletionNamespace> namespaces, CompletionFormat format = CompletionFormat.Xaml, IList<Assembly> projectAssemblies = null)
{
projectAssemblies = projectAssemblies ?? Array.Empty<Assembly>();
if (format == CompletionFormat.Json)
{
// json has no namespace declarations, everything resolves against Eto.Forms
// json has no namespace declarations, Eto.Forms types go by name and anything else by full name
yield return new JsonCompletion();
yield return new TypeCompletion { Assembly = typeof(Eto.Widget).Assembly, Namespace = "Eto.Forms" };
yield return new TypeCompletion { Assembly = typeof(Eto.Widget).Assembly, Namespace = "Eto" };
foreach (var assembly in projectAssemblies)
yield return new TypeCompletion { Assembly = assembly, UseFullName = true };
yield break;
}

yield return new GeneralCompletion();
yield return new GeneralCompletion { ProjectAssemblies = projectAssemblies };

foreach (var ns in GetEffectiveNamespaces(namespaces))
var effective = GetEffectiveNamespaces(namespaces).ToList();
foreach (var ns in effective)
{
if (ProjectTypes.TryParseClrNamespace(ns.Namespace, out var clrNamespace, out var assemblyName))
{
var assembly = ProjectTypes.FindAssembly(assemblyName, projectAssemblies);
if (assembly != null)
yield return new TypeCompletion { Prefix = ns.Prefix, Assembly = assembly, Namespace = clrNamespace };
}
if (ns.Namespace == EtoFormsNamespace)
{
yield return new TypeCompletion
Expand All @@ -199,6 +210,9 @@ public static IEnumerable<Completion> GetCompletions(IEnumerable<CompletionNames
yield return new XamlCompletion { Prefix = ns.Prefix };
}
}

if (projectAssemblies.Count > 0)
yield return new ProjectTypeCompletion(effective, projectAssemblies);
}
}
}
3 changes: 3 additions & 0 deletions src/Eto.Designer/Completion/CompletionItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public class CompletionItem
public string Suffix { get; set; }

public CompletionBehavior Behavior { get; set; }

/// <summary>Namespace the document must declare for this item to resolve, when it hasn't yet.</summary>
public CompletionNamespace Namespace { get; set; }
}

}
86 changes: 78 additions & 8 deletions src/Eto.Designer/Completion/DocumentCompletion.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

namespace Eto.Designer.Completion
{
Expand All @@ -12,6 +14,9 @@ public class DocumentCompletionItem
public string Label { get; set; }

public string InsertText { get; set; }

/// <summary>Declaration to add to the root element so the item resolves, or null when none is needed.</summary>
public DocumentTextEdit NamespaceEdit { get; set; }
}

/// <summary>What the cursor sits on, and the span of text a completion should replace.</summary>
Expand All @@ -27,6 +32,18 @@ public class DocumentCompletionContext

/// <summary>True when the cursor is already inside a quoted value.</summary>
public bool Quoted { get; set; }

public IList<Assembly> ProjectAssemblies { get; set; }

public string Text { get; set; }
}

/// <summary>Text to insert at an offset of the document the completion was computed for.</summary>
public class DocumentTextEdit
{
public int Offset { get; set; }

public string Text { get; set; }
}

/// <summary>
Expand All @@ -44,7 +61,7 @@ public static CompletionFormat GetFormat(string path) =>
: CompletionFormat.Xaml;

/// <returns>The context, or null when there is nothing to complete at the offset.</returns>
public static DocumentCompletionContext GetContext(string text, int offset, CompletionFormat format, string rootTypeName = null)
public static DocumentCompletionContext GetContext(string text, int offset, CompletionFormat format, string rootTypeName = null, IList<Assembly> projectAssemblies = null)
{
offset = Math.Min(Math.Max(offset, 0), text.Length);

Expand All @@ -71,7 +88,7 @@ public static DocumentCompletionContext GetContext(string text, int offset, Comp
? ScanForward(text, offset, IsValueChar)
: ScanForward(text, offset, format == CompletionFormat.Json ? (Func<char, bool>)IsJsonTokenChar : IsTokenChar);

return new DocumentCompletionContext { Info = info, Format = format, Start = start, End = end, Quoted = quoted };
return new DocumentCompletionContext { Info = info, Format = format, Start = start, End = end, Quoted = quoted, Text = text, ProjectAssemblies = projectAssemblies };
}

public static List<DocumentCompletionItem> GetItems(DocumentCompletionContext context)
Expand All @@ -83,7 +100,7 @@ public static List<DocumentCompletionItem> GetItems(DocumentCompletionContext co
var addColon = addQuotes && info.Mode == CompletionMode.Property;

var results = new List<DocumentCompletionItem>();
foreach (var item in Completion.GetCompletionItems(info.Namespaces, info.Mode, info.Path, info.Context, format).OrderBy(r => r.Name))
foreach (var item in Completion.GetCompletionItems(info.Namespaces, info.Mode, info.Path, info.Context, format, context.ProjectAssemblies).OrderBy(r => r.Name))
{
var label = item.Name;
var insert = label;
Expand All @@ -103,19 +120,20 @@ public static List<DocumentCompletionItem> GetItems(DocumentCompletionContext co
insert += ": ";
}
}
results.Add(new DocumentCompletionItem { Item = item, Label = label, InsertText = insert });
var namespaceEdit = item.Namespace != null ? GetNamespaceDeclaration(context.Text, item.Namespace) : null;
results.Add(new DocumentCompletionItem { Item = item, Label = label, InsertText = insert, NamespaceEdit = namespaceEdit });
}
return results;
}

public static List<DocumentCompletionItem> GetCompletions(string text, int offset, CompletionFormat format, string rootTypeName = null)
public static List<DocumentCompletionItem> GetCompletions(string text, int offset, CompletionFormat format, string rootTypeName = null, IList<Assembly> projectAssemblies = null)
{
var context = GetContext(text, offset, format, rootTypeName);
var context = GetContext(text, offset, format, rootTypeName, projectAssemblies);
return context == null ? new List<DocumentCompletionItem>() : GetItems(context);
}

/// <summary>The completion item for the word under the cursor, used for hover text.</summary>
public static CompletionItem FindItemAt(string text, int offset, CompletionFormat format, string rootTypeName, out int start, out int end)
public static CompletionItem FindItemAt(string text, int offset, CompletionFormat format, string rootTypeName, out int start, out int end, IList<Assembly> projectAssemblies = null)
{
offset = Math.Min(Math.Max(offset, 0), text.Length);
start = GetTokenStart(text, offset);
Expand All @@ -130,10 +148,62 @@ public static CompletionItem FindItemAt(string text, int offset, CompletionForma
if (info.Mode == CompletionMode.None)
return null;

return Completion.GetCompletionItems(info.Namespaces, info.Mode, info.Path, info.Context, format)
return Completion.GetCompletionItems(info.Namespaces, info.Mode, info.Path, info.Context, format, projectAssemblies)
.FirstOrDefault(r => r.Name == word);
}

static readonly Regex rootTagReg = new Regex(@"<(?![?!])[A-Za-z_][\w.:-]*", RegexOptions.Compiled);
static readonly Regex xmlnsAttributeReg = new Regex(@"\s+xmlns(:[\w.-]+)?\s*=\s*(""[^""]*""|'[^']*')", RegexOptions.Compiled);

/// <summary>
/// Where to declare a namespace on the root element: after its last xmlns, or its name when it has none.
/// </summary>
/// <returns>The edit, or null when there is no complete root start tag to add it to.</returns>
public static DocumentTextEdit GetNamespaceDeclaration(string text, CompletionNamespace ns)
{
var root = rootTagReg.Match(text);
if (!root.Success)
return null;

var tagEnd = FindTagEnd(text, root.Index + root.Length);
if (tagEnd < 0)
return null;

var offset = root.Index + root.Length;
var tag = text.Substring(offset, tagEnd - offset);
var last = xmlnsAttributeReg.Matches(tag).Cast<Match>().LastOrDefault();
if (last != null)
offset += last.Index + last.Length;

return new DocumentTextEdit
{
Offset = offset,
Text = " xmlns:" + ns.Prefix + "=\"" + ns.Namespace + "\""
};
}

/// <summary>Index of the '>' closing a start tag, skipping quoted attribute values.</summary>
static int FindTagEnd(string text, int start)
{
var quote = '\0';
for (var i = start; i < text.Length; i++)
{
var ch = text[i];
if (quote != '\0')
{
if (ch == quote)
quote = '\0';
}
else if (ch == '"' || ch == '\'')
quote = ch;
else if (ch == '>')
return i;
else if (ch == '<')
return -1;
}
return -1;
}

static int ScanBack(string text, int offset, Func<char, bool> include)
{
var i = Math.Min(offset, text.Length);
Expand Down
7 changes: 7 additions & 0 deletions src/Eto.Designer/Completion/GeneralCompletion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ namespace Eto.Designer.Completion

class GeneralCompletion : Completion
{
public IList<Assembly> ProjectAssemblies { get; set; }

public override IEnumerable<CompletionItem> GetClasses(IEnumerable<string> path, Func<Type, bool> filter)
{
yield break;
Expand All @@ -38,6 +40,11 @@ public override IEnumerable<CompletionItem> GetPropertyValues(string objectName,
yield return new CompletionItem { Name = XamlNamespace2006, Type = CompletionType.Literal };
yield return new CompletionItem { Name = EtoFormsNamespace, Type = CompletionType.Literal };
yield return new CompletionItem { Name = "clr-namespace:[namespace];assembly=[assembly]", Type = CompletionType.Literal };
foreach (var assembly in ProjectAssemblies ?? Enumerable.Empty<Assembly>())
{
foreach (var ns in ProjectTypes.GetNamespaces(assembly))
yield return new CompletionItem { Name = ProjectTypes.GetClrNamespace(ns, assembly), Type = CompletionType.Literal };
}
}
}
}
Expand Down
86 changes: 86 additions & 0 deletions src/Eto.Designer/Completion/ProjectTypeCompletion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Eto.Designer.Completion
{
/// <summary>
/// Offers project types from namespaces the xaml document hasn't declared, each carrying the xmlns to add.
/// </summary>
class ProjectTypeCompletion : Completion
{
readonly List<KeyValuePair<TypeCompletion, CompletionNamespace>> sources = new List<KeyValuePair<TypeCompletion, CompletionNamespace>>();

public ProjectTypeCompletion(IList<CompletionNamespace> declared, IList<Assembly> projectAssemblies)
{
var usedPrefixes = new HashSet<string>(declared.Select(r => r.Prefix ?? string.Empty));
foreach (var assembly in projectAssemblies)
{
foreach (var ns in ProjectTypes.GetNamespaces(assembly))
{
if (declared.Any(r => IsDeclaration(r, ns, assembly, projectAssemblies)))
continue;

var prefix = CreatePrefix(ns, usedPrefixes);
sources.Add(new KeyValuePair<TypeCompletion, CompletionNamespace>(
new TypeCompletion { Prefix = prefix, Assembly = assembly, Namespace = ns },
new CompletionNamespace { Prefix = prefix, Namespace = ProjectTypes.GetClrNamespace(ns, assembly) }));
}
}
}

static bool IsDeclaration(CompletionNamespace declared, string ns, Assembly assembly, IList<Assembly> projectAssemblies) =>
ProjectTypes.TryParseClrNamespace(declared.Namespace, out var declaredNs, out var assemblyName)
&& declaredNs == ns
&& ProjectTypes.FindAssembly(assemblyName, projectAssemblies) == assembly;

/// <summary>Prefix from the last part of the namespace, eg. "controls" for MyApp.Controls.</summary>
static string CreatePrefix(string ns, HashSet<string> used)
{
var name = ns.Substring(ns.LastIndexOf('.') + 1);
var sb = new StringBuilder();
foreach (var ch in name)
{
if (char.IsLetterOrDigit(ch) || ch == '_')
sb.Append(char.ToLowerInvariant(ch));
}
if (sb.Length == 0 || !char.IsLetter(sb[0]))
sb.Insert(0, "local");

var baseName = sb.ToString();
var prefix = baseName;
// "x" is taken implicitly by Eto's reader even when not declared
for (var i = 2; prefix == "x" || !used.Add(prefix); i++)
prefix = baseName + i;
return prefix;
}

public override IEnumerable<CompletionItem> GetClasses(IEnumerable<string> path, Func<Type, bool> filter)
{
// with no content type to match there is nothing sensible to offer
if (filter == null)
yield break;

foreach (var source in sources)
{
foreach (var item in source.Key.GetClasses(path, filter))
{
if (item.Type != CompletionType.Class)
continue;
item.Namespace = source.Value;
item.Suffix = source.Key.Namespace;
yield return item;
}
}
}

public override IEnumerable<CompletionItem> GetProperties(string objectName, IEnumerable<string> path)
{
yield break;
}

public override bool HandlesPrefix(string prefix) => true;
}
}
Loading
Loading