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