From 6f979c50071e70b2b6dc9486b0fd501462f23638 Mon Sep 17 00:00:00 2001 From: john pierson Date: Fri, 13 Jun 2025 11:27:25 -0600 Subject: [PATCH 01/10] Add local node help lookup as fallback for DNA --- src/DynamoCore/Models/DynamoModel.cs | 2 +- .../Properties/AssemblyInfo.cs | 1 + .../NodeAutoCompleteBarViewModel.cs | 87 ++++++++++++++++++- .../ViewModels/SingleResultItem.cs | 11 +++ 4 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/DynamoCore/Models/DynamoModel.cs b/src/DynamoCore/Models/DynamoModel.cs index 5a8b921de8a..2ff460b5cee 100644 --- a/src/DynamoCore/Models/DynamoModel.cs +++ b/src/DynamoCore/Models/DynamoModel.cs @@ -2431,7 +2431,7 @@ private void SetPeriodicEvaluation(WorkspaceModel ws) } } - private bool OpenJsonFile( + internal bool OpenJsonFile( string filePath, string fileContents, DynamoPreferencesData dynamoPreferences, diff --git a/src/DynamoUtilities/Properties/AssemblyInfo.cs b/src/DynamoUtilities/Properties/AssemblyInfo.cs index d20f423c2f3..1af76109f57 100644 --- a/src/DynamoUtilities/Properties/AssemblyInfo.cs +++ b/src/DynamoUtilities/Properties/AssemblyInfo.cs @@ -31,3 +31,4 @@ [assembly: InternalsVisibleTo("LibraryViewExtensionWebView2")] [assembly: InternalsVisibleTo("Notifications")] [assembly: InternalsVisibleTo("SystemTestServices")] +[assembly: InternalsVisibleTo("NodeAutoCompleteViewExtension")] diff --git a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs index 226f13a00f5..dbda1e168af 100644 --- a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs +++ b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs @@ -536,7 +536,8 @@ private IEnumerable GetNodeAutocompleMLResults() AutocompleteMLTitle = Resources.AutocompleteNoRecommendationsTitle; AutocompleteMLMessage = Resources.AutocompleteNoRecommendationsMessage; Analytics.TrackEvent(Actions.View, Categories.NodeAutoCompleteOperations, "NoRecommendation"); - return new List(); + + return TryGetLocalAutoCompleteResult(); } ServiceVersion = MLresults.Version; var results = new List(); @@ -609,6 +610,90 @@ private IEnumerable GetNodeAutocompleMLResults() return results; } + + /// + /// Use local node help files to at least return one result that will work. + /// + /// + private List TryGetLocalAutoCompleteResult() + { + var dynamoModel = this.dynamoViewModel.Model; + + var nodeHelpDocPath = new DirectoryInfo(Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, + Configurations.DynamoNodeHelpDocs)); + + var selectedNode = this.PortViewModel.NodeViewModel.NodeModel; + var selectedPortModel = this.PortViewModel.PortModel; + + string nodeFullName; + + switch (selectedNode) + { + case DSFunction dsFunction: + string fullSignature = dsFunction.FunctionSignature; + nodeFullName = fullSignature.Split('@')[0]; + break; + case Function function: + string fullFunctionSignature = function.FunctionSignature.ToString(); + nodeFullName = fullFunctionSignature.Split('@')[0]; + break; + default: + nodeFullName = selectedNode.GetType().ToString(); + break; + } + + string sampleDynPath = Path.Combine(nodeHelpDocPath.FullName, $"{nodeFullName}.dyn"); + + if (!File.Exists(sampleDynPath)) + { + var minimumQualifiedName = dynamoViewModel.GetMinimumQualifiedName(selectedNode); + var shortName = Hash.GetHashFilenameFromString(minimumQualifiedName); + sampleDynPath = Path.Combine(nodeHelpDocPath.FullName, $"{shortName}.dyn"); + } + + //no help file found, return nothing + if (!File.Exists(sampleDynPath)) + { + return new List(); + } + + Exception ex; + DynamoUtilities.PathHelper.isValidJson(sampleDynPath, out var fileContents, out ex); + + //get the workspace model from the sample to lookup the node + var workspace = WorkspaceModel.FromJson(fileContents, dynamoModel.LibraryServices, + dynamoModel.EngineController, dynamoModel.Scheduler, + dynamoModel.NodeFactory, false, false, + dynamoModel.CustomNodeManager, + dynamoModel.LinterManager); + + var matchingNode = workspace.Nodes + .FirstOrDefault(n => n.CreationName == selectedNode.CreationName); + + //no matching node found, return nothing + if (matchingNode is null) return new List(); + + //find the port model and the node to place + NodeModel predictedNode = null; + PortModel matchingPortModel = null; + switch (selectedPortModel.PortType) + { + case PortType.Input: + matchingPortModel = matchingNode.InPorts[selectedPortModel.Index]; + predictedNode = matchingPortModel.Connectors.First().Start.Owner; + break; + case PortType.Output: + matchingPortModel = matchingNode.OutPorts[selectedPortModel.Index]; + predictedNode = matchingPortModel.Connectors.First().End.Owner; + break; + } + + if (predictedNode is null) return new List(); + + SingleResultItem singleResultItem = new SingleResultItem(predictedNode, matchingPortModel.Index); + return [singleResultItem]; + } + private T GetGenericAutocompleteResult(string endpoint) { var requestDTO = GenerateRequestForMLAutocomplete(); diff --git a/src/NodeAutoCompleteViewExtension/ViewModels/SingleResultItem.cs b/src/NodeAutoCompleteViewExtension/ViewModels/SingleResultItem.cs index 13e6ac7f07e..da04db19b37 100644 --- a/src/NodeAutoCompleteViewExtension/ViewModels/SingleResultItem.cs +++ b/src/NodeAutoCompleteViewExtension/ViewModels/SingleResultItem.cs @@ -1,3 +1,4 @@ +using Dynamo.Graph.Nodes; using Dynamo.Search.SearchElements; using Dynamo.Wpf.ViewModels; @@ -17,6 +18,16 @@ public SingleResultItem(NodeSearchElement model, double score = 1.0) Score = score; } + public SingleResultItem(NodeModel nodeModel, int portToConnect, double score = 1.0) + { + Assembly = nodeModel.GetType().Assembly.GetName().Name; + IconName = nodeModel.GetType().Name; + Description = nodeModel.Name; + CreationName = nodeModel.CreationName; + PortToConnect = portToConnect; + Score = score; + } + public SingleResultItem(NodeSearchElementViewModel x) : this(x.Model) { //Convert percent to probability From 80d7936216997d8d23444cb5d0c7d7705050e167 Mon Sep 17 00:00:00 2001 From: john pierson Date: Mon, 23 Jun 2025 14:24:04 -0600 Subject: [PATCH 02/10] Progress on Package Local AutoComplete --- .../NodeAutoCompleteBarViewModel.cs | 513 ++++++++++++++++-- .../ViewModels/SingleResultItem.cs | 2 +- 2 files changed, 475 insertions(+), 40 deletions(-) diff --git a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs index dbda1e168af..777c763c0c7 100644 --- a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs +++ b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Dynamo.Configuration; @@ -613,85 +614,304 @@ private IEnumerable GetNodeAutocompleMLResults() /// /// Use local node help files to at least return one result that will work. + /// Enhanced to support both built-in nodes and package nodes. /// /// private List TryGetLocalAutoCompleteResult() { var dynamoModel = this.dynamoViewModel.Model; - - var nodeHelpDocPath = new DirectoryInfo(Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, - Configurations.DynamoNodeHelpDocs)); - var selectedNode = this.PortViewModel.NodeViewModel.NodeModel; var selectedPortModel = this.PortViewModel.PortModel; - string nodeFullName; + string nodeFullName = GetNodeFullName(selectedNode); + + // First try to find help files in package directories if the node belongs to a package + var packageResult = TryGetPackageAutoCompleteResult(selectedNode, selectedPortModel, nodeFullName); + if (packageResult.Any()) + { + return packageResult; + } + + // Fall back to built-in node help files + return TryGetBuiltInAutoCompleteResult(selectedNode, selectedPortModel, nodeFullName); + } + /// + /// Extract the full name from a node for help file lookup + /// + private string GetNodeFullName(NodeModel selectedNode) + { switch (selectedNode) { case DSFunction dsFunction: string fullSignature = dsFunction.FunctionSignature; - nodeFullName = fullSignature.Split('@')[0]; - break; + return fullSignature.Split('@')[0]; case Function function: string fullFunctionSignature = function.FunctionSignature.ToString(); - nodeFullName = fullFunctionSignature.Split('@')[0]; - break; + return fullFunctionSignature.Split('@')[0]; default: - nodeFullName = selectedNode.GetType().ToString(); - break; + return selectedNode.GetType().ToString(); } + } - string sampleDynPath = Path.Combine(nodeHelpDocPath.FullName, $"{nodeFullName}.dyn"); + /// + /// Try to get autocomplete results from package help files + /// + private List TryGetPackageAutoCompleteResult(NodeModel selectedNode, PortModel selectedPortModel, string nodeFullName) + { + // Get the package manager extension + var packageManagerExtension = dynamoViewModel.Model.GetPackageManagerExtension(); + if (packageManagerExtension?.PackageLoader == null) + { + return new List(); + } - if (!File.Exists(sampleDynPath)) + // Try to find which package owns this node + Package ownerPackage = null; + + // For custom nodes (Function type), check by file path + if (selectedNode is Function function) + { + // Get the custom node info from the custom node manager + if (dynamoViewModel.Model.CustomNodeManager.TryGetNodeInfo(function.Definition.FunctionId, out CustomNodeInfo customNodeInfo) + && !string.IsNullOrEmpty(customNodeInfo.Path)) + { + ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(customNodeInfo.Path); + } + } + // For DSFunction nodes (zero-touch), check by assembly path + else if (selectedNode is DSFunction dsFunction) + { + ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(dsFunction.Controller.Definition.Assembly); + } + // For other DSFunctionBase nodes, check by assembly path + else if (selectedNode is DSFunctionBase dsFunctionBase) + { + ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(dsFunctionBase.Controller.Definition.Assembly); + } + // For other nodes, try to find by type + else { - var minimumQualifiedName = dynamoViewModel.GetMinimumQualifiedName(selectedNode); - var shortName = Hash.GetHashFilenameFromString(minimumQualifiedName); - sampleDynPath = Path.Combine(nodeHelpDocPath.FullName, $"{shortName}.dyn"); + ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(selectedNode.GetType()); } - //no help file found, return nothing - if (!File.Exists(sampleDynPath)) + if (ownerPackage == null) { return new List(); } - Exception ex; - DynamoUtilities.PathHelper.isValidJson(sampleDynPath, out var fileContents, out ex); + // Search for help files in package directories + var searchDirectories = new List(); + + // Add package documentation directory + if (Directory.Exists(ownerPackage.NodeDocumentaionDirectory)) + { + searchDirectories.Add(ownerPackage.NodeDocumentaionDirectory); + } + + // Add extra directory as fallback for sample files + if (Directory.Exists(ownerPackage.ExtraDirectory)) + { + searchDirectories.Add(ownerPackage.ExtraDirectory); + } + + // Add root directory as fallback + if (Directory.Exists(ownerPackage.RootDirectory)) + { + searchDirectories.Add(ownerPackage.RootDirectory); + } + + foreach (var searchDir in searchDirectories) + { + var result = SearchForHelpFileInDirectory(searchDir, nodeFullName, selectedNode, selectedPortModel); + if (result.Any()) + { + return result; + } + } - //get the workspace model from the sample to lookup the node - var workspace = WorkspaceModel.FromJson(fileContents, dynamoModel.LibraryServices, - dynamoModel.EngineController, dynamoModel.Scheduler, - dynamoModel.NodeFactory, false, false, - dynamoModel.CustomNodeManager, - dynamoModel.LinterManager); + return new List(); + } + + /// + /// Try to get autocomplete results from built-in node help files (original logic) + /// + private List TryGetBuiltInAutoCompleteResult(NodeModel selectedNode, PortModel selectedPortModel, string nodeFullName) + { + var nodeHelpDocPath = new DirectoryInfo(Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, + Configurations.DynamoNodeHelpDocs)); + + if (!nodeHelpDocPath.Exists) + { + return new List(); + } + + return SearchForHelpFileInDirectory(nodeHelpDocPath.FullName, nodeFullName, selectedNode, selectedPortModel); + } + + /// + /// Search for help files in a specific directory and return autocomplete results + /// + private List SearchForHelpFileInDirectory(string searchDirectory, string nodeFullName, NodeModel selectedNode, PortModel selectedPortModel) + { + if (!Directory.Exists(searchDirectory)) + { + return new List(); + } + + var candidateFiles = FindCandidateHelpFiles(searchDirectory, nodeFullName, selectedNode); + + foreach (var filePath in candidateFiles) + { + try + { + var result = ProcessHelpFile(filePath, selectedNode, selectedPortModel); + if (result.Any()) + { + return result; // Return the first successful result + } + } + catch (Exception ex) + { + // Log the error but continue trying other files + dynamoViewModel.Model.Logger.Log($"Error processing help file {filePath}: {ex.Message}"); + } + } + + return new List(); + } + + /// + /// Find all candidate help files that might contain relevant information + /// + private List FindCandidateHelpFiles(string searchDirectory, string nodeFullName, NodeModel selectedNode) + { + var candidates = new List(); + + // Primary candidate: exact nodeFullName match + var primaryCandidate = Path.Combine(searchDirectory, $"{nodeFullName}.dyn"); + if (File.Exists(primaryCandidate)) + { + candidates.Add(primaryCandidate); + } + + // Secondary candidate: hashed filename + var minimumQualifiedName = dynamoViewModel.GetMinimumQualifiedName(selectedNode); + var hashedName = Hash.GetHashFilenameFromString(minimumQualifiedName); + var hashedCandidate = Path.Combine(searchDirectory, $"{hashedName}.dyn"); + if (File.Exists(hashedCandidate) && !candidates.Contains(hashedCandidate)) + { + candidates.Add(hashedCandidate); + } + + return candidates; + } + + + + /// + /// Process a specific help file and extract autocomplete results + /// + private List ProcessHelpFile(string filePath, NodeModel selectedNode, PortModel selectedPortModel) + { + // Early validation: check if file contains JSON and is not empty + if (!IsValidHelpFile(filePath)) + { + return new List(); + } + + // Read and validate JSON content + var fileContents = File.ReadAllText(filePath); + if (string.IsNullOrWhiteSpace(fileContents)) + { + return new List(); + } + + // Quick check if the file likely contains our target node before expensive parsing + if (!fileContents.Contains(selectedNode.CreationName, StringComparison.OrdinalIgnoreCase)) + { + return new List(); + } + + // Parse the workspace + var workspace = WorkspaceModel.FromJson(fileContents, dynamoViewModel.Model.LibraryServices, + dynamoViewModel.Model.EngineController, dynamoViewModel.Model.Scheduler, + dynamoViewModel.Model.NodeFactory, false, false, + dynamoViewModel.Model.CustomNodeManager, + dynamoViewModel.Model.LinterManager); var matchingNode = workspace.Nodes .FirstOrDefault(n => n.CreationName == selectedNode.CreationName); - //no matching node found, return nothing - if (matchingNode is null) return new List(); - - //find the port model and the node to place + if (matchingNode == null) + return new List(); + + // Extract the prediction result + return ExtractPredictionFromWorkspace(matchingNode, selectedPortModel); + } + + /// + /// Validate if a file is a potentially valid help file + /// + private bool IsValidHelpFile(string filePath) + { + try + { + var fileInfo = new FileInfo(filePath); + if (!fileInfo.Exists || fileInfo.Length == 0) + { + return false; + } + + // Quick JSON format check - read first few characters + using (var reader = new StreamReader(filePath)) + { + var firstChar = (char)reader.Read(); + return firstChar == '{' || firstChar == '['; + } + } + catch + { + return false; + } + } + + /// + /// Extract prediction result from a workspace containing the matching node + /// + private List ExtractPredictionFromWorkspace(NodeModel matchingNode, PortModel selectedPortModel) + { NodeModel predictedNode = null; PortModel matchingPortModel = null; + switch (selectedPortModel.PortType) { case PortType.Input: - matchingPortModel = matchingNode.InPorts[selectedPortModel.Index]; - predictedNode = matchingPortModel.Connectors.First().Start.Owner; + if (matchingNode.InPorts.Count > selectedPortModel.Index) + { + matchingPortModel = matchingNode.InPorts[selectedPortModel.Index]; + if (matchingPortModel.Connectors.Any()) + { + predictedNode = matchingPortModel.Connectors.First().Start.Owner; + } + } break; case PortType.Output: - matchingPortModel = matchingNode.OutPorts[selectedPortModel.Index]; - predictedNode = matchingPortModel.Connectors.First().End.Owner; + if (matchingNode.OutPorts.Count > selectedPortModel.Index) + { + matchingPortModel = matchingNode.OutPorts[selectedPortModel.Index]; + if (matchingPortModel.Connectors.Any()) + { + predictedNode = matchingPortModel.Connectors.First().End.Owner; + } + } break; } - if (predictedNode is null) return new List(); - - SingleResultItem singleResultItem = new SingleResultItem(predictedNode, matchingPortModel.Index); - return [singleResultItem]; + if (predictedNode == null) + return new List(); + var singleResultItem = new SingleResultItem(predictedNode, matchingPortModel.Index); + return new List { singleResultItem }; } private T GetGenericAutocompleteResult(string endpoint) @@ -909,7 +1129,9 @@ internal void AddCluster(ClusterResultItem clusterResultItem) foreach (var nodeItem in nodeStack) { var typeInfo = new NodeModelTypeId(nodeItem.Type.Id); - var newNode = dynamoModel.CreateNodeFromNameOrType(Guid.NewGuid(), typeInfo.FullName, true); + + NodeModel newNode = dynamoModel.CreateNodeFromNameOrType(Guid.NewGuid(), typeInfo.FullName, true); + if (newNode != null) { newNode.X = offset; // Adjust X position @@ -1488,5 +1710,218 @@ private static int GetTypeDistance(string typea, string typeb, ProtoCore.Core co } } + + + + /// + /// Calculate fuzzy matching score between two strings + /// + private double CalculateFuzzyScore(string text, string pattern) + { + if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(pattern)) + return 0.0; + + // Convert to lowercase for case-insensitive comparison + text = text.ToLowerInvariant(); + pattern = pattern.ToLowerInvariant(); + + // Exact match gets highest score + if (text == pattern) return 1.0; + + // Contains check (original logic) + if (text.Contains(pattern)) return 0.8; + + // Fuzzy matching using multiple techniques + var scores = new List(); + + // 1. Levenshtein distance-based score + var levenshteinScore = 1.0 - (double)LevenshteinDistance(text, pattern) / Math.Max(text.Length, pattern.Length); + scores.Add(levenshteinScore); + + // 2. Subsequence matching (order matters) + var subsequenceScore = LongestCommonSubsequenceRatio(text, pattern); + scores.Add(subsequenceScore); + + // 3. Word boundary matching (useful for camelCase or snake_case) + var wordScore = CalculateWordBoundaryScore(text, pattern); + scores.Add(wordScore); + + // 4. Character frequency similarity + var frequencyScore = CalculateCharacterFrequencyScore(text, pattern); + scores.Add(frequencyScore); + + // Return the highest score from all techniques + return scores.Max(); + } + + /// + /// Calculate Levenshtein distance between two strings + /// + private int LevenshteinDistance(string source, string target) + { + if (source.Length == 0) return target.Length; + if (target.Length == 0) return source.Length; + + var matrix = new int[source.Length + 1, target.Length + 1]; + + for (int i = 0; i <= source.Length; i++) + matrix[i, 0] = i; + for (int j = 0; j <= target.Length; j++) + matrix[0, j] = j; + + for (int i = 1; i <= source.Length; i++) + { + for (int j = 1; j <= target.Length; j++) + { + var cost = (target[j - 1] == source[i - 1]) ? 0 : 1; + matrix[i, j] = Math.Min( + Math.Min(matrix[i - 1, j] + 1, matrix[i, j - 1] + 1), + matrix[i - 1, j - 1] + cost); + } + } + + return matrix[source.Length, target.Length]; + } + + /// + /// Calculate ratio of longest common subsequence + /// + private double LongestCommonSubsequenceRatio(string text, string pattern) + { + var lcs = LongestCommonSubsequence(text, pattern); + return (double)lcs / Math.Max(text.Length, pattern.Length); + } + + /// + /// Find length of longest common subsequence + /// + private int LongestCommonSubsequence(string text, string pattern) + { + var matrix = new int[text.Length + 1, pattern.Length + 1]; + + for (int i = 1; i <= text.Length; i++) + { + for (int j = 1; j <= pattern.Length; j++) + { + if (text[i - 1] == pattern[j - 1]) + matrix[i, j] = matrix[i - 1, j - 1] + 1; + else + matrix[i, j] = Math.Max(matrix[i - 1, j], matrix[i, j - 1]); + } + } + + return matrix[text.Length, pattern.Length]; + } + + /// + /// Score based on word boundary matches (camelCase, snake_case, etc.) + /// + private double CalculateWordBoundaryScore(string text, string pattern) + { + // Extract "words" from both strings + var textWords = ExtractWords(text); + var patternWords = ExtractWords(pattern); + + if (!patternWords.Any()) return 0.0; + + var matchCount = 0; + foreach (var patternWord in patternWords) + { + if (textWords.Any(tw => tw.StartsWith(patternWord, StringComparison.OrdinalIgnoreCase))) + { + matchCount++; + } + } + + return (double)matchCount / patternWords.Count; + } + + /// + /// Extract words from camelCase, snake_case, or space-separated text + /// + private List ExtractWords(string text) + { + var words = new List(); + + // Split on common separators + var parts = text.Split(new char[] { '_', '-', ' ', '.', '/' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var part in parts) + { + // Handle camelCase + var camelWords = SplitCamelCase(part); + words.AddRange(camelWords.Where(w => w.Length > 1)); // Ignore single characters + } + + return words.Where(w => !string.IsNullOrWhiteSpace(w)).ToList(); + } + + /// + /// Split camelCase string into individual words + /// + private List SplitCamelCase(string text) + { + var words = new List(); + var currentWord = new StringBuilder(); + + foreach (char c in text) + { + if (char.IsUpper(c) && currentWord.Length > 0) + { + words.Add(currentWord.ToString()); + currentWord.Clear(); + } + currentWord.Append(c); + } + + if (currentWord.Length > 0) + { + words.Add(currentWord.ToString()); + } + + return words; + } + + /// + /// Score based on character frequency similarity + /// + private double CalculateCharacterFrequencyScore(string text, string pattern) + { + var textFreq = GetCharacterFrequency(text); + var patternFreq = GetCharacterFrequency(pattern); + + var allChars = textFreq.Keys.Union(patternFreq.Keys); + var dotProduct = 0.0; + var textMagnitude = 0.0; + var patternMagnitude = 0.0; + + foreach (var c in allChars) + { + var textCount = textFreq.GetValueOrDefault(c, 0); + var patternCount = patternFreq.GetValueOrDefault(c, 0); + + dotProduct += textCount * patternCount; + textMagnitude += textCount * textCount; + patternMagnitude += patternCount * patternCount; + } + + if (textMagnitude == 0 || patternMagnitude == 0) return 0.0; + + // Cosine similarity + return dotProduct / (Math.Sqrt(textMagnitude) * Math.Sqrt(patternMagnitude)); + } + + /// + /// Get character frequency map for a string + /// + private Dictionary GetCharacterFrequency(string text) + { + var frequency = new Dictionary(); + foreach (char c in text) + { + frequency[c] = frequency.GetValueOrDefault(c, 0) + 1; + } + return frequency; + } } } diff --git a/src/NodeAutoCompleteViewExtension/ViewModels/SingleResultItem.cs b/src/NodeAutoCompleteViewExtension/ViewModels/SingleResultItem.cs index da04db19b37..9524b79ee3a 100644 --- a/src/NodeAutoCompleteViewExtension/ViewModels/SingleResultItem.cs +++ b/src/NodeAutoCompleteViewExtension/ViewModels/SingleResultItem.cs @@ -23,7 +23,7 @@ public SingleResultItem(NodeModel nodeModel, int portToConnect, double score = 1 Assembly = nodeModel.GetType().Assembly.GetName().Name; IconName = nodeModel.GetType().Name; Description = nodeModel.Name; - CreationName = nodeModel.CreationName; + CreationName = string.IsNullOrWhiteSpace(nodeModel.CreationName) ? nodeModel.GetOriginalName(): nodeModel.CreationName; PortToConnect = portToConnect; Score = score; } From 53fa04d47e047e1cdaa895c57d677fcb25bd6cdc Mon Sep 17 00:00:00 2001 From: john pierson Date: Mon, 23 Jun 2025 14:40:05 -0600 Subject: [PATCH 03/10] Refactor to Move Local Autocomplete to its own Service --- .../Services/LocalAutoCompleteService.cs | 370 ++++++++++++++++++ .../NodeAutoCompleteBarViewModel.cs | 309 +-------------- 2 files changed, 378 insertions(+), 301 deletions(-) create mode 100644 src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs diff --git a/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs new file mode 100644 index 00000000000..99cab0e566e --- /dev/null +++ b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs @@ -0,0 +1,370 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using Dynamo.Configuration; +using Dynamo.Graph.Nodes; +using Dynamo.Graph.Nodes.CustomNodes; +using Dynamo.Graph.Nodes.ZeroTouch; +using Dynamo.Graph.Workspaces; +using Dynamo.Graph; +using Dynamo.Models; +using Dynamo.PackageManager; +using Dynamo.Search.SearchElements; +using Dynamo.Utilities; +using Dynamo.ViewModels; +using Dynamo.NodeAutoComplete.ViewModels; + +namespace Dynamo.NodeAutoComplete.Services +{ + /// + /// Service class for handling local autocomplete functionality using help files + /// + internal class LocalAutoCompleteService + { + private readonly DynamoViewModel dynamoViewModel; + + /// + /// Constructor for LocalAutoCompleteService + /// + /// The Dynamo view model instance + public LocalAutoCompleteService(DynamoViewModel dynamoViewModel) + { + this.dynamoViewModel = dynamoViewModel ?? throw new ArgumentNullException(nameof(dynamoViewModel)); + } + + /// + /// Use local node help files to at least return one result that will work. + /// Enhanced to support both built-in nodes and package nodes. + /// + /// The selected node + /// The selected port model + /// List of single result items from local help files + public List TryGetLocalAutoCompleteResult(NodeModel selectedNode, PortModel selectedPortModel) + { + if (selectedNode == null || selectedPortModel == null) + { + return new List(); + } + + string nodeFullName = GetNodeFullName(selectedNode); + + // First try to find help files in package directories if the node belongs to a package + var packageResult = TryGetPackageAutoCompleteResult(selectedNode, selectedPortModel, nodeFullName); + if (packageResult.Any()) + { + return packageResult; + } + + // Fall back to built-in node help files + return TryGetBuiltInAutoCompleteResult(selectedNode, selectedPortModel, nodeFullName); + } + + /// + /// Extract the full name from a node for help file lookup + /// + /// The node to extract the full name from + /// The full name of the node + private string GetNodeFullName(NodeModel selectedNode) + { + switch (selectedNode) + { + case DSFunction dsFunction: + string fullSignature = dsFunction.FunctionSignature; + return fullSignature.Split('@')[0]; + case Function function: + string fullFunctionSignature = function.FunctionSignature.ToString(); + return fullFunctionSignature.Split('@')[0]; + default: + return selectedNode.GetType().ToString(); + } + } + + /// + /// Try to get autocomplete results from package help files + /// + /// The selected node + /// The selected port model + /// The full name of the node + /// List of autocomplete results from package help files + private List TryGetPackageAutoCompleteResult(NodeModel selectedNode, PortModel selectedPortModel, string nodeFullName) + { + // Get the package manager extension + var packageManagerExtension = dynamoViewModel.Model.GetPackageManagerExtension(); + if (packageManagerExtension?.PackageLoader == null) + { + return new List(); + } + + // Try to find which package owns this node + Package ownerPackage = null; + + // For custom nodes (Function type), check by file path + if (selectedNode is Function function) + { + // Get the custom node info from the custom node manager + if (dynamoViewModel.Model.CustomNodeManager.TryGetNodeInfo(function.Definition.FunctionId, out CustomNodeInfo customNodeInfo) + && !string.IsNullOrEmpty(customNodeInfo.Path)) + { + ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(customNodeInfo.Path); + } + } + // For DSFunction nodes (zero-touch), check by assembly path + else if (selectedNode is DSFunction dsFunction) + { + ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(dsFunction.Controller.Definition.Assembly); + } + // For other DSFunctionBase nodes, check by assembly path + else if (selectedNode is DSFunctionBase dsFunctionBase) + { + ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(dsFunctionBase.Controller.Definition.Assembly); + } + // For other nodes, try to find by type + else + { + ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(selectedNode.GetType()); + } + + if (ownerPackage == null) + { + return new List(); + } + + // Search for help files in package directories + var searchDirectories = new List(); + + // Add package documentation directory + if (Directory.Exists(ownerPackage.NodeDocumentaionDirectory)) + { + searchDirectories.Add(ownerPackage.NodeDocumentaionDirectory); + } + + // Add extra directory as fallback for sample files + if (Directory.Exists(ownerPackage.ExtraDirectory)) + { + searchDirectories.Add(ownerPackage.ExtraDirectory); + } + + // Add root directory as fallback + if (Directory.Exists(ownerPackage.RootDirectory)) + { + searchDirectories.Add(ownerPackage.RootDirectory); + } + + foreach (var searchDir in searchDirectories) + { + var result = SearchForHelpFileInDirectory(searchDir, nodeFullName, selectedNode, selectedPortModel); + if (result.Any()) + { + return result; + } + } + + return new List(); + } + + /// + /// Try to get autocomplete results from built-in node help files (original logic) + /// + /// The selected node + /// The selected port model + /// The full name of the node + /// List of autocomplete results from built-in help files + private List TryGetBuiltInAutoCompleteResult(NodeModel selectedNode, PortModel selectedPortModel, string nodeFullName) + { + var nodeHelpDocPath = new DirectoryInfo(Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, + Configurations.DynamoNodeHelpDocs)); + + if (!nodeHelpDocPath.Exists) + { + return new List(); + } + + return SearchForHelpFileInDirectory(nodeHelpDocPath.FullName, nodeFullName, selectedNode, selectedPortModel); + } + + /// + /// Search for help files in a specific directory and return autocomplete results + /// + /// The directory to search in + /// The full name of the node + /// The selected node + /// The selected port model + /// List of autocomplete results from the directory + private List SearchForHelpFileInDirectory(string searchDirectory, string nodeFullName, NodeModel selectedNode, PortModel selectedPortModel) + { + if (!Directory.Exists(searchDirectory)) + { + return new List(); + } + + var candidateFiles = FindCandidateHelpFiles(searchDirectory, nodeFullName, selectedNode); + + foreach (var filePath in candidateFiles) + { + try + { + var result = ProcessHelpFile(filePath, selectedNode, selectedPortModel); + if (result.Any()) + { + return result; // Return the first successful result + } + } + catch (Exception ex) + { + // Log the error but continue trying other files + dynamoViewModel.Model.Logger.Log($"Error processing help file {filePath}: {ex.Message}"); + } + } + + return new List(); + } + + /// + /// Find all candidate help files that might contain relevant information + /// + /// The directory to search in + /// The full name of the node + /// The selected node + /// List of candidate file paths + private List FindCandidateHelpFiles(string searchDirectory, string nodeFullName, NodeModel selectedNode) + { + var candidates = new List(); + + // Primary candidate: exact nodeFullName match + var primaryCandidate = Path.Combine(searchDirectory, $"{nodeFullName}.dyn"); + if (File.Exists(primaryCandidate)) + { + candidates.Add(primaryCandidate); + } + + // Secondary candidate: hashed filename + var minimumQualifiedName = dynamoViewModel.GetMinimumQualifiedName(selectedNode); + var hashedName = Hash.GetHashFilenameFromString(minimumQualifiedName); + var hashedCandidate = Path.Combine(searchDirectory, $"{hashedName}.dyn"); + if (File.Exists(hashedCandidate) && !candidates.Contains(hashedCandidate)) + { + candidates.Add(hashedCandidate); + } + + return candidates; + } + + /// + /// Process a specific help file and extract autocomplete results + /// + /// The path to the help file + /// The selected node + /// The selected port model + /// List of autocomplete results from the help file + private List ProcessHelpFile(string filePath, NodeModel selectedNode, PortModel selectedPortModel) + { + // Early validation: check if file contains JSON and is not empty + if (!IsValidHelpFile(filePath)) + { + return new List(); + } + + // Read and validate JSON content + var fileContents = File.ReadAllText(filePath); + if (string.IsNullOrWhiteSpace(fileContents)) + { + return new List(); + } + + // Quick check if the file likely contains our target node before expensive parsing + if (!fileContents.Contains(selectedNode.CreationName, StringComparison.OrdinalIgnoreCase)) + { + return new List(); + } + + // Parse the workspace + var workspace = WorkspaceModel.FromJson(fileContents, dynamoViewModel.Model.LibraryServices, + dynamoViewModel.Model.EngineController, dynamoViewModel.Model.Scheduler, + dynamoViewModel.Model.NodeFactory, false, false, + dynamoViewModel.Model.CustomNodeManager, + dynamoViewModel.Model.LinterManager); + + var matchingNode = workspace.Nodes + .FirstOrDefault(n => n.CreationName == selectedNode.CreationName); + + if (matchingNode == null) + return new List(); + + // Extract the prediction result + return ExtractPredictionFromWorkspace(matchingNode, selectedPortModel); + } + + /// + /// Validate if a file is a potentially valid help file + /// + /// The path to the file to validate + /// True if the file appears to be a valid help file + private bool IsValidHelpFile(string filePath) + { + try + { + var fileInfo = new FileInfo(filePath); + if (!fileInfo.Exists || fileInfo.Length == 0) + { + return false; + } + + // Quick JSON format check - read first few characters + using (var reader = new StreamReader(filePath)) + { + var firstChar = (char)reader.Read(); + return firstChar == '{' || firstChar == '['; + } + } + catch + { + return false; + } + } + + /// + /// Extract prediction result from a workspace containing the matching node + /// + /// The node that matches the selected node + /// The selected port model + /// List of prediction results + private List ExtractPredictionFromWorkspace(NodeModel matchingNode, PortModel selectedPortModel) + { + NodeModel predictedNode = null; + PortModel matchingPortModel = null; + + switch (selectedPortModel.PortType) + { + case PortType.Input: + if (matchingNode.InPorts.Count > selectedPortModel.Index) + { + matchingPortModel = matchingNode.InPorts[selectedPortModel.Index]; + if (matchingPortModel.Connectors.Any()) + { + predictedNode = matchingPortModel.Connectors.First().Start.Owner; + } + } + break; + case PortType.Output: + if (matchingNode.OutPorts.Count > selectedPortModel.Index) + { + matchingPortModel = matchingNode.OutPorts[selectedPortModel.Index]; + if (matchingPortModel.Connectors.Any()) + { + predictedNode = matchingPortModel.Connectors.First().End.Owner; + } + } + break; + } + + if (predictedNode == null || predictedNode is CodeBlockNodeModel) + return new List(); + + var singleResultItem = new SingleResultItem(predictedNode, matchingPortModel.Index); + return new List { singleResultItem }; + } + } +} diff --git a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs index 777c763c0c7..36c9d7fbbdd 100644 --- a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs +++ b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs @@ -34,6 +34,7 @@ using System.Windows.Media; using System.ComponentModel; using System.Windows.Data; +using Dynamo.NodeAutoComplete.Services; namespace Dynamo.NodeAutoComplete.ViewModels { @@ -385,6 +386,11 @@ public bool IsDNAClusterPlacementEnabled internal List FullSingleResults { set; get; } private Guid LastRequestGuid; + /// + /// Service for handling local autocomplete functionality + /// + private readonly LocalAutoCompleteService localAutoCompleteService; + /// /// Constructor /// @@ -394,6 +400,7 @@ internal NodeAutoCompleteBarViewModel(DynamoViewModel dynamoViewModel) : base(dy // Off load some time consuming operation here DefaultResults = dynamoViewModel.DefaultAutocompleteCandidates.Values; ServiceVersion = string.Empty; + localAutoCompleteService = new LocalAutoCompleteService(dynamoViewModel); } /// @@ -538,7 +545,7 @@ private IEnumerable GetNodeAutocompleMLResults() AutocompleteMLMessage = Resources.AutocompleteNoRecommendationsMessage; Analytics.TrackEvent(Actions.View, Categories.NodeAutoCompleteOperations, "NoRecommendation"); - return TryGetLocalAutoCompleteResult(); + return localAutoCompleteService.TryGetLocalAutoCompleteResult(PortViewModel.NodeViewModel.NodeModel, PortViewModel.PortModel); } ServiceVersion = MLresults.Version; var results = new List(); @@ -612,307 +619,7 @@ private IEnumerable GetNodeAutocompleMLResults() return results; } - /// - /// Use local node help files to at least return one result that will work. - /// Enhanced to support both built-in nodes and package nodes. - /// - /// - private List TryGetLocalAutoCompleteResult() - { - var dynamoModel = this.dynamoViewModel.Model; - var selectedNode = this.PortViewModel.NodeViewModel.NodeModel; - var selectedPortModel = this.PortViewModel.PortModel; - - string nodeFullName = GetNodeFullName(selectedNode); - - // First try to find help files in package directories if the node belongs to a package - var packageResult = TryGetPackageAutoCompleteResult(selectedNode, selectedPortModel, nodeFullName); - if (packageResult.Any()) - { - return packageResult; - } - - // Fall back to built-in node help files - return TryGetBuiltInAutoCompleteResult(selectedNode, selectedPortModel, nodeFullName); - } - - /// - /// Extract the full name from a node for help file lookup - /// - private string GetNodeFullName(NodeModel selectedNode) - { - switch (selectedNode) - { - case DSFunction dsFunction: - string fullSignature = dsFunction.FunctionSignature; - return fullSignature.Split('@')[0]; - case Function function: - string fullFunctionSignature = function.FunctionSignature.ToString(); - return fullFunctionSignature.Split('@')[0]; - default: - return selectedNode.GetType().ToString(); - } - } - - /// - /// Try to get autocomplete results from package help files - /// - private List TryGetPackageAutoCompleteResult(NodeModel selectedNode, PortModel selectedPortModel, string nodeFullName) - { - // Get the package manager extension - var packageManagerExtension = dynamoViewModel.Model.GetPackageManagerExtension(); - if (packageManagerExtension?.PackageLoader == null) - { - return new List(); - } - - // Try to find which package owns this node - Package ownerPackage = null; - - // For custom nodes (Function type), check by file path - if (selectedNode is Function function) - { - // Get the custom node info from the custom node manager - if (dynamoViewModel.Model.CustomNodeManager.TryGetNodeInfo(function.Definition.FunctionId, out CustomNodeInfo customNodeInfo) - && !string.IsNullOrEmpty(customNodeInfo.Path)) - { - ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(customNodeInfo.Path); - } - } - // For DSFunction nodes (zero-touch), check by assembly path - else if (selectedNode is DSFunction dsFunction) - { - ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(dsFunction.Controller.Definition.Assembly); - } - // For other DSFunctionBase nodes, check by assembly path - else if (selectedNode is DSFunctionBase dsFunctionBase) - { - ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(dsFunctionBase.Controller.Definition.Assembly); - } - // For other nodes, try to find by type - else - { - ownerPackage = packageManagerExtension.PackageLoader.GetOwnerPackage(selectedNode.GetType()); - } - - if (ownerPackage == null) - { - return new List(); - } - - // Search for help files in package directories - var searchDirectories = new List(); - - // Add package documentation directory - if (Directory.Exists(ownerPackage.NodeDocumentaionDirectory)) - { - searchDirectories.Add(ownerPackage.NodeDocumentaionDirectory); - } - - // Add extra directory as fallback for sample files - if (Directory.Exists(ownerPackage.ExtraDirectory)) - { - searchDirectories.Add(ownerPackage.ExtraDirectory); - } - - // Add root directory as fallback - if (Directory.Exists(ownerPackage.RootDirectory)) - { - searchDirectories.Add(ownerPackage.RootDirectory); - } - - foreach (var searchDir in searchDirectories) - { - var result = SearchForHelpFileInDirectory(searchDir, nodeFullName, selectedNode, selectedPortModel); - if (result.Any()) - { - return result; - } - } - - return new List(); - } - - /// - /// Try to get autocomplete results from built-in node help files (original logic) - /// - private List TryGetBuiltInAutoCompleteResult(NodeModel selectedNode, PortModel selectedPortModel, string nodeFullName) - { - var nodeHelpDocPath = new DirectoryInfo(Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, - Configurations.DynamoNodeHelpDocs)); - - if (!nodeHelpDocPath.Exists) - { - return new List(); - } - - return SearchForHelpFileInDirectory(nodeHelpDocPath.FullName, nodeFullName, selectedNode, selectedPortModel); - } - - /// - /// Search for help files in a specific directory and return autocomplete results - /// - private List SearchForHelpFileInDirectory(string searchDirectory, string nodeFullName, NodeModel selectedNode, PortModel selectedPortModel) - { - if (!Directory.Exists(searchDirectory)) - { - return new List(); - } - - var candidateFiles = FindCandidateHelpFiles(searchDirectory, nodeFullName, selectedNode); - - foreach (var filePath in candidateFiles) - { - try - { - var result = ProcessHelpFile(filePath, selectedNode, selectedPortModel); - if (result.Any()) - { - return result; // Return the first successful result - } - } - catch (Exception ex) - { - // Log the error but continue trying other files - dynamoViewModel.Model.Logger.Log($"Error processing help file {filePath}: {ex.Message}"); - } - } - - return new List(); - } - - /// - /// Find all candidate help files that might contain relevant information - /// - private List FindCandidateHelpFiles(string searchDirectory, string nodeFullName, NodeModel selectedNode) - { - var candidates = new List(); - - // Primary candidate: exact nodeFullName match - var primaryCandidate = Path.Combine(searchDirectory, $"{nodeFullName}.dyn"); - if (File.Exists(primaryCandidate)) - { - candidates.Add(primaryCandidate); - } - - // Secondary candidate: hashed filename - var minimumQualifiedName = dynamoViewModel.GetMinimumQualifiedName(selectedNode); - var hashedName = Hash.GetHashFilenameFromString(minimumQualifiedName); - var hashedCandidate = Path.Combine(searchDirectory, $"{hashedName}.dyn"); - if (File.Exists(hashedCandidate) && !candidates.Contains(hashedCandidate)) - { - candidates.Add(hashedCandidate); - } - - return candidates; - } - - - - /// - /// Process a specific help file and extract autocomplete results - /// - private List ProcessHelpFile(string filePath, NodeModel selectedNode, PortModel selectedPortModel) - { - // Early validation: check if file contains JSON and is not empty - if (!IsValidHelpFile(filePath)) - { - return new List(); - } - - // Read and validate JSON content - var fileContents = File.ReadAllText(filePath); - if (string.IsNullOrWhiteSpace(fileContents)) - { - return new List(); - } - - // Quick check if the file likely contains our target node before expensive parsing - if (!fileContents.Contains(selectedNode.CreationName, StringComparison.OrdinalIgnoreCase)) - { - return new List(); - } - - // Parse the workspace - var workspace = WorkspaceModel.FromJson(fileContents, dynamoViewModel.Model.LibraryServices, - dynamoViewModel.Model.EngineController, dynamoViewModel.Model.Scheduler, - dynamoViewModel.Model.NodeFactory, false, false, - dynamoViewModel.Model.CustomNodeManager, - dynamoViewModel.Model.LinterManager); - var matchingNode = workspace.Nodes - .FirstOrDefault(n => n.CreationName == selectedNode.CreationName); - - if (matchingNode == null) - return new List(); - - // Extract the prediction result - return ExtractPredictionFromWorkspace(matchingNode, selectedPortModel); - } - - /// - /// Validate if a file is a potentially valid help file - /// - private bool IsValidHelpFile(string filePath) - { - try - { - var fileInfo = new FileInfo(filePath); - if (!fileInfo.Exists || fileInfo.Length == 0) - { - return false; - } - - // Quick JSON format check - read first few characters - using (var reader = new StreamReader(filePath)) - { - var firstChar = (char)reader.Read(); - return firstChar == '{' || firstChar == '['; - } - } - catch - { - return false; - } - } - - /// - /// Extract prediction result from a workspace containing the matching node - /// - private List ExtractPredictionFromWorkspace(NodeModel matchingNode, PortModel selectedPortModel) - { - NodeModel predictedNode = null; - PortModel matchingPortModel = null; - - switch (selectedPortModel.PortType) - { - case PortType.Input: - if (matchingNode.InPorts.Count > selectedPortModel.Index) - { - matchingPortModel = matchingNode.InPorts[selectedPortModel.Index]; - if (matchingPortModel.Connectors.Any()) - { - predictedNode = matchingPortModel.Connectors.First().Start.Owner; - } - } - break; - case PortType.Output: - if (matchingNode.OutPorts.Count > selectedPortModel.Index) - { - matchingPortModel = matchingNode.OutPorts[selectedPortModel.Index]; - if (matchingPortModel.Connectors.Any()) - { - predictedNode = matchingPortModel.Connectors.First().End.Owner; - } - } - break; - } - - if (predictedNode == null) - return new List(); - var singleResultItem = new SingleResultItem(predictedNode, matchingPortModel.Index); - return new List { singleResultItem }; - } private T GetGenericAutocompleteResult(string endpoint) { From de47ddae9963566d85d3f6da6460502820057a36 Mon Sep 17 00:00:00 2001 From: john pierson Date: Mon, 23 Jun 2025 14:49:11 -0600 Subject: [PATCH 04/10] Update NodeAutoCompleteBarViewModel.cs --- .../NodeAutoCompleteBarViewModel.cs | 214 ------------------ 1 file changed, 214 deletions(-) diff --git a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs index 36c9d7fbbdd..f9f3c3233bf 100644 --- a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs +++ b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs @@ -1415,220 +1415,6 @@ private static int GetTypeDistance(string typea, string typeb, ProtoCore.Core co //if we can't find a match then dist should indicate that. return int.MaxValue; } - - } - - - - /// - /// Calculate fuzzy matching score between two strings - /// - private double CalculateFuzzyScore(string text, string pattern) - { - if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(pattern)) - return 0.0; - - // Convert to lowercase for case-insensitive comparison - text = text.ToLowerInvariant(); - pattern = pattern.ToLowerInvariant(); - - // Exact match gets highest score - if (text == pattern) return 1.0; - - // Contains check (original logic) - if (text.Contains(pattern)) return 0.8; - - // Fuzzy matching using multiple techniques - var scores = new List(); - - // 1. Levenshtein distance-based score - var levenshteinScore = 1.0 - (double)LevenshteinDistance(text, pattern) / Math.Max(text.Length, pattern.Length); - scores.Add(levenshteinScore); - - // 2. Subsequence matching (order matters) - var subsequenceScore = LongestCommonSubsequenceRatio(text, pattern); - scores.Add(subsequenceScore); - - // 3. Word boundary matching (useful for camelCase or snake_case) - var wordScore = CalculateWordBoundaryScore(text, pattern); - scores.Add(wordScore); - - // 4. Character frequency similarity - var frequencyScore = CalculateCharacterFrequencyScore(text, pattern); - scores.Add(frequencyScore); - - // Return the highest score from all techniques - return scores.Max(); - } - - /// - /// Calculate Levenshtein distance between two strings - /// - private int LevenshteinDistance(string source, string target) - { - if (source.Length == 0) return target.Length; - if (target.Length == 0) return source.Length; - - var matrix = new int[source.Length + 1, target.Length + 1]; - - for (int i = 0; i <= source.Length; i++) - matrix[i, 0] = i; - for (int j = 0; j <= target.Length; j++) - matrix[0, j] = j; - - for (int i = 1; i <= source.Length; i++) - { - for (int j = 1; j <= target.Length; j++) - { - var cost = (target[j - 1] == source[i - 1]) ? 0 : 1; - matrix[i, j] = Math.Min( - Math.Min(matrix[i - 1, j] + 1, matrix[i, j - 1] + 1), - matrix[i - 1, j - 1] + cost); - } - } - - return matrix[source.Length, target.Length]; - } - - /// - /// Calculate ratio of longest common subsequence - /// - private double LongestCommonSubsequenceRatio(string text, string pattern) - { - var lcs = LongestCommonSubsequence(text, pattern); - return (double)lcs / Math.Max(text.Length, pattern.Length); - } - - /// - /// Find length of longest common subsequence - /// - private int LongestCommonSubsequence(string text, string pattern) - { - var matrix = new int[text.Length + 1, pattern.Length + 1]; - - for (int i = 1; i <= text.Length; i++) - { - for (int j = 1; j <= pattern.Length; j++) - { - if (text[i - 1] == pattern[j - 1]) - matrix[i, j] = matrix[i - 1, j - 1] + 1; - else - matrix[i, j] = Math.Max(matrix[i - 1, j], matrix[i, j - 1]); - } - } - - return matrix[text.Length, pattern.Length]; - } - - /// - /// Score based on word boundary matches (camelCase, snake_case, etc.) - /// - private double CalculateWordBoundaryScore(string text, string pattern) - { - // Extract "words" from both strings - var textWords = ExtractWords(text); - var patternWords = ExtractWords(pattern); - - if (!patternWords.Any()) return 0.0; - - var matchCount = 0; - foreach (var patternWord in patternWords) - { - if (textWords.Any(tw => tw.StartsWith(patternWord, StringComparison.OrdinalIgnoreCase))) - { - matchCount++; - } - } - - return (double)matchCount / patternWords.Count; - } - - /// - /// Extract words from camelCase, snake_case, or space-separated text - /// - private List ExtractWords(string text) - { - var words = new List(); - - // Split on common separators - var parts = text.Split(new char[] { '_', '-', ' ', '.', '/' }, StringSplitOptions.RemoveEmptyEntries); - - foreach (var part in parts) - { - // Handle camelCase - var camelWords = SplitCamelCase(part); - words.AddRange(camelWords.Where(w => w.Length > 1)); // Ignore single characters - } - - return words.Where(w => !string.IsNullOrWhiteSpace(w)).ToList(); - } - - /// - /// Split camelCase string into individual words - /// - private List SplitCamelCase(string text) - { - var words = new List(); - var currentWord = new StringBuilder(); - - foreach (char c in text) - { - if (char.IsUpper(c) && currentWord.Length > 0) - { - words.Add(currentWord.ToString()); - currentWord.Clear(); - } - currentWord.Append(c); - } - - if (currentWord.Length > 0) - { - words.Add(currentWord.ToString()); - } - - return words; - } - - /// - /// Score based on character frequency similarity - /// - private double CalculateCharacterFrequencyScore(string text, string pattern) - { - var textFreq = GetCharacterFrequency(text); - var patternFreq = GetCharacterFrequency(pattern); - - var allChars = textFreq.Keys.Union(patternFreq.Keys); - var dotProduct = 0.0; - var textMagnitude = 0.0; - var patternMagnitude = 0.0; - - foreach (var c in allChars) - { - var textCount = textFreq.GetValueOrDefault(c, 0); - var patternCount = patternFreq.GetValueOrDefault(c, 0); - - dotProduct += textCount * patternCount; - textMagnitude += textCount * textCount; - patternMagnitude += patternCount * patternCount; - } - - if (textMagnitude == 0 || patternMagnitude == 0) return 0.0; - - // Cosine similarity - return dotProduct / (Math.Sqrt(textMagnitude) * Math.Sqrt(patternMagnitude)); - } - - /// - /// Get character frequency map for a string - /// - private Dictionary GetCharacterFrequency(string text) - { - var frequency = new Dictionary(); - foreach (char c in text) - { - frequency[c] = frequency.GetValueOrDefault(c, 0) + 1; - } - return frequency; } } } From 16142de3a1aa126aceb336d020f0d5be15c704cb Mon Sep 17 00:00:00 2001 From: john pierson Date: Mon, 23 Jun 2025 14:49:52 -0600 Subject: [PATCH 05/10] Update src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Services/LocalAutoCompleteService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs index 99cab0e566e..2201f44b7bf 100644 --- a/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs +++ b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs @@ -174,7 +174,7 @@ private List TryGetPackageAutoCompleteResult(NodeModel selecte /// List of autocomplete results from built-in help files private List TryGetBuiltInAutoCompleteResult(NodeModel selectedNode, PortModel selectedPortModel, string nodeFullName) { - var nodeHelpDocPath = new DirectoryInfo(Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, + var nodeHelpDocPath = new DirectoryInfo(Path.Combine(new FileInfo(Assembly.GetAssembly(typeof(DynamoModel)).Location).DirectoryName, Configurations.DynamoNodeHelpDocs)); if (!nodeHelpDocPath.Exists) From ad5dd9df7ea2484633a680f720046896d447201f Mon Sep 17 00:00:00 2001 From: john pierson Date: Mon, 23 Jun 2025 14:54:18 -0600 Subject: [PATCH 06/10] Update src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ViewModels/NodeAutoCompleteBarViewModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs index f9f3c3233bf..7fa8c27232d 100644 --- a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs +++ b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Dynamo.Configuration; From d5c9b644ae535482c825c5c3acad3ea491ef3c8b Mon Sep 17 00:00:00 2001 From: john pierson Date: Thu, 26 Jun 2025 21:26:09 -0600 Subject: [PATCH 07/10] Update LocalAutoCompleteService.cs --- .../Services/LocalAutoCompleteService.cs | 42 +++++-------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs index 2201f44b7bf..430289728c3 100644 --- a/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs +++ b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs @@ -16,6 +16,7 @@ using Dynamo.Utilities; using Dynamo.ViewModels; using Dynamo.NodeAutoComplete.ViewModels; +using DynamoUtilities; namespace Dynamo.NodeAutoComplete.Services { @@ -261,19 +262,26 @@ private List FindCandidateHelpFiles(string searchDirectory, string nodeF /// List of autocomplete results from the help file private List ProcessHelpFile(string filePath, NodeModel selectedNode, PortModel selectedPortModel) { - // Early validation: check if file contains JSON and is not empty - if (!IsValidHelpFile(filePath)) + // Early validation: check if file exists and is not empty + var fileInfo = new FileInfo(filePath); + if (!fileInfo.Exists || fileInfo.Length == 0) { return new List(); } - // Read and validate JSON content var fileContents = File.ReadAllText(filePath); + if (string.IsNullOrWhiteSpace(fileContents)) { return new List(); } + if (!PathHelper.isValidJson(filePath, out fileContents, out var exception)) + { + dynamoViewModel.Model.Logger.LogError($"Could not read help file: {exception.Message}"); + return new List(); + } + // Quick check if the file likely contains our target node before expensive parsing if (!fileContents.Contains(selectedNode.CreationName, StringComparison.OrdinalIgnoreCase)) { @@ -297,34 +305,6 @@ private List ProcessHelpFile(string filePath, NodeModel select return ExtractPredictionFromWorkspace(matchingNode, selectedPortModel); } - /// - /// Validate if a file is a potentially valid help file - /// - /// The path to the file to validate - /// True if the file appears to be a valid help file - private bool IsValidHelpFile(string filePath) - { - try - { - var fileInfo = new FileInfo(filePath); - if (!fileInfo.Exists || fileInfo.Length == 0) - { - return false; - } - - // Quick JSON format check - read first few characters - using (var reader = new StreamReader(filePath)) - { - var firstChar = (char)reader.Read(); - return firstChar == '{' || firstChar == '['; - } - } - catch - { - return false; - } - } - /// /// Extract prediction result from a workspace containing the matching node /// From c1cc57611de9112ed9fd4705898e70a6b4644fa9 Mon Sep 17 00:00:00 2001 From: john pierson Date: Thu, 26 Jun 2025 21:38:13 -0600 Subject: [PATCH 08/10] Update LocalAutoCompleteService.cs Fix for DYF nodes. --- .../Services/LocalAutoCompleteService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs index 430289728c3..79621f41f07 100644 --- a/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs +++ b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs @@ -76,8 +76,8 @@ private string GetNodeFullName(NodeModel selectedNode) string fullSignature = dsFunction.FunctionSignature; return fullSignature.Split('@')[0]; case Function function: - string fullFunctionSignature = function.FunctionSignature.ToString(); - return fullFunctionSignature.Split('@')[0]; + var daName = $"{selectedNode.Category}.{selectedNode.GetOriginalName()}"; + return $"{selectedNode.Category}.{selectedNode.GetOriginalName()}"; default: return selectedNode.GetType().ToString(); } From 22303d209385873035ada9cf58be10277e674f5e Mon Sep 17 00:00:00 2001 From: john pierson Date: Mon, 30 Jun 2025 08:54:57 -0600 Subject: [PATCH 09/10] Update src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ViewModels/NodeAutoCompleteBarViewModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs index 7fa8c27232d..66cf8bba95e 100644 --- a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs +++ b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs @@ -544,6 +544,7 @@ private IEnumerable GetNodeAutocompleMLResults() AutocompleteMLMessage = Resources.AutocompleteNoRecommendationsMessage; Analytics.TrackEvent(Actions.View, Categories.NodeAutoCompleteOperations, "NoRecommendation"); + Analytics.TrackEvent(Actions.View, Categories.NodeAutoCompleteOperations, "FallbackToLocalAutocomplete"); return localAutoCompleteService.TryGetLocalAutoCompleteResult(PortViewModel.NodeViewModel.NodeModel, PortViewModel.PortModel); } ServiceVersion = MLresults.Version; From 5284371116cb396b5bd4a4fcd9e9bc342f0a859c Mon Sep 17 00:00:00 2001 From: john pierson Date: Mon, 30 Jun 2025 11:25:25 -0600 Subject: [PATCH 10/10] Update LocalAutoCompleteService.cs --- .../Services/LocalAutoCompleteService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs index 79621f41f07..043d45316c5 100644 --- a/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs +++ b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs @@ -283,7 +283,7 @@ private List ProcessHelpFile(string filePath, NodeModel select } // Quick check if the file likely contains our target node before expensive parsing - if (!fileContents.Contains(selectedNode.CreationName, StringComparison.OrdinalIgnoreCase)) + if (!fileContents.Contains(selectedNode.GetOriginalName(), StringComparison.OrdinalIgnoreCase)) { return new List(); } @@ -296,7 +296,7 @@ private List ProcessHelpFile(string filePath, NodeModel select dynamoViewModel.Model.LinterManager); var matchingNode = workspace.Nodes - .FirstOrDefault(n => n.CreationName == selectedNode.CreationName); + .FirstOrDefault(n => n.GetOriginalName() == selectedNode.GetOriginalName()); if (matchingNode == null) return new List();