From fd803c26975310d1f61fd298afcc44cce33de899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geiser?= Date: Mon, 31 Mar 2025 10:02:49 +0200 Subject: [PATCH 1/9] feat: working intellisense with special regions --- .../CSharpReplaceAutoCompleteTemplate.txt | 14 +- RegexDialog/Sevices/RoslynService.cs | 143 +++++++++++++++++- 2 files changed, 145 insertions(+), 12 deletions(-) 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..aae4bee 100644 --- a/RegexDialog/Sevices/RoslynService.cs +++ b/RegexDialog/Sevices/RoslynService.cs @@ -24,6 +24,13 @@ 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; @@ -33,6 +40,7 @@ public RoslynService() _references = new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(File).Assembly.Location), MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location), MetadataReference.CreateFromFile(typeof(Regex).Assembly.Location), MetadataReference.CreateFromFile(typeof(Match).Assembly.Location), @@ -53,6 +61,12 @@ public RoslynService() LogDebug("RoslynService initialized"); } + 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) { @@ -60,14 +74,39 @@ public RoslynService() { 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; + int positionInBloc; + + // Déterminer la région actuelle + var currentRegion = DetermineRegion(editorContent, position, out 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, mainCode); + + LogDebug($"Adjusted position: {adjustedPosition}"); - LogDebug($"Code marker position: {codeMarkerPosition}"); LogDebug($"Adjusted position: {adjustedPosition}"); LogDebug($"Code to analyze: {codeToAnalyze}"); @@ -133,6 +172,100 @@ public RoslynService() } } + private enum CodeRegion + { + Usings, + Global, + ReplaceMethod, + 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().Max(m => m.Index + m.Length)); + // Par défaut, on est dans la méthode Replace + return CodeRegion.ReplaceMethod; + } + + 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, string mainCode) + { + 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.ReplaceMethod: + 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) { if (tags.Contains(WellKnownTags.Class)) From c188b0cfeb9a049918da34896b85567809ddc2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geiser?= Date: Mon, 31 Mar 2025 10:03:26 +0200 Subject: [PATCH 2/9] fix: stucked TextSource RadioButton --- RegexDialog/UIHelpers/Converters/EnumBooleanConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 51cb48aa2edfbd1b6044fd5b1d040486b9109a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geiser?= Date: Mon, 31 Mar 2025 14:17:53 +0200 Subject: [PATCH 3/9] fix: autocomplete position calc exception --- RegexDialog/Sevices/RoslynService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/RegexDialog/Sevices/RoslynService.cs b/RegexDialog/Sevices/RoslynService.cs index aae4bee..7476aea 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; @@ -37,8 +37,8 @@ public class RoslynService public RoslynService() { // Charger toutes les références nécessaires - _references = new[] - { + _references = + [ MetadataReference.CreateFromFile(typeof(object).Assembly.Location), MetadataReference.CreateFromFile(typeof(File).Assembly.Location), MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location), @@ -51,7 +51,7 @@ public RoslynService() MetadataReference.CreateFromFile(typeof(IEnumerable<>).Assembly.Location), MetadataReference.CreateFromFile(typeof(CultureInfo).Assembly.Location), MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location) - }; + ]; _compilationOptions = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, @@ -212,7 +212,7 @@ private CodeRegion DetermineRegion(string editorContent, int position, out int p return CodeRegion.AfterMethod; } - positionInBloc = position - Math.Max(0,removeAllRegionRegex.Matches(editorContent).Cast().Max(m => m.Index + m.Length)); + 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.ReplaceMethod; } From 06f8222785fcf67b2f7b1b9335194ecc7921ad28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geiser?= Date: Mon, 31 Mar 2025 14:18:28 +0200 Subject: [PATCH 4/9] feat: add first autocompletion on C#SCript text source --- RegexDialog/RegExToolDialog.xaml | 4 +++- RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) 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/UIHelpers/Behaviors/RoslynCompletionBehavior.cs b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs index ef6ea86..93f437f 100644 --- a/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs +++ b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs @@ -175,7 +175,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}"); } } From 633729aa8c8329863473eefcec2feed34d94b050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geiser?= Date: Tue, 1 Apr 2025 14:31:23 +0200 Subject: [PATCH 5/9] Implements signature help functionality Adds signature help using Roslyn to provide contextual information about method parameters and overloads as the user types. This includes creating a tooltip that displays the method signature, highlights the current parameter, and provides documentation. Also enables navigation between method overloads using arrow keys. --- RegexDialog/Model/IntellisenseModels.cs | 29 +- RegexDialog/RegExToolDialog.xaml.cs | 2 +- RegexDialog/Sevices/RoslynService.cs | 192 +++++++- .../Behaviors/RoslynCompletionBehavior.cs | 52 ++- .../Behaviors/RoslynSignatureHelpBehavior.cs | 434 ++++++++++++++++++ 5 files changed, 672 insertions(+), 37 deletions(-) create mode 100644 RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs diff --git a/RegexDialog/Model/IntellisenseModels.cs b/RegexDialog/Model/IntellisenseModels.cs index f9f580e..269f6e1 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; @@ -25,10 +27,7 @@ public ImageSource Image { get { - if (_image == null) - { - _image = GetImageForKind(Kind); - } + _image ??= GetImageForKind(Kind); return _image; } } @@ -97,4 +96,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.cs b/RegexDialog/RegExToolDialog.xaml.cs index 4306bcb..d929869 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/Sevices/RoslynService.cs b/RegexDialog/Sevices/RoslynService.cs index 7476aea..85d77ce 100644 --- a/RegexDialog/Sevices/RoslynService.cs +++ b/RegexDialog/Sevices/RoslynService.cs @@ -16,6 +16,7 @@ using System.ComponentModel; using System.Globalization; using Microsoft.CodeAnalysis.Tags; +using RegexDialog.Model; namespace RegexDialog.Services { @@ -32,7 +33,7 @@ public class RoslynService 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() { @@ -40,13 +41,11 @@ public RoslynService() _references = [ MetadataReference.CreateFromFile(typeof(object).Assembly.Location), - MetadataReference.CreateFromFile(typeof(File).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), @@ -67,8 +66,7 @@ private string ExtractRegionContent(string editorContent, Regex regionRegex) return match.Success ? match.Groups[1].Value : ""; } - public async Task> GetCompletionItemsAsync( - string editorContent, int position, string templateCode) + public async Task> GetCompletionItemsAsync(string editorContent, int position, string templateCode) { try { @@ -76,10 +74,9 @@ private string ExtractRegionContent(string editorContent, Regex regionRegex) string codeToAnalyze; int adjustedPosition; - int positionInBloc; // Déterminer la région actuelle - var currentRegion = DetermineRegion(editorContent, position, out positionInBloc); + var currentRegion = DetermineRegion(editorContent, position, out int positionInBloc); LogDebug($"Current region: {currentRegion}"); // Extraire les parties du code de l'utilisateur selon les régions @@ -103,7 +100,7 @@ private string ExtractRegionContent(string editorContent, Regex regionRegex) // Calculer la position ajustée adjustedPosition = CalculateAdjustedPosition(templateCode, currentRegion, positionInBloc, - usingsCode, globalCode, beforeCode, afterCode, mainCode); + usingsCode, globalCode, beforeCode, afterCode); LogDebug($"Adjusted position: {adjustedPosition}"); @@ -131,7 +128,7 @@ private string ExtractRegionContent(string editorContent, Regex regionRegex) if (completionService == null) { LogDebug("CompletionService is null"); - return Enumerable.Empty(); + return []; } // Obtenir les suggestions de complétion @@ -140,7 +137,7 @@ private string ExtractRegionContent(string editorContent, Regex regionRegex) if (completionList == null) { LogDebug("CompletionList is null"); - return Enumerable.Empty(); + return []; } // Correction : utiliser ItemsList au lieu de Items (obsolète) @@ -168,7 +165,172 @@ private string ExtractRegionContent(string editorContent, Regex regionRegex) { 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 []; } } @@ -176,7 +338,7 @@ private enum CodeRegion { Usings, Global, - ReplaceMethod, + MainCodeMethod, BeforeMethod, AfterMethod } @@ -214,7 +376,7 @@ private CodeRegion DetermineRegion(string editorContent, int position, out int p 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.ReplaceMethod; + return CodeRegion.MainCodeMethod; } private bool IsPositionInMatch(int position, Match match) @@ -225,7 +387,7 @@ private bool IsPositionInMatch(int position, Match match) private int CalculateAdjustedPosition(string template, CodeRegion region, int positionInBlock, string usingsCode, string globalCode, - string beforeCode, string afterCode, string mainCode) + string beforeCode, string afterCode) { int adjustedPosition = 0; @@ -253,7 +415,7 @@ private int CalculateAdjustedPosition(string template, CodeRegion region, int po .Replace("//before", beforeCode) .IndexOf("//after") + positionInBlock; break; - case CodeRegion.ReplaceMethod: + case CodeRegion.MainCodeMethod: adjustedPosition = template .Replace("//usings", usingsCode) .Replace("//global", globalCode) diff --git a/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs index 93f437f..6b8b927 100644 --- a/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs +++ b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs @@ -19,7 +19,7 @@ public class RoslynCompletionBehavior private string _templateCode; // Pour le débogage - private bool _isDebugMode = true; + private readonly bool _isDebugMode = true; public RoslynCompletionBehavior(TextEditor editor, string templateCode) { @@ -121,18 +121,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 +130,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) @@ -201,11 +195,14 @@ public void Detach() // Attached behavior pour l'utilisation en XAML public static class RoslynCompletionBehaviorExtension { - private static readonly Dictionary _behaviors = - new Dictionary(); + 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 = @@ -250,8 +247,10 @@ public static void SetTemplateCode(DependencyObject obj, string value) private static void OnEnableCompletionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - if (!(d is TextEditor editor)) + if (d is not TextEditor editor) + { return; + } var enableCompletion = (bool)e.NewValue; var templateCode = GetTemplateCode(editor); @@ -263,6 +262,12 @@ private static void OnEnableCompletionChanged(DependencyObject d, DependencyProp var behavior = new RoslynCompletionBehavior(editor, templateCode); _behaviors[editor] = behavior; } + + if (!_signatureHelpBehaviors.ContainsKey(editor)) + { + var signatureHelpBehavior = new RoslynSignatureHelpBehavior(editor, templateCode); + _signatureHelpBehaviors[editor] = signatureHelpBehavior; + } } else { @@ -271,18 +276,31 @@ private static void OnEnableCompletionChanged(DependencyObject d, DependencyProp 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 TextEditor editor)) + 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..ae5bf4a --- /dev/null +++ b/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs @@ -0,0 +1,434 @@ +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 = true; + + 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) + { + if (_signatureTooltip != null && _signatureTooltip.IsOpen) + { + // Vérifier si nous sommes toujours dans une liste d'arguments + var caretOffset = _editor.CaretOffset; + var document = _editor.Document; + + // 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; + } + } + } + + 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 From f5cd08b5d428631ebf6416b420fb8e6a7925db0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geiser?= Date: Tue, 1 Apr 2025 15:31:00 +0200 Subject: [PATCH 6/9] Improves code completion and signature help Adds caching for completion item images to improve performance. Automatically adds parentheses for method and extension completions. Closes completion window only for truly invalid identifier characters. Displays signature help when typing '(' or ','. --- RegexDialog/Model/IntellisenseModels.cs | 14 +++++++++++++- .../Behaviors/RoslynCompletionBehavior.cs | 6 ++++-- .../Behaviors/RoslynSignatureHelpBehavior.cs | 10 ++++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/RegexDialog/Model/IntellisenseModels.cs b/RegexDialog/Model/IntellisenseModels.cs index 269f6e1..3ea06d7 100644 --- a/RegexDialog/Model/IntellisenseModels.cs +++ b/RegexDialog/Model/IntellisenseModels.cs @@ -13,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; } @@ -34,13 +36,21 @@ public ImageSource 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"; @@ -59,6 +69,8 @@ private ImageSource GetImageForKind(CompletionItemKind kind) bitmap.EndInit(); bitmap.Freeze(); // Pour une utilisation thread-safe + imageCache[kind] = bitmap; + return bitmap; } diff --git a/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs index 6b8b927..916d350 100644 --- a/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs +++ b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs @@ -55,12 +55,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 : diff --git a/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs b/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs index ae5bf4a..a6fde19 100644 --- a/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs +++ b/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs @@ -52,11 +52,12 @@ private async void TextArea_TextEntered(object sender, TextCompositionEventArgs 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 - var caretOffset = _editor.CaretOffset; - var document = _editor.Document; // Trouver la parenthèse fermante correspondante int openParenCount = 1; @@ -93,6 +94,11 @@ private async void Caret_PositionChanged(object sender, EventArgs e) _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) From b1ae5ad7297b0673fc28c2e1746a962f2b3f82d0 Mon Sep 17 00:00:00 2001 From: CodingSeb Date: Tue, 1 Apr 2025 20:09:01 +0200 Subject: [PATCH 7/9] Moves attached behavior to extension class Separates the attached behavior logic into a dedicated extension class for better organization and clarity. This enhances code maintainability and promotes reusability of the attached behavior functionality. --- .../Behaviors/RoslynCompletionBehavior.cs | 113 ----------------- .../RoslynCompletionBehaviorExtension.cs | 118 ++++++++++++++++++ 2 files changed, 118 insertions(+), 113 deletions(-) create mode 100644 RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehaviorExtension.cs diff --git a/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs index 916d350..a79733d 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; @@ -193,116 +192,4 @@ public void Detach() } } } - - // 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/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 From fff13f7ca5f6151d91cb599dd606171bb12dbd85 Mon Sep 17 00:00:00 2001 From: CodingSeb Date: Tue, 1 Apr 2025 20:19:05 +0200 Subject: [PATCH 8/9] Disables debug mode Disables debug mode in Roslyn completion and signature help behaviors. This improves performance in production environments by removing unnecessary debugging overhead. --- RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs | 2 +- RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs index a79733d..2cc82f0 100644 --- a/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs +++ b/RegexDialog/UIHelpers/Behaviors/RoslynCompletionBehavior.cs @@ -18,7 +18,7 @@ public class RoslynCompletionBehavior private string _templateCode; // Pour le débogage - private readonly bool _isDebugMode = true; + private readonly bool _isDebugMode = false; public RoslynCompletionBehavior(TextEditor editor, string templateCode) { diff --git a/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs b/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs index a6fde19..c620469 100644 --- a/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs +++ b/RegexDialog/UIHelpers/Behaviors/RoslynSignatureHelpBehavior.cs @@ -23,7 +23,7 @@ public class RoslynSignatureHelpBehavior private readonly RoslynService _roslynService; private string _templateCode; private MethodSignatureTooltip _signatureTooltip; - private readonly bool _isDebugMode = true; + private readonly bool _isDebugMode = false; public RoslynSignatureHelpBehavior(TextEditor editor, string templateCode) { From 6a9df5244375c6f9eed16b59902667bf16c29e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geiser?= Date: Mon, 7 Apr 2025 11:38:25 +0200 Subject: [PATCH 9/9] Removes unnecessary line break Removes an unnecessary line break in the GetSignatureHelpItemsAsync method declaration for improved code readability. --- RegexDialog/Sevices/RoslynService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RegexDialog/Sevices/RoslynService.cs b/RegexDialog/Sevices/RoslynService.cs index 85d77ce..a93afc1 100644 --- a/RegexDialog/Sevices/RoslynService.cs +++ b/RegexDialog/Sevices/RoslynService.cs @@ -169,8 +169,7 @@ private string ExtractRegionContent(string editorContent, Regex regionRegex) } } - public async Task> GetSignatureHelpItemsAsync( - string editorContent, int position, string templateCode) + public async Task> GetSignatureHelpItemsAsync(string editorContent, int position, string templateCode) { try {