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/Services/LocalAutoCompleteService.cs b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs new file mode 100644 index 00000000000..043d45316c5 --- /dev/null +++ b/src/NodeAutoCompleteViewExtension/Services/LocalAutoCompleteService.cs @@ -0,0 +1,350 @@ +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; +using DynamoUtilities; + +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: + var daName = $"{selectedNode.Category}.{selectedNode.GetOriginalName()}"; + return $"{selectedNode.Category}.{selectedNode.GetOriginalName()}"; + 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.GetAssembly(typeof(DynamoModel)).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 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.GetOriginalName(), 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.GetOriginalName() == selectedNode.GetOriginalName()); + + if (matchingNode == null) + return new List(); + + // Extract the prediction result + return ExtractPredictionFromWorkspace(matchingNode, selectedPortModel); + } + + /// + /// 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 226f13a00f5..66cf8bba95e 100644 --- a/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs +++ b/src/NodeAutoCompleteViewExtension/ViewModels/NodeAutoCompleteBarViewModel.cs @@ -33,6 +33,7 @@ using System.Windows.Media; using System.ComponentModel; using System.Windows.Data; +using Dynamo.NodeAutoComplete.Services; namespace Dynamo.NodeAutoComplete.ViewModels { @@ -384,6 +385,11 @@ public bool IsDNAClusterPlacementEnabled internal List FullSingleResults { set; get; } private Guid LastRequestGuid; + /// + /// Service for handling local autocomplete functionality + /// + private readonly LocalAutoCompleteService localAutoCompleteService; + /// /// Constructor /// @@ -393,6 +399,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); } /// @@ -536,7 +543,9 @@ private IEnumerable GetNodeAutocompleMLResults() AutocompleteMLTitle = Resources.AutocompleteNoRecommendationsTitle; AutocompleteMLMessage = Resources.AutocompleteNoRecommendationsMessage; Analytics.TrackEvent(Actions.View, Categories.NodeAutoCompleteOperations, "NoRecommendation"); - return new List(); + + Analytics.TrackEvent(Actions.View, Categories.NodeAutoCompleteOperations, "FallbackToLocalAutocomplete"); + return localAutoCompleteService.TryGetLocalAutoCompleteResult(PortViewModel.NodeViewModel.NodeModel, PortViewModel.PortModel); } ServiceVersion = MLresults.Version; var results = new List(); @@ -609,6 +618,9 @@ private IEnumerable GetNodeAutocompleMLResults() return results; } + + + private T GetGenericAutocompleteResult(string endpoint) { var requestDTO = GenerateRequestForMLAutocomplete(); @@ -824,7 +836,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 @@ -1401,7 +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; } - } } } diff --git a/src/NodeAutoCompleteViewExtension/ViewModels/SingleResultItem.cs b/src/NodeAutoCompleteViewExtension/ViewModels/SingleResultItem.cs index 13e6ac7f07e..9524b79ee3a 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 = string.IsNullOrWhiteSpace(nodeModel.CreationName) ? nodeModel.GetOriginalName(): nodeModel.CreationName; + PortToConnect = portToConnect; + Score = score; + } + public SingleResultItem(NodeSearchElementViewModel x) : this(x.Model) { //Convert percent to probability