diff --git a/src/dotnet/APIView/APIView.sln b/src/dotnet/APIView/APIView.sln index 1aa0a367db1..0af2fc2164e 100644 --- a/src/dotnet/APIView/APIView.sln +++ b/src/dotnet/APIView/APIView.sln @@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpAPIParserTests", "..\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestReferenceWithInternalsVisibleTo", "..\Azure.ClientSdk.Analyzers\TestReferenceWithInternalsVisibleTo\TestReferenceWithInternalsVisibleTo.csproj", "{0FE36A2D-EB25-4119-A7DA-2605BB2516C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "APIViewJsonUtility", "APIViewJsonUtility\APIViewJsonUtility.csproj", "{C1C37681-2D54-4BDF-A4B8-834EC29AAFCF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,10 @@ Global {0FE36A2D-EB25-4119-A7DA-2605BB2516C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {0FE36A2D-EB25-4119-A7DA-2605BB2516C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {0FE36A2D-EB25-4119-A7DA-2605BB2516C2}.Release|Any CPU.Build.0 = Release|Any CPU + {C1C37681-2D54-4BDF-A4B8-834EC29AAFCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1C37681-2D54-4BDF-A4B8-834EC29AAFCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1C37681-2D54-4BDF-A4B8-834EC29AAFCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1C37681-2D54-4BDF-A4B8-834EC29AAFCF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/dotnet/APIView/APIView/Model/CodeFile.cs b/src/dotnet/APIView/APIView/Model/CodeFile.cs index f5b7fe1c666..3a9414544b4 100644 --- a/src/dotnet/APIView/APIView/Model/CodeFile.cs +++ b/src/dotnet/APIView/APIView/Model/CodeFile.cs @@ -11,7 +11,6 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using System.Text.RegularExpressions; using System.Threading.Tasks; namespace ApiView @@ -148,14 +147,207 @@ public async Task SerializeAsync(Stream stream) /// Generates a complete text representation of API surface to help generating the content. /// One use case of this function will be to support download request of entire API review surface. /// - public string GetApiText() + public string GetApiText(bool skipDocs = true) { StringBuilder sb = new(); foreach (var line in ReviewLines) { - line.AppendApiTextToBuilder(sb, 0, true); + line.AppendApiTextToBuilder(sb, 0, skipDocs, GetIndentationForLanguage(Language)); } return sb.ToString(); - } + } + + public static int GetIndentationForLanguage(string language) + { + switch (language) + { + case "C++": + case "C": + return 2; + default: + return 4; + } + } + + public void ConvertToTreeTokenModel() + { + Dictionary navigationItems = new Dictionary(); + ReviewLine reviewLine = new ReviewLine(); + ReviewLine previousLine = null; + bool isDocumentation = false; + bool isHidden = false; + bool skipDiff = false; + bool isDeprecated = false; + bool skipIndent = false; + string className = ""; + //Process all navigation items in old model to generate a map + GetNavigationMap(navigationItems, Navigation); + + List currentLineTokens = new List(); + foreach(var oldToken in Tokens) + { + ReviewToken token = null; + switch(oldToken.Kind) + { + case CodeFileTokenKind.DocumentRangeStart: + isDocumentation = true; break; + case CodeFileTokenKind.DocumentRangeEnd: + isDocumentation = false; break; + case CodeFileTokenKind.DeprecatedRangeStart: + isDeprecated = true; break; + case CodeFileTokenKind.DeprecatedRangeEnd: + isDeprecated = false; break; + case CodeFileTokenKind.SkipDiffRangeStart: + skipDiff = true; break; + case CodeFileTokenKind.SkipDiffRangeEnd: + skipDiff = false; break; + case CodeFileTokenKind.HiddenApiRangeStart: + isHidden = true; break; + case CodeFileTokenKind.HiddenApiRangeEnd: + isHidden = false; break; + case CodeFileTokenKind.Keyword: + token = ReviewToken.CreateKeywordToken(oldToken.Value, false); + var keywordValue = oldToken.Value.ToLower(); + if (keywordValue == "class" || keywordValue == "enum" || keywordValue == "struct" || keywordValue == "interface" || keywordValue == "type" || keywordValue == "namespace") + className = keywordValue; + break; + case CodeFileTokenKind.Comment: + token = ReviewToken.CreateCommentToken(oldToken.Value, false); + break; + case CodeFileTokenKind.Text: + token = ReviewToken.CreateTextToken(oldToken.Value, oldToken.NavigateToId, false); + break; + case CodeFileTokenKind.Punctuation: + token = ReviewToken.CreatePunctuationToken(oldToken.Value, false); + break; + case CodeFileTokenKind.TypeName: + token = ReviewToken.CreateTypeNameToken(oldToken.Value, false); + if (currentLineTokens.Any(t => t.Kind == TokenKind.Keyword && t.Value.ToLower() == className)) + token.RenderClasses.Add(className); + className = ""; + break; + case CodeFileTokenKind.MemberName: + token = ReviewToken.CreateMemberNameToken(oldToken.Value, false); + break; + case CodeFileTokenKind.StringLiteral: + token = ReviewToken.CreateStringLiteralToken(oldToken.Value, false); + break; + case CodeFileTokenKind.Literal: + token = ReviewToken.CreateLiteralToken(oldToken.Value, false); + break; + case CodeFileTokenKind.ExternalLinkStart: + token = ReviewToken.CreateStringLiteralToken(oldToken.Value, false); + break; + case CodeFileTokenKind.Whitespace: + if (currentLineTokens.Count > 0) { + currentLineTokens.Last().HasSuffixSpace = true; + } + else if (!skipIndent) { + reviewLine.Indent += oldToken.Value.Length; + } + break; + case CodeFileTokenKind.Newline: + var parent = previousLine; + skipIndent = false; + if (currentLineTokens.Count > 0) + { + while (parent != null && parent.Indent >= reviewLine.Indent) + parent = parent.parentLine; + } + else + { + //If current line is empty line then add it as an empty line under previous line's parent + parent = previousLine?.parentLine; + } + + if (parent == null) + { + this.ReviewLines.Add(reviewLine); + } + else + { + parent.Children.Add(reviewLine); + reviewLine.parentLine = parent; + } + + if (currentLineTokens.Count == 0) + { + //Empty line. So just add previous line as related line + reviewLine.RelatedToLine = previousLine?.LineId; + } + else + { + reviewLine.Tokens = currentLineTokens; + previousLine = reviewLine; + } + + reviewLine = new ReviewLine(); + // If previous line ends with "," then next line will be sub line to show split content in multiple lines. + // Set next line's indent same as current line + // This is required to convert C++ tokens correctly + if (previousLine != null && previousLine.Tokens.LastOrDefault()?.Value == "," && Language == "C++") + { + reviewLine.Indent = previousLine.Indent; + skipIndent = true; + } + currentLineTokens = new List(); + break; + case CodeFileTokenKind.LineIdMarker: + if (string.IsNullOrEmpty(reviewLine.LineId)) + reviewLine.LineId = oldToken.Value; + break; + default: + Console.WriteLine($"Unsupported token kind to convert to new model, Kind: {oldToken.Kind}, value: {oldToken.Value}, Line Id: {oldToken.DefinitionId}"); + break; + } + + if (token != null) + { + currentLineTokens.Add(token); + + if (oldToken.Equals("}") || oldToken.Equals("};")) + reviewLine.IsContextEndLine = true; + if (isHidden) + reviewLine.IsHidden = true; + if (oldToken.DefinitionId != null) + reviewLine.LineId = oldToken.DefinitionId; + if (oldToken.CrossLanguageDefinitionId != null) + reviewLine.CrossLanguageId = oldToken.CrossLanguageDefinitionId; + if (isDeprecated) + token.IsDeprecated = true; + if (skipDiff) + token.SkipDiff = true; + if (isDocumentation) + token.IsDocumentation = true; + } + } + + //Process last line + if (currentLineTokens.Count > 0) + { + reviewLine.Tokens = currentLineTokens; + var parent = previousLine; + while (parent != null && parent.Indent >= reviewLine.Indent) + parent = parent.parentLine; + + if (parent == null) + this.ReviewLines.Add(reviewLine); + else + parent.Children.Add(reviewLine); + } + } + + private static void GetNavigationMap(Dictionary navigationItems, NavigationItem[] items) + { + if (items == null) + return; + + foreach (var item in items) + { + var key = string.IsNullOrEmpty(item.NavigationId) ? item.Text : item.NavigationId; + navigationItems.Add(key, item.Text); + GetNavigationMap(navigationItems, item.ChildItems); + } + } } } diff --git a/src/dotnet/APIView/APIView/Model/V2/ReviewLine.cs b/src/dotnet/APIView/APIView/Model/V2/ReviewLine.cs index 2779cd4b0c8..9b0c758615a 100644 --- a/src/dotnet/APIView/APIView/Model/V2/ReviewLine.cs +++ b/src/dotnet/APIView/APIView/Model/V2/ReviewLine.cs @@ -56,12 +56,16 @@ public class ReviewLine [JsonIgnore] public bool Processed { get; set; } = false; + [JsonIgnore] + public int Indent { get; set; } + [JsonIgnore] + public ReviewLine parentLine { get; set; } public void AddToken(ReviewToken token) { Tokens.Add(token); } - public void AppendApiTextToBuilder(StringBuilder sb, int indent = 0, bool skipDocs = true) + public void AppendApiTextToBuilder(StringBuilder sb, int indent = 0, bool skipDocs = true, int lineIndentSpaces = 4) { if (skipDocs && Tokens.Count > 0 && Tokens[0].IsDocumentation == true) { @@ -77,7 +81,10 @@ public void AppendApiTextToBuilder(StringBuilder sb, int indent = 0, bool skipDo //Add spaces for indentation for (int i = 0; i < indent; i++) { - sb.Append(" "); + for(int j = 0; j < lineIndentSpaces; j++) + { + sb.Append(" "); + } } //Process all tokens sb.Append(ToString(true)); @@ -85,7 +92,7 @@ public void AppendApiTextToBuilder(StringBuilder sb, int indent = 0, bool skipDo sb.Append(Environment.NewLine); foreach (var child in Children) { - child.AppendApiTextToBuilder(sb, indent + 1, skipDocs); + child.AppendApiTextToBuilder(sb, indent + 1, skipDocs, lineIndentSpaces); } } diff --git a/src/dotnet/APIView/APIViewJsonUtility/APIViewJsonUtility.csproj b/src/dotnet/APIView/APIViewJsonUtility/APIViewJsonUtility.csproj new file mode 100644 index 00000000000..7d37d10fb3b --- /dev/null +++ b/src/dotnet/APIView/APIViewJsonUtility/APIViewJsonUtility.csproj @@ -0,0 +1,20 @@ + + + + Exe + true + net8.0 + enable + enable + APIViewJsonTool + + + + + + + + + + + diff --git a/src/dotnet/APIView/APIViewJsonUtility/Program.cs b/src/dotnet/APIView/APIViewJsonUtility/Program.cs new file mode 100644 index 00000000000..92232f5ba56 --- /dev/null +++ b/src/dotnet/APIView/APIViewJsonUtility/Program.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.CommandLine.Invocation; +using System.CommandLine; +using System.IO.Compression; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using APIView; +using ApiView; + +public static class Program +{ + public static int Main(string[] args) + { + var jsonFilePath = new Option("--path", "Path to the input json file").ExistingOnly(); + jsonFilePath.IsRequired = true; + var outputDir = new Option("--outputDir", "Path to the output Directory(Optional)."); + var dumpOption = new Option("--dumpApiText", "Dump the API text to a txt file."); + var convertOption = new Option("--convertToTree", "Convert APIView flat token model into a parent - child token model"); + + var rootCommand = new RootCommand("Generate API review output from token JSON file to verify the input json file") + { + jsonFilePath, + outputDir, + dumpOption, + convertOption + }; + + rootCommand.SetHandler(static async (jsonFilePath, outputDir, dumpOption, convertOption) => + { + if(!dumpOption && !convertOption) + { + Console.Error.WriteLine("Please specify either --dumpApiText or --convertToTree option."); + return; + } + + try + { + var outputFileDirectory = outputDir?.FullName ?? jsonFilePath.Directory?.FullName; + outputFileDirectory = outputFileDirectory == null? Path.GetTempPath() : outputFileDirectory; + if (dumpOption) + { + + var outputFilePath = Path.Combine(outputFileDirectory, jsonFilePath.Name.Replace(".json", ".txt")); + Console.WriteLine($"Writing API text to {outputFilePath}"); + using (var stream = jsonFilePath.OpenRead()) + { + await GenerateReviewTextFromJson(stream, outputFilePath); + } + } + + if (convertOption) + { + var outputFilePath = Path.Combine(outputFileDirectory, jsonFilePath.Name.Replace(".json", "_new.json")); + Console.WriteLine($"Converting previous Json APIView flat tokens to new Json parent - child token model. New file: {outputFilePath}"); + using (var stream = jsonFilePath.OpenRead()) + { + await ConvertToTreeModel(stream, outputFilePath); + } + } + + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error reading input json file : {ex.Message}"); + throw; + } + }, jsonFilePath, outputDir, dumpOption, convertOption); + return rootCommand.InvokeAsync(args).Result; + } + + private static async Task GenerateReviewTextFromJson(Stream stream, string outputFilePath) + { + var codeFile = await CodeFile.DeserializeAsync(stream, false, true); + string apiOutput = codeFile.GetApiText(false); + await File.WriteAllTextAsync(outputFilePath, apiOutput); + } + + private static async Task ConvertToTreeModel(Stream stream, string outputFilePath) + { + try + { + var codeFile = await CodeFile.DeserializeAsync(stream, false, false); + if (codeFile != null) + { + codeFile.ConvertToTreeTokenModel(); + Console.WriteLine("Converted APIView token model to parent - child token model"); + codeFile.Tokens = null; + using var fileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write); + await codeFile.SerializeAsync(fileStream); + Console.WriteLine($"New APIView json parent - child token model file generated at {outputFilePath}"); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Input json is probably not using flat token model. Error reading input json file. : {ex.Message}"); + throw; + } + } +} diff --git a/src/dotnet/APIView/APIViewJsonUtility/ci.yml b/src/dotnet/APIView/APIViewJsonUtility/ci.yml new file mode 100644 index 00000000000..c266eb35ecb --- /dev/null +++ b/src/dotnet/APIView/APIViewJsonUtility/ci.yml @@ -0,0 +1,27 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. +trigger: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + paths: + include: + - src/dotnet/APIView/APIViewJsonUtility + +pr: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + paths: + include: + - src/dotnet/APIView/APIViewJsonUtility + +extends: + template: /eng/pipelines/templates/stages/archetype-sdk-tool-dotnet.yml + parameters: + PackageDirectory: $(Build.SourcesDirectory)/src/dotnet/APIView/APIViewJsonUtility diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/CodeFileHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/CodeFileHelpers.cs index aabe09e095e..62d636ef7b0 100644 --- a/src/dotnet/APIView/APIViewWeb/Helpers/CodeFileHelpers.cs +++ b/src/dotnet/APIView/APIViewWeb/Helpers/CodeFileHelpers.cs @@ -7,6 +7,7 @@ using APIViewWeb.LeanModels; using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Threading.Tasks; @@ -35,7 +36,7 @@ public static async Task GenerateCodePanelDataAsync(CodePanelRawD CollectDocumentationLines(codePanelData, diffLines, codePanelData.DiffDocumentationMap, 1, "root", true); // Check if diff is required for active revision and diff revision to avoid unnecessary diff calculation bool hasSameApis = AreCodeFilesSame(codePanelRawData.activeRevisionCodeFile, codePanelRawData.diffRevisionCodeFile); - if(!hasSameApis) + if (!hasSameApis) { reviewLines = FindDiff(reviewLines, codePanelRawData.diffRevisionCodeFile.ReviewLines); // Remap nodeIdHashed for documentation to diff adjusted nodeIdHashed so that documentation is correctly listed on review. @@ -52,20 +53,23 @@ public static async Task GenerateCodePanelDataAsync(CodePanelRawD int idx = 0; string nodeHashId = ""; - Dictionary relatedLineMap = new Dictionary(); + Dictionary relatedLineMap = new Dictionary(); foreach (var reviewLine in reviewLines) { if (reviewLine.IsDocumentation) continue; nodeHashId = await BuildAPITree(codePanelData: codePanelData, codePanelRawData: codePanelRawData, reviewLine: reviewLines[idx], parentNodeIdHashed: rootNodeId, nodePositionAtLevel: idx, prevNodeHashId: nodeHashId, relatedLineMap: relatedLineMap); - idx++; + idx++; } //Set related line's node ID hashed in tree metadata - foreach(var key in relatedLineMap.Keys) + foreach (var key in relatedLineMap.Keys) { codePanelData.SetLineAsRelated(key, relatedLineMap[key]); } + + // Create navigation tree using information is code file for backward compatibility when existing code file object is converted to new model. + CreateNavigationTree(codePanelData, codePanelRawData.activeRevisionCodeFile); return codePanelData; } @@ -98,13 +102,13 @@ private static async Task BuildAPITree(CodePanelData codePanelData, Code // Process all child lines int idx = 0; - string childNodeHashId = ""; + string childNodeHashId = ""; foreach (var childLine in reviewLine.Children) { if (childLine.IsDocumentation) continue; childNodeHashId = await BuildAPITree(codePanelData: codePanelData, codePanelRawData: codePanelRawData, reviewLine: childLine, - parentNodeIdHashed: nodeIdHashed, nodePositionAtLevel: idx, prevNodeHashId: childNodeHashId, relatedLineMap: relatedLineMap, indent: indent + 1); + parentNodeIdHashed: nodeIdHashed, nodePositionAtLevel: idx, prevNodeHashId: childNodeHashId, relatedLineMap: relatedLineMap, indent: indent + 1); idx++; }; @@ -117,7 +121,7 @@ private static async Task BuildAPITree(CodePanelData codePanelData, Code var classes = codePanelData.NodeMetaDataObj[prevNodeHashId].CodeLinesObj.LastOrDefault()?.RowClassesObj; if (classes != null) { - classes = classes.Where(c=> c == "added" || c == "removed").ToHashSet(); + classes = classes.Where(c => c == "added" || c == "removed").ToHashSet(); codePanelData.NodeMetaDataObj[nodeIdHashed].CodeLinesObj.LastOrDefault()?.RowClassesObj.UnionWith(classes); } } @@ -188,7 +192,7 @@ private static void BuildNodeTokens(CodePanelData codePanelData, CodePanelRawDat bool skipDocDiff = diffDocLines.Count == 0 || activeDocLines.Count == 0; foreach (var docRow in docLines) { - if(!skipDocDiff && !docsIntersect.Contains(docRow)) + if (!skipDocDiff && !docsIntersect.Contains(docRow)) { if (activeDocs.Contains(docRow)) { @@ -199,14 +203,14 @@ private static void BuildNodeTokens(CodePanelData codePanelData, CodePanelRawDat { docRow.DiffKind = DiffKind.Removed; docRow.RowClassesObj.Add("removed"); - } + } } docRow.NodeId = codePanelRow.NodeId; docRow.NodeIdHashed = codePanelRow.NodeIdHashed; InsertCodePanelRowData(codePanelData: codePanelData, rowData: docRow, nodeIdHashed: nodeIdHashed); } } - + // Get comment for current row var commentsForRow = CollectUserCommentsForRow(codePanelRawData, reviewLine.LineId, nodeIdHashed, codePanelRow); //Add code line and comment to code panel data @@ -220,7 +224,7 @@ private static CodePanelRowData GetCodePanelRowData(CodePanelData codePanelData, { CodePanelRowData codePanelRowData = new() { - Type = reviewLine.Tokens.Any(t => t.IsDocumentation == true)? CodePanelRowDatatype.Documentation : CodePanelRowDatatype.CodeLine, + Type = reviewLine.Tokens.Any(t => t.IsDocumentation == true) ? CodePanelRowDatatype.Documentation : CodePanelRowDatatype.CodeLine, NodeIdHashed = nodeIdHashed, NodeId = reviewLine.LineId, Indent = indent, @@ -269,7 +273,7 @@ private static CodePanelRowData GetCodePanelRowData(CodePanelData codePanelData, return codePanelRowData; } - + private static CodePanelRowData CollectUserCommentsForRow(CodePanelRawData codePanelRawData, string nodeId, string nodeIdHashed, CodePanelRowData codePanelRowData) { var commentRowData = new CodePanelRowData(); @@ -302,7 +306,7 @@ private static void InsertCodePanelRowData(CodePanelData codePanelData, CodePane codePanelData.NodeMetaDataObj[nodeIdHashed] = new CodePanelNodeMetaData(); if (rowData.Type == CodePanelRowDatatype.Documentation) - { + { rowData.RowPositionInGroup = codePanelData.NodeMetaDataObj[nodeIdHashed].DocumentationObj.Count(); codePanelData.NodeMetaDataObj[nodeIdHashed].DocumentationObj.Add(rowData); } @@ -315,7 +319,7 @@ private static void InsertCodePanelRowData(CodePanelData codePanelData, CodePane { codePanelData.NodeMetaDataObj[nodeIdHashed].IsNodeWithDiff = true; var parentId = codePanelData.NodeMetaDataObj[nodeIdHashed].ParentNodeIdHashed; - while (parentId != null && !parentId.Equals("root") && codePanelData.NodeMetaDataObj.ContainsKey(parentId) + while (parentId != null && !parentId.Equals("root") && codePanelData.NodeMetaDataObj.ContainsKey(parentId) && !codePanelData.NodeMetaDataObj[parentId].IsNodeWithDiffInDescendants) { codePanelData.NodeMetaDataObj[parentId].IsNodeWithDiffInDescendants = true; @@ -371,14 +375,14 @@ private static bool AreReviewLinesSame(List reviewLinesA, List FindDiff(List activeLines, List diffLines) { List resultLines = []; - Dictionary refCountMap = []; + Dictionary refCountMap = []; //Set lines from diff revision as not from active revision foreach (var line in diffLines) @@ -389,11 +393,11 @@ public static List FindDiff(List activeLines, List FindDiff(List activeLines, List FindDiff(List activeLines, List l.LineId == line.LineId && l.Processed == false && l.Equals(line)); - var diffLineChildren = diffLine != null ? diffLine.Children: new List(); + var diffLineChildren = diffLine != null ? diffLine.Children : new List(); var resultantSubTree = FindDiff(activeLine.Children, diffLineChildren); //Update new resulting subtree as children of current node activeLine.Children.Clear(); @@ -450,8 +454,8 @@ private static void MarkTreeNodeAsModified(ReviewLine line, DiffKind diffKind) line.DiffKind = diffKind; foreach (var child in line.Children) { - if(!child.IsDocumentation) - MarkTreeNodeAsModified(child, diffKind); + if (!child.IsDocumentation) + MarkTreeNodeAsModified(child, diffKind); } } /*** @@ -460,7 +464,7 @@ private static void MarkTreeNodeAsModified(ReviewLine line, DiffKind diffKind) * */ private static void CollectDocumentationLines(CodePanelData codePanelData, List reviewLines, Dictionary> documentationRowMap, int indent, string parentNodeIdHash, bool enableSkipDiff = false) { - if(reviewLines?.Count == 0) + if (reviewLines?.Count == 0) return; List docRows = []; @@ -470,7 +474,7 @@ private static void CollectDocumentationLines(CodePanelData codePanelData, List< { //Find if current line has at least one token that's not marked as skip from diff check bool hasNonSkippedTokens = line.Tokens.Any(t => t.SkipDiff != true); - if(line.IsDocumentation && (!enableSkipDiff ||hasNonSkippedTokens)) + if (line.IsDocumentation && (!enableSkipDiff || hasNonSkippedTokens)) { docRows.Add(GetCodePanelRowData(codePanelData, line, parentNodeIdHash, indent)); continue; @@ -478,7 +482,8 @@ private static void CollectDocumentationLines(CodePanelData codePanelData, List< //Create hash id for code line and set node ID and hash Id for documentation rows var nodeIdHashed = line.GetTokenNodeIdHash(parentNodeIdHash, idx); - docRows.ForEach( d=> { + docRows.ForEach(d => + { d.NodeIdHashed = nodeIdHashed; d.NodeId = line.LineId; }); @@ -520,7 +525,7 @@ private static void RemapDocumentationLines(List activeLines, Dictio if (line.Children.Count > 0) { RemapDocumentationLines(line.Children, documentationRowMap, oldHashId, newHashId); - } + } idx++; // Move previous review line index before diff calculation based on diff type so we can calculate old HashId correctly. @@ -529,7 +534,7 @@ private static void RemapDocumentationLines(List activeLines, Dictio activeIdx++; diffIdx++; } - else if(line.DiffKind == DiffKind.Added) + else if (line.DiffKind == DiffKind.Added) { activeIdx++; } @@ -543,7 +548,7 @@ private static void RemapDocumentationLines(List activeLines, Dictio private static void FindModifiedTokens(ReviewLine lineA, ReviewLine lineB) { var lineATokenMap = new Dictionary(); - foreach(var token in lineA.Tokens) + foreach (var token in lineA.Tokens) { lineATokenMap[token.Value] = token; } @@ -553,7 +558,7 @@ private static void FindModifiedTokens(ReviewLine lineA, ReviewLine lineB) lineBTokenMap[token.Value] = token; } - foreach( var key in lineBTokenMap.Keys.Except(lineATokenMap.Keys)) + foreach (var key in lineBTokenMap.Keys.Except(lineATokenMap.Keys)) { lineBTokenMap[key].RenderClasses.Add("diff-change"); } @@ -563,5 +568,41 @@ private static void FindModifiedTokens(ReviewLine lineA, ReviewLine lineB) } } + private static void CreateNavigationTree(CodePanelData codePanelData, CodeFile codeFile) + { + if (codeFile.Navigation != null && codeFile.Navigation.Count() > 0) + { + //Use navigation tree generated by the parser + foreach (var navigation in codeFile.Navigation) + { + codePanelData.NavigationTreeNodesObj.Add(ProcessNavigationNodeFromOldModel(codePanelData.LineIdToNodeIdHashed, navigation)); + } + } + } + + private static NavigationTreeNode ProcessNavigationNodeFromOldModel(Dictionary nodeIdMap, NavigationItem navItem) + { + NavigationTreeNode node = new NavigationTreeNode() + { + Label = navItem.Text, + Expanded = true, + Data = new NavigationTreeNodeData() + }; + + if(navItem.Tags != null) + { + node.Data.Kind = navItem.Tags["TypeKind"]; + node.Data.Icon = node.Data.Kind; + } + + if (nodeIdMap.ContainsKey(navItem.NavigationId)) + node.Data.NodeIdHashed = nodeIdMap[navItem.NavigationId]; + + foreach (var childItem in navItem.ChildItems) + { + node.ChildrenObj.Add(ProcessNavigationNodeFromOldModel(nodeIdMap, childItem)); + } + return node; + } } } diff --git a/src/dotnet/APIView/APIViewWeb/LeanModels/CodePanelModels.cs b/src/dotnet/APIView/APIViewWeb/LeanModels/CodePanelModels.cs index 5e8bf732388..c6bc06c772d 100644 --- a/src/dotnet/APIView/APIViewWeb/LeanModels/CodePanelModels.cs +++ b/src/dotnet/APIView/APIViewWeb/LeanModels/CodePanelModels.cs @@ -106,6 +106,9 @@ public class CodePanelData public Dictionary> ActiveDocumentationMap { get; set; } = new Dictionary>(); [JsonIgnore] public Dictionary> DiffDocumentationMap { get; set; } = new Dictionary>(); + [JsonIgnore] + public List NavigationTreeNodesObj { get; set; } = []; + public NavigationTreeNode[] NavigationTreeNodes => NavigationTreeNodesObj != null ? NavigationTreeNodesObj.ToArray() : null; public void AddLineIdNodeHashMapping(string lineId, string nodeId) { diff --git a/src/dotnet/APIView/ClientSPA/src/app/_models/codePanelModels.ts b/src/dotnet/APIView/ClientSPA/src/app/_models/codePanelModels.ts index 2844d2ab0d4..27f4537f89e 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_models/codePanelModels.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_models/codePanelModels.ts @@ -58,6 +58,7 @@ export interface CodePanelData { nodeMetaData: { [key: string]: CodePanelNodeMetaData }; hasDiff: boolean; hasHiddenAPIThatIsDiff: boolean; + navigationTreeNodes: NavigationTreeNode[]; } export class CodePanelNodeMetaData { diff --git a/src/dotnet/APIView/ClientSPA/src/app/_workers/apitree-builder.worker.ts b/src/dotnet/APIView/ClientSPA/src/app/_workers/apitree-builder.worker.ts index 8f60846afda..a0719becd25 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_workers/apitree-builder.worker.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_workers/apitree-builder.worker.ts @@ -17,6 +17,7 @@ let toggleDocumentationClassPart = "bi-arrow-up-square"; let hasHiddenAPI: boolean = false; let visibleNodes: Set = new Set(); let addPostDiffContext: boolean = false; +let isNavigationTreeCreated: boolean = false; addEventListener('message', ({ data }) => { if (data instanceof ArrayBuffer) { @@ -27,6 +28,11 @@ addEventListener('message', ({ data }) => { apiTreeBuilderData!.diffStyle = FULL_DIFF_STYLE; // If there is no diff nodes and tree diff will not work } + if (codePanelData?.navigationTreeNodes && codePanelData?.navigationTreeNodes.length > 0) + { + isNavigationTreeCreated = true; + navigationTree = codePanelData?.navigationTreeNodes; + } buildCodePanelRows("root", navigationTree); const codePanelRowDataMessage : InsertCodePanelRowDataMessage = { directive: ReviewPageWorkerMessageDirective.UpdateCodePanelRowData, @@ -60,6 +66,7 @@ addEventListener('message', ({ data }) => { apiTreeBuilderData = null; visibleNodes = new Set(); addPostDiffContext = false; + isNavigationTreeCreated = false; } else { apiTreeBuilderData = data; @@ -106,12 +113,12 @@ function buildCodePanelRows(nodeIdHashed: string, navigationTree: NavigationTree } let navigationChildren = navigationTree; - if (node.navigationTreeNode) { + if (!isNavigationTreeCreated && node.navigationTreeNode) { if (!node.navigationTreeNode.children) { node.navigationTreeNode.children = []; } navigationChildren = node.navigationTreeNode.children; - } + } if ((!node.childrenNodeIdsInOrder || Object.keys(node.childrenNodeIdsInOrder).length === 0) && node.isNodeWithDiff) { codePanelRowData.push(...diffBuffer); @@ -191,7 +198,7 @@ function buildCodePanelRows(nodeIdHashed: string, navigationTree: NavigationTree } } - if (buildNode && node.navigationTreeNode) { + if (buildNode && node.navigationTreeNode && !isNavigationTreeCreated) { navigationTree.push(node.navigationTreeNode); }