diff --git a/Rubberduck.CodeAnalysis/CodePathAnalysis/Extensions/NodeExtensions.cs b/Rubberduck.CodeAnalysis/CodePathAnalysis/Extensions/NodeExtensions.cs index 9ddfe2f42c..f9c66e5622 100644 --- a/Rubberduck.CodeAnalysis/CodePathAnalysis/Extensions/NodeExtensions.cs +++ b/Rubberduck.CodeAnalysis/CodePathAnalysis/Extensions/NodeExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Rubberduck.Parsing.Symbols; namespace Rubberduck.Inspections.CodePathAnalysis.Extensions { @@ -50,5 +51,34 @@ public static INode GetFirstNode(this INode node, IEnumerable excludedType return GetFirstNode(node.Children[0], excludedTypes); } + + public static List GetIdentifierReferences(this INode node) + { + var nodes = new List(); + + var blockNodes = node.GetNodes(new[] { typeof(BlockNode) }); + foreach (var block in blockNodes) + { + INode lastNode = default; + foreach (var flattenedNode in block.GetFlattenedNodes(new[] { typeof(GenericNode), typeof(BlockNode) })) + { + if (flattenedNode is AssignmentNode && + lastNode is AssignmentNode) + { + nodes.Add(lastNode.Reference); + } + + lastNode = flattenedNode; + } + + if (lastNode is AssignmentNode && + block.Children[0].GetFirstNode(new[] { typeof(GenericNode) }) is DeclarationNode) + { + nodes.Add(lastNode.Reference); + } + } + + return nodes; + } } } diff --git a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Abstract/NodeBase.cs b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Abstract/NodeBase.cs new file mode 100644 index 0000000000..826ff1fb61 --- /dev/null +++ b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Abstract/NodeBase.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using Antlr4.Runtime.Tree; +using Rubberduck.Parsing.Symbols; + +namespace Rubberduck.Inspections.CodePathAnalysis.Nodes +{ + public abstract class NodeBase : INode + { + protected NodeBase(IParseTree tree) + { + Children = new List().ToImmutableList(); + ParseTree = tree; + } + + public int SortOrder { get; set; } + public ImmutableList Children { get; set; } + public INode Parent { get; set; } + public IParseTree ParseTree { get; } + public Declaration Declaration { get; set; } + public IdentifierReference Reference { get; set; } + } +} diff --git a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/INode.cs b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/INode.cs index 6a11f7ff6a..e771fa8c7c 100644 --- a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/INode.cs +++ b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/INode.cs @@ -1,5 +1,6 @@ using Rubberduck.Parsing.Symbols; using System.Collections.Immutable; +using Antlr4.Runtime.Tree; namespace Rubberduck.Inspections.CodePathAnalysis.Nodes { @@ -8,7 +9,7 @@ public interface INode int SortOrder { get; set; } ImmutableList Children { get; set; } INode Parent { get; set; } - + IParseTree ParseTree { get; } Declaration Declaration { get; set; } IdentifierReference Reference { get; set; } } diff --git a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/AssignmentNode.cs b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/AssignmentNode.cs index 5203bfa6ef..a4437ef0da 100644 --- a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/AssignmentNode.cs +++ b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/AssignmentNode.cs @@ -1,21 +1,9 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using Rubberduck.Parsing.Symbols; +using Antlr4.Runtime.Tree; namespace Rubberduck.Inspections.CodePathAnalysis.Nodes { - public class AssignmentNode : INode + public class AssignmentNode : NodeBase { - public AssignmentNode() - { - Children = new List().ToImmutableList(); - } - - public int SortOrder { get; set; } - public ImmutableList Children { get; set; } - public INode Parent { get; set; } - - public Declaration Declaration { get; set; } - public IdentifierReference Reference { get; set; } + public AssignmentNode(IParseTree tree) : base(tree) { } } } diff --git a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/BlockNode.cs b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/BlockNode.cs index 353add1db7..035052bb52 100644 --- a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/BlockNode.cs +++ b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/BlockNode.cs @@ -1,21 +1,9 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using Rubberduck.Parsing.Symbols; +using Antlr4.Runtime.Tree; namespace Rubberduck.Inspections.CodePathAnalysis.Nodes { - public class BlockNode : INode + public class BlockNode : NodeBase { - public BlockNode() - { - Children = new List().ToImmutableList(); - } - - public int SortOrder { get; set; } - public ImmutableList Children { get; set; } - public INode Parent { get; set; } - - public Declaration Declaration { get; set; } - public IdentifierReference Reference { get; set; } + public BlockNode(IParseTree tree) : base(tree) { } } } diff --git a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/BranchNode.cs b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/BranchNode.cs index bef1687b54..fbc40de824 100644 --- a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/BranchNode.cs +++ b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/BranchNode.cs @@ -1,21 +1,9 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using Rubberduck.Parsing.Symbols; +using Antlr4.Runtime.Tree; namespace Rubberduck.Inspections.CodePathAnalysis.Nodes { - public class BranchNode : IBranchNode + public class BranchNode : NodeBase, IBranchNode { - public BranchNode() - { - Children = new List().ToImmutableList(); - } - - public int SortOrder { get; set; } - public ImmutableList Children { get; set; } - public INode Parent { get; set; } - - public Declaration Declaration { get; set; } - public IdentifierReference Reference { get; set; } + public BranchNode(IParseTree tree) : base(tree) { } } } diff --git a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/DeclarationNode.cs b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/DeclarationNode.cs index f7173b13e9..dbf0ae2d49 100644 --- a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/DeclarationNode.cs +++ b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/DeclarationNode.cs @@ -1,21 +1,9 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using Rubberduck.Parsing.Symbols; +using Antlr4.Runtime.Tree; namespace Rubberduck.Inspections.CodePathAnalysis.Nodes { - public class DeclarationNode : INode + public class DeclarationNode : NodeBase { - public DeclarationNode() - { - Children = new List().ToImmutableList(); - } - - public int SortOrder { get; set; } - public ImmutableList Children { get; set; } - public INode Parent { get; set; } - - public Declaration Declaration { get; set; } - public IdentifierReference Reference { get; set; } + public DeclarationNode(IParseTree tree) : base(tree) { } } } diff --git a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/GenericNode.cs b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/GenericNode.cs index e8fb1db965..3906d4a6e5 100644 --- a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/GenericNode.cs +++ b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/GenericNode.cs @@ -1,21 +1,9 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using Rubberduck.Parsing.Symbols; +using Antlr4.Runtime.Tree; namespace Rubberduck.Inspections.CodePathAnalysis.Nodes { - public class GenericNode : INode + public class GenericNode : NodeBase { - public GenericNode() - { - Children = new List().ToImmutableList(); - } - - public int SortOrder { get; set; } - public ImmutableList Children { get; set; } - public INode Parent { get; set; } - - public Declaration Declaration { get; set; } - public IdentifierReference Reference { get; set; } + public GenericNode(IParseTree tree) : base(tree) { } } } diff --git a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/LoopNode.cs b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/LoopNode.cs index 56fd6b2d89..8f6d011e59 100644 --- a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/LoopNode.cs +++ b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/LoopNode.cs @@ -1,21 +1,9 @@ -using Rubberduck.Parsing.Symbols; -using System.Collections.Generic; -using System.Collections.Immutable; +using Antlr4.Runtime.Tree; namespace Rubberduck.Inspections.CodePathAnalysis.Nodes { - public class LoopNode : ILoopNode + public class LoopNode : NodeBase, ILoopNode { - public LoopNode() - { - Children = new List().ToImmutableList(); - } - - public int SortOrder { get; set; } - public ImmutableList Children { get; set; } - public INode Parent { get; set; } - - public Declaration Declaration { get; set; } - public IdentifierReference Reference { get; set; } + public LoopNode(IParseTree tree) : base(tree) { } } } diff --git a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/ReferenceNode.cs b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/ReferenceNode.cs index 538fa01798..f1a1d66ed6 100644 --- a/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/ReferenceNode.cs +++ b/Rubberduck.CodeAnalysis/CodePathAnalysis/Nodes/Implementations/ReferenceNode.cs @@ -1,21 +1,9 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using Rubberduck.Parsing.Symbols; +using Antlr4.Runtime.Tree; namespace Rubberduck.Inspections.CodePathAnalysis.Nodes { - public class ReferenceNode : INode + public class ReferenceNode : NodeBase { - public ReferenceNode() - { - Children = new List().ToImmutableList(); - } - - public int SortOrder { get; set; } - public ImmutableList Children { get; set; } - public INode Parent { get; set; } - - public Declaration Declaration { get; set; } - public IdentifierReference Reference { get; set; } + public ReferenceNode(IParseTree tree) : base(tree) { } } } diff --git a/Rubberduck.CodeAnalysis/CodePathAnalysis/Walker.cs b/Rubberduck.CodeAnalysis/CodePathAnalysis/Walker.cs index 080ecbdf5d..a9c2ba6522 100644 --- a/Rubberduck.CodeAnalysis/CodePathAnalysis/Walker.cs +++ b/Rubberduck.CodeAnalysis/CodePathAnalysis/Walker.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Antlr4.Runtime; namespace Rubberduck.Inspections.CodePathAnalysis { @@ -19,7 +20,7 @@ public INode GenerateTree(IParseTree tree, Declaration declaration) case VBAParser.ForEachStmtContext _: case VBAParser.WhileWendStmtContext _: case VBAParser.DoLoopStmtContext _: - node = new LoopNode(); + node = new LoopNode(tree); break; case VBAParser.IfStmtContext _: case VBAParser.ElseBlockContext _: @@ -28,16 +29,16 @@ public INode GenerateTree(IParseTree tree, Declaration declaration) case VBAParser.SingleLineElseClauseContext _: case VBAParser.CaseClauseContext _: case VBAParser.CaseElseClauseContext _: - node = new BranchNode(); + node = new BranchNode(tree); break; case VBAParser.BlockContext _: - node = new BlockNode(); + node = new BlockNode(tree); break; } if (declaration.Context == tree) { - node = new DeclarationNode + node = new DeclarationNode(tree) { Declaration = declaration }; @@ -48,14 +49,14 @@ public INode GenerateTree(IParseTree tree, Declaration declaration) { if (reference.IsAssignment) { - node = new AssignmentNode + node = new AssignmentNode(tree) { Reference = reference }; } else { - node = new ReferenceNode + node = new ReferenceNode(tree) { Reference = reference }; @@ -64,7 +65,7 @@ public INode GenerateTree(IParseTree tree, Declaration declaration) if (node == null) { - node = new GenericNode(); + node = new GenericNode(tree); } var children = new List(); diff --git a/Rubberduck.CodeAnalysis/Inspections/Abstract/MemberAccessMayReturnNothingInspectionBase.cs b/Rubberduck.CodeAnalysis/Inspections/Abstract/MemberAccessMayReturnNothingInspectionBase.cs new file mode 100644 index 0000000000..b78e9cf604 --- /dev/null +++ b/Rubberduck.CodeAnalysis/Inspections/Abstract/MemberAccessMayReturnNothingInspectionBase.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Linq; +using Antlr4.Runtime.Tree; +using Rubberduck.Inspections.CodePathAnalysis; +using Rubberduck.Inspections.CodePathAnalysis.Nodes; +using Rubberduck.Inspections.Results; +using Rubberduck.Parsing; +using Rubberduck.Parsing.Grammar; +using Rubberduck.Parsing.Inspections.Abstract; +using Rubberduck.Parsing.Symbols; +using Rubberduck.Parsing.VBA; + +namespace Rubberduck.Inspections.Abstract +{ + public abstract class MemberAccessMayReturnNothingInspectionBase : InspectionBase + { + protected MemberAccessMayReturnNothingInspectionBase(RubberduckParserState state) : base(state) { } + + public abstract List MembersUnderTest { get; } + public abstract string ResultTemplate { get; } + + protected override IEnumerable DoGetInspectionResults() + { + var interesting = MembersUnderTest.SelectMany(member => member.References).ToList(); + if (!interesting.Any()) + { + return Enumerable.Empty(); + } + + var output = new List(); + foreach (var reference in interesting.Where(use => !IsIgnoringInspectionResultFor(use, AnnotationName))) + { + var access = reference.Context.GetAncestor(); + var usageContext = access.Parent is VBAParser.IndexExprContext + ? access.Parent.Parent + : access.Parent; + + var setter = usageContext is VBAParser.LExprContext lexpr && lexpr.Parent is VBAParser.SetStmtContext + ? lexpr.Parent + : null; + + if (setter is null) + { + if (usageContext is VBAParser.MemberAccessExprContext || !ContextIsNothingTest(usageContext)) + { + output.Add(new IdentifierReferenceInspectionResult(this, + string.Format(ResultTemplate, + $"{reference.Declaration.ParentDeclaration.IdentifierName}.{reference.IdentifierName}"), + State, reference)); + } + continue; + } + + var assignedTo = Declarations.SelectMany(decl => decl.References).SingleOrDefault(assign => + assign.IsAssignment && (assign.Context.GetAncestor()?.Equals(setter) ?? false)); + if (assignedTo is null) + { + continue; + } + + var tree = new Walker().GenerateTree(assignedTo.Declaration.ParentScopeDeclaration.Context, assignedTo.Declaration); + var firstUse = GetReferenceNodes(tree).FirstOrDefault(); + if (firstUse is null || ContextIsNothingTest(firstUse.Reference.Context.Parent)) + { + continue; + } + + output.Add(new IdentifierReferenceInspectionResult(this, + string.Format(ResultTemplate, + $"{reference.Declaration.ParentDeclaration.IdentifierName}.{reference.IdentifierName}"), + State, reference)); + } + + return output; + } + + private bool ContextIsNothingTest(IParseTree context) + { + return context is VBAParser.LExprContext && + context.Parent is VBAParser.RelationalOpContext comparison && + comparison.IS() != null + && comparison.GetDescendent() != null; + } + + private IEnumerable GetReferenceNodes(INode node) + { + if (node is ReferenceNode && node.Reference != null) + { + yield return node; + } + + foreach (var child in node.Children) + { + foreach (var childNode in GetReferenceNodes(child)) + { + yield return childNode; + } + } + } + } +} diff --git a/Rubberduck.CodeAnalysis/Inspections/Concrete/AssignmentNotUsedInspection.cs b/Rubberduck.CodeAnalysis/Inspections/Concrete/AssignmentNotUsedInspection.cs index 47f0d0be62..d3e42e68fd 100644 --- a/Rubberduck.CodeAnalysis/Inspections/Concrete/AssignmentNotUsedInspection.cs +++ b/Rubberduck.CodeAnalysis/Inspections/Concrete/AssignmentNotUsedInspection.cs @@ -4,7 +4,6 @@ using Rubberduck.Parsing.VBA; using Rubberduck.Inspections.CodePathAnalysis; using Rubberduck.Parsing.Symbols; -using Rubberduck.Inspections.CodePathAnalysis.Nodes; using Rubberduck.Inspections.CodePathAnalysis.Extensions; using System.Linq; using Rubberduck.Inspections.Results; @@ -29,41 +28,12 @@ protected override IEnumerable DoGetInspectionResults() { var tree = _walker.GenerateTree(variable.ParentScopeDeclaration.Context, variable); - nodes.AddRange(GetIdentifierReferences(tree, variable)); + nodes.AddRange(tree.GetIdentifierReferences()); } return nodes .Select(issue => new IdentifierReferenceInspectionResult(this, Description, State, issue)) .ToList(); } - - private List GetIdentifierReferences(INode node, Declaration declaration) - { - var nodes = new List(); - - var blockNodes = node.GetNodes(new[] { typeof(BlockNode) }); - foreach (var block in blockNodes) - { - INode lastNode = default; - foreach (var flattenedNode in block.GetFlattenedNodes(new[] { typeof(GenericNode), typeof(BlockNode) })) - { - if (flattenedNode is AssignmentNode && - lastNode is AssignmentNode) - { - nodes.Add(lastNode.Reference); - } - - lastNode = flattenedNode; - } - - if (lastNode is AssignmentNode && - block.Children[0].GetFirstNode(new[] { typeof(GenericNode) }) is DeclarationNode) - { - nodes.Add(lastNode.Reference); - } - } - - return nodes; - } } } diff --git a/Rubberduck.CodeAnalysis/Inspections/Concrete/MemberAccessMayReturnNothingInspection/ExcelMemberMayReturnNothingInspection.cs b/Rubberduck.CodeAnalysis/Inspections/Concrete/MemberAccessMayReturnNothingInspection/ExcelMemberMayReturnNothingInspection.cs new file mode 100644 index 0000000000..7d422f5abc --- /dev/null +++ b/Rubberduck.CodeAnalysis/Inspections/Concrete/MemberAccessMayReturnNothingInspection/ExcelMemberMayReturnNothingInspection.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Rubberduck.Inspections.Abstract; +using Rubberduck.Parsing.Inspections; +using Rubberduck.Parsing.Symbols; +using Rubberduck.Parsing.VBA; +using Rubberduck.Resources.Inspections; + +namespace Rubberduck.Inspections.Concrete +{ + [RequiredLibrary("Excel")] + public class ExcelMemberMayReturnNothingInspection : MemberAccessMayReturnNothingInspectionBase + { + public ExcelMemberMayReturnNothingInspection(RubberduckParserState state) : base(state) { } + + private static readonly List ExcelMembers = new List + { + "Range.Find", + "Range.FindNext", + "Range.FindPrevious" + }; + + public override List MembersUnderTest => BuiltInDeclarations + .Where(decl => decl.ProjectName.Equals("Excel") && ExcelMembers.Any(member => decl.QualifiedName.ToString().EndsWith(member))) + .ToList(); + + public override string ResultTemplate => InspectionResults.ExcelMemberMayReturnNothingInspection; + } +} diff --git a/Rubberduck.CodeAnalysis/Inspections/Concrete/UnderscoreInPublicClassModuleMemberInspection.cs b/Rubberduck.CodeAnalysis/Inspections/Concrete/UnderscoreInPublicClassModuleMemberInspection.cs new file mode 100644 index 0000000000..d28ec90c9b --- /dev/null +++ b/Rubberduck.CodeAnalysis/Inspections/Concrete/UnderscoreInPublicClassModuleMemberInspection.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using Rubberduck.Inspections.Abstract; +using Rubberduck.Inspections.Results; +using Rubberduck.Parsing.Inspections.Abstract; +using Rubberduck.Resources.Inspections; +using Rubberduck.Parsing.VBA; + +namespace Rubberduck.Inspections.Concrete +{ + public sealed class UnderscoreInPublicClassModuleMemberInspection : InspectionBase + { + public UnderscoreInPublicClassModuleMemberInspection(RubberduckParserState state) + : base(state) { } + + protected override IEnumerable DoGetInspectionResults() + { + var interfaceMembers = State.DeclarationFinder.FindAllInterfaceImplementingMembers().ToList(); + var eventHandlers = State.DeclarationFinder.FindEventHandlers().ToList(); + + var names = State.DeclarationFinder.UserDeclarations(Parsing.Symbols.DeclarationType.Member) + .Where(w => w.ParentDeclaration.DeclarationType == Parsing.Symbols.DeclarationType.ClassModule) + .Where(w => !interfaceMembers.Contains(w) && !eventHandlers.Contains(w)) + .Where(w => w.Accessibility == Parsing.Symbols.Accessibility.Public || w.Accessibility == Parsing.Symbols.Accessibility.Implicit) + .Where(w => w.IdentifierName.Contains('_')) + .ToList(); + + return names.Select(issue => + new DeclarationInspectionResult(this, string.Format(InspectionResults.UnderscoreInPublicClassModuleMemberInspection, issue.IdentifierName), issue)); + } + } +} diff --git a/Rubberduck.CodeAnalysis/QuickFixes/RenameDeclarationQuickFix.cs b/Rubberduck.CodeAnalysis/QuickFixes/RenameDeclarationQuickFix.cs index ab6c2817a8..327b42826d 100644 --- a/Rubberduck.CodeAnalysis/QuickFixes/RenameDeclarationQuickFix.cs +++ b/Rubberduck.CodeAnalysis/QuickFixes/RenameDeclarationQuickFix.cs @@ -18,7 +18,7 @@ public sealed class RenameDeclarationQuickFix : QuickFixBase private readonly IMessageBox _messageBox; public RenameDeclarationQuickFix(IVBE vbe, RubberduckParserState state, IMessageBox messageBox) - : base(typeof(HungarianNotationInspection), typeof(UseMeaningfulNameInspection), typeof(DefaultProjectNameInspection)) + : base(typeof(HungarianNotationInspection), typeof(UseMeaningfulNameInspection), typeof(DefaultProjectNameInspection), typeof(UnderscoreInPublicClassModuleMemberInspection)) { _vbe = vbe; _state = state; diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteHandlerBase.cs b/Rubberduck.Core/AutoComplete/AutoCompleteHandlerBase.cs index 7dd5eabfcb..2eb2da1fec 100644 --- a/Rubberduck.Core/AutoComplete/AutoCompleteHandlerBase.cs +++ b/Rubberduck.Core/AutoComplete/AutoCompleteHandlerBase.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using Rubberduck.Parsing.VBA.Extensions; -using Rubberduck.Settings; +using Rubberduck.Settings; using Rubberduck.VBEditor; using Rubberduck.VBEditor.Events; using Rubberduck.VBEditor.SourceCodeHandling; @@ -17,6 +14,6 @@ protected AutoCompleteHandlerBase(ICodePaneHandler pane) protected ICodePaneHandler CodePaneHandler { get; } - public abstract CodeString Handle(AutoCompleteEventArgs e, AutoCompleteSettings settings); + public abstract bool Handle(AutoCompleteEventArgs e, AutoCompleteSettings settings, out CodeString result); } } \ No newline at end of file diff --git a/Rubberduck.Core/AutoComplete/Service/AutoCompleteService.cs b/Rubberduck.Core/AutoComplete/Service/AutoCompleteService.cs index 4dc4a5f344..251cd628e3 100644 --- a/Rubberduck.Core/AutoComplete/Service/AutoCompleteService.cs +++ b/Rubberduck.Core/AutoComplete/Service/AutoCompleteService.cs @@ -135,11 +135,13 @@ private void HandleKeyDown(object sender, AutoCompleteEventArgs e) foreach (var handler in _handlers) { - var result = handler.Handle(e, _settings); - if (result != null && e.Handled) + if (!handler.Handle(e, _settings, out _)) { - return; + continue; } + + e.Handled = true; + return; } } diff --git a/Rubberduck.Core/AutoComplete/Service/SelfClosingPair.cs b/Rubberduck.Core/AutoComplete/Service/SelfClosingPair.cs index e2157d98f3..1266439f2b 100644 --- a/Rubberduck.Core/AutoComplete/Service/SelfClosingPair.cs +++ b/Rubberduck.Core/AutoComplete/Service/SelfClosingPair.cs @@ -1,6 +1,9 @@ -namespace Rubberduck.AutoComplete.Service +using Rubberduck.VBEditor; +using System; + +namespace Rubberduck.AutoComplete.Service { - public class SelfClosingPair + public class SelfClosingPair : IEquatable { public SelfClosingPair(char opening, char closing) { @@ -15,5 +18,15 @@ public SelfClosingPair(char opening, char closing) /// True if is the same as . /// public bool IsSymetric => OpeningChar == ClosingChar; + + public bool Equals(SelfClosingPair other) => other?.OpeningChar == OpeningChar && + other.ClosingChar == ClosingChar; + + public override bool Equals(object obj) + { + return obj is SelfClosingPair scp && Equals(scp); + } + + public override int GetHashCode() => HashCode.Compute(OpeningChar, ClosingChar); } } \ No newline at end of file diff --git a/Rubberduck.Core/AutoComplete/Service/SelfClosingPairCompletionService.cs b/Rubberduck.Core/AutoComplete/Service/SelfClosingPairCompletionService.cs index 24b0fff2ee..fbe81b7f65 100644 --- a/Rubberduck.Core/AutoComplete/Service/SelfClosingPairCompletionService.cs +++ b/Rubberduck.Core/AutoComplete/Service/SelfClosingPairCompletionService.cs @@ -12,18 +12,25 @@ namespace Rubberduck.AutoComplete.Service { public class SelfClosingPairCompletionService { + /* + // note: this works... but the VBE makes an annoying DING! when the command isn't available. + // todo: implement our own intellisense, then uncomment this code. private readonly IShowIntelliSenseCommand _showIntelliSense; public SelfClosingPairCompletionService(IShowIntelliSenseCommand showIntelliSense) { _showIntelliSense = showIntelliSense; } + */ - public CodeString Execute(SelfClosingPair pair, CodeString original, char input) + public bool Execute(SelfClosingPair pair, CodeString original, char input, out CodeString result) { + result = null; + var previousCharIsClosingChar = original.CaretPosition.StartColumn > 0 && original.CaretLine[original.CaretPosition.StartColumn - 1] == pair.ClosingChar; + var nextCharIsClosingChar = original.CaretPosition.StartColumn < original.CaretLine.Length && original.CaretLine[original.CaretPosition.StartColumn] == pair.ClosingChar; @@ -33,49 +40,45 @@ public CodeString Execute(SelfClosingPair pair, CodeString original, char input) previousCharIsClosingChar && !nextCharIsClosingChar || original.IsComment || (original.IsInsideStringLiteral && !nextCharIsClosingChar)) { - return null; + return false; } if (input == pair.OpeningChar) { - var result = HandleOpeningChar(pair, original); - return result; + return HandleOpeningChar(pair, original, out result); } - + if (input == pair.ClosingChar) { - return HandleClosingChar(pair, original); + return HandleClosingChar(pair, original, out result); } if (input == '\b') { - return Execute(pair, original, Keys.Back); + return Execute(pair, original, Keys.Back, out result); } - return null; + return false; } - public CodeString Execute(SelfClosingPair pair, CodeString original, Keys input) + public bool Execute(SelfClosingPair pair, CodeString original, Keys input, out CodeString result) { + result = null; if (original.IsComment) { - return null; + // not handling backspace in comments + return false; } - if (input == Keys.Back) - { - return HandleBackspace(pair, original); - } - - return null; + return input == Keys.Back && HandleBackspace(pair, original, out result); } - private CodeString HandleOpeningChar(SelfClosingPair pair, CodeString original) + private bool HandleOpeningChar(SelfClosingPair pair, CodeString original, out CodeString result) { var nextPosition = original.CaretPosition.ShiftRight(); var autoCode = new string(new[] { pair.OpeningChar, pair.ClosingChar }); var lines = original.Lines; - var line = lines[original.CaretPosition.StartLine]; + var line = original.CaretLine; string newCode; if (string.IsNullOrEmpty(line)) @@ -94,14 +97,17 @@ private CodeString HandleOpeningChar(SelfClosingPair pair, CodeString original) } lines[original.CaretPosition.StartLine] = newCode; - return new CodeString(string.Join("\r\n", lines), nextPosition, new Selection(original.SnippetPosition.StartLine, 1, original.SnippetPosition.EndLine, 1)); + result = new CodeString(string.Join("\r\n", lines), nextPosition, new Selection(original.SnippetPosition.StartLine, 1, original.SnippetPosition.EndLine, 1)); + return true; } - private CodeString HandleClosingChar(SelfClosingPair pair, CodeString original) + private bool HandleClosingChar(SelfClosingPair pair, CodeString original, out CodeString result) { + result = null; if (pair.IsSymetric) { - return null; + // a symetric pair would have already been handled with the opening character. + return false; } var nextIsClosingChar = original.CaretLine.Length > original.CaretCharIndex && @@ -111,18 +117,16 @@ private CodeString HandleClosingChar(SelfClosingPair pair, CodeString original) var nextPosition = original.CaretPosition.ShiftRight(); var newCode = original.Code; - return new CodeString(newCode, nextPosition, new Selection(original.SnippetPosition.StartLine, 1, original.SnippetPosition.EndLine, 1)); + result = new CodeString(newCode, nextPosition, new Selection(original.SnippetPosition.StartLine, 1, original.SnippetPosition.EndLine, 1)); + return true; } - return null; - } - private CodeString HandleBackspace(SelfClosingPair pair, CodeString original) - { - return DeleteMatchingTokens(pair, original); + return false; } - private CodeString DeleteMatchingTokens(SelfClosingPair pair, CodeString original) + private bool HandleBackspace(SelfClosingPair pair, CodeString original, out CodeString result) { + result = null; var position = original.CaretPosition; var lines = original.Lines; @@ -130,7 +134,7 @@ private CodeString DeleteMatchingTokens(SelfClosingPair pair, CodeString origina if (line.Length == 0) { // nothing to delete at caret position... bail out. - return null; + return false; } var previous = Math.Max(0, position.StartColumn - 1); @@ -146,24 +150,25 @@ private CodeString DeleteMatchingTokens(SelfClosingPair pair, CodeString origina if (line.Length == 2) { // entire line consists in the self-closing pair itself. - return new CodeString(string.Empty, default, Selection.Empty.ShiftRight()); + result = new CodeString(string.Empty, default, Selection.Empty.ShiftRight()); } // simple case; caret is between the opening and closing chars - remove both. lines[original.CaretPosition.StartLine] = line.Remove(previous, 2); - return new CodeString(string.Join("\r\n", lines), original.CaretPosition.ShiftLeft(), original.SnippetPosition); + result = new CodeString(string.Join("\r\n", lines), original.CaretPosition.ShiftLeft(), original.SnippetPosition); } if (previous < line.Length - 1 && previousChar == pair.OpeningChar) { - return DeleteMatchingTokensMultiline(pair, original); + return DeleteMatchingTokensMultiline(pair, original, out result); } - return null; + return result != null; } - private CodeString DeleteMatchingTokensMultiline(SelfClosingPair pair, CodeString original) + private bool DeleteMatchingTokensMultiline(SelfClosingPair pair, CodeString original, out CodeString result) { + result = null; var position = original.CaretPosition; var lines = original.Lines; var line = lines[original.CaretPosition.StartLine]; @@ -177,7 +182,7 @@ private CodeString DeleteMatchingTokensMultiline(SelfClosingPair pair, CodeStrin if (closingTokenPosition == default) { // could not locate the closing token... bail out. - return null; + return false; } var closingLine = lines[closingTokenPosition.EndLine].Remove(closingTokenPosition.StartColumn, 1); @@ -243,8 +248,9 @@ private CodeString DeleteMatchingTokensMultiline(SelfClosingPair pair, CodeStrin // remove any dangling empty lines... lines = lines.Where((x, i) => i <= position.StartLine || !string.IsNullOrWhiteSpace(x)).ToArray(); - return new CodeString(string.Join("\r\n", lines), finalCaretPosition, + result = new CodeString(string.Join("\r\n", lines), finalCaretPosition, new Selection(original.SnippetPosition.StartLine, 1, original.SnippetPosition.EndLine, 1)); + return true; } private static Selection HandleBackspaceContinuations(string[] nonEmptyLines, Selection finalCaretPosition) diff --git a/Rubberduck.Core/AutoComplete/Service/SelfClosingPairHandler.cs b/Rubberduck.Core/AutoComplete/Service/SelfClosingPairHandler.cs index f50212287f..253ca22e45 100644 --- a/Rubberduck.Core/AutoComplete/Service/SelfClosingPairHandler.cs +++ b/Rubberduck.Core/AutoComplete/Service/SelfClosingPairHandler.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; -using System.Windows.Forms; +using System.Linq; using Rubberduck.Settings; using Rubberduck.VBEditor; using Rubberduck.VBEditor.Events; @@ -10,86 +10,126 @@ namespace Rubberduck.AutoComplete.Service { public class SelfClosingPairHandler : AutoCompleteHandlerBase { - private static readonly IEnumerable SelfClosingPairs = new List - { - new SelfClosingPair('(', ')'), - new SelfClosingPair('"', '"'), - new SelfClosingPair('[', ']'), - new SelfClosingPair('{', '}'), - }; + private const int MaximumLines = 25; + private readonly IReadOnlyList _selfClosingPairs; + private readonly IDictionary _scpInputLookup; private readonly SelfClosingPairCompletionService _scpService; public SelfClosingPairHandler(ICodePaneHandler pane, SelfClosingPairCompletionService scpService) : base(pane) { + _selfClosingPairs = new[] + { + new SelfClosingPair('(', ')'), + new SelfClosingPair('"', '"'), + new SelfClosingPair('[', ']'), + new SelfClosingPair('{', '}'), + }; + _scpInputLookup = _selfClosingPairs + .Select(p => new {Key = p.OpeningChar, Pair = p}) + .Union(_selfClosingPairs.Where(p => !p.IsSymetric).Select(p => new {Key = p.ClosingChar, Pair = p})) + .ToDictionary(p => p.Key, p => p.Pair); + _scpService = scpService; } - public override CodeString Handle(AutoCompleteEventArgs e, AutoCompleteSettings settings) + public override bool Handle(AutoCompleteEventArgs e, AutoCompleteSettings settings, out CodeString result) { + result = null; + if (!_scpInputLookup.TryGetValue(e.Character, out var pair) && e.Character != '\b') + { + return false; + } + var original = CodePaneHandler.GetCurrentLogicalLine(e.Module); - foreach (var pair in SelfClosingPairs) + if (original == null || original.Lines.Length == MaximumLines) { - var isPresent = original.CaretLine.EndsWith($"{pair.OpeningChar}{pair.ClosingChar}"); + // selection spans more than a single logical line, or + // logical line somehow spans more than the maximum number of physical lines in a logical line of code (25). + return false; + } - var result = ExecuteSelfClosingPair(e, original, pair); - if (result == null) + if (pair != null) + { + if (!HandleInternal(e, original, pair, out result)) { - continue; + return false; } - - var prettified = CodePaneHandler.Prettify(e.Module, original); - if (!isPresent && original.CaretLine.Length + 2 == prettified.CaretLine.Length && - prettified.CaretLine.EndsWith($"{pair.OpeningChar}{pair.ClosingChar}")) + } + else if (e.Character == '\b') + { + foreach (var scp in _selfClosingPairs) { - // prettifier just added the pair for us; likely a Sub or Function statement. - prettified = original; // pretend this didn't happen. note: probably breaks if original has extra whitespace. + if (HandleInternal(e, original, scp, out result)) + { + break; + } } - result = ExecuteSelfClosingPair(e, prettified, pair); if (result == null) { - continue; + return false; } + } - result = CodePaneHandler.Prettify(e.Module, result); + var snippetPosition = new Selection(result.SnippetPosition.StartLine, 1, result.SnippetPosition.EndLine, 1); + result = new CodeString(result.Code, result.CaretPosition, snippetPosition); - var currentLine = result.Lines[result.CaretPosition.StartLine]; - if (!string.IsNullOrWhiteSpace(currentLine) && - currentLine.EndsWith(" ") && - result.CaretPosition.StartColumn == currentLine.Length) - { - result = result.ReplaceLine(result.CaretPosition.StartLine, currentLine.TrimEnd()); - } + e.Handled = true; + return true; + } - if (pair.OpeningChar == '(' && e.Character != '\b' && !result.CaretLine.EndsWith($"{pair.OpeningChar}{pair.ClosingChar}")) - { - // VBE eats it. just bail out. - return null; - } + private bool HandleInternal(AutoCompleteEventArgs e, CodeString original, SelfClosingPair pair, out CodeString result) + { + if (!original.CaretPosition.IsSingleCharacter) + { + // todo: WrapSelection? + result = null; + return false; + } - e.Handled = true; - result = new CodeString(result.Code, result.CaretPosition, new Selection(result.SnippetPosition.StartLine, 1, result.SnippetPosition.EndLine, 1)); - return result; + var isPresent = original.CaretLine.EndsWith($"{pair.OpeningChar}{pair.ClosingChar}"); + + if (!_scpService.Execute(pair, original, e.Character, out result)) + { + return false; } - return null; - } + var prettified = CodePaneHandler.Prettify(e.Module, original); + if (!isPresent && original.CaretLine.Length + 2 == prettified.CaretLine.Length && + prettified.CaretLine.EndsWith($"{pair.OpeningChar}{pair.ClosingChar}")) + { + // prettifier just added the pair for us; likely a Sub or Function statement. + prettified = original; // pretend this didn't happen. note: probably breaks if original has extra whitespace. + } - private CodeString ExecuteSelfClosingPair(AutoCompleteEventArgs e, CodeString original, SelfClosingPair pair) - { - CodeString result; - if (e.Character == '\b' && original.CaretPosition.StartColumn > 1) + if (!_scpService.Execute(pair, prettified, e.Character, out result)) + { + return false; + } + + result = CodePaneHandler.Prettify(e.Module, result); + + var currentLine = result.Lines[result.CaretPosition.StartLine]; + if (!string.IsNullOrWhiteSpace(currentLine) && + currentLine.EndsWith(" ") && + result.CaretPosition.StartColumn == currentLine.Length) { - result = _scpService.Execute(pair, original, Keys.Back); + result = result.ReplaceLine(result.CaretPosition.StartLine, currentLine.TrimEnd()); } - else + + if (pair.OpeningChar == '(' && + e.Character == pair.OpeningChar && + !result.CaretLine.EndsWith($"{pair.OpeningChar}{pair.ClosingChar}")) { - result = _scpService.Execute(pair, original, e.Character); + // VBE eats it. bail out but still swallow the keypress, since we've already re-prettified. + e.Handled = true; + result = null; + return false; } - return result; + return true; } } } \ No newline at end of file diff --git a/Rubberduck.Core/AutoComplete/Service/ShowIntelliSenseCommand.cs b/Rubberduck.Core/AutoComplete/Service/ShowIntelliSenseCommand.cs index 4663c29b39..63b7d057ad 100644 --- a/Rubberduck.Core/AutoComplete/Service/ShowIntelliSenseCommand.cs +++ b/Rubberduck.Core/AutoComplete/Service/ShowIntelliSenseCommand.cs @@ -7,6 +7,9 @@ namespace Rubberduck.AutoComplete.Service { public interface IShowIntelliSenseCommand { + /// + /// WARNING! Makes an utterly annoying DING! in the VBE if the "QuickInfo" command is unavailable. + /// void Execute(); } diff --git a/Rubberduck.Core/AutoComplete/Service/SmartConcatenationHandler.cs b/Rubberduck.Core/AutoComplete/Service/SmartConcatenationHandler.cs index b52ddb4d13..042438adaf 100644 --- a/Rubberduck.Core/AutoComplete/Service/SmartConcatenationHandler.cs +++ b/Rubberduck.Core/AutoComplete/Service/SmartConcatenationHandler.cs @@ -16,17 +16,21 @@ public SmartConcatenationHandler(ICodePaneHandler pane) { } - public override CodeString Handle(AutoCompleteEventArgs e, AutoCompleteSettings settings) + public override bool Handle(AutoCompleteEventArgs e, AutoCompleteSettings settings, out CodeString result) { + result = null; if (e.Character != '\r' || (!settings?.SmartConcat.IsEnabled ?? true)) { - return null; + return false; } var currentContent = CodePaneHandler.GetCurrentLogicalLine(e.Module); - if (!currentContent.IsInsideStringLiteral) + if ((!currentContent?.IsInsideStringLiteral ?? true) + || currentContent.Lines.Length >= settings.SmartConcat.ConcatMaxLines) { - return null; + // selection spans more than a single logical line, or spans too many lines to be legal; + // too many line continuations throws COMException if we attempt to modify. + return false; } var lastIndexLeftOfCaret = currentContent.CaretLine.Length > 2 ? currentContent.CaretLine.Substring(0, currentContent.CaretPosition.StartColumn).LastIndexOf('"') : 0; @@ -51,16 +55,16 @@ public override CodeString Handle(AutoCompleteEventArgs e, AutoCompleteSettings var newPosition = new Selection(newContent.CaretPosition.StartLine + 1, indent + 1); e.Handled = true; - var result = new CodeString(newContent.Code, newPosition, + result = new CodeString(newContent.Code, newPosition, new Selection(newContent.SnippetPosition.StartLine, 1, newContent.SnippetPosition.EndLine, 1)); CodePaneHandler.SubstituteCode(e.Module, result); var finalSelection = new Selection(result.SnippetPosition.StartLine, 1).Offset(result.CaretPosition); CodePaneHandler.SetSelection(e.Module, finalSelection); - return result; + return true; } - return null; + return false; } } } \ No newline at end of file diff --git a/Rubberduck.Core/Properties/Settings.Designer.cs b/Rubberduck.Core/Properties/Settings.Designer.cs index a616d2866d..7fdfde34b0 100644 --- a/Rubberduck.Core/Properties/Settings.Designer.cs +++ b/Rubberduck.Core/Properties/Settings.Designer.cs @@ -429,7 +429,8 @@ public static Settings Default { "=\"RubberduckOpportunities\" />\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n <?xml version="1.0" encoding="utf-16"?> -<AutoCompleteSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" IsEnabled="false" CompleteBlockOnTab="true" CompleteBlockOnEnter="true" EnableSmartConcat="true"> - <AutoCompletes> - <AutoComplete Key="AutoCompleteClosingBrace" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteClosingBracket" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteClosingParenthese" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteClosingString" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteDoBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteEnumBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteForBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteFunctionBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteIfBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteOnErrorResumeNextBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompletePrecompilerIfBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompletePropertyBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteSelectBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteSubBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteTypeBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteWhileBlock" IsEnabled="true" /> - <AutoComplete Key="AutoCompleteWithBlock" IsEnabled="true" /> - </AutoCompletes> +<AutoCompleteSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" IsEnabled="false"> + <SmartConcat> + <IsEnabled>false</IsEnabled> + <ConcatVbNewLineModifier>None</ConcatVbNewLineModifier> + </SmartConcat> + <SelfClosingPairs IsEnabled="false" /> + <BlockCompletion IsEnabled="false" CompleteOnEnter="false" CompleteOnTab="false" /> </AutoCompleteSettings> diff --git a/Rubberduck.Core/Settings/AutoCompleteSettings.cs b/Rubberduck.Core/Settings/AutoCompleteSettings.cs index a8fc519e70..c95411069e 100644 --- a/Rubberduck.Core/Settings/AutoCompleteSettings.cs +++ b/Rubberduck.Core/Settings/AutoCompleteSettings.cs @@ -1,8 +1,5 @@ -using Rubberduck.AutoComplete; -using System; -using System.Collections.Generic; +using System; using System.Configuration; -using System.Linq; using System.Xml.Serialization; namespace Rubberduck.Settings @@ -27,6 +24,27 @@ public interface IAutoCompleteSettings [XmlType(AnonymousType = true)] public class AutoCompleteSettings : IAutoCompleteSettings, IEquatable { + /// + /// Less than that would be useless (wouldn't concat). + /// + public static readonly int ConcatMaxLinesMinValue = 2; + /// + /// /More than that would be illegal (wouldn't compile). + /// + public static readonly int ConcatMaxLinesMaxValue = 25; + + public static AutoCompleteSettings AllEnabled => + new AutoCompleteSettings + { + IsEnabled = true, + BlockCompletion = + new BlockCompletionSettings {IsEnabled = true, CompleteOnEnter = true, CompleteOnTab = true}, + SmartConcat = + new SmartConcatSettings {IsEnabled = true, ConcatVbNewLineModifier = ModifierKeySetting.CtrlKey}, + SelfClosingPairs = + new SelfClosingPairSettings {IsEnabled = true} + }; + public AutoCompleteSettings() { SmartConcat = new SmartConcatSettings(); @@ -45,13 +63,35 @@ public AutoCompleteSettings() public class SmartConcatSettings : IEquatable { + private int _concatMaxLines; + + [XmlAttribute] public bool IsEnabled { get; set; } public ModifierKeySetting ConcatVbNewLineModifier { get; set; } + public int ConcatMaxLines + { + get => _concatMaxLines; + set + { + if (value > ConcatMaxLinesMaxValue) + { + value = ConcatMaxLinesMaxValue; + } + else if (value < ConcatMaxLinesMinValue) + { + value = ConcatMaxLinesMinValue; + } + + _concatMaxLines = value; + } + } + public bool Equals(SmartConcatSettings other) => other != null && other.IsEnabled == IsEnabled && - other.ConcatVbNewLineModifier == ConcatVbNewLineModifier; + other.ConcatVbNewLineModifier == ConcatVbNewLineModifier && + other.ConcatMaxLines == ConcatMaxLines; } public class SelfClosingPairSettings : IEquatable diff --git a/Rubberduck.Core/UI/Settings/AutoCompleteSettings.xaml b/Rubberduck.Core/UI/Settings/AutoCompleteSettings.xaml index 8b22ed6052..a54d24fc96 100644 --- a/Rubberduck.Core/UI/Settings/AutoCompleteSettings.xaml +++ b/Rubberduck.Core/UI/Settings/AutoCompleteSettings.xaml @@ -111,6 +111,12 @@ IsChecked="{Binding ConcatVbNewLine}" Content="{Resx ResxName=Rubberduck.Resources.Settings.AutoCompletesPage, Key=ConcatVbNewLine}" /> +