diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index e5954d1..9a011d0 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -10,7 +10,7 @@ permissions: contents: read env: - version: '10.10.${{ github.run_number }}' + version: '10.11.${{ github.run_number }}' dotnetVersion: '8' repoUrl: ${{ github.server_url }}/${{ github.repository }} vsixPath: src/CodeNav/bin/Release/net472/CodeNav.vsix @@ -142,20 +142,20 @@ jobs: uses: actions/download-artifact@v8 - name: Publish to Marketplace - uses: cezarypiatek/VsixPublisherAction@1.2 + uses: madskristensen/publish-marketplace@v2 with: extension-file: ${{ github.workspace }}/CodeNav/${{ env.vsixPath }} publish-manifest-file: CodeNav/publish-manifest.json personal-access-code: ${{ secrets.VS_PUBLISHER_ACCESS_TOKEN }} - name: Publish to Marketplace 2022 - uses: cezarypiatek/VsixPublisherAction@1.2 + uses: madskristensen/publish-marketplace@v2 with: extension-file: ${{ github.workspace }}/CodeNav2022/${{ env.vsixPath }} publish-manifest-file: CodeNav2022/publish-manifest-2022.json personal-access-code: ${{ secrets.VS_PUBLISHER_ACCESS_TOKEN }} - name: Publish to Open VSIX Gallery - uses: timheuer/openvsixpublish@v1 + uses: madskristensen/publish-vsixgallery@v1 with: vsix-file: ${{ github.workspace }}/CodeNav/${{ env.vsixPath }} \ No newline at end of file diff --git a/README.md b/README.md index 1e9c389..404df21 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Visual Studio extension to show the code structure of your current document # 🔤 Language support - C# +- Visual Basic (enable it in the CodeNav settings) # ⬇️ Installing [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=SamirBoulema.CodeNav2026) @@ -60,4 +61,4 @@ The CodeNav tool window can be opened via the entry in the `Extensions` menu. ![Compact](https://raw.githubusercontent.com/sboulema/CodeNav/main/art/Compact.png) # 📄 Documentation -Looking for more info? Read the full [Documentation](https://github.com/sboulema/CodeNav/blob/main/docs/index.md) with links, guides, troubleshooting, and tips. \ No newline at end of file +Looking for more info? Read the full [Documentation](https://github.com/sboulema/CodeNav/blob/main/docs/index.md) with links, guides, troubleshooting, and tips. diff --git a/src/CodeNav.OutOfProc/CodeNav.OutOfProc.csproj b/src/CodeNav.OutOfProc/CodeNav.OutOfProc.csproj index 94a6565..1f77323 100644 --- a/src/CodeNav.OutOfProc/CodeNav.OutOfProc.csproj +++ b/src/CodeNav.OutOfProc/CodeNav.OutOfProc.csproj @@ -19,7 +19,8 @@ - + + diff --git a/src/CodeNav.OutOfProc/Dialogs/SettingsDialog/SettingsDialogControl.xaml b/src/CodeNav.OutOfProc/Dialogs/SettingsDialog/SettingsDialogControl.xaml index 0201876..aa05bce 100644 --- a/src/CodeNav.OutOfProc/Dialogs/SettingsDialog/SettingsDialogControl.xaml +++ b/src/CodeNav.OutOfProc/Dialogs/SettingsDialog/SettingsDialogControl.xaml @@ -42,6 +42,21 @@ + + + + C# + + + + Visual Basic + + + + - \ No newline at end of file + diff --git a/src/CodeNav.OutOfProc/Dialogs/SettingsDialog/SettingsDialogData.cs b/src/CodeNav.OutOfProc/Dialogs/SettingsDialog/SettingsDialogData.cs index 2c30e7f..eb925ed 100644 --- a/src/CodeNav.OutOfProc/Dialogs/SettingsDialog/SettingsDialogData.cs +++ b/src/CodeNav.OutOfProc/Dialogs/SettingsDialog/SettingsDialogData.cs @@ -1,4 +1,5 @@ -using System.Runtime.Serialization; +using CodeNav.OutOfProc.Constants; +using System.Runtime.Serialization; namespace CodeNav.OutOfProc.Dialogs.SettingsDialog; @@ -52,4 +53,16 @@ public class SettingsDialogData /// [DataMember] public bool UseCompactMode { get; set; } + + /// + /// Setting which languages are enabled for CodeNav. + /// + [DataMember] + public bool EnableCSharp { get; set; } = true; + + /// + /// Setting which languages are enabled for CodeNav. + /// + [DataMember] + public bool EnableVisualBasic { get; set; } = false; } diff --git a/src/CodeNav.OutOfProc/Interfaces/IDocumentMapper.cs b/src/CodeNav.OutOfProc/Interfaces/IDocumentMapper.cs new file mode 100644 index 0000000..04ac3a4 --- /dev/null +++ b/src/CodeNav.OutOfProc/Interfaces/IDocumentMapper.cs @@ -0,0 +1,19 @@ +using CodeNav.OutOfProc.Models; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.VisualStudio.Extensibility; + +namespace CodeNav.OutOfProc.Interfaces; + +public interface IDocumentMapper +{ + bool CanMapDocument( + string filePath, + GlobalSettings settings); + + Task> MapDocument( + string text, + string? excludeFilePath, + CodeDocumentViewModel codeDocumentViewModel, + VisualStudioExtensibility extensibility, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/DocumentMapper.cs b/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/DocumentMapper.cs index e873f01..e0b7989 100644 --- a/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/DocumentMapper.cs +++ b/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/DocumentMapper.cs @@ -1,3 +1,5 @@ +using CodeNav.OutOfProc.Interfaces; +using CodeNav.OutOfProc.Models; using CodeNav.OutOfProc.ViewModels; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -7,7 +9,7 @@ namespace CodeNav.OutOfProc.Languages.CSharp.Mappers; -public class DocumentMapper +public class DocumentMapper : IDocumentMapper { /// /// Map text document to list of code items. @@ -18,7 +20,7 @@ public class DocumentMapper /// Visual Studio extensibility used to retrieve all solution files for compilation /// Cancellation token /// List of code items - public static async Task> MapDocument( + public async Task> MapDocument( string text, string? excludeFilePath, CodeDocumentViewModel codeDocumentViewModel, @@ -138,6 +140,9 @@ SyntaxKind.ExtensionBlockDeclaration when member is ExtensionBlockDeclarationSyn _ => null, }; - public static bool CanMapDocument(string filePath) - => filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase); + public bool CanMapDocument( + string filePath, + GlobalSettings settings) + => settings.EnableCSharp && + filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase); } \ No newline at end of file diff --git a/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/RegionMapper.cs b/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/RegionMapper.cs index 9fa8ffa..ecb3d42 100644 --- a/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/RegionMapper.cs +++ b/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/RegionMapper.cs @@ -8,7 +8,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -using System.Drawing; namespace CodeNav.OutOfProc.Languages.CSharp.Mappers; diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/BaseMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/BaseMapper.cs new file mode 100644 index 0000000..5a1852e --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/BaseMapper.cs @@ -0,0 +1,166 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class BaseMapper +{ + public static T MapBase( + SyntaxNode source, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel, + SyntaxToken? identifier = null, + NameSyntax? nameSyntax = null, + string name = "", + SyntaxTokenList? modifiers = null) where T : CodeItem + { + var codeItem = Activator.CreateInstance(); + + var codeItemName = MapName(identifier, nameSyntax, name); + + codeItem.Name = codeItemName; + codeItem.FullName = MapFullName( + source, + codeItemName, + semanticModel); + + codeItem.FilePath = + string.IsNullOrEmpty(source.SyntaxTree.FilePath) + ? null + : new Uri(source.SyntaxTree.FilePath); + + codeItem.Id = codeItem.FullName; + codeItem.Tooltip = codeItemName; + codeItem.Access = MapAccess(modifiers, source); + codeItem.CodeDocumentViewModel = codeDocumentViewModel; + + codeItem.Span = source.Span; + codeItem.IdentifierSpan = identifier?.Span; + codeItem.OutlineSpan = MapOutlineSpan( + codeItem.Span, + codeItem.IdentifierSpan, + nameSyntax?.Span); + + return codeItem; + } + + private static TextSpan MapOutlineSpan( + TextSpan span, + TextSpan? identifierSpan, + TextSpan? nameSpan) + { + var outlineSpanStart = 0; + + if (nameSpan != null) + { + outlineSpanStart = nameSpan.Value.End; + } + + if (identifierSpan != null) + { + outlineSpanStart = identifierSpan.Value.End; + } + + return new TextSpan( + outlineSpanStart, + span.End - outlineSpanStart); + } + + private static string MapFullName( + SyntaxNode source, + string name, + SemanticModel semanticModel) + { + try + { + var symbol = semanticModel.GetDeclaredSymbol(source); + + return symbol?.ToString() ?? name; + } + catch (Exception) + { + return name; + } + } + + private static string MapName( + SyntaxToken? identifier, + NameSyntax? nameSyntax, + string name = "") + { + if (identifier != null && + !identifier.Value.IsKind(SyntaxKind.None)) + { + return identifier.Value.Text; + } + + if (nameSyntax != null) + { + return nameSyntax.ToString(); + } + + return name; + } + + private static CodeItemAccessEnum MapAccess( + SyntaxTokenList? modifiers, + SyntaxNode source) + { + if (modifiers == null) + { + return MapDefaultAccess(source); + } + + if (modifiers.Value.Any(m => + m.IsKind(SyntaxKind.PublicKeyword))) + { + return CodeItemAccessEnum.Public; + } + + if (modifiers.Value.Any(m => + m.IsKind(SyntaxKind.PrivateKeyword))) + { + return CodeItemAccessEnum.Private; + } + + if (modifiers.Value.Any(m => + m.IsKind(SyntaxKind.ProtectedKeyword))) + { + return CodeItemAccessEnum.Protected; + } + + if (modifiers.Value.Any(m => + m.IsKind(SyntaxKind.FriendKeyword))) + { + return CodeItemAccessEnum.Internal; + } + + return MapDefaultAccess(source); + } + + private static CodeItemAccessEnum MapDefaultAccess( + SyntaxNode source) + { + if (source.Parent is CompilationUnitSyntax) + { + return source switch + { + EnumBlockSyntax => CodeItemAccessEnum.Public, + NamespaceBlockSyntax => CodeItemAccessEnum.Public, + _ => CodeItemAccessEnum.Internal, + }; + } + + return source switch + { + NamespaceBlockSyntax => CodeItemAccessEnum.Public, + EnumBlockSyntax => CodeItemAccessEnum.Public, + InterfaceBlockSyntax => CodeItemAccessEnum.Public, + _ => CodeItemAccessEnum.Private, + }; + } +} \ No newline at end of file diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/ClassMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/ClassMapper.cs new file mode 100644 index 0000000..1a44a55 --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/ClassMapper.cs @@ -0,0 +1,89 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class ClassMapper +{ + public static CodeClassItem MapClass( + ClassBlockSyntax member, + SemanticModel semanticModel, + SyntaxTree tree, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + member.ClassStatement.Identifier, + modifiers: member.ClassStatement.Modifiers); + + codeItem.Kind = CodeItemKindEnum.Class; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + codeItem.Parameters = MapInheritance(member); + + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + string.Empty, + codeItem.Name, + codeItem.Parameters); + + var regions = RegionMapper.MapRegions( + tree, + member.Span, + codeDocumentViewModel); + + foreach (var classMember in member.Members) + { + foreach (var memberItem in DocumentMapper.MapMembers( + classMember, + tree, + semanticModel, + codeDocumentViewModel)) + { + if (RegionMapper.AddToRegion(regions, memberItem)) + { + continue; + } + + codeItem.Members.Add(memberItem); + } + } + + RegionMapper.AddRegionsIfNotPresent( + codeItem.Members, + regions); + + return codeItem; + } + + private static string MapInheritance( + ClassBlockSyntax member) + { + var inheritance = new List(); + + foreach (var inheritsStatement in member.Inherits) + { + inheritance.AddRange( + inheritsStatement.Types.Select(type => type.ToString())); + } + + foreach (var implementsStatement in member.Implements) + { + inheritance.AddRange( + implementsStatement.Types.Select( + interfaceType => interfaceType.ToString())); + } + + return inheritance.Any() + ? $" : {string.Join(", ", inheritance)}" + : string.Empty; + } +} diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/DeclareMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/DeclareMapper.cs new file mode 100644 index 0000000..20404a0 --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/DeclareMapper.cs @@ -0,0 +1,42 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class DeclareMapper +{ + public static CodeItem MapDeclare( + DeclareStatementSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + member.Identifier, + modifiers: member.Modifiers); + + codeItem.Kind = CodeItemKindEnum.Method; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + codeItem.Parameters = + member.ParameterList?.ToString() ?? string.Empty; + + codeItem.ReturnType = TypeMapper.Map(member.AsClause); + + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + codeItem.ReturnType, + codeItem.Name, + codeItem.Parameters); + + return codeItem; + } +} \ No newline at end of file diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/DelegateMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/DelegateMapper.cs new file mode 100644 index 0000000..e1a9b0e --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/DelegateMapper.cs @@ -0,0 +1,42 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class DelegateMapper +{ + public static CodeItem MapDelegate( + DelegateStatementSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + member.Identifier, + modifiers: member.Modifiers); + + codeItem.Kind = CodeItemKindEnum.Delegate; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + codeItem.Parameters = + member.ParameterList?.ToString() ?? string.Empty; + + codeItem.ReturnType = TypeMapper.Map(member.AsClause); + + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + codeItem.ReturnType, + codeItem.Name, + codeItem.Parameters); + + return codeItem; + } +} \ No newline at end of file diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/DocumentMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/DocumentMapper.cs new file mode 100644 index 0000000..0b04b4e --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/DocumentMapper.cs @@ -0,0 +1,265 @@ +using CodeNav.OutOfProc.Interfaces; +using CodeNav.OutOfProc.Models; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; +using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.ProjectSystem.Query; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public class DocumentMapper : IDocumentMapper +{ + /// + /// Map text document to list of code items. + /// + public async Task> MapDocument( + string text, + string? excludeFilePath, + CodeDocumentViewModel codeDocumentViewModel, + VisualStudioExtensibility extensibility, + CancellationToken cancellationToken) + { + var projects = await extensibility + .Workspaces() + .QueryProjectsAsync(project + => project.With(project + => project.Files + .Where(file => file.Extension == ".vb") + .Where(file => file.Path != excludeFilePath) + .With(file => file.Path)), cancellationToken); + + var filePaths = projects + .SelectMany(project => project.Files) + .Select(file => file.Path); + + return await MapDocument( + text, + codeDocumentViewModel, + filePaths, + cancellationToken); + } + + /// + /// Map text document to list of code items. + /// + public static async Task> MapDocument( + string text, + CodeDocumentViewModel codeDocumentViewModel, + IEnumerable filePaths, + CancellationToken cancellationToken) + { + var syntaxTree = VisualBasicSyntaxTree.ParseText( + text, + cancellationToken: cancellationToken); + + var semanticModel = await GetVisualBasicSemanticModel( + filePaths, + syntaxTree, + cancellationToken); + + var root = + (CompilationUnitSyntax) + await syntaxTree.GetRootAsync(cancellationToken); + + if (semanticModel == null) + { + return []; + } + + return [.. root.Members + .Where(member => member != null) + .SelectMany(member => + MapMembers( + member, + syntaxTree, + semanticModel, + codeDocumentViewModel)) + ]; + } + + public static async Task GetVisualBasicSemanticModel( + IEnumerable filePaths, + SyntaxTree syntaxTree, + CancellationToken cancellationToken) + { + var syntaxTrees = await Task.WhenAll( + filePaths.Select(async filePath => + { + var fileText = await File.ReadAllTextAsync( + filePath, + cancellationToken); + + return VisualBasicSyntaxTree + .ParseText( + fileText, + cancellationToken: cancellationToken) + .WithFilePath(filePath); + })); + + var compilation = VisualBasicCompilation + .Create("CodeNavCompilation") + .AddReferences( + MetadataReference.CreateFromFile( + typeof(object).Assembly.Location)) + .AddSyntaxTrees(syntaxTrees) + .AddSyntaxTrees(syntaxTree); + + return compilation.GetSemanticModel(syntaxTree); + } + + public static CodeItem? MapMember( + SyntaxNode member, + SyntaxTree tree, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + => member switch + { + NamespaceBlockSyntax namespaceSyntax + => NamespaceMapper.MapNamespace( + namespaceSyntax, + semanticModel, + tree, + codeDocumentViewModel), + + ClassBlockSyntax classSyntax + => ClassMapper.MapClass( + classSyntax, + semanticModel, + tree, + codeDocumentViewModel), + + StructureBlockSyntax structureSyntax + => StructureMapper.MapStructure( + structureSyntax, + semanticModel, + tree, + codeDocumentViewModel), + + InterfaceBlockSyntax interfaceSyntax + => InterfaceMapper.MapInterface( + interfaceSyntax, + semanticModel, + tree, + codeDocumentViewModel), + + MethodBlockSyntax methodSyntax + => MethodMapper.MapMethod( + methodSyntax, + semanticModel, + codeDocumentViewModel), + + MethodStatementSyntax methodSyntax + => MethodMapper.MapMethod( + methodSyntax, + semanticModel, + codeDocumentViewModel), + + ConstructorBlockSyntax constructorSyntax + => MethodMapper.MapConstructor( + constructorSyntax, + semanticModel, + codeDocumentViewModel), + + PropertyBlockSyntax propertySyntax + => PropertyMapper.MapProperty( + propertySyntax, + semanticModel, + codeDocumentViewModel), + + PropertyStatementSyntax propertySyntax + => PropertyMapper.MapProperty( + propertySyntax, + semanticModel, + codeDocumentViewModel), + + FieldDeclarationSyntax fieldSyntax + => FieldMapper.MapFields( + fieldSyntax, + semanticModel, + codeDocumentViewModel) + .FirstOrDefault(), + + EventBlockSyntax eventSyntax + => EventMapper.MapEvent( + eventSyntax, + semanticModel, + codeDocumentViewModel), + + EventStatementSyntax eventSyntax + => EventMapper.MapEvent( + eventSyntax, + semanticModel, + codeDocumentViewModel), + + EnumBlockSyntax enumSyntax + => EnumMapper.MapEnum( + enumSyntax, + semanticModel, + tree, + codeDocumentViewModel), + + DelegateStatementSyntax delegateSyntax + => DelegateMapper.MapDelegate( + delegateSyntax, + semanticModel, + codeDocumentViewModel), + + ModuleBlockSyntax moduleSyntax + => ModuleMapper.MapModule( + moduleSyntax, + semanticModel, + tree, + codeDocumentViewModel), + + OperatorStatementSyntax operatorSyntax + => OperatorMapper.MapOperator( + operatorSyntax, + semanticModel, + codeDocumentViewModel), + + OperatorBlockSyntax operatorSyntax + => OperatorMapper.MapOperator( + operatorSyntax, + semanticModel, + codeDocumentViewModel), + + DeclareStatementSyntax declareSyntax + => DeclareMapper.MapDeclare( + declareSyntax, + semanticModel, + codeDocumentViewModel), + + _ => null, + }; + + public static IEnumerable MapMembers( + SyntaxNode member, + SyntaxTree tree, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + if (member is FieldDeclarationSyntax fieldSyntax) + { + return FieldMapper.MapFields( + fieldSyntax, + semanticModel, + codeDocumentViewModel); + } + + var codeItem = MapMember( + member, + tree, + semanticModel, + codeDocumentViewModel); + + return codeItem == null ? [] : [codeItem]; + } + + public bool CanMapDocument( + string filePath, + GlobalSettings settings) + => settings.EnableVisualBasic && + filePath.EndsWith(".vb", StringComparison.OrdinalIgnoreCase); +} diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/EnumMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/EnumMapper.cs new file mode 100644 index 0000000..db2e2f3 --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/EnumMapper.cs @@ -0,0 +1,83 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class EnumMapper +{ + public static CodeItem MapEnum( + EnumBlockSyntax member, + SemanticModel semanticModel, + SyntaxTree tree, + CodeDocumentViewModel codeDocumentViewModel) + { + var statement = member.EnumStatement; + + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + statement.Identifier, + modifiers: statement.Modifiers); + + codeItem.Kind = CodeItemKindEnum.Enum; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + var regions = RegionMapper.MapRegions( + tree, + member.Span, + codeDocumentViewModel); + + foreach (var enumMember in member.Members.OfType()) + { + var memberItem = MapEnumMember( + enumMember, + semanticModel, + codeDocumentViewModel); + + if (RegionMapper.AddToRegion(regions, memberItem)) + { + continue; + } + + codeItem.Members.Add(memberItem); + } + + RegionMapper.AddRegionsIfNotPresent( + codeItem.Members, + regions); + + return codeItem; + } + + private static CodeItem MapEnumMember( + EnumMemberDeclarationSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + member.Identifier); + + codeItem.Kind = CodeItemKindEnum.EnumMember; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + string.Empty, + codeItem.Name, + string.Empty); + + return codeItem; + } +} \ No newline at end of file diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/EventMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/EventMapper.cs new file mode 100644 index 0000000..d6bce9b --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/EventMapper.cs @@ -0,0 +1,65 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class EventMapper +{ + public static CodeItem MapEvent( + EventBlockSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var statement = member.EventStatement; + + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + statement.Identifier, + modifiers: statement.Modifiers); + + codeItem.Kind = CodeItemKindEnum.Event; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + string.Empty, + codeItem.Name, + string.Empty); + + return codeItem; + } + + public static CodeItem MapEvent( + EventStatementSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + member.Identifier, + modifiers: member.Modifiers); + + codeItem.Kind = CodeItemKindEnum.Event; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + string.Empty, + codeItem.Name, + string.Empty); + + return codeItem; + } +} diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/FieldMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/FieldMapper.cs new file mode 100644 index 0000000..7cac81d --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/FieldMapper.cs @@ -0,0 +1,59 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class FieldMapper +{ + public static IEnumerable MapFields( + FieldDeclarationSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + => member.Declarators + .SelectMany(declarator => declarator.Names) + .Select(variable => MapField( + member, + variable, + semanticModel, + codeDocumentViewModel)); + + private static CodeItem MapField( + FieldDeclarationSyntax declaration, + ModifiedIdentifierSyntax variable, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + variable, + semanticModel, + codeDocumentViewModel, + variable.Identifier, + modifiers: declaration.Modifiers); + + codeItem.Kind = IsConstant(declaration.Modifiers) + ? CodeItemKindEnum.Constant + : CodeItemKindEnum.Variable; + + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + codeItem.Tooltip = TooltipMapper.Map( + variable, + codeItem.Access, + string.Empty, + codeItem.Name, + string.Empty); + + return codeItem; + } + + private static bool IsConstant( + SyntaxTokenList modifiers) + => modifiers.Any( + m => m.RawKind == (int)SyntaxKind.ConstKeyword); +} diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/InterfaceMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/InterfaceMapper.cs new file mode 100644 index 0000000..e574a8b --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/InterfaceMapper.cs @@ -0,0 +1,83 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class InterfaceMapper +{ + public static CodeInterfaceItem MapInterface( + InterfaceBlockSyntax member, + SemanticModel semanticModel, + SyntaxTree tree, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + member.InterfaceStatement.Identifier, + modifiers: member.InterfaceStatement.Modifiers); + + codeItem.Kind = CodeItemKindEnum.Interface; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + codeItem.Parameters = MapInheritance(member); + + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + string.Empty, + codeItem.Name, + codeItem.Parameters); + + var regions = RegionMapper.MapRegions( + tree, + member.Span, + codeDocumentViewModel); + + foreach (var interfaceMember in member.Members) + { + foreach (var memberItem in DocumentMapper.MapMembers( + interfaceMember, + tree, + semanticModel, + codeDocumentViewModel)) + { + if (RegionMapper.AddToRegion(regions, memberItem)) + { + continue; + } + + codeItem.Members.Add(memberItem); + } + } + + RegionMapper.AddRegionsIfNotPresent( + codeItem.Members, + regions); + + return codeItem; + } + + private static string MapInheritance( + InterfaceBlockSyntax member) + { + var inheritance = new List(); + + foreach (var inheritsStatement in member.Inherits) + { + inheritance.AddRange( + inheritsStatement.Types.Select( + type => type.ToString())); + } + + return inheritance.Any() + ? $" : {string.Join(", ", inheritance)}" + : string.Empty; + } +} diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/MethodMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/MethodMapper.cs new file mode 100644 index 0000000..607ebac --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/MethodMapper.cs @@ -0,0 +1,116 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class MethodMapper +{ + public static CodeItem MapMethod( + MethodBlockSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var statement = member.SubOrFunctionStatement; + + var identifier = statement.Identifier; + var modifiers = statement.Modifiers; + + var returnType = statement.AsClause; + var parameters = statement.ParameterList; + + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + identifier, + modifiers: modifiers); + + var functionItem = codeItem; + + functionItem.ReturnType = TypeMapper.Map(returnType); + functionItem.Parameters = parameters?.ToString() ?? string.Empty; + + codeItem.IdentifierSpan = identifier.Span; + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + functionItem.ReturnType, + codeItem.Name, + functionItem.Parameters); + + codeItem.Kind = CodeItemKindEnum.Method; + + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + return codeItem; + } + + public static CodeItem MapMethod( + MethodStatementSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + member.Identifier, + modifiers: member.Modifiers); + + codeItem.ReturnType = TypeMapper.Map(member.AsClause); + codeItem.Parameters = member.ParameterList?.ToString() ?? string.Empty; + codeItem.IdentifierSpan = member.Identifier.Span; + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + codeItem.ReturnType, + codeItem.Name, + codeItem.Parameters); + codeItem.Kind = CodeItemKindEnum.Method; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + return codeItem; + } + + public static CodeItem MapConstructor( + ConstructorBlockSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var statement = member.SubNewStatement; + + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + name: "New", + modifiers: statement.Modifiers); + + var functionItem = codeItem; + + functionItem.Parameters = + statement.ParameterList?.ToString() ?? string.Empty; + + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + string.Empty, + codeItem.Name, + functionItem.Parameters); + + codeItem.Kind = CodeItemKindEnum.Constructor; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + return codeItem; + } +} diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/ModuleMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/ModuleMapper.cs new file mode 100644 index 0000000..c9c1580 --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/ModuleMapper.cs @@ -0,0 +1,66 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class ModuleMapper +{ + public static CodeItem MapModule( + ModuleBlockSyntax member, + SemanticModel semanticModel, + SyntaxTree tree, + CodeDocumentViewModel codeDocumentViewModel) + { + var statement = member.ModuleStatement; + + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + statement.Identifier, + modifiers: statement.Modifiers); + + codeItem.Kind = CodeItemKindEnum.Class; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + string.Empty, + codeItem.Name, + string.Empty); + + var regions = RegionMapper.MapRegions( + tree, + member.Span, + codeDocumentViewModel); + + foreach (var moduleMember in member.Members) + { + foreach (var memberItem in DocumentMapper.MapMembers( + moduleMember, + tree, + semanticModel, + codeDocumentViewModel)) + { + if (RegionMapper.AddToRegion(regions, memberItem)) + { + continue; + } + + codeItem.Members.Add(memberItem); + } + } + + RegionMapper.AddRegionsIfNotPresent( + codeItem.Members, + regions); + + return codeItem; + } +} diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/NamespaceMember.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/NamespaceMember.cs new file mode 100644 index 0000000..d0ad152 --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/NamespaceMember.cs @@ -0,0 +1,62 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Extensions; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class NamespaceMapper +{ + public static CodeNamespaceItem MapNamespace( + NamespaceBlockSyntax member, + SemanticModel semanticModel, + SyntaxTree tree, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + name: member.NamespaceStatement.Name.ToString()); + + codeItem.Kind = CodeItemKindEnum.Namespace; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + string.Empty, + codeItem.Name, + string.Empty); + + var regions = RegionMapper.MapRegions( + tree, + member.Span, + codeDocumentViewModel); + + foreach (var namespaceMember in member.Members) + { + foreach (var memberItem in DocumentMapper.MapMembers( + namespaceMember, + tree, + semanticModel, + codeDocumentViewModel)) + { + if (!RegionMapper.AddToRegion(regions, memberItem)) + { + codeItem.Members.AddIfNotNull(memberItem); + } + } + } + + RegionMapper.AddRegionsIfNotPresent( + codeItem.Members, + regions); + + return codeItem; + } +} diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/OperatorMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/OperatorMapper.cs new file mode 100644 index 0000000..b523a78 --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/OperatorMapper.cs @@ -0,0 +1,63 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class OperatorMapper +{ + public static CodeItem MapOperator( + OperatorStatementSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + => MapOperator( + member, + member, + semanticModel, + codeDocumentViewModel); + + public static CodeItem MapOperator( + OperatorBlockSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + => MapOperator( + member, + member.OperatorStatement, + semanticModel, + codeDocumentViewModel); + + private static CodeItem MapOperator( + SyntaxNode source, + OperatorStatementSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + source, + semanticModel, + codeDocumentViewModel, + name: member.OperatorToken.Text, + modifiers: member.Modifiers); + + codeItem.Kind = CodeItemKindEnum.Method; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + codeItem.Parameters = + member.ParameterList?.ToString() ?? string.Empty; + + codeItem.ReturnType = TypeMapper.Map(member.AsClause); + + codeItem.Tooltip = TooltipMapper.Map( + source, + codeItem.Access, + codeItem.ReturnType, + codeItem.Name, + codeItem.Parameters); + + return codeItem; + } +} diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/PropertyMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/PropertyMapper.cs new file mode 100644 index 0000000..dd20a30 --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/PropertyMapper.cs @@ -0,0 +1,94 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class PropertyMapper +{ + public static CodePropertyItem MapProperty( + PropertyBlockSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var statement = member.PropertyStatement; + + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + statement.Identifier, + modifiers: statement.Modifiers); + + codeItem.IdentifierSpan = statement.Identifier.Span; + + codeItem.ReturnType = TypeMapper.Map(statement.AsClause); + codeItem.Parameters = statement.ParameterList?.ToString() ?? string.Empty; + + var accessors = new List(); + + if (member.Accessors.Any( + accessor => accessor.Kind() == SyntaxKind.GetAccessorBlock)) + { + accessors.Add("Get"); + } + + if (member.Accessors.Any( + accessor => accessor.Kind() == SyntaxKind.SetAccessorBlock)) + { + accessors.Add("Set"); + } + + if (accessors.Any()) + { + codeItem.Parameters += + $" {{{string.Join(",", accessors)}}}"; + } + + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + codeItem.ReturnType, + codeItem.Name, + codeItem.Parameters); + + codeItem.Kind = CodeItemKindEnum.Property; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + return codeItem; + } + + public static CodePropertyItem MapProperty( + PropertyStatementSyntax member, + SemanticModel semanticModel, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + member.Identifier, + modifiers: member.Modifiers); + + codeItem.IdentifierSpan = member.Identifier.Span; + codeItem.ReturnType = TypeMapper.Map(member.AsClause); + codeItem.Parameters = member.ParameterList?.ToString() ?? string.Empty; + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + codeItem.ReturnType, + codeItem.Name, + codeItem.Parameters); + codeItem.Kind = CodeItemKindEnum.Property; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + return codeItem; + } +} diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/RegionMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/RegionMapper.cs new file mode 100644 index 0000000..05f7eef --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/RegionMapper.cs @@ -0,0 +1,326 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Extensions; +using CodeNav.OutOfProc.Helpers; +using CodeNav.OutOfProc.Interfaces; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class RegionMapper +{ + /// + /// Find all regions in a file and get their start and end span. + /// + /// Syntax tree for the given file. + /// Span in which to search for regions. + /// Code document view model. + /// Flat list of regions. + public static List MapRegions( + SyntaxTree tree, + TextSpan span, + CodeDocumentViewModel codeDocumentViewModel) + { + var regionList = new List(); + + if (tree == null) + { + return regionList; + } + + // Check if regions should be ignored based on the filter rules. + var filterRule = FilterRuleHelper.GetFilterRule( + codeDocumentViewModel, + CodeItemKindEnum.Region); + + if (filterRule?.Ignore == true) + { + return regionList; + } + + var root = tree.GetRoot(); + + // Find all region start trivia. + var regionStarts = root + .DescendantTrivia() + .Where(syntaxTrivia => + syntaxTrivia.IsKind(SyntaxKind.RegionDirectiveTrivia)) + .Where(syntaxTrivia => + span.Contains(syntaxTrivia.Span)); + + regionList.AddRange( + regionStarts.Select(regionStart => + MapRegion(regionStart, codeDocumentViewModel))); + + // Find all region end trivia. + var regionEnds = root + .DescendantTrivia() + .Where(syntaxTrivia => + syntaxTrivia.IsKind(SyntaxKind.EndRegionDirectiveTrivia)) + .Where(syntaxTrivia => + span.Contains(syntaxTrivia.Span)); + + // Match a region start with the closest region end. + foreach (var regionEnd in regionEnds) + { + var region = regionList + .LastOrDefault(regionStart => + regionStart.Span.Start < regionEnd.Span.Start && + regionStart.Span.End == regionStart.Span.Start); + + if (region == null) + { + continue; + } + + region.Span = new( + region.Span.Start, + regionEnd.Span.End - region.Span.Start); + + region.OutlineSpan = new( + region.OutlineSpan.Start, + regionEnd.Span.End - region.Span.Start); + } + + return ToHierarchy(regionList, span); + } + + /// + /// Transform the flat list of regions into a nested hierarchy. + /// + private static List ToHierarchy( + List regionList, + TextSpan textSpan) + { + var nestedRegions = new List(); + + foreach (var region in regionList) + { + if (!StrictContains(textSpan, region.Span) || + regionList.Any(otherBiggerRegion => + StrictContains( + otherBiggerRegion.Span, + region.Span) && + otherBiggerRegion.Span.Length < textSpan.Length)) + { + continue; + } + + region.Members = + [.. ToHierarchy(regionList, region.Span).Cast()]; + + nestedRegions.Add(region); + } + + return nestedRegions; + } + + private static CodeRegionItem MapRegion( + SyntaxTrivia regionStart, + CodeDocumentViewModel codeDocumentViewModel) + { + var name = MapRegionName(regionStart); + + return new() + { + Name = name, + FullName = name, + Id = name, + Tooltip = name, + Kind = CodeItemKindEnum.Region, + Span = new( + regionStart.Span.Start, + 0), + OutlineSpan = new( + regionStart.Span.Start, + 0), + IdentifierSpan = MapIdentifierSpan(regionStart), + Moniker = IconMapper.MapMoniker( + CodeItemKindEnum.Region, + CodeItemAccessEnum.Unknown), + CodeDocumentViewModel = codeDocumentViewModel, + }; + } + + private static TextSpan MapIdentifierSpan( + SyntaxTrivia regionStart) + { + return regionStart.Span; + } + + private static string MapRegionName( + SyntaxTrivia regionStart) + { + const string defaultRegionName = "Region"; + const string regionKeyword = "#Region"; + + var syntaxNode = regionStart.GetStructure(); + + if (syntaxNode is not RegionDirectiveTriviaSyntax regionSyntax) + { + return defaultRegionName; + } + + var text = regionSyntax.ToString(); + + if (!text.StartsWith( + regionKeyword, + StringComparison.OrdinalIgnoreCase)) + { + return defaultRegionName; + } + + var name = text[regionKeyword.Length..] + .Trim() + .Trim('"'); + + return string.IsNullOrEmpty(name) + ? defaultRegionName + : name; + } + + /// + /// Add a code item to the list of regions, recursively finding + /// the correct region. + /// + public static bool AddToRegion( + List regions, + CodeItem codeItem) + { + if (codeItem?.Span == null) + { + return false; + } + + foreach (var region in regions) + { + if (region?.Kind != CodeItemKindEnum.Region) + { + continue; + } + + if (AddToRegion(region.Members, codeItem)) + { + return true; + } + + if (codeItem.Span.Start >= region.Span.Start && + codeItem.Span.Start <= region.Span.End) + { + region.Members.Add(codeItem); + return true; + } + } + + return false; + } + + /// + /// Help add a code item to an inner region structure. + /// + private static bool AddToRegion( + List members, + CodeItem codeItem) + { + foreach (var member in members) + { + if (member == null) + { + continue; + } + + if (member is IMembers memberItem && + AddToRegion(memberItem.Members, codeItem)) + { + return true; + } + + if (member is CodeRegionItem regionItem && + member.Span.Contains(codeItem.Span)) + { + regionItem.Members.Add(codeItem); + return true; + } + } + + return false; + } + + /// + /// Determines whether a small span falls completely within + /// a bigger span without touching its boundaries. + /// + public static bool StrictContains( + TextSpan bigSpan, + TextSpan smallSpan) + => smallSpan.Start > bigSpan.Start && + smallSpan.End < bigSpan.End; + + /// + /// Check if a code item is part of any region. + /// + public static bool IsPartOfRegion( + IEnumerable regions, + CodeItem codeItem) + => regions.Any(region => + region.Span.Contains(codeItem.Span)); + + public static CodeRegionItem? GetRegion( + IEnumerable regions, + CodeItem codeItem) + { + foreach (var region in regions) + { + if (region.Kind != CodeItemKindEnum.Region) + { + continue; + } + + if (!region.Span.Contains(codeItem.Span)) + { + continue; + } + + // Try to find a more specific nested region first. + if (region is IMembers regionMembersItem) + { + var nestedMatch = GetRegion( + regionMembersItem.Members, + codeItem); + + return nestedMatch ?? + region as CodeRegionItem; + } + + return region as CodeRegionItem; + } + + return null; + } + + /// + /// Add regions to the given list of members, but only if a region + /// with the same ID is not already present somewhere within those members. + /// + public static void AddRegionsIfNotPresent( + List members, + List regions) + { + foreach (var region in regions) + { + var alreadyPresent = members + .Flatten() + .FilterNull() + .Any(item => item.Id == region?.Id); + + if (!alreadyPresent) + { + members.AddIfNotNull(region); + } + } + } +} \ No newline at end of file diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/StructureMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/StructureMapper.cs new file mode 100644 index 0000000..4d79c7e --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/StructureMapper.cs @@ -0,0 +1,90 @@ +using CodeNav.OutOfProc.Constants; +using CodeNav.OutOfProc.Mappers; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class StructureMapper +{ + public static CodeClassItem MapStructure( + StructureBlockSyntax member, + SemanticModel semanticModel, + SyntaxTree tree, + CodeDocumentViewModel codeDocumentViewModel) + { + var codeItem = BaseMapper.MapBase( + member, + semanticModel, + codeDocumentViewModel, + member.StructureStatement.Identifier, + modifiers: member.StructureStatement.Modifiers); + + codeItem.Kind = CodeItemKindEnum.Struct; + codeItem.Moniker = IconMapper.MapMoniker( + codeItem.Kind, + codeItem.Access); + + codeItem.Parameters = MapInheritance(member); + + codeItem.Tooltip = TooltipMapper.Map( + member, + codeItem.Access, + string.Empty, + codeItem.Name, + codeItem.Parameters); + + var regions = RegionMapper.MapRegions( + tree, + member.Span, + codeDocumentViewModel); + + foreach (var structureMember in member.Members) + { + foreach (var memberItem in DocumentMapper.MapMembers( + structureMember, + tree, + semanticModel, + codeDocumentViewModel)) + { + if (RegionMapper.AddToRegion(regions, memberItem)) + { + continue; + } + + codeItem.Members.Add(memberItem); + } + } + + RegionMapper.AddRegionsIfNotPresent( + codeItem.Members, + regions); + + return codeItem; + } + + private static string MapInheritance( + StructureBlockSyntax member) + { + var inheritance = new List(); + + foreach (var inheritsStatement in member.Inherits) + { + inheritance.AddRange( + inheritsStatement.Types.Select( + type => type.ToString())); + } + + foreach (var implementsStatement in member.Implements) + { + inheritance.AddRange( + implementsStatement.Types.Select( + type => type.ToString())); + } + + return inheritance.Any() + ? $" : {string.Join(", ", inheritance)}" + : string.Empty; + } +} diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/TooltipMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/TooltipMapper.cs new file mode 100644 index 0000000..4ea087d --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/TooltipMapper.cs @@ -0,0 +1,48 @@ +using CodeNav.OutOfProc.Constants; +using Microsoft.CodeAnalysis; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class TooltipMapper +{ + public static string Map( + SyntaxNode node, + CodeItemAccessEnum access, + string returnType, + string name, + string parameters) + { + var accessText = access switch + { + CodeItemAccessEnum.Public => "Public", + CodeItemAccessEnum.Private => "Private", + CodeItemAccessEnum.Protected => "Protected", + CodeItemAccessEnum.Internal => "Friend", + _ => string.Empty + }; + + var parts = new List(); + + if (!string.IsNullOrEmpty(accessText)) + { + parts.Add(accessText); + } + + if (!string.IsNullOrEmpty(name)) + { + parts.Add(name); + } + + if (!string.IsNullOrEmpty(parameters)) + { + parts.Add(parameters); + } + + if (!string.IsNullOrEmpty(returnType)) + { + parts.Add($"As {returnType}"); + } + + return string.Join(" ", parts); + } +} \ No newline at end of file diff --git a/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/TypeMapper.cs b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/TypeMapper.cs new file mode 100644 index 0000000..2228bef --- /dev/null +++ b/src/CodeNav.OutOfProc/Languages/VisualBasic/Mappers/TypeMapper.cs @@ -0,0 +1,14 @@ +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace CodeNav.OutOfProc.Languages.VisualBasic.Mappers; + +public static class TypeMapper +{ + public static string Map(SimpleAsClauseSyntax? asClause) + => asClause?.Type?.ToString() ?? string.Empty; + + public static string Map(AsClauseSyntax? asClause) + => asClause?.ToString() + .Replace("As ", string.Empty, StringComparison.OrdinalIgnoreCase) + .Trim() ?? string.Empty; +} \ No newline at end of file diff --git a/src/CodeNav.OutOfProc/Models/GlobalSettings.cs b/src/CodeNav.OutOfProc/Models/GlobalSettings.cs index 33ee6cc..28d33db 100644 --- a/src/CodeNav.OutOfProc/Models/GlobalSettings.cs +++ b/src/CodeNav.OutOfProc/Models/GlobalSettings.cs @@ -44,6 +44,13 @@ public class GlobalSettings /// public bool UseCompactMode { get; set; } + /// + /// Settings dialog - Languages enabled for CodeNav. + /// + public bool EnableCSharp { get; set; } = true; + + public bool EnableVisualBasic { get; set; } = false; + /// /// Main toolbar - Setting to store the selected sort order for code items /// diff --git a/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs b/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs index d593013..971ae3c 100644 --- a/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs +++ b/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs @@ -1,13 +1,15 @@ using CodeNav.OutOfProc.Dialogs.FilterDialog; using CodeNav.OutOfProc.Dialogs.SettingsDialog; using CodeNav.OutOfProc.Helpers; -using CodeNav.OutOfProc.Languages.CSharp.Mappers; +using CodeNav.OutOfProc.Interfaces; using CodeNav.OutOfProc.Models; using CodeNav.OutOfProc.ViewModels; using Microsoft.VisualStudio.Extensibility; using Microsoft.VisualStudio.Extensibility.ToolWindows; using Microsoft.VisualStudio.Extensibility.UI; using System.Windows; +using CSharpDocumentMapper = CodeNav.OutOfProc.Languages.CSharp.Mappers.DocumentMapper; +using VisualBasicDocumentMapper = CodeNav.OutOfProc.Languages.VisualBasic.Mappers.DocumentMapper; namespace CodeNav.OutOfProc.Services; @@ -16,6 +18,12 @@ public class CodeDocumentService( OutliningService outliningService, WindowFrameService windowFrameService) { + private readonly List documentMappers = + [ + new CSharpDocumentMapper(), + new VisualBasicDocumentMapper(), + ]; + /// /// DataContext for the tool window. /// @@ -62,9 +70,14 @@ public async Task UpdateCodeDocumentViewModel( return CodeDocumentViewModel; } - if (!DocumentMapper.CanMapDocument(filePath)) + var documentMapper = documentMappers + .FirstOrDefault(mapper => + mapper.CanMapDocument(filePath, GlobalSettings!)); + + if (documentMapper == null) { - CodeDocumentViewModel.CodeItems = PlaceholderHelper.CreateNoCodeItemsFound(); + CodeDocumentViewModel.CodeItems = + PlaceholderHelper.CreateNoCodeItemsFound(); // No code items found, hide the tool window after showing the "No code items found" message await HideToolWindow(cancellationToken); @@ -82,7 +95,7 @@ public async Task UpdateCodeDocumentViewModel( loadingCancellationToken); // Get the new list of code items - var codeItems = await DocumentMapper.MapDocument( + var codeItems = await documentMapper.MapDocument( text, filePath, CodeDocumentViewModel, @@ -185,6 +198,8 @@ public async Task LoadGlobalSettings(bool readFromDisk = false) EnableCrashAnalytics = GlobalSettings.EnableCrashAnalytics, ShowToolWindowForUnsupportedFiles = GlobalSettings.ShowToolWindowForUnsupportedFiles, UseCompactMode = GlobalSettings.UseCompactMode, + EnableCSharp = GlobalSettings.EnableCSharp, + EnableVisualBasic = GlobalSettings.EnableVisualBasic, }; var filterRules = GlobalSettings diff --git a/src/CodeNav.OutOfProc/TextViewEventListener.cs b/src/CodeNav.OutOfProc/TextViewEventListener.cs index 4b12d61..77e9d47 100644 --- a/src/CodeNav.OutOfProc/TextViewEventListener.cs +++ b/src/CodeNav.OutOfProc/TextViewEventListener.cs @@ -32,8 +32,7 @@ internal class TextViewEventListener( { AppliesTo = [ - DocumentFilter.FromDocumentType("CSharp"), - DocumentFilter.FromGlobPattern("**/*.cs", true), + DocumentFilter.FromGlobPattern("**/*.{cs,vb}", true), ], }; @@ -140,4 +139,4 @@ await codeDocumentService.UpdateCodeDocumentViewModel( await LogHelper.LogException(codeDocumentService, "Error listening to TextViewOpened", e); } } -} \ No newline at end of file +} diff --git a/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs b/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs index c29693c..22e5e49 100644 --- a/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs +++ b/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs @@ -255,11 +255,18 @@ private async Task Settings(object? commandParameter, IClientContext clientConte CodeDocumentService.GlobalSettings.EnableCrashAnalytics = CodeDocumentService.SettingsDialogData.EnableCrashAnalytics; CodeDocumentService.GlobalSettings.ShowToolWindowForUnsupportedFiles = CodeDocumentService.SettingsDialogData.ShowToolWindowForUnsupportedFiles; CodeDocumentService.GlobalSettings.UseCompactMode = CodeDocumentService.SettingsDialogData.UseCompactMode; + CodeDocumentService.GlobalSettings.EnableCSharp = CodeDocumentService.SettingsDialogData.EnableCSharp; + CodeDocumentService.GlobalSettings.EnableVisualBasic = CodeDocumentService.SettingsDialogData.EnableVisualBasic; await SettingsHelper.SaveGlobalSettings(CodeDocumentService); // Refresh the tool window with the new settings await CodeDocumentService.LoadGlobalSettings(readFromDisk: true); + + await Refresh( + commandParameter, + clientContext, + cancellationToken); } [DataMember] diff --git a/test/Files/NonCSharp/VisualBasicSmokeTest.vb b/test/Files/NonCSharp/VisualBasicSmokeTest.vb new file mode 100644 index 0000000..0276a22 --- /dev/null +++ b/test/Files/NonCSharp/VisualBasicSmokeTest.vb @@ -0,0 +1,94 @@ +Imports System + +Namespace CodeNav.SmokeTest + + Public Interface IWorker + Sub Execute(value As Integer) + Property Name As String + Event Completed As EventHandler + End Interface + + Public MustInherit Class WorkerBase + Public MustOverride Sub Run() + End Class + + Public Class SampleWorker + Inherits WorkerBase + Implements IWorker + + Private _first, _second As Integer + + Public Const Maximum As Integer = 10 + + Public Sub New(value As Integer) + _first = value + End Sub + + Public Property Name As String Implements IWorker.Name + + Public Event Completed As EventHandler Implements IWorker.Completed + + Public Overrides Sub Run() + Execute(_first) + End Sub + +#Region "Worker members" +#Region "Execution" + Public Sub Execute(value As Integer) Implements IWorker.Execute + _second = value + RaiseEvent Completed(Me, EventArgs.Empty) + End Sub +#End Region + + Public Function Calculate(value As Integer) As String + Return (value + _second).ToString() + End Function +#End Region + + Public Default Property Item(index As Integer) As Integer + Get + Return If(index = 0, _first, _second) + End Get + Set(value As Integer) + If index = 0 Then + _first = value + Else + _second = value + End If + End Set + End Property + + Public Custom Event Changed As EventHandler + AddHandler(value As EventHandler) + End AddHandler + RemoveHandler(value As EventHandler) + End RemoveHandler + RaiseEvent() + End RaiseEvent + End Event + End Class + + Public Structure Point + Public X As Integer + Public Y As Integer + + Public Shared Operator +(left As Point, right As Point) As Point + Return New Point With { + .X = left.X + right.X, + .Y = left.Y + right.Y + } + End Operator + End Structure + + Public Enum WorkState + NotStarted + Running + Complete + End Enum + + Public Delegate Function Transformer(value As Integer) As String + + Public Module NativeMethods + Public Declare Function GetTickCount Lib "kernel32" () As UInteger + End Module +End Namespace diff --git a/test/VisualBasicDocumentMapperTests.cs b/test/VisualBasicDocumentMapperTests.cs new file mode 100644 index 0000000..4d331a1 --- /dev/null +++ b/test/VisualBasicDocumentMapperTests.cs @@ -0,0 +1,256 @@ +using CodeNav.OutOfProc.Models; +using VisualBasicDocumentMapper = CodeNav.OutOfProc.Languages.VisualBasic.Mappers.DocumentMapper; + +namespace CodeNav.Test; + +[TestFixture] +internal class VisualBasicDocumentMapperTests +{ + [Test] + public async Task MapsStatementMembersInInterfacesAndClasses() + { + const string source = """ + Namespace Example + Public Interface IExample + Sub Execute(value As Integer) + Property Name As String + Event Changed As EventHandler + End Interface + + Public MustInherit Class ExampleClass + Public MustOverride Sub Run() + Public Property Count As Integer + Public Event Updated As EventHandler + End Class + End Namespace + """; + + var codeItems = await VisualBasicDocumentMapper.MapDocument( + source, + new CodeDocumentViewModel(), + filePaths: [], + cancellationToken: default); + + var namespaceItem = codeItems.Single() as CodeNamespaceItem; + Assert.That(namespaceItem, Is.Not.Null); + + var interfaceItem = namespaceItem!.Members + .OfType() + .Single(item => item.Name == "IExample"); + var classItem = namespaceItem.Members + .OfType() + .Single(item => item.Name == "ExampleClass"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(interfaceItem.Members.Select(item => item.Name), + Is.EquivalentTo(["Execute", "Name", "Changed"])); + Assert.That(classItem.Members.Select(item => item.Name), + Is.EquivalentTo(["Run", "Count", "Updated"])); + Assert.That(interfaceItem.Members.Select(item => item.Kind), + Is.EquivalentTo([ + CodeItemKindEnum.Method, + CodeItemKindEnum.Property, + CodeItemKindEnum.Event, + ])); + } + } + + [Test] + public async Task MapsClassMemberBlocksAndTheirDetails() + { + const string source = """ + Namespace Example + Public Class Sample + Private _first, _second As Integer + Public Const Maximum As Integer = 10 + + Public Sub New(value As Integer) + End Sub + + Public Function Calculate(value As Integer) As String + Return value.ToString() + End Function + + Public Default Property Value(index As Integer) As Integer + Get + Return _first + End Get + Set(value As Integer) + _first = value + End Set + End Property + + Public Custom Event Changed As EventHandler + AddHandler(value As EventHandler) + End AddHandler + RemoveHandler(value As EventHandler) + End RemoveHandler + RaiseEvent() + End RaiseEvent + End Event + End Class + End Namespace + """; + + var classItem = GetNamespace(await Map(source)).Members + .OfType() + .Single(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(classItem.Members.Select(item => item.Name), Is.EquivalentTo([ + "_first", "_second", "Maximum", "New", "Calculate", "Value", "Changed", + ])); + Assert.That(classItem.Members.Select(item => item.Kind), Is.EquivalentTo([ + CodeItemKindEnum.Variable, + CodeItemKindEnum.Variable, + CodeItemKindEnum.Constant, + CodeItemKindEnum.Constructor, + CodeItemKindEnum.Method, + CodeItemKindEnum.Property, + CodeItemKindEnum.Event, + ])); + } + + var calculate = classItem.Members + .OfType() + .Single(item => item.Name == "Calculate"); + var property = classItem.Members + .OfType() + .Single(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(calculate.Parameters, Is.EqualTo("(value As Integer)")); + Assert.That(calculate.ReturnType, Is.EqualTo("String")); + Assert.That(property.Parameters, Is.EqualTo("(index As Integer) {Get,Set}")); + Assert.That(property.ReturnType, Is.EqualTo("Integer")); + } + } + + [Test] + public async Task MapsNamespacesAndTopLevelDeclarationKinds() + { + const string source = """ + Namespace Example.Outer + Public Delegate Function Transformer(value As Integer) As String + + Public Enum Choice + First + Second + End Enum + + Public Structure Point + Public X As Integer + Public Shared Operator +(left As Point, right As Point) As Point + Return left + End Operator + End Structure + + Public Module Utilities + Public Declare Function GetTickCount Lib "kernel32" () As Integer + End Module + End Namespace + """; + + var namespaceItem = GetNamespace(await Map(source)); + var enumItem = namespaceItem.Members + .OfType() + .Single(item => item.Name == "Choice"); + var structureItem = namespaceItem.Members + .OfType() + .Single(item => item.Name == "Point"); + var moduleItem = namespaceItem.Members + .OfType() + .Single(item => item.Name == "Utilities"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(namespaceItem.Name, Is.EqualTo("Example.Outer")); + Assert.That(namespaceItem.Members.Select(item => item.Kind), Is.EquivalentTo([ + CodeItemKindEnum.Delegate, + CodeItemKindEnum.Enum, + CodeItemKindEnum.Struct, + CodeItemKindEnum.Class, + ])); + Assert.That(enumItem.Members.Select(item => item.Name), + Is.EquivalentTo(["First", "Second"])); + Assert.That(enumItem.Members.Select(item => item.Kind), + Is.All.EqualTo(CodeItemKindEnum.EnumMember)); + Assert.That(structureItem.Members.Select(item => item.Name), + Is.EquivalentTo(["X", "+"])); + Assert.That(moduleItem.Members.Single().Name, Is.EqualTo("GetTickCount")); + Assert.That(moduleItem.Members.Single().Kind, Is.EqualTo(CodeItemKindEnum.Method)); + } + } + + [Test] + public async Task MapsNestedRegionsAndPlacesMembersInTheInnermostRegion() + { + const string source = """ + Namespace Example + Public Class Sample + #Region "Outer" + #Region "Inner" + Public Sub Execute() + End Sub + #End Region + #End Region + End Class + End Namespace + """; + + var classItem = GetNamespace(await Map(source)).Members + .OfType() + .Single(); + var outerRegion = classItem.Members.OfType().Single(); + var innerRegion = outerRegion.Members.OfType().Single(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(outerRegion.Name, Is.EqualTo("Outer")); + Assert.That(innerRegion.Name, Is.EqualTo("Inner")); + Assert.That(innerRegion.Members.Single().Name, Is.EqualTo("Execute")); + Assert.That(innerRegion.Members.Single().Kind, Is.EqualTo(CodeItemKindEnum.Method)); + } + } + + [Test] + public void OnlyMapsVisualBasicFilesWhenVisualBasicIsEnabled() + { + var mapper = new VisualBasicDocumentMapper(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(mapper.CanMapDocument("Example.vb", new GlobalSettings + { + EnableVisualBasic = true, + }), Is.True); + Assert.That(mapper.CanMapDocument("Example.VB", new GlobalSettings + { + EnableVisualBasic = true, + }), Is.True); + Assert.That(mapper.CanMapDocument("Example.vb", new GlobalSettings()), Is.False); + Assert.That(mapper.CanMapDocument("Example.cs", new GlobalSettings + { + EnableVisualBasic = true, + }), Is.False); + } + } + + private static async Task> Map(string source) + => await VisualBasicDocumentMapper.MapDocument( + source, + new CodeDocumentViewModel(), + filePaths: [], + cancellationToken: default); + + private static CodeNamespaceItem GetNamespace(IEnumerable codeItems) + { + var namespaceItem = codeItems.Single() as CodeNamespaceItem; + Assert.That(namespaceItem, Is.Not.Null); + + return namespaceItem!; + } +}