#region Disclaimer / License // Copyright (C) 2013, Jackie Ng // http://trac.osgeo.org/mapguide/wiki/maestro, jumpinjackie@gmail.com // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // #endregion Disclaimer / License using ICSharpCode.TextEditor; using ICSharpCode.TextEditor.Document; using ICSharpCode.TextEditor.Gui.CompletionWindow; using OSGeo.MapGuide.MaestroAPI; using OSGeo.MapGuide.MaestroAPI.Schema; using OSGeo.MapGuide.ObjectModels.Capabilities; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; namespace Maestro.Editors.Common.Expression { //NOTE: // //It seems the auto-complete capabilities of the ICSharpCode.TextEditor assume object-oriented languages or languages //that involve a member access operator (., -> or anything similar), so we have to use a custom ICompletionData that //compensates for the lack of such contexts // //NOTE/TODO: //Auto-completions are currently case-sensitive and will only trigger on the correct case. internal class FdoExpressionCompletionDataProvider : ICompletionDataProvider, IDisposable { private ClassDefinition _klass; private IFdoProviderCapabilities _caps; public FdoExpressionCompletionDataProvider(ClassDefinition cls, IFdoProviderCapabilities caps) { _klass = cls; _caps = caps; this.DefaultIndex = 0; this.PreSelection = null; this.ImageList = new System.Windows.Forms.ImageList(); this.ImageList.Images.Add(Properties.Resources.block); this.ImageList.Images.Add(Properties.Resources.property); this.ImageList.Images.Add(Properties.Resources.funnel); } ~FdoExpressionCompletionDataProvider() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (disposing) { this.ImageList?.Dispose(); this.ImageList = null; } } public System.Windows.Forms.ImageList ImageList { get; private set; } public string PreSelection { get; } public int DefaultIndex { get; } public bool InsertSpace { get; set; } public CompletionDataProviderKeyResult ProcessKey(char key) { CompletionDataProviderKeyResult res; if (key == ' ' && this.InsertSpace) { this.InsertSpace = false; // insert space only once res = CompletionDataProviderKeyResult.BeforeStartKey; } else if (char.IsLetterOrDigit(key) || key == '_') { this.InsertSpace = false; // don't insert space if user types normally res = CompletionDataProviderKeyResult.NormalKey; } else { // do not reset insertSpace when doing an insertion! res = CompletionDataProviderKeyResult.InsertionKey; } return res; } public bool InsertAction(ICompletionData data, TextArea textArea, int insertionOffset, char key) { if (this.InsertSpace) { textArea.Document.Insert(insertionOffset++, " "); } textArea.Caret.Position = textArea.Document.OffsetToPosition(insertionOffset); var res = data.InsertAction(textArea, key); var fdoComp = (FdoCompletionData)data; if (fdoComp.ImageIndex == 0 && fdoComp.AppendText.Length > 2) //Function and not an empty function call { //Rewind caret so it is at the start of the function call (at first parameter) var offset = textArea.Caret.Offset; offset -= (fdoComp.AppendText.Length - 1); textArea.Caret.Position = textArea.Document.OffsetToPosition(offset); textArea.SelectionManager.ClearSelection(); textArea.SelectionManager.SetSelection(textArea.Caret.Position, textArea.Document.OffsetToPosition(offset + (fdoComp.HighlightLength - 1))); } return res; } /// /// Gets the line of text up to the cursor position. /// private string GetLineText(TextArea textArea) { LineSegment lineSegment = textArea.Document.GetLineSegmentForOffset(textArea.Caret.Offset); return textArea.Document.GetText(lineSegment); } public ICompletionData[] GenerateCompletionData(string fileName, TextArea textArea, char charTyped) => GenerateCompletionData((GetLineText(textArea) + charTyped).Trim()); private class FdoCompletionData : DefaultCompletionData { private readonly string _insertText; private readonly string _appendText; private int _highlightLength = 0; public int HighlightLength => _highlightLength; public string InsertText => _insertText; public string AppendText => _appendText; public FdoCompletionData(string prefix, string text, string description, int imageIndex) : base(text, description, imageIndex) { _insertText = text.Substring(prefix.Length); _appendText = string.Empty; } public FdoCompletionData(string prefix, string text, string description, string appendText, int highlightLength, int imageIndex) : this(prefix, text, description, imageIndex) { _appendText = appendText; _highlightLength = highlightLength; } public override bool InsertAction(TextArea textArea, char ch) { textArea.InsertString(_insertText + _appendText); return false; } } private ICompletionData[] GenerateCompletionData(string line) { Debug.WriteLine("FDO auto-complete: " + line); List items = new List(); string name = GetName(line); if (!String.IsNullOrEmpty(name)) { try { foreach (var func in GetMatchingFdoFunctions(name)) { foreach (var sign in func.Signatures) { var member = CreateFdoFunctionSignatureDescriptor(func, sign); int highlightLength = 0; var args = sign.Arguments; if (args.Length > 0) { highlightLength = args[0].Name.Length + 2; // [ and ] } items.Add(new FdoCompletionData(name, member.Name, member.Description, member.AppendText, highlightLength, 0)); } } foreach (var member in GetMatchingClassProperties(name)) { items.Add(new FdoCompletionData(name, member.Name, member.Description, 1)); } foreach (var member in GetMatchingFdoConditions(name)) { if (string.IsNullOrEmpty(member.AppendText)) items.Add(new FdoCompletionData(name, member.Name, member.Description, 2)); else items.Add(new FdoCompletionData(name, member.Name, member.Description, member.AppendText, member.AppendText.Length - 1, 2)); } foreach (var member in GetMatchingFdoOperators(name)) { if (string.IsNullOrEmpty(member.AppendText)) items.Add(new FdoCompletionData(name, member.Name, member.Description, 2)); else items.Add(new FdoCompletionData(name, member.Name, member.Description, member.AppendText, 0, 2)); } items.Sort((a, b) => { return a.Text.CompareTo(b.Text); }); } catch { // Do nothing. } } return items.ToArray(); } private class Descriptor { public string Name; public string Description; public string AppendText; } private IEnumerable GetMatchingFdoFunctions(string name) { foreach (var func in _caps.Expression.SupportedFunctions.Concat(Utility.GetStylizationFunctions())) { if (func.Name.StartsWith(name)) yield return func; } } private IEnumerable GetMatchingFdoConditions(string name) { foreach (var cond in _caps.Filter.ConditionTypes) { if (cond.ToString().ToUpper().StartsWith(name)) { var desc = CreateFdoConditionDescriptor(cond); if (desc != null) yield return desc; } } } private Descriptor CreateFdoConditionDescriptor(string cond) { switch (cond) { case "Null": return new Descriptor() { Name = cond.ToString().ToUpper(), Description = "[property] NULL" //NOXLATE }; case "In": return new Descriptor() { Name = cond.ToString().ToUpper(), Description = "[property] IN ([value1], [value2], ..., [valueN])", //NOXLATE AppendText = " ([value1], [value2])" //NOXLATE }; case "Like": return new Descriptor() { Name = cond.ToString().ToUpper(), Description = "[property] LIKE [string value]", //NOXLATE AppendText = " [string value]" //NOXLATE }; } return null; //Handled by operators } private static Descriptor CreateBinaryDistanceOperator(string opName) { return new Descriptor() { Name = opName.ToUpper(), Description = $"[property] {opName} [number]", //NOXLATE AppendText = " [number]" //NOXLATE }; } private static Descriptor CreateBinarySpatialOperator(string opName) { return new Descriptor() { Name = opName.ToUpper(), Description = $"[geometry] {opName} GeomFromText('geometry wkt')", //NOXLATE AppendText = " GeomFromText('geometry wkt')" //NOXLATE }; } private IEnumerable GetMatchingFdoOperators(string name) { foreach (var op in _caps.Filter.DistanceOperations) { var opName = op.ToUpper(); if (opName.StartsWith(name)) yield return CreateBinaryDistanceOperator(opName); } foreach (var op in _caps.Filter.SpatialOperations) { var opName = op.ToUpper(); if (opName.StartsWith(name)) yield return CreateBinarySpatialOperator(opName); } } private IEnumerable GetMatchingClassProperties(string name) { foreach (var prop in _klass.Properties) { if (prop.Name.StartsWith(name)) yield return CreatePropertyDescriptor(prop); } } private Descriptor CreateFdoFunctionSignatureDescriptor(IFdoFunctionDefintion func, IFdoFunctionDefintionSignature sig) { var desc = new Descriptor(); List args = new List(); foreach (var argDef in sig.Arguments) { args.Add(argDef.Name.Trim()); } string argsStr = StringifyFunctionArgs(args); string argDesc = DescribeSignature(sig); string expr = $"{func.Name}({argsStr})"; //NOXLAT desc.Name = expr; desc.Description = string.Format(Strings.ExprEditorFunctionDesc, expr, func.Description, argDesc, sig.ReturnType, Environment.NewLine); desc.AppendText = string.Empty; return desc; } internal static string DescribeSignature(IFdoFunctionDefintionSignature sig) { string argDesc = Strings.None; var args = sig.Arguments; if (args.Length > 0) { StringBuilder sb = new StringBuilder(); sb.Append(Environment.NewLine); foreach (var argDef in sig.Arguments) { sb.Append($" [{argDef.Name}] - {argDef.Description}{Environment.NewLine}"); //NOXLATE } argDesc = sb.ToString(); } return argDesc; } internal static string StringifyFunctionArgs(List args) { string argsStr = args.Count > 0 ? "[" + string.Join("], [", args.ToArray()) + "]" : string.Empty; //NOXLATE return argsStr; } private static Descriptor CreatePropertyDescriptor(PropertyDefinition prop) { var desc = new Descriptor(); desc.Name = prop.Name; switch (prop.Type) { case PropertyDefinitionType.Geometry: { var g = (GeometricPropertyDefinition)prop; desc.Description = string.Format(Strings.FsPreview_GeometryPropertyNodeTooltip, g.Name, g.Description, g.GeometryTypesToString(), g.IsReadOnly, g.HasElevation, g.HasMeasure, g.SpatialContextAssociation, Environment.NewLine); } break; case PropertyDefinitionType.Data: { var d = (DataPropertyDefinition)prop; desc.Description = string.Format(Strings.FsPreview_DataPropertyNodeTooltip, d.Name, d.Description, d.DataType.ToString(), d.IsNullable, d.IsReadOnly, d.Length, d.Precision, d.Scale, Environment.NewLine); } break; case PropertyDefinitionType.Raster: { var r = (RasterPropertyDefinition)prop; desc.Description = string.Format(Strings.FsPreview_RasterPropertyNodeTooltip, r.Name, r.Description, r.IsNullable, r.DefaultImageXSize, r.DefaultImageYSize, r.SpatialContextAssociation, Environment.NewLine); } break; default: { desc.Description = string.Format(Strings.FsPreview_GenericPropertyTooltip, prop.Name, prop.Type.ToString(), Environment.NewLine); } break; } return desc; } private string GetName(string text) { int startIndex = text.LastIndexOfAny(new char[] { ' ', '+', '/', '*', '-', '%', '=', '>', '<', '&', '|', '^', '~', '(', ',', ')' }); //NOXLATE string res = text.Substring(startIndex + 1); Debug.WriteLine("Evaluating FDO auto-complete options for: " + res); return res; } } }