diff --git a/RegexDialog/Model/IntellisenseModels.cs b/RegexDialog/Model/IntellisenseModels.cs index f9f580e..3ea06d7 100644 --- a/RegexDialog/Model/IntellisenseModels.cs +++ b/RegexDialog/Model/IntellisenseModels.cs @@ -2,7 +2,9 @@ using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Editing; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -11,6 +13,8 @@ namespace RegexDialog.Model { public class CompletionData : ICompletionData { + private static readonly Dictionary imageCache = []; + public string Text { get; set; } public string Description { get; set; } public CompletionItemKind Kind { get; set; } @@ -25,23 +29,28 @@ public ImageSource Image { get { - if (_image == null) - { - _image = GetImageForKind(Kind); - } + _image ??= GetImageForKind(Kind); return _image; } } public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs) { - textArea.Document.Replace(completionSegment, Text); + string completion = Text; + + if (Kind == CompletionItemKind.Method || Kind == CompletionItemKind.Extension) + completion += "("; + + textArea.Document.Replace(completionSegment, completion); } private ImageSource GetImageForKind(CompletionItemKind kind) { try { + if (imageCache.ContainsKey(kind)) + return imageCache[kind]; + // Construire le chemin de l'image en fonction du type string imageName = kind.ToString(); string resourcePath = $"/RegexDialog;component/img/{imageName}.png"; @@ -60,6 +69,8 @@ private ImageSource GetImageForKind(CompletionItemKind kind) bitmap.EndInit(); bitmap.Freeze(); // Pour une utilisation thread-safe + imageCache[kind] = bitmap; + return bitmap; } @@ -97,4 +108,26 @@ public enum CompletionItemKind Struct, Other } + + public class SignatureHelpItem + { + public string PrefixDisplayParts { get; set; } + public string SeparatorDisplayParts { get; set; } + public string SuffixDisplayParts { get; set; } + public List Parameters { get; set; } = []; + public int ArgumentIndex { get; set; } + public string Documentation { get; set; } + + public override string ToString() + { + return $"{PrefixDisplayParts}{string.Join(SeparatorDisplayParts, Parameters.Select(p => p.DisplayParts))}{SuffixDisplayParts}"; + } + } + + public class ParameterItem + { + public string Name { get; set; } + public string DisplayParts { get; set; } + public string Documentation { get; set; } + } } \ No newline at end of file diff --git a/RegexDialog/RegExToolDialog.xaml b/RegexDialog/RegExToolDialog.xaml index b399bca..7dc9873 100644 --- a/RegexDialog/RegExToolDialog.xaml +++ b/RegexDialog/RegExToolDialog.xaml @@ -1019,7 +1019,9 @@ FontSize="12" SyntaxHighlighting="C#" ShowLineNumbers="{Binding ShowLinesNumbersCSharpTextSourceEditorOption}" - ScrollViewer.HorizontalScrollBarVisibility="Auto" > + ScrollViewer.HorizontalScrollBarVisibility="Auto" + behaviors:RoslynCompletionBehaviorExtension.EnableCompletion="True" + behaviors:RoslynCompletionBehaviorExtension.TemplateCode="{Binding Source={x:Static local:Res.TextSourceContainer}}"> diff --git a/RegexDialog/RegExToolDialog.xaml.cs b/RegexDialog/RegExToolDialog.xaml.cs index 312ff77..bce1a23 100644 --- a/RegexDialog/RegExToolDialog.xaml.cs +++ b/RegexDialog/RegExToolDialog.xaml.cs @@ -254,7 +254,7 @@ private void DelayAction(Action action, int milliseconds = 10) }); } - private async void KeyboardHookManager_KeyPressed(object sender, KeyPressedEventArgs e) + private void KeyboardHookManager_KeyPressed(object sender, KeyPressedEventArgs e) { hotkeyToAction ??= new() { diff --git a/RegexDialog/Resources/CSharpReplaceAutoCompleteTemplate.txt b/RegexDialog/Resources/CSharpReplaceAutoCompleteTemplate.txt index dbe888c..d70b2b9 100644 --- a/RegexDialog/Resources/CSharpReplaceAutoCompleteTemplate.txt +++ b/RegexDialog/Resources/CSharpReplaceAutoCompleteTemplate.txt @@ -13,13 +13,6 @@ public class CSharpReplaceContainer { //global - //match - public string Replace(Match match, int matchIndex, string fileName, int globalIndex, int fileIndex) - { - //code - } - //endmatch - public string Before(string text, string fileName) { //before @@ -29,4 +22,11 @@ public class CSharpReplaceContainer { //after } + + //match + public string Replace(Match match, int matchIndex, string fileName, int globalIndex, int fileIndex) + { + //code + } + //endmatch } \ No newline at end of file diff --git a/RegexDialog/Sevices/RoslynService.cs b/RegexDialog/Sevices/RoslynService.cs index c38f2de..a93afc1 100644 --- a/RegexDialog/Sevices/RoslynService.cs +++ b/RegexDialog/Sevices/RoslynService.cs @@ -1,4 +1,4 @@ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Text; @@ -16,6 +16,7 @@ using System.ComponentModel; using System.Globalization; using Microsoft.CodeAnalysis.Tags; +using RegexDialog.Model; namespace RegexDialog.Services { @@ -24,26 +25,32 @@ public class RoslynService private readonly MetadataReference[] _references; private readonly CSharpCompilationOptions _compilationOptions; + // Regex patterns to identify template regions + private static readonly Regex usingsRegex = new(@"#usings\r?\n(.*?)\r?\n#endusings", RegexOptions.Singleline); + private static readonly Regex globalRegex = new(@"#global\r?\n(.*?)\r?\n#endglobal", RegexOptions.Singleline); + private static readonly Regex beforeRegex = new(@"#before\r?\n(.*?)\r?\n#endbefore", RegexOptions.Singleline); + private static readonly Regex afterRegex = new(@"#after\r?\n(.*?)\r?\n#endafter", RegexOptions.Singleline); + private static readonly Regex removeAllRegionRegex = new(@"#(?\w+)\r?\n(.*?)\r?\n#end\k", RegexOptions.Singleline); + // Pour le debugging - private bool _isDebugMode = true; + private readonly bool _isDebugMode = true; public RoslynService() { // Charger toutes les références nécessaires - _references = new[] - { + _references = + [ MetadataReference.CreateFromFile(typeof(object).Assembly.Location), MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location), MetadataReference.CreateFromFile(typeof(Regex).Assembly.Location), MetadataReference.CreateFromFile(typeof(Match).Assembly.Location), MetadataReference.CreateFromFile(typeof(List<>).Assembly.Location), MetadataReference.CreateFromFile(typeof(StringBuilder).Assembly.Location), - MetadataReference.CreateFromFile(typeof(INotifyPropertyChanged).Assembly.Location), MetadataReference.CreateFromFile(typeof(ObservableCollection<>).Assembly.Location), MetadataReference.CreateFromFile(typeof(IEnumerable<>).Assembly.Location), MetadataReference.CreateFromFile(typeof(CultureInfo).Assembly.Location), MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location) - }; + ]; _compilationOptions = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, @@ -53,21 +60,50 @@ public RoslynService() LogDebug("RoslynService initialized"); } - public async Task> GetCompletionItemsAsync( - string editorContent, int position, string templateCode) + private string ExtractRegionContent(string editorContent, Regex regionRegex) + { + Match match = regionRegex.Match(editorContent); + return match.Success ? match.Groups[1].Value : ""; + } + + public async Task> GetCompletionItemsAsync(string editorContent, int position, string templateCode) { try { LogDebug($"GetCompletionItemsAsync called with position {position}"); - // Utiliser le template fourni et injecter le code de l'utilisateur - string codeToAnalyze = templateCode.Replace("//code", editorContent); + string codeToAnalyze; + int adjustedPosition; + + // Déterminer la région actuelle + var currentRegion = DetermineRegion(editorContent, position, out int positionInBloc); + LogDebug($"Current region: {currentRegion}"); + + // Extraire les parties du code de l'utilisateur selon les régions + string usingsCode = ExtractRegionContent(editorContent, usingsRegex); + string globalCode = ExtractRegionContent(editorContent, globalRegex); + string beforeCode = ExtractRegionContent(editorContent, beforeRegex); + string afterCode = ExtractRegionContent(editorContent, afterRegex); + + // Le code principal est ce qui n'est pas dans une région spéciale + string mainCode = removeAllRegionRegex.Replace(editorContent, ""); + + // Injecter le code dans le template + codeToAnalyze = templateCode + .Replace("//usings", usingsCode) + .Replace("//global", globalCode) + .Replace("//code", mainCode) + .Replace("//before", beforeCode) + .Replace("//after", afterCode); + + LogDebug("Code to analyze created"); // Calculer la position ajustée - int codeMarkerPosition = templateCode.IndexOf("//code"); - int adjustedPosition = codeMarkerPosition + position; + adjustedPosition = CalculateAdjustedPosition(templateCode, currentRegion, positionInBloc, + usingsCode, globalCode, beforeCode, afterCode); + + LogDebug($"Adjusted position: {adjustedPosition}"); - LogDebug($"Code marker position: {codeMarkerPosition}"); LogDebug($"Adjusted position: {adjustedPosition}"); LogDebug($"Code to analyze: {codeToAnalyze}"); @@ -92,7 +128,7 @@ public RoslynService() if (completionService == null) { LogDebug("CompletionService is null"); - return Enumerable.Empty(); + return []; } // Obtenir les suggestions de complétion @@ -101,7 +137,7 @@ public RoslynService() if (completionList == null) { LogDebug("CompletionList is null"); - return Enumerable.Empty(); + return []; } // Correction : utiliser ItemsList au lieu de Items (obsolète) @@ -129,8 +165,266 @@ public RoslynService() { LogDebug($"Error in GetCompletionItemsAsync: {ex.Message}"); LogDebug(ex.StackTrace); - return Enumerable.Empty(); + return []; + } + } + + public async Task> GetSignatureHelpItemsAsync(string editorContent, int position, string templateCode) + { + try + { + LogDebug($"GetSignatureHelpItemsAsync called with position {position}"); + + string codeToAnalyze; + int adjustedPosition; + + // Déterminer la région actuelle + var currentRegion = DetermineRegion(editorContent, position, out int positionInBloc); + LogDebug($"Current region: {currentRegion}"); + + // Extraire les parties du code de l'utilisateur selon les régions + string usingsCode = ExtractRegionContent(editorContent, usingsRegex); + string globalCode = ExtractRegionContent(editorContent, globalRegex); + string beforeCode = ExtractRegionContent(editorContent, beforeRegex); + string afterCode = ExtractRegionContent(editorContent, afterRegex); + + // Le code principal est ce qui n'est pas dans une région spéciale + string mainCode = removeAllRegionRegex.Replace(editorContent, ""); + + // Injecter le code dans le template + codeToAnalyze = templateCode + .Replace("//usings", usingsCode) + .Replace("//global", globalCode) + .Replace("//code", mainCode) + .Replace("//before", beforeCode) + .Replace("//after", afterCode); + + LogDebug("Code to analyze created"); + + // Calculer la position ajustée + adjustedPosition = CalculateAdjustedPosition(templateCode, currentRegion, positionInBloc, + usingsCode, globalCode, beforeCode, afterCode); + + LogDebug($"Adjusted position: {adjustedPosition}"); + LogDebug($"Code to analyze: {codeToAnalyze}"); + + // Créer le workspace pour l'analyse + using var workspace = new AdhocWorkspace(); + var projectId = ProjectId.CreateNewId(); + var projectInfo = ProjectInfo.Create( + projectId, VersionStamp.Create(), "SignatureHelpProject", "SignatureHelpProject", + LanguageNames.CSharp, compilationOptions: _compilationOptions, + metadataReferences: _references); + + var project = workspace.AddProject(projectInfo); + + // Ajouter le document avec le code à analyser + var sourceText = SourceText.From(codeToAnalyze); + var document = workspace.AddDocument(project.Id, "SignatureHelp.cs", sourceText); + + // Obtenir l'arbre syntaxique et le modèle sémantique + var syntaxRoot = await document.GetSyntaxRootAsync(); + var semanticModel = await document.GetSemanticModelAsync(); + + if (syntaxRoot == null || semanticModel == null) + { + LogDebug("SyntaxRoot or SemanticModel is null"); + return []; + } + + // Trouver l'invocation de méthode ou l'expression de création à la position actuelle + var token = syntaxRoot.FindToken(adjustedPosition); + var node = token.Parent; + + // Trouver la liste d'arguments la plus proche + while (node != null && + node is not Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax && + node is not Microsoft.CodeAnalysis.CSharp.Syntax.AttributeArgumentListSyntax && + node is not Microsoft.CodeAnalysis.CSharp.Syntax.BracketedArgumentListSyntax) + { + node = node.Parent; + } + + if (node == null) + { + LogDebug("No argument list found at position"); + return []; + } + + // Obtenir la méthode ou le constructeur appelé + var invocationOrCreation = node.Parent; + if (invocationOrCreation == null) + { + LogDebug("No invocation or creation expression found"); + return []; + } + + // Obtenir le symbole pour la méthode + var symbolInfo = semanticModel.GetSymbolInfo(invocationOrCreation); + if (symbolInfo.Symbol == null && (symbolInfo.CandidateSymbols == null || !symbolInfo.CandidateSymbols.Any())) + { + LogDebug("No symbol found for invocation"); + return []; + } + + // Obtenir toutes les méthodes candidates (surcharges) + var methodSymbols = symbolInfo.CandidateSymbols.Length > 0 + ? symbolInfo.CandidateSymbols.ToArray() + : [symbolInfo.Symbol]; + + // Trouver l'index de l'argument actuel + var argumentList = (Microsoft.CodeAnalysis.CSharp.Syntax.BaseArgumentListSyntax)node; + int currentArgumentIndex = 0; + + foreach (var arg in argumentList.Arguments) + { + if (arg.Span.Contains(adjustedPosition)) + { + break; + } + else if (arg.Span.End < adjustedPosition) + { + currentArgumentIndex++; + } + } + + // Si nous sommes juste après une virgule, nous sommes au prochain argument + var prevChar = adjustedPosition > 0 ? codeToAnalyze[adjustedPosition - 1] : '\0'; + //if (prevChar == ',') + //{ + // currentArgumentIndex++; + //} + + // Créer les éléments d'aide à la signature + var result = new List(); + + foreach (var methodSymbol in methodSymbols.OfType()) + { + var signatureItem = new SignatureHelpItem + { + PrefixDisplayParts = methodSymbol.Name + "(", + SeparatorDisplayParts = ", ", + SuffixDisplayParts = ")", + ArgumentIndex = currentArgumentIndex, + Documentation = methodSymbol.GetDocumentationCommentXml() ?? "" + }; + + foreach (var parameter in methodSymbol.Parameters) + { + signatureItem.Parameters.Add(new ParameterItem + { + Name = parameter.Name, + DisplayParts = parameter.Type.ToDisplayString() + " " + parameter.Name, + Documentation = parameter.GetDocumentationCommentXml() ?? "" + }); + } + + result.Add(signatureItem); + } + + LogDebug($"Returning {result.Count} signature help items"); + return result; + } + catch (Exception ex) + { + LogDebug($"Error in GetSignatureHelpItemsAsync: {ex.Message}"); + LogDebug(ex.StackTrace); + return []; + } + } + + private enum CodeRegion + { + Usings, + Global, + MainCodeMethod, + BeforeMethod, + AfterMethod + } + + private CodeRegion DetermineRegion(string editorContent, int position, out int positionInBloc) + { + // Vérifier si la position est dans une région spéciale + var usingsMatch = usingsRegex.Match(editorContent); + if (usingsMatch.Success && IsPositionInMatch(position, usingsMatch)) + { + positionInBloc = position - usingsMatch.Groups[1].Index; + return CodeRegion.Usings; } + + var globalMatch = globalRegex.Match(editorContent); + if (globalMatch.Success && IsPositionInMatch(position, globalMatch)) + { + positionInBloc = position - globalMatch.Groups[1].Index; + return CodeRegion.Global; + } + + var beforeMatch = beforeRegex.Match(editorContent); + if (beforeMatch.Success && IsPositionInMatch(position, beforeMatch)) + { + positionInBloc = position - beforeMatch.Groups[1].Index; + return CodeRegion.BeforeMethod; + } + + var afterMatch = afterRegex.Match(editorContent); + if (afterMatch.Success && IsPositionInMatch(position, afterMatch)) + { + positionInBloc = position - afterMatch.Groups[1].Index; + return CodeRegion.AfterMethod; + } + + positionInBloc = position - Math.Max(0,removeAllRegionRegex.Matches(editorContent).Cast().DefaultIfEmpty().Max(m => m == null ? 0 : m.Index + m.Length)); + // Par défaut, on est dans la méthode Replace + return CodeRegion.MainCodeMethod; + } + + private bool IsPositionInMatch(int position, Match match) + { + return position >= match.Groups[1].Index && + position <= match.Groups[1].Index + match.Groups[1].Length; + } + + private int CalculateAdjustedPosition(string template, CodeRegion region, int positionInBlock, + string usingsCode, string globalCode, + string beforeCode, string afterCode) + { + int adjustedPosition = 0; + + // Selon la région, trouver la position du placeholder dans le template + switch (region) + { + case CodeRegion.Usings: + adjustedPosition = template.IndexOf("//usings") + positionInBlock; + break; + case CodeRegion.Global: + adjustedPosition = template + .Replace("//usings", usingsCode) + .IndexOf("//global") + positionInBlock; + break; + case CodeRegion.BeforeMethod: + adjustedPosition = template + .Replace("//usings", usingsCode) + .Replace("//global", globalCode) + .IndexOf("//before") + positionInBlock; + break; + case CodeRegion.AfterMethod: + adjustedPosition = template + .Replace("//usings", usingsCode) + .Replace("//global", globalCode) + .Replace("//before", beforeCode) + .IndexOf("//after") + positionInBlock; + break; + case CodeRegion.MainCodeMethod: + adjustedPosition = template + .Replace("//usings", usingsCode) + .Replace("//global", globalCode) + .Replace("//before", beforeCode) + .Replace("//after", afterCode) + .IndexOf("//code") + positionInBlock; + break; + } + + return adjustedPosition; } private Model.CompletionItemKind GetCompletionItemKind(ImmutableArray tags) diff --git a/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs index ef6ea86..2cc82f0 100644 --- a/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs +++ b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs @@ -2,7 +2,6 @@ using ICSharpCode.AvalonEdit.CodeCompletion; using RegexDialog.Services; using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -19,7 +18,7 @@ public class RoslynCompletionBehavior private string _templateCode; // Pour le débogage - private bool _isDebugMode = true; + private readonly bool _isDebugMode = false; public RoslynCompletionBehavior(TextEditor editor, string templateCode) { @@ -55,12 +54,14 @@ private async void TextArea_TextEntered(object sender, TextCompositionEventArgs { // Si l'utilisateur tape un caractère qui n'est pas valide pour un identifiant, // fermer la fenêtre de complétion - if (!char.IsLetterOrDigit(e.Text[0]) && e.Text[0] != '_' && e.Text[0] != '.') + if (!char.IsLetterOrDigit(e.Text[0]) && e.Text[0] != '_') { LogDebug("Closing completion window due to non-identifier character"); _completionWindow.Close(); } - return; + + if(e.Text[0] != '.') + return; } // Déclencher l'autocomplétion dans ces cas : @@ -121,18 +122,7 @@ public async Task ShowCompletionWindowAsync() // Définir la position de début et de fin int caretOffset = _editor.CaretOffset; - int startOffset = caretOffset; - - // Si nous sommes après un point, reculer d'un caractère - if (caretOffset > 0 && _editor.Document.GetCharAt(caretOffset - 1) == '.') - { - startOffset = caretOffset; - } - else - { - // Sinon, trouver le début du mot actuel - startOffset = FindWordStart(caretOffset); - } + int startOffset = (caretOffset > 0 && _editor.Document.GetCharAt(caretOffset - 1) == '.') ? caretOffset : FindWordStart(caretOffset); LogDebug($"Setting completion window range: {startOffset} to {caretOffset}"); _completionWindow.StartOffset = startOffset; @@ -141,6 +131,11 @@ public async Task ShowCompletionWindowAsync() LogDebug("Showing completion window"); _completionWindow.Show(); + if(startOffset < caretOffset) + { + _completionWindow.CompletionList.SelectItem(_editor.Document.GetText(startOffset, caretOffset - startOffset)); + } + LogDebug("Completion window should be visible now"); } catch (Exception ex) @@ -175,7 +170,7 @@ private void LogDebug(string message) Console.WriteLine($"[RoslynCompletion] {message}"); // Ajouter un log visuel pour le débogage - System.Diagnostics.Trace.WriteLine($"[RoslynCompletion] {message}"); + Trace.WriteLine($"[RoslynCompletion] {message}"); } } @@ -197,92 +192,4 @@ public void Detach() } } } - - // Attached behavior pour l'utilisation en XAML - public static class RoslynCompletionBehaviorExtension - { - private static readonly Dictionary _behaviors = - new Dictionary(); - - public static RoslynCompletionBehavior GetBehaviorForEditor(TextEditor editor) => _behaviors.TryGetValue(editor, out RoslynCompletionBehavior behavior) ? behavior : null; - - #region EnableCompletion Property - - public static readonly DependencyProperty EnableCompletionProperty = - DependencyProperty.RegisterAttached( - "EnableCompletion", - typeof(bool), - typeof(RoslynCompletionBehaviorExtension), - new PropertyMetadata(false, OnEnableCompletionChanged)); - - public static bool GetEnableCompletion(DependencyObject obj) - { - return (bool)obj.GetValue(EnableCompletionProperty); - } - - public static void SetEnableCompletion(DependencyObject obj, bool value) - { - obj.SetValue(EnableCompletionProperty, value); - } - - #endregion - - #region TemplateCode Property - - public static readonly DependencyProperty TemplateCodeProperty = - DependencyProperty.RegisterAttached( - "TemplateCode", - typeof(string), - typeof(RoslynCompletionBehaviorExtension), - new PropertyMetadata(string.Empty, OnTemplateCodeChanged)); - - public static string GetTemplateCode(DependencyObject obj) - { - return (string)obj.GetValue(TemplateCodeProperty); - } - - public static void SetTemplateCode(DependencyObject obj, string value) - { - obj.SetValue(TemplateCodeProperty, value); - } - - #endregion - - private static void OnEnableCompletionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (!(d is TextEditor editor)) - return; - - var enableCompletion = (bool)e.NewValue; - var templateCode = GetTemplateCode(editor); - - if (enableCompletion) - { - if (!_behaviors.ContainsKey(editor)) - { - var behavior = new RoslynCompletionBehavior(editor, templateCode); - _behaviors[editor] = behavior; - } - } - else - { - if (_behaviors.TryGetValue(editor, out var behavior)) - { - behavior.Detach(); - _behaviors.Remove(editor); - } - } - } - - private static void OnTemplateCodeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (!(d is TextEditor editor)) - return; - - if (_behaviors.TryGetValue(editor, out var behavior)) - { - behavior.UpdateTemplateCode((string)e.NewValue); - } - } - } } \ No newline at end of file diff --git a/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehaviorExtension.cs b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehaviorExtension.cs new file mode 100644 index 0000000..c32b8cc --- /dev/null +++ b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehaviorExtension.cs @@ -0,0 +1,118 @@ +using ICSharpCode.AvalonEdit; +using System.Collections.Generic; +using System.Windows; + +namespace RegexDialog.Behaviors +{ + // Attached behavior pour l'utilisation en XAML + public static class RoslynCompletionBehaviorExtension + { + private static readonly Dictionary _behaviors = []; + + private static readonly Dictionary _signatureHelpBehaviors = []; + + public static RoslynCompletionBehavior GetBehaviorForEditor(TextEditor editor) => _behaviors.TryGetValue(editor, out RoslynCompletionBehavior behavior) ? behavior : null; + + public static RoslynSignatureHelpBehavior GetSignatureHelpBehaviorForEditor(TextEditor editor) => _signatureHelpBehaviors.TryGetValue(editor, out RoslynSignatureHelpBehavior behavior) ? behavior : null; + + #region EnableCompletion Property + + public static readonly DependencyProperty EnableCompletionProperty = + DependencyProperty.RegisterAttached( + "EnableCompletion", + typeof(bool), + typeof(RoslynCompletionBehaviorExtension), + new PropertyMetadata(false, OnEnableCompletionChanged)); + + public static bool GetEnableCompletion(DependencyObject obj) + { + return (bool)obj.GetValue(EnableCompletionProperty); + } + + public static void SetEnableCompletion(DependencyObject obj, bool value) + { + obj.SetValue(EnableCompletionProperty, value); + } + + #endregion + + #region TemplateCode Property + + public static readonly DependencyProperty TemplateCodeProperty = + DependencyProperty.RegisterAttached( + "TemplateCode", + typeof(string), + typeof(RoslynCompletionBehaviorExtension), + new PropertyMetadata(string.Empty, OnTemplateCodeChanged)); + + public static string GetTemplateCode(DependencyObject obj) + { + return (string)obj.GetValue(TemplateCodeProperty); + } + + public static void SetTemplateCode(DependencyObject obj, string value) + { + obj.SetValue(TemplateCodeProperty, value); + } + + #endregion + + private static void OnEnableCompletionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not TextEditor editor) + { + return; + } + + var enableCompletion = (bool)e.NewValue; + var templateCode = GetTemplateCode(editor); + + if (enableCompletion) + { + if (!_behaviors.ContainsKey(editor)) + { + var behavior = new RoslynCompletionBehavior(editor, templateCode); + _behaviors[editor] = behavior; + } + + if (!_signatureHelpBehaviors.ContainsKey(editor)) + { + var signatureHelpBehavior = new RoslynSignatureHelpBehavior(editor, templateCode); + _signatureHelpBehaviors[editor] = signatureHelpBehavior; + } + } + else + { + if (_behaviors.TryGetValue(editor, out var behavior)) + { + behavior.Detach(); + _behaviors.Remove(editor); + } + + if (_signatureHelpBehaviors.TryGetValue(editor, out var signatureHelpBehavior)) + { + signatureHelpBehavior.Detach(); + _signatureHelpBehaviors.Remove(editor); + } + } + } + + private static void OnTemplateCodeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not TextEditor editor) + { + return; + } + + if (_behaviors.TryGetValue(editor, out var behavior)) + { + behavior.UpdateTemplateCode((string)e.NewValue); + } + + if (_signatureHelpBehaviors.TryGetValue(editor, out var signatureHelpBehavior)) + { + signatureHelpBehavior.UpdateTemplateCode((string)e.NewValue); + } + } + } +} \ No newline at end of file diff --git a/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs b/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs new file mode 100644 index 0000000..c620469 --- /dev/null +++ b/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs @@ -0,0 +1,440 @@ +using ICSharpCode.AvalonEdit; +using ICSharpCode.AvalonEdit.Rendering; +using Microsoft.CodeAnalysis; +using Mono.CSharp; +using RegexDialog.Model; +using RegexDialog.Services; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; + +namespace RegexDialog.Behaviors +{ + public class RoslynSignatureHelpBehavior + { + private readonly TextEditor _editor; + private readonly RoslynService _roslynService; + private string _templateCode; + private MethodSignatureTooltip _signatureTooltip; + private readonly bool _isDebugMode = false; + + public RoslynSignatureHelpBehavior(TextEditor editor, string templateCode) + { + _editor = editor ?? throw new ArgumentNullException(nameof(editor)); + _templateCode = templateCode ?? throw new ArgumentNullException(nameof(templateCode)); + _roslynService = new RoslynService(); + + // Attacher les événements + _editor.TextArea.TextEntered += TextArea_TextEntered; + _editor.TextArea.PreviewKeyDown += TextArea_KeyDown; + _editor.TextArea.Caret.PositionChanged += Caret_PositionChanged; + + LogDebug("RoslynSignatureHelpBehavior initialized"); + } + + private async void TextArea_TextEntered(object sender, TextCompositionEventArgs e) + { + LogDebug($"TextEntered: '{e.Text}'"); + + // Afficher l'aide à la signature lors de l'ouverture d'une parenthèse ou de la saisie d'une virgule + if (e.Text == "(" || e.Text == ",") + { + await ShowSignatureHelpAsync(); + } + } + + private async void Caret_PositionChanged(object sender, EventArgs e) + { + var caretOffset = _editor.CaretOffset; + var document = _editor.Document; + + if (_signatureTooltip != null && _signatureTooltip.IsOpen) + { + // Vérifier si nous sommes toujours dans une liste d'arguments + + // Trouver la parenthèse fermante correspondante + int openParenCount = 1; + int closeParenOffset = -1; + + for (int i = _signatureTooltip.OpenParenOffset + 1; i < document.TextLength; i++) + { + char c = document.GetCharAt(i); + if (c == '(') + openParenCount++; + else if (c == ')') + { + openParenCount--; + if (openParenCount == 0) + { + closeParenOffset = i; + break; + } + } + } + + // Si nous avons trouvé la parenthèse fermante et que le caret est entre les parenthèses + if (closeParenOffset != -1 && + caretOffset > _signatureTooltip.OpenParenOffset && + caretOffset <= closeParenOffset) + { + // Mettre à jour l'index du paramètre actuel + await UpdateSignatureHelpAsync(); + } + else + { + // Fermer le tooltip si nous ne sommes plus dans la liste d'arguments + _signatureTooltip.IsOpen = false; + _signatureTooltip = null; + } + } + else if(caretOffset > 0 && (document.Text[caretOffset - 1] == '(' || document.Text[caretOffset - 1] == ',')) + { + if(_editor.IsInitialized) + await ShowSignatureHelpAsync(); + } + } + + private async void TextArea_KeyDown(object sender, KeyEventArgs e) + { + if (_signatureTooltip == null || !_signatureTooltip.IsOpen) + return; + + // Naviguer entre les surcharges avec les touches fléchées haut/bas + if (e.Key == Key.Up) + { + _signatureTooltip.SelectPreviousOverload(); + e.Handled = true; + } + else if (e.Key == Key.Down) + { + _signatureTooltip.SelectNextOverload(); + e.Handled = true; + } + // Fermer l'aide à la signature avec Escape + else if (e.Key == Key.Escape) + { + _signatureTooltip.IsOpen = false; + _signatureTooltip = null; + e.Handled = true; + } + // Mettre à jour la mise en évidence des paramètres lors de la navigation dans l'appel de méthode + else if (e.Key == Key.Left || e.Key == Key.Right || e.Key == Key.Back || e.Key == Key.Delete) + { + await Task.Delay(10); // Petit délai pour laisser l'éditeur se mettre à jour + await UpdateSignatureHelpAsync(); + } + } + + private async Task ShowSignatureHelpAsync() + { + try + { + LogDebug("ShowSignatureHelpAsync called"); + + // Obtenir les éléments d'aide à la signature de Roslyn + var signatureHelpItems = await _roslynService.GetSignatureHelpItemsAsync( + _editor.Text, + _editor.CaretOffset, + _templateCode); + + if (signatureHelpItems == null || !signatureHelpItems.Any()) + { + LogDebug("No signature help items returned from Roslyn"); + return; + } + + LogDebug($"Received {signatureHelpItems.Count()} signature help items from Roslyn"); + + if (_signatureTooltip != null) + { + _signatureTooltip.IsOpen = false; + } + + // Trouver la position de la parenthèse ouvrante + int openParenOffset = _editor.CaretOffset - 1; + while (openParenOffset >= 0) + { + if (_editor.Document.GetCharAt(openParenOffset) == '(') + break; + openParenOffset--; + } + + if (openParenOffset < 0) + { + LogDebug("Could not find opening parenthesis"); + return; + } + + // Créer et afficher le tooltip + _signatureTooltip = new MethodSignatureTooltip(_editor) + { + OpenParenOffset = openParenOffset + }; + + _signatureTooltip.SetSignatureHelpItems(signatureHelpItems); + + // Positionner le tooltip près de la parenthèse ouvrante + var textView = _editor.TextArea.TextView; + var visualPos = textView.GetVisualPosition( + new TextViewPosition(_editor.Document.GetLocation(openParenOffset)), + VisualYPosition.LineTop); + + _signatureTooltip.HorizontalOffset = visualPos.X; + _signatureTooltip.IsOpen = true; + _signatureTooltip.VerticalOffset = visualPos.Y + textView.DefaultLineHeight; + + LogDebug("Signature help tooltip should be visible now"); + } + catch (Exception ex) + { + LogDebug($"Error in ShowSignatureHelpAsync: {ex.Message}"); + MessageBox.Show($"Error in ShowSignatureHelpAsync: {ex.Message}\n\n{ex.StackTrace}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private async Task UpdateSignatureHelpAsync() + { + if (_signatureTooltip == null || !_signatureTooltip.IsOpen) + return; + + try + { + // Obtenir l'aide à la signature mise à jour pour actualiser la mise en évidence des paramètres + var signatureHelpItems = await _roslynService.GetSignatureHelpItemsAsync( + _editor.Text, + _editor.CaretOffset, + _templateCode); + + if (signatureHelpItems == null || !signatureHelpItems.Any()) + { + _signatureTooltip.IsOpen = false; + _signatureTooltip = null; + return; + } + + // Mettre à jour le tooltip avec les nouvelles données + _signatureTooltip.UpdateSignatureHelp(signatureHelpItems); + } + catch (Exception ex) + { + LogDebug($"Error in UpdateSignatureHelpAsync: {ex.Message}"); + } + } + + private void LogDebug(string message) + { + if (_isDebugMode) + { + Debug.WriteLine($"[RoslynSignatureHelp] {message}"); + Console.WriteLine($"[RoslynSignatureHelp] {message}"); + Trace.WriteLine($"[RoslynSignatureHelp] {message}"); + } + } + + public void UpdateTemplateCode(string templateCode) + { + _templateCode = templateCode ?? throw new ArgumentNullException(nameof(templateCode)); + } + + public void Detach() + { + LogDebug("Detaching behavior"); + _editor.TextArea.TextEntered -= TextArea_TextEntered; + _editor.TextArea.KeyDown -= TextArea_KeyDown; + _editor.TextArea.Caret.PositionChanged -= Caret_PositionChanged; + + if (_signatureTooltip != null) + { + _signatureTooltip.IsOpen = false; + _signatureTooltip = null; + } + } + } + + // Tooltip pour afficher les signatures de méthode et les paramètres + public class MethodSignatureTooltip : ToolTip + { + private readonly TextEditor _editor; + private readonly StackPanel _mainPanel; + private readonly TextBlock _signatureText; + private readonly TextBlock _documentationText; + private readonly TextBlock _navigationHint; + private List _signatureHelpItems; + private int _selectedOverloadIndex = 0; + private int _currentParameterIndex = 0; + + public int OpenParenOffset { get; set; } + + public MethodSignatureTooltip(TextEditor editor) + { + _editor = editor; + + // Créer les éléments d'interface utilisateur + _mainPanel = new StackPanel + { + Margin = new Thickness(5) + }; + + // Texte de la signature + _signatureText = new TextBlock + { + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 0, 0, 5) + }; + _mainPanel.Children.Add(_signatureText); + + // Documentation + _documentationText = new TextBlock + { + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 0, 0, 5), + MaxWidth = 400 + }; + _mainPanel.Children.Add(_documentationText); + + // Indice de navigation + _navigationHint = new TextBlock + { + Text = "Use ↑↓ to navigate betweeen overloads", + FontStyle = FontStyles.Italic, + Foreground = Brushes.Gray, + FontSize = 10 + }; + _mainPanel.Children.Add(_navigationHint); + + // Configurer le tooltip + Content = _mainPanel; + this.PlacementTarget = editor; + Placement = System.Windows.Controls.Primitives.PlacementMode.Relative; + StaysOpen = true; + IsOpen = false; + } + + public void SetSignatureHelpItems(IEnumerable items) + { + _signatureHelpItems = [.. items]; + _selectedOverloadIndex = 0; + UpdateUI(); + } + + public void UpdateSignatureHelp(IEnumerable items) + { + var newItems = items.ToList(); + + // Essayer de garder la même surcharge sélectionnée si possible + var currentSignature = _selectedOverloadIndex < _signatureHelpItems.Count + ? _signatureHelpItems[_selectedOverloadIndex] + : null; + + _signatureHelpItems = newItems; + + if (currentSignature != null) + { + // Trouver la même signature dans la nouvelle liste + for (int i = 0; i < _signatureHelpItems.Count; i++) + { + if (_signatureHelpItems[i].ToString() == currentSignature.ToString()) + { + _selectedOverloadIndex = i; + break; + } + } + } + + // Mettre à jour l'index du paramètre à partir de la nouvelle aide à la signature + if (_signatureHelpItems.Count > 0 && _selectedOverloadIndex < _signatureHelpItems.Count) + { + _currentParameterIndex = _signatureHelpItems[_selectedOverloadIndex].ArgumentIndex; + } + + UpdateUI(); + } + + public void SelectNextOverload() + { + if (_signatureHelpItems.Count > 0) + { + _selectedOverloadIndex = (_selectedOverloadIndex + 1) % _signatureHelpItems.Count; + UpdateUI(); + } + } + + public void SelectPreviousOverload() + { + if (_signatureHelpItems.Count > 0) + { + _selectedOverloadIndex = (_selectedOverloadIndex - 1 + _signatureHelpItems.Count) % _signatureHelpItems.Count; + UpdateUI(); + } + } + + private void UpdateUI() + { + _signatureText.Inlines.Clear(); + + if (_signatureHelpItems == null || _signatureHelpItems.Count == 0) + return; + + // Obtenir la signature actuelle + var currentSignature = _signatureHelpItems[_selectedOverloadIndex]; + _currentParameterIndex = currentSignature.ArgumentIndex; + + // Afficher le nombre de surcharges + if (_signatureHelpItems.Count > 1) + { + _signatureText.Inlines.Add(new Run($"({_selectedOverloadIndex + 1} sur {_signatureHelpItems.Count}) ") + { + FontWeight = FontWeights.Bold, + Foreground = Brushes.Gray + }); + } + + // Formater la signature avec mise en évidence des paramètres + _signatureText.Inlines.Add(new Run(currentSignature.PrefixDisplayParts)); + + for (int i = 0; i < currentSignature.Parameters.Count; i++) + { + var param = currentSignature.Parameters[i]; + + if (i > 0) + { + _signatureText.Inlines.Add(new Run(currentSignature.SeparatorDisplayParts)); + } + + // Mettre en évidence le paramètre actuel + var paramRun = new Run(param.DisplayParts); + + if (i == _currentParameterIndex) + { + paramRun.FontWeight = FontWeights.Bold; + paramRun.Background = new SolidColorBrush(Color.FromArgb(50, 100, 100, 255)); + } + + _signatureText.Inlines.Add(paramRun); + } + + _signatureText.Inlines.Add(new Run(currentSignature.SuffixDisplayParts)); + + // Mettre à jour la documentation + if (_currentParameterIndex >= 0 && _currentParameterIndex < currentSignature.Parameters.Count) + { + var param = currentSignature.Parameters[_currentParameterIndex]; + _documentationText.Text = $"{param.Name}: {param.Documentation}"; + } + else + { + _documentationText.Text = currentSignature.Documentation; + } + + // Afficher l'indice de navigation uniquement s'il y a plusieurs surcharges + _navigationHint.Visibility = _signatureHelpItems.Count > 1 ? Visibility.Visible : Visibility.Collapsed; + } + } +} \ No newline at end of file diff --git a/RegexDialog/UIHelpers/Converters/EnumBooleanConverter.cs b/RegexDialog/UIHelpers/Converters/EnumBooleanConverter.cs index 931eed5..d75a77c 100644 --- a/RegexDialog/UIHelpers/Converters/EnumBooleanConverter.cs +++ b/RegexDialog/UIHelpers/Converters/EnumBooleanConverter.cs @@ -24,7 +24,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - if (!(parameter is string parameterString)) + if (!(parameter is string parameterString) || !(bool)value) return DependencyProperty.UnsetValue; return Enum.Parse(targetType, parameterString);