diff --git a/Rubberduck.CodeAnalysis/Inspections/Concrete/IllegalAnnotationInspection.cs b/Rubberduck.CodeAnalysis/Inspections/Concrete/IllegalAnnotationInspection.cs index 450efecd1b..7a908dd9c8 100644 --- a/Rubberduck.CodeAnalysis/Inspections/Concrete/IllegalAnnotationInspection.cs +++ b/Rubberduck.CodeAnalysis/Inspections/Concrete/IllegalAnnotationInspection.cs @@ -20,18 +20,12 @@ public IllegalAnnotationInspection(RubberduckParserState state) protected override IEnumerable DoGetInspectionResults() { - var illegalAnnotations = new List(); - var userDeclarations = State.DeclarationFinder.AllUserDeclarations.ToList(); var identifierReferences = State.DeclarationFinder.AllIdentifierReferences().ToList(); var annotations = State.AllAnnotations; - illegalAnnotations.AddRange(UnboundAnnotations(annotations, userDeclarations, identifierReferences)); - illegalAnnotations.AddRange(NonIdentifierAnnotationsOnIdentifiers(identifierReferences)); - illegalAnnotations.AddRange(NonModuleAnnotationsOnModules(userDeclarations)); - illegalAnnotations.AddRange(NonMemberAnnotationsOnMembers(userDeclarations)); - illegalAnnotations.AddRange(NonVariableAnnotationsOnVariables(userDeclarations)); - illegalAnnotations.AddRange(NonGeneralAnnotationsWhereOnlyGeneralAnnotationsBelong(userDeclarations)); + var illegalAnnotations = UnboundAnnotations(annotations, userDeclarations, identifierReferences) + .Where(annotation => !annotation.AnnotationType.HasFlag(AnnotationType.GeneralAnnotation)); return illegalAnnotations.Select(annotation => new QualifiedContextInspectionResult( @@ -50,64 +44,5 @@ private static ICollection UnboundAnnotations(IEnumerable !boundAnnotationsSelections.Contains(annotation.QualifiedSelection)).ToList(); } - - private static ICollection NonIdentifierAnnotationsOnIdentifiers(IEnumerable identifierReferences) - { - return identifierReferences - .SelectMany(reference => reference.Annotations) - .Where(annotation => !annotation.AnnotationType.HasFlag(AnnotationType.IdentifierAnnotation)) - .ToList(); - } - - private static ICollection NonModuleAnnotationsOnModules(IEnumerable userDeclarations) - { - return userDeclarations - .Where(declaration => declaration.DeclarationType.HasFlag(DeclarationType.Module)) - .SelectMany(moduleDeclaration => moduleDeclaration.Annotations) - .Where(annotation => !annotation.AnnotationType.HasFlag(AnnotationType.ModuleAnnotation)) - .ToList(); - } - - private static ICollection NonMemberAnnotationsOnMembers(IEnumerable userDeclarations) - { - return userDeclarations - .Where(declaration => declaration.DeclarationType.HasFlag(DeclarationType.Member)) - .SelectMany(member => member.Annotations) - .Where(annotation => !annotation.AnnotationType.HasFlag(AnnotationType.MemberAnnotation)) - .ToList(); - } - - private static ICollection NonVariableAnnotationsOnVariables(IEnumerable userDeclarations) - { - return userDeclarations - .Where(declaration => VariableAnnotationDeclarationTypes.Contains(declaration.DeclarationType)) - .SelectMany(declaration => declaration.Annotations) - .Where(annotation => !annotation.AnnotationType.HasFlag(AnnotationType.VariableAnnotation)) - .ToList(); - } - - private static readonly HashSet VariableAnnotationDeclarationTypes = new HashSet() - { - DeclarationType.Variable, - DeclarationType.Control, - DeclarationType.Constant, - DeclarationType.Enumeration, - DeclarationType.EnumerationMember, - DeclarationType.UserDefinedType, - DeclarationType.UserDefinedType, - DeclarationType.UserDefinedTypeMember - }; - - private static ICollection NonGeneralAnnotationsWhereOnlyGeneralAnnotationsBelong(IEnumerable userDeclarations) - { - return userDeclarations - .Where(declaration => !declaration.DeclarationType.HasFlag(DeclarationType.Module) - && !declaration.DeclarationType.HasFlag(DeclarationType.Member) - && !VariableAnnotationDeclarationTypes.Contains(declaration.DeclarationType) - && declaration.DeclarationType != DeclarationType.Project) - .SelectMany(member => member.Annotations) - .Where(annotation => !annotation.AnnotationType.HasFlag(AnnotationType.GeneralAnnotation)) - .ToList(); - } } } \ No newline at end of file diff --git a/Rubberduck.CodeAnalysis/QuickFixes/IgnoreOnceQuickFix.cs b/Rubberduck.CodeAnalysis/QuickFixes/IgnoreOnceQuickFix.cs index a5aa066e0b..c30ffa7e94 100644 --- a/Rubberduck.CodeAnalysis/QuickFixes/IgnoreOnceQuickFix.cs +++ b/Rubberduck.CodeAnalysis/QuickFixes/IgnoreOnceQuickFix.cs @@ -1,15 +1,17 @@ using System; using System.Collections.Generic; using System.Linq; -using Antlr4.Runtime; using Antlr4.Runtime.Misc; using Antlr4.Runtime.Tree; using Rubberduck.Inspections.Abstract; +using Rubberduck.Parsing.Annotations; using Rubberduck.Parsing.Grammar; using Rubberduck.Parsing.Inspections; using Rubberduck.Parsing.Inspections.Abstract; using Rubberduck.Parsing.Rewriter; +using Rubberduck.Parsing.Symbols; using Rubberduck.Parsing.VBA; +using Rubberduck.Parsing.VBA.Parsing; namespace Rubberduck.Inspections.QuickFixes { @@ -29,98 +31,109 @@ public IgnoreOnceQuickFix(RubberduckParserState state, IEnumerable public override void Fix(IInspectionResult result, IRewriteSession rewriteSession) { - var annotationText = $"'@Ignore {result.Inspection.AnnotationName}"; - - int annotationLine; - //TODO: Make this use the parse tree instead of the code module. - var component = _state.ProjectsProvider.Component(result.QualifiedSelection.QualifiedName); - using (var module = component.CodeModule) + if (result.Target?.DeclarationType.HasFlag(DeclarationType.Module) ?? false) { - annotationLine = result.QualifiedSelection.Selection.StartLine; - while (annotationLine != 1 && module.GetLines(annotationLine - 1, 1).EndsWith(" _")) - { - annotationLine--; - } + FixModule(result, rewriteSession); } - - RuleContext treeRoot = result.Context; - while (treeRoot.Parent != null) + else { - treeRoot = treeRoot.Parent; + FixNonModule(result, rewriteSession); } + } - var listener = new CommentOrAnnotationListener(); - ParseTreeWalker.Default.Walk(listener, treeRoot); - var commentContext = listener.Contexts.LastOrDefault(i => i.Stop.TokenIndex <= result.Context.Start.TokenIndex); - var commented = commentContext?.Stop.Line + 1 == annotationLine; + private void FixNonModule(IInspectionResult result, IRewriteSession rewriteSession) + { + int insertionIndex; + string insertText; + var annotationText = $"'@Ignore {result.Inspection.AnnotationName}"; - var rewriter = rewriteSession.CheckOutModuleRewriter(result.QualifiedSelection.QualifiedName); + var module = result.QualifiedSelection.QualifiedName; + var parseTree = _state.GetParseTree(module, CodeKind.CodePaneCode); + var eolListener = new EndOfLineListener(); + ParseTreeWalker.Default.Walk(eolListener, parseTree); + var previousEol = eolListener.Contexts + .OrderBy(eol => eol.Start.TokenIndex) + .LastOrDefault(eol => eol.Start.Line < result.QualifiedSelection.Selection.StartLine); - if (commented) + var rewriter = rewriteSession.CheckOutModuleRewriter(module); + + if (previousEol == null) { - var annotation = commentContext.annotationList()?.annotation(0); - if (annotation != null && annotation.GetText().StartsWith("Ignore")) - { - rewriter.InsertAfter(annotation.annotationName().Stop.TokenIndex, $" {result.Inspection.AnnotationName},"); - } - else - { - var indent = new string(Enumerable.Repeat(' ', commentContext.Start.Column).ToArray()); - rewriter.InsertAfter(commentContext.Stop.TokenIndex, $"{indent}{annotationText}{Environment.NewLine}"); - } + // The context to get annotated is on the first line; we need to insert before token index 0. + insertionIndex = 0; + insertText = annotationText + Environment.NewLine; + rewriter.InsertBefore(insertionIndex, insertText); + return; } - else + + var commentContext = previousEol.commentOrAnnotation(); + if (commentContext == null) + { + insertionIndex = previousEol.Start.TokenIndex; + var indent = WhitespaceAfter(previousEol); + insertText = $"{Environment.NewLine}{indent}{annotationText}"; + rewriter.InsertBefore(insertionIndex, insertText); + return; + } + + var ignoreAnnotation = commentContext.annotationList()?.annotation() + .FirstOrDefault(annotationContext => annotationContext.annotationName().GetText() == AnnotationType.Ignore.ToString()); + if (ignoreAnnotation == null) { - int insertIndex; - - // this value is used when the annotation should be on line 1--we need to insert before token index 0 - if (annotationLine == 1) - { - insertIndex = 0; - annotationText += Environment.NewLine; - } - else - { - var eol = new EndOfLineListener(); - ParseTreeWalker.Default.Walk(eol, treeRoot); - - // we subtract 2 here to get the insertion index to A) account for VBE's one-based indexing - // and B) to get the newline token that introduces that line - var eolContext = eol.Contexts.OrderBy(o => o.Start.TokenIndex).ElementAt(annotationLine - 2); - insertIndex = eolContext.Start.TokenIndex; - - annotationText = Environment.NewLine + annotationText; - } - - rewriter.InsertBefore(insertIndex, annotationText); + insertionIndex = commentContext.Stop.TokenIndex; + var indent = WhitespaceAfter(previousEol); + insertText = $"{indent}{annotationText}{Environment.NewLine}"; + rewriter.InsertAfter(insertionIndex, insertText); + return; } + + insertionIndex = ignoreAnnotation.annotationName().Stop.TokenIndex; + insertText = $" {result.Inspection.AnnotationName},"; + rewriter.InsertAfter(insertionIndex, insertText); } - public override string Description(IInspectionResult result) => Resources.Inspections.QuickFixes.IgnoreOnce; + private static string WhitespaceAfter(VBAParser.EndOfLineContext endOfLine) + { + var individualEndOfStatement = (VBAParser.IndividualNonEOFEndOfStatementContext) endOfLine.Parent; + var whiteSpaceOnNextLine = individualEndOfStatement.whiteSpace(0); + return whiteSpaceOnNextLine != null + ? whiteSpaceOnNextLine.GetText() + : string.Empty; + } - private class CommentOrAnnotationListener : VBAParserBaseListener + private void FixModule(IInspectionResult result, IRewriteSession rewriteSession) { - private readonly IList _contexts = new List(); - public IEnumerable Contexts => _contexts; + var module = result.QualifiedSelection.QualifiedName; + var moduleAnnotations = _state.GetModuleAnnotations(module); + var firstIgnoreModuleAnnotation = moduleAnnotations + .Where(annotation => annotation.AnnotationType == AnnotationType.IgnoreModule) + .OrderBy(annotation => annotation.Context.Start.TokenIndex) + .FirstOrDefault(); - public override void ExitCommentOrAnnotation([NotNull] VBAParser.CommentOrAnnotationContext context) + var rewriter = rewriteSession.CheckOutModuleRewriter(module); + + int insertionIndex; + string insertText; + + if (firstIgnoreModuleAnnotation == null) { - _contexts.Add(context); + insertionIndex = 0; + insertText = $"'@IgnoreModule {result.Inspection.AnnotationName}{Environment.NewLine}"; + rewriter.InsertBefore(insertionIndex, insertText); + return; } + + insertionIndex = firstIgnoreModuleAnnotation.Context.annotationName().Stop.TokenIndex; + insertText = $" {result.Inspection.AnnotationName},"; + rewriter.InsertAfter(insertionIndex, insertText); } + public override string Description(IInspectionResult result) => Resources.Inspections.QuickFixes.IgnoreOnce; + private class EndOfLineListener : VBAParserBaseListener { - private readonly IList _contexts = new List(); - public IEnumerable Contexts => _contexts; - - public override void ExitWhiteSpace([NotNull] VBAParser.WhiteSpaceContext context) - { - if (context.GetText().Contains(Environment.NewLine)) - { - _contexts.Add(context); - } - } + private readonly IList _contexts = new List(); + public IEnumerable Contexts => _contexts; public override void ExitEndOfLine([NotNull] VBAParser.EndOfLineContext context) { diff --git a/Rubberduck.Core/AddRemoveReferences/IReferenceInfo.cs b/Rubberduck.Core/AddRemoveReferences/IReferenceInfo.cs new file mode 100644 index 0000000000..c0e3de2dc5 --- /dev/null +++ b/Rubberduck.Core/AddRemoveReferences/IReferenceInfo.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Rubberduck.AddRemoveReferences +{ + public interface IReferenceInfo + { + Guid Guid { get; } + string Name { get; } + string FullPath { get; } + int Major { get; } + int Minor { get; } + int? Priority { get; } + } +} diff --git a/Rubberduck.Core/AddRemoveReferences/ReferenceModel.cs b/Rubberduck.Core/AddRemoveReferences/ReferenceModel.cs new file mode 100644 index 0000000000..9adbbcfa09 --- /dev/null +++ b/Rubberduck.Core/AddRemoveReferences/ReferenceModel.cs @@ -0,0 +1,127 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.ComTypes; +using Rubberduck.VBEditor.SafeComWrappers; +using Rubberduck.VBEditor.SafeComWrappers.Abstract; + +namespace Rubberduck.AddRemoveReferences +{ + public class ReferenceModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + public ReferenceModel(IVBProject project, int priority) + { + Name = project.Name ?? string.Empty; + Priority = priority; + Guid = Guid.Empty; + Description = project.Description ?? string.Empty; + FullPath = project.FileName ?? string.Empty; + IsBuiltIn = false; + Type = ReferenceKind.Project; + } + + public ReferenceModel(RegisteredLibraryInfo info) + { + Name = info.Name ?? string.Empty; + Guid = info.Guid; + Description = string.IsNullOrEmpty(info.Description) ? Path.GetFileNameWithoutExtension(info.FullPath) : info.Description; + Major = info.Major; + Minor = info.Minor; + FullPath = info.FullPath; + LocaleName = info.LocaleName; + IsBuiltIn = false; + Type = ReferenceKind.TypeLibrary; + Flags = info.Flags; + IsRegistered = true; + } + + public ReferenceModel(RegisteredLibraryInfo info, IReference reference, int priority) : this(info) + { + Priority = priority; + IsBuiltIn = reference.IsBuiltIn; + IsBroken = reference.IsBroken; + IsReferenced = true; + } + + public ReferenceModel(IReference reference, int priority) + { + Priority = priority; + Name = reference.Name; + Guid = new Guid(reference.Guid); + Description = string.IsNullOrEmpty(reference.Description) ? Path.GetFileNameWithoutExtension(reference.FullPath) : reference.Description; + Major = reference.Major; + Minor = reference.Minor; + FullPath = reference.FullPath; + IsBuiltIn = reference.IsBuiltIn; + IsBroken = reference.IsBroken; + IsReferenced = true; + Type = reference.Type; + } + + private bool _pinned; + public bool IsPinned + { + get => _pinned; + set + { + _pinned = value; + NotifyPropertyChanged(); + } + } + + public bool IsRecent { get; set; } + + public bool IsRegistered { get; set; } + public bool IsReferenced { get; set; } + public int? Priority { get; set; } + + public string Name { get; } + public Guid Guid { get; } + public string Description { get; } + public string FullPath { get; } + public string LocaleName { get; } + + public bool IsBuiltIn { get; } + public bool IsBroken { get; } + public LIBFLAGS Flags { get; } + public ReferenceKind Type { get; } + + private string FullPath32 { get; } + private string FullPath64 { get; } + public int Major { get; set; } + public int Minor { get; set; } + public string Version => $"{Major}.{Minor}"; + + public ReferenceStatus Status + { + get + { + var status = IsPinned ? ReferenceStatus.Pinned : ReferenceStatus.None; + if (!Priority.HasValue) + { + return IsRecent ? status | ReferenceStatus.Recent : status; + } + + if (IsBroken) + { + return status | ReferenceStatus.Broken; + } + + if (IsBuiltIn) + { + return status | ReferenceStatus.BuiltIn; + } + + return status | (IsReferenced ? ReferenceStatus.Loaded : ReferenceStatus.Added); + } + } + + private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} \ No newline at end of file diff --git a/Rubberduck.Core/AddRemoveReferences/ReferenceStatus.cs b/Rubberduck.Core/AddRemoveReferences/ReferenceStatus.cs new file mode 100644 index 0000000000..6e38f2ff2d --- /dev/null +++ b/Rubberduck.Core/AddRemoveReferences/ReferenceStatus.cs @@ -0,0 +1,16 @@ +using System; + +namespace Rubberduck.AddRemoveReferences +{ + [Flags] + public enum ReferenceStatus + { + None = 0, + BuiltIn = 1 << 1, + Loaded = 1 << 2, + Broken = 1 << 3, + Pinned = 1 << 4, + Recent = 1 << 5, + Added = 1 << 6 + } +} \ No newline at end of file diff --git a/Rubberduck.Core/AddRemoveReferences/RegisteredLibraryFinderService.cs b/Rubberduck.Core/AddRemoveReferences/RegisteredLibraryFinderService.cs new file mode 100644 index 0000000000..e1648e244a --- /dev/null +++ b/Rubberduck.Core/AddRemoveReferences/RegisteredLibraryFinderService.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.ComTypes; +using Microsoft.Win32; + +namespace Rubberduck.AddRemoveReferences +{ + public static class RegistryKeyExtensions + { + public static string GetKeyName(this RegistryKey key) + { + var name = key?.Name; + return name?.Substring(name.LastIndexOf(@"\", StringComparison.InvariantCultureIgnoreCase) + 1) ?? string.Empty; + } + } + + public interface IRegisteredLibraryFinderService + { + IEnumerable FindRegisteredLibraries(); + } + + // inspired from https://github.com/rossknudsen/Kavod.ComReflection + public class RegisteredLibraryFinderService : IRegisteredLibraryFinderService + { + private static readonly List IgnoredKeys = new List { "FLAGS", "HELPDIR" }; + + public IEnumerable FindRegisteredLibraries() + { + using (var typelibSubKey = Registry.ClassesRoot.OpenSubKey("TypeLib")) + { + if (typelibSubKey == null) { yield break; } + + foreach (var guidKey in EnumerateSubKeys(typelibSubKey)) + { + var guid = Guid.TryParseExact(guidKey.GetKeyName().ToLowerInvariant(), "B", out var clsid) + ? clsid + : Guid.Empty; + + foreach (var versionKey in EnumerateSubKeys(guidKey)) + { + var name = versionKey.GetValue(string.Empty)?.ToString(); + var version = versionKey.GetKeyName(); + + var flagValue = (LIBFLAGS)0; + using (var flagsKey = versionKey.OpenSubKey("FLAGS")) + { + if (flagsKey != null) + { + var flags = flagsKey.GetValue(string.Empty)?.ToString() ?? "0"; + Enum.TryParse(flags, out flagValue); + } + } + + foreach (var lcid in versionKey.GetSubKeyNames().Where(key => !IgnoredKeys.Contains(key))) + { + if (!int.TryParse(lcid, out var id)) + { + continue; + } + using (var paths = versionKey.OpenSubKey(lcid)) + { + string bit32; + string bit64; + using (var win32 = paths?.OpenSubKey("win32")) + { + bit32 = win32?.GetValue(string.Empty)?.ToString() ?? string.Empty; + } + using (var win64 = paths?.OpenSubKey("win64")) + { + bit64 = win64?.GetValue(string.Empty)?.ToString() ?? string.Empty; + } + + yield return new RegisteredLibraryInfo(guid, name, version, bit32, bit64) + { + Flags = flagValue, + LocaleId = id + }; + } + } + } + } + } + } + + private IEnumerable EnumerateSubKeys(RegistryKey key) + { + foreach (var keyName in key.GetSubKeyNames()) + { + using (var subKey = key.OpenSubKey(keyName)) + { + if (subKey != null) + { + yield return subKey; + } + } + } + } + } +} \ No newline at end of file diff --git a/Rubberduck.Core/AddRemoveReferences/RegisteredLibraryInfo.cs b/Rubberduck.Core/AddRemoveReferences/RegisteredLibraryInfo.cs new file mode 100644 index 0000000000..72d2967194 --- /dev/null +++ b/Rubberduck.Core/AddRemoveReferences/RegisteredLibraryInfo.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices.ComTypes; + +namespace Rubberduck.AddRemoveReferences +{ + public struct RegisteredLibraryKey + { + public Guid Guid { get; } + public int Major { get; } + public int Minor { get; } + + public RegisteredLibraryKey(Guid guid, int major, int minor) + { + Guid = guid; + Major = major; + Minor = minor; + } + } + + public class RegisteredLibraryInfo : IReferenceInfo + { + private static readonly Dictionary NativeLocaleNames = new Dictionary + { + { 0, "Standard" } + }; + + public RegisteredLibraryKey UniqueId { get; } + public string Name { get; set; } + public Guid Guid { get; set; } + public string Description { get; set; } + public string Version => $"{Major}.{Minor}"; + public string FullPath => string.IsNullOrEmpty(FullPath32) || Has64BitVersion && Environment.Is64BitProcess ? FullPath64 : FullPath32; + public int Major { get; set; } + public int Minor { get; set; } + public int LocaleId { get; set; } + + public string LocaleName + { + get + { + if (NativeLocaleNames.ContainsKey(LocaleId)) + { + return NativeLocaleNames[LocaleId]; + } + + try + { + var name = CultureInfo.GetCultureInfo(LocaleId).NativeName; + NativeLocaleNames.Add(LocaleId, name); + return name; + } + catch + { + NativeLocaleNames.Add(LocaleId, "Standard"); + return "Standard"; + } + } + } + + public LIBFLAGS Flags { get; set; } + + private string FullPath32 { get; } + private string FullPath64 { get; } + public bool Has32BitVersion => !string.IsNullOrEmpty(FullPath32); + public bool Has64BitVersion => !string.IsNullOrEmpty(FullPath64); + + public int? Priority => null; + + public RegisteredLibraryInfo(Guid guid, string description, string version, string path32, string path64) + { + Guid = guid; + + var majorMinor = version.Split('.'); + if (majorMinor.Length == 2) + { + Major = int.TryParse(majorMinor[0], NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var major) ? major : 0; + Minor = int.TryParse(majorMinor[1], NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var minor) ? minor : 0; + } + + FullPath32 = path32 ?? string.Empty; + FullPath64 = path64 ?? string.Empty; + + Description = !string.IsNullOrEmpty(description) ? description : Path.GetFileNameWithoutExtension(FullPath); + UniqueId = new RegisteredLibraryKey(guid, Major, Minor); + } + + public override string ToString() => Description; + } +} \ No newline at end of file diff --git a/Rubberduck.Core/App.cs b/Rubberduck.Core/App.cs index 98b08fbff5..783ccd3288 100644 --- a/Rubberduck.Core/App.cs +++ b/Rubberduck.Core/App.cs @@ -9,6 +9,7 @@ using System; using System.Diagnostics; using System.Globalization; +using System.Linq; using Rubberduck.Parsing.UIContext; using Rubberduck.Resources; using Rubberduck.UI.Command; @@ -182,24 +183,22 @@ private void ApplyCultureConfig() private static void LocalizeResources(CultureInfo culture) { - //TODO: this method needs something better - maybe use reflection to discover all resourcees - // to set culture for all resources files? - Resources.RubberduckUI.Culture = culture; - Resources.About.AboutUI.Culture = culture; - Resources.Inspections.InspectionInfo.Culture = culture; - Resources.Inspections.InspectionNames.Culture = culture; - Resources.Inspections.InspectionResults.Culture = culture; - Resources.Inspections.InspectionsUI.Culture = culture; - Resources.Inspections.QuickFixes.Culture = culture; - Resources.Menus.RubberduckMenus.Culture = culture; - Resources.RegexAssistant.RegexAssistantUI.Culture = culture; - Resources.Settings.SettingsUI.Culture = culture; - Resources.Settings.ToDoExplorerPage.Culture = culture; - Resources.Settings.UnitTestingPage.Culture = culture; - Resources.ToDoExplorer.ToDoExplorerUI.Culture = culture; - Resources.UnitTesting.AssertMessages.Culture = culture; - Resources.UnitTesting.TestExplorer.Culture = culture; - Resources.Templates.Culture = culture; + var localizers = AppDomain.CurrentDomain.GetAssemblies() + .SingleOrDefault(assembly => assembly.GetName().Name == "Rubberduck.Resources") + ?.DefinedTypes.SelectMany(type => type.DeclaredProperties.Where(prop => + prop.CanWrite && prop.Name.Equals("Culture") && prop.PropertyType == typeof(CultureInfo) && + (prop.SetMethod?.IsStatic ?? false))); + + if (localizers == null) + { + return; + } + + var args = new object[] { culture }; + foreach (var localizer in localizers) + { + localizer.SetMethod.Invoke(null, args); + } } private void CheckForLegacyIndenterSettings() @@ -230,14 +229,26 @@ public void LogRubberduckStart() { var version = _version.CurrentVersion; GlobalDiagnosticsContext.Set("RubberduckVersion", version.ToString()); + var headers = new List { $"\r\n\tRubberduck version {version} loading:", - $"\tOperating System: {Environment.OSVersion.VersionString} {(Environment.Is64BitOperatingSystem ? "x64" : "x86")}", - $"\tHost Product: {Application.ProductName} {(Environment.Is64BitProcess ? "x64" : "x86")}", - $"\tHost Version: {Application.ProductVersion}", - $"\tHost Executable: {Path.GetFileName(Application.ExecutablePath).ToUpper()}", // .ToUpper() used to convert ExceL.EXE -> EXCEL.EXE + $"\tOperating System: {Environment.OSVersion.VersionString} {(Environment.Is64BitOperatingSystem ? "x64" : "x86")}" }; + try + { + headers.AddRange(new [] + { + $"\tHost Product: {Application.ProductName} {(Environment.Is64BitProcess ? "x64" : "x86")}", + $"\tHost Version: {Application.ProductVersion}", + $"\tHost Executable: {Path.GetFileName(Application.ExecutablePath).ToUpper()}", // .ToUpper() used to convert ExceL.EXE -> EXCEL.EXE + }); + } + catch + { + headers.Add("\tHost could not be determined."); + } + LogLevelHelper.SetDebugInfo(string.Join(Environment.NewLine, headers)); } diff --git a/Rubberduck.Core/CodeAnalysis/CodeMetrics/CodeMetricsViewModel.cs b/Rubberduck.Core/CodeAnalysis/CodeMetrics/CodeMetricsViewModel.cs index 954ea99a66..d2e836d6b9 100644 --- a/Rubberduck.Core/CodeAnalysis/CodeMetrics/CodeMetricsViewModel.cs +++ b/Rubberduck.Core/CodeAnalysis/CodeMetrics/CodeMetricsViewModel.cs @@ -65,13 +65,9 @@ private void UpdateData() .GroupBy(declaration => declaration.ProjectId) .ToList(); - if (userDeclarations.Any( - grouping => grouping.All(declaration => declaration.DeclarationType != DeclarationType.Project))) - { - return; - } - - var newProjects = userDeclarations.Select(grouping => + var newProjects = userDeclarations + .Where(grouping => grouping.Any(declaration => declaration.DeclarationType == DeclarationType.Project)) + .Select(grouping => new CodeExplorerProjectViewModel(_folderHelper, grouping.SingleOrDefault(declaration => declaration.DeclarationType == DeclarationType.Project), grouping, diff --git a/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerItemViewModel.cs b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerItemViewModel.cs index 783f555e33..87a8566d51 100644 --- a/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerItemViewModel.cs +++ b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerItemViewModel.cs @@ -143,7 +143,21 @@ public override int Compare(CodeExplorerItemViewModel x, CodeExplorerItemViewMod return 0; } - // folders come first + // references come first + if (x is CodeExplorerReferenceFolderViewModel ^ + y is CodeExplorerReferenceFolderViewModel) + { + return x is CodeExplorerReferenceFolderViewModel ? -1 : 1; + } + + // references always sort by priority + if (x is CodeExplorerReferenceViewModel first && + y is CodeExplorerReferenceViewModel second) + { + return first.Priority > second.Priority ? 1 : - 1; + } + + // folders come next if (x is CodeExplorerCustomFolderViewModel ^ y is CodeExplorerCustomFolderViewModel) { diff --git a/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerProjectViewModel.cs b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerProjectViewModel.cs index f7fec93465..7e1b844aa1 100644 --- a/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerProjectViewModel.cs +++ b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerProjectViewModel.cs @@ -27,7 +27,7 @@ public class CodeExplorerProjectViewModel : CodeExplorerItemViewModel, ICodeExpl DeclarationType.UserForm, }; - public CodeExplorerProjectViewModel(FolderHelper folderHelper, Declaration declaration, IEnumerable declarations, IVBE vbe) + public CodeExplorerProjectViewModel(FolderHelper folderHelper, Declaration declaration, IEnumerable declarations, IVBE vbe, bool references = false) { Declaration = declaration; _name = Declaration.IdentifierName; @@ -37,8 +37,14 @@ public CodeExplorerProjectViewModel(FolderHelper folderHelper, Declaration decla try { + Items = new List(); + if (references) + { + Items.Add(new CodeExplorerReferenceFolderViewModel(this)); + } + FillFolders(declarations.ToList()); - Items = _folderTree.Items.ToList(); + Items.AddRange(_folderTree.Items); _icon = Declaration.Project?.Protection == ProjectProtection.Locked ? GetImageSource(resx.lock__exclamation) diff --git a/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerReferenceFolderViewModel.cs b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerReferenceFolderViewModel.cs new file mode 100644 index 0000000000..0ebbbd8244 --- /dev/null +++ b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerReferenceFolderViewModel.cs @@ -0,0 +1,45 @@ +using System.Windows.Media.Imaging; +using Rubberduck.AddRemoveReferences; +using Rubberduck.VBEditor; + +namespace Rubberduck.Navigation.CodeExplorer +{ + public class CodeExplorerReferenceFolderViewModel : CodeExplorerItemViewModel + { + private readonly CodeExplorerProjectViewModel _parent; + + public CodeExplorerReferenceFolderViewModel(CodeExplorerProjectViewModel parent) + { + _parent = parent; + CollapsedIcon = GetImageSource(Resources.CodeExplorer.CodeExplorerUI.ObjectAssembly); + ExpandedIcon = GetImageSource(Resources.CodeExplorer.CodeExplorerUI.ObjectAssembly); + AddReferenceNodes(); + } + + public override string Name => "References"; + public override string NameWithSignature => "References"; + public override BitmapImage CollapsedIcon { get; } + public override BitmapImage ExpandedIcon { get; } + public override CodeExplorerItemViewModel Parent => _parent; + public override QualifiedSelection? QualifiedSelection => null; + + private void AddReferenceNodes() + { + var project = _parent?.Declaration?.Project; + if (project == null) + { + return; + } + + using (var references = project.References) + { + var priority = 1; + foreach (var reference in references) + { + AddChild(new CodeExplorerReferenceViewModel(this, new ReferenceModel(reference, priority++))); + reference.Dispose(); + } + } + } + } +} diff --git a/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerReferenceViewModel.cs b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerReferenceViewModel.cs new file mode 100644 index 0000000000..1ec0a41eaa --- /dev/null +++ b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerReferenceViewModel.cs @@ -0,0 +1,40 @@ +using System; +using System.Windows.Media.Imaging; +using Rubberduck.AddRemoveReferences; +using Rubberduck.Resources.CodeExplorer; +using Rubberduck.VBEditor; + +namespace Rubberduck.Navigation.CodeExplorer +{ + public class CodeExplorerReferenceViewModel : CodeExplorerItemViewModel + { + private readonly ReferenceModel _reference; + + public CodeExplorerReferenceViewModel(CodeExplorerReferenceFolderViewModel parent, ReferenceModel reference) + { + Parent = parent; + _reference = reference; + } + + public override string NameWithSignature => $"{_reference.Name} ({_reference.Version})"; + public override string Name => _reference.Description + Environment.NewLine + _reference.FullPath; + public override CodeExplorerItemViewModel Parent { get; } + public override QualifiedSelection? QualifiedSelection => null; + + public override BitmapImage CollapsedIcon => GetIcon(); + public override BitmapImage ExpandedIcon => GetIcon(); + + public int? Priority => _reference.Priority; + public bool Locked => _reference.IsBuiltIn; + + private BitmapImage GetIcon() + { + if (_reference.Status.HasFlag(ReferenceStatus.Broken)) + { + GetImageSource(CodeExplorerUI.BrokenReference); + } + + return _reference.IsBuiltIn ? GetImageSource(CodeExplorerUI.LockedReference) : GetImageSource(CodeExplorerUI.Reference); + } + } +} diff --git a/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerViewModel.cs b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerViewModel.cs index b06b8802e8..255b4aff58 100644 --- a/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerViewModel.cs +++ b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerViewModel.cs @@ -317,7 +317,8 @@ private void HandleStateChanged(object sender, ParserStateEventArgs e) new CodeExplorerProjectViewModel(_folderHelper, grouping.SingleOrDefault(declaration => declaration.DeclarationType == DeclarationType.Project), grouping, - _vbe)).ToList(); + _vbe, + true)).ToList(); UpdateNodes(Projects, newProjects); @@ -546,7 +547,7 @@ public double FontSize public AddTestModuleCommand AddTestModuleCommand { get; set; } public AddTestModuleWithStubsCommand AddTestModuleWithStubsCommand { get; set; } public AddTemplateCommand AddTemplateCommand { get; set; } - public CommandBase OpenDesignerCommand { get; set; } + public OpenDesignerCommand OpenDesignerCommand { get; set; } public CommandBase OpenProjectPropertiesCommand { get; set; } public SetAsStartupProjectCommand SetAsStartupProjectCommand { get; set; } public RenameCommand RenameCommand { get; set; } @@ -560,6 +561,7 @@ public double FontSize public ExportAllCommand ExportAllCommand { get; set; } public CommandBase RemoveCommand { get; } public PrintCommand PrintCommand { get; set; } + public AddRemoveReferencesCommand AddRemoveReferencesCommand { get; set; } private readonly RemoveCommand _externalRemoveCommand; diff --git a/Rubberduck.Core/Rubberduck.Core.csproj b/Rubberduck.Core/Rubberduck.Core.csproj index 166fabea08..6f321fa208 100644 --- a/Rubberduck.Core/Rubberduck.Core.csproj +++ b/Rubberduck.Core/Rubberduck.Core.csproj @@ -65,6 +65,7 @@ 2.7.6684 + 1.8.4 diff --git a/Rubberduck.Core/Settings/GeneralSettings.cs b/Rubberduck.Core/Settings/GeneralSettings.cs index 8446eed04c..a16d961004 100644 --- a/Rubberduck.Core/Settings/GeneralSettings.cs +++ b/Rubberduck.Core/Settings/GeneralSettings.cs @@ -19,6 +19,9 @@ public interface IGeneralSettings bool UserEditedLogLevel { get; set; } int MinimumLogLevel { get; set; } List EnableExperimentalFeatures { get; set; } + int RecentReferencesTracked { get; set; } + List RecentReferences { get; set; } + List PinnedReferences { get; set; } } [SettingsSerializeAs(SettingsSerializeAs.Xml)] @@ -58,6 +61,10 @@ public int MinimumLogLevel public List EnableExperimentalFeatures { get; set; } = new List(); + public int RecentReferencesTracked { get; set; } + public List RecentReferences { get; set; } + public List PinnedReferences { get; set; } + public GeneralSettings() { //Enforce non-default default value for members @@ -78,8 +85,11 @@ public bool Equals(GeneralSettings other) AutoSavePeriod == other.AutoSavePeriod && UserEditedLogLevel == other.UserEditedLogLevel && MinimumLogLevel == other.MinimumLogLevel && - EnableExperimentalFeatures.All(a => other.EnableExperimentalFeatures.Contains(a)) && - EnableExperimentalFeatures.Count == other.EnableExperimentalFeatures.Count; + RecentReferencesTracked == other.RecentReferencesTracked && + EnableExperimentalFeatures.Count == other.EnableExperimentalFeatures.Count && + EnableExperimentalFeatures.All(other.EnableExperimentalFeatures.Contains) && + RecentReferences.SequenceEqual(other.RecentReferences, StringComparer.OrdinalIgnoreCase) && + PinnedReferences.OrderBy(x => x).SequenceEqual(other.PinnedReferences.OrderBy(x => x), StringComparer.OrdinalIgnoreCase); } } } \ No newline at end of file diff --git a/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesDialog.Designer.cs b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesDialog.Designer.cs new file mode 100644 index 0000000000..1bebbe5c2b --- /dev/null +++ b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesDialog.Designer.cs @@ -0,0 +1,67 @@ +namespace Rubberduck.UI.AddRemoveReferences +{ + partial class AddRemoveReferencesDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AddRemoveReferencesDialog)); + this.elementHost1 = new System.Windows.Forms.Integration.ElementHost(); + this.addRemoveReferencesWindow1 = new Rubberduck.UI.AddRemoveReferences.AddRemoveReferencesWindow(); + this.SuspendLayout(); + // + // elementHost1 + // + this.elementHost1.Dock = System.Windows.Forms.DockStyle.Fill; + this.elementHost1.Location = new System.Drawing.Point(0, 0); + this.elementHost1.Name = "elementHost1"; + this.elementHost1.Size = new System.Drawing.Size(800, 550); + this.elementHost1.TabIndex = 0; + this.elementHost1.Text = "elementHost1"; + this.elementHost1.Child = this.addRemoveReferencesWindow1; + // + // AddRemoveReferencesDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 550); + this.Controls.Add(this.elementHost1); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.Name = "AddRemoveReferencesDialog"; + this.ShowInTaskbar = false; + this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Show; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Rubberduck - Add/Remove References"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Integration.ElementHost elementHost1; + private AddRemoveReferencesWindow addRemoveReferencesWindow1; + } +} \ No newline at end of file diff --git a/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesDialog.cs b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesDialog.cs new file mode 100644 index 0000000000..1cf105c7ab --- /dev/null +++ b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesDialog.cs @@ -0,0 +1,21 @@ +using System.Windows.Forms; +using Rubberduck.UI.Refactorings; + +namespace Rubberduck.UI.AddRemoveReferences +{ + public partial class AddRemoveReferencesDialog : Form, IRefactoringDialog + { + public AddRemoveReferencesViewModel ViewModel { get; } + + public AddRemoveReferencesDialog() + { + InitializeComponent(); + } + + public AddRemoveReferencesDialog(AddRemoveReferencesViewModel viewModel) : this() + { + ViewModel = viewModel; + addRemoveReferencesWindow1.DataContext = viewModel; + } + } +} diff --git a/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesDialog.resx b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesDialog.resx new file mode 100644 index 0000000000..0a0a5023e2 --- /dev/null +++ b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesDialog.resx @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAEAAAAAAAAABwAAAAgAAAAGAAAABwAAAAgAAAADAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAABAwNEAAAA/zpkZP8uXmP/LGBl/y9hY/80ZGP/OWlo/0t2dP8AAAD/AAAAxwEC + AogAAAAAAAAAAAAAAAAAAQGTCicr/zTk+f9U3v//RtX//0rV//9P3f//Ve3//y/p//855/v/Qv3//1T/ + //8MLS/+AAAA0QAAAAAAAQGRBBMW/zXn/f846v//SNz9/0jL/v8xvv//OMv//0DU//8/3v3/L+j//zTo + //8r4/z/Svr//0mAf/4BAgKIISko/ynQ5v8x6P//Men//y7Z7v9ApLn/LrH4/y+y9f84vfT/K57K/y/l + +v8x5///L+j//0v2//+M////AAAAihIsLf8w7f//MOr//zXq//8x6f//JK6+/yeIp/8gZoL/LYyq/ynF + 2f8w5/7/MOj//zTq//9K9P//g////wAAAJUAAADSROX//1LK//9E5f//OO7//zLr//8w4ff/L+X7/y/j + +v8x6P//N+z//zHo//8s5v//gv3//7j///4AAACcAAAAlD+Cof87c4z/RGNj/zRdY/8xoK7+LfX//xrm + //8c2v//IeD//zDx//9G+P//Vvr//9z///8kKir/AAAAAAAAAFYAAABtAAAAGgAAAAgAAAALAAAA2yTb + +v8Y0f//HNr//xzf//8z+///Yv///2y8vP4qMjL/BAUFMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3Cac + r/8t0/7/Icn//x7Y//8o+P7/Sv///wAAAP8AAACgAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAEwwh + I/873P//JMj//xS8//8Wyv//Gpi2/wIADv8LCgC5AAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + ACUqYWT/Mtb//xy6//8Gt/7/Htr//xlhbP8fRvX/O1Hs/jlCctsAAAADAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAaSGpo/0vZ//4Zs///n+z//wAAAP9FVFz/E0Gh/z1G//9AS2j/AAAAAwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAABAAAAKiS8/7MOr3//pLc/v/v////Pmxs/zBBPcIAAAD/ISEv/wAAAAQAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAABNAAAAtqXk8shM0v//Ppac/wAAAP8AAAA2AAAAAAAAAAcAAAABAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAACfAAAAlAAAAIAAAAA3AAAAAQAAAAAAAAAAAAAAAAAA + AAAAAAAA//8AAOADAACAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAD4AwAA8AcAAPAPAADwBwAA8AcAAPAH + AAD4PwAA/H8AAA== + + + \ No newline at end of file diff --git a/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesModel.cs b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesModel.cs new file mode 100644 index 0000000000..c798e69ea3 --- /dev/null +++ b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesModel.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using Rubberduck.AddRemoveReferences; +using Rubberduck.Parsing.Symbols; +using Rubberduck.Settings; + +namespace Rubberduck.UI.AddRemoveReferences +{ + public interface IAddRemoveReferencesModel + { + IGeneralSettings Settings { get; set; } + ProjectDeclaration Project { get; set; } + List References { get; set; } + IReadOnlyList NewReferences { get; set; } + } + + public class AddRemoveReferencesModel : IAddRemoveReferencesModel + { + public AddRemoveReferencesModel(ProjectDeclaration project, IEnumerable references, IGeneralSettings settings) + { + Settings = settings; + Project = project; + References = references.ToList(); + } + + public IGeneralSettings Settings { get; set; } + + public ProjectDeclaration Project { get; set; } + + public List References { get; set; } + + public IReadOnlyList NewReferences { get; set; } + } +} diff --git a/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesPresenter.cs b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesPresenter.cs new file mode 100644 index 0000000000..73ee20e69d --- /dev/null +++ b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesPresenter.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using Rubberduck.AddRemoveReferences; +using Rubberduck.Parsing.Symbols; +using Rubberduck.UI.Refactorings; + +namespace Rubberduck.UI.AddRemoveReferences +{ + public interface IAddRemoveReferencesPresenter + { + IAddRemoveReferencesModel Show(); + IAddRemoveReferencesModel Show(ProjectDeclaration project); + IAddRemoveReferencesModel Model { get; } + } + + public class AddRemoveReferencesPresenter : IAddRemoveReferencesPresenter + { + private readonly IRefactoringDialog _view; + + public AddRemoveReferencesPresenter(IRefactoringDialog view) + { + _view = view; + Model = _view.ViewModel.Model; + } + + public IAddRemoveReferencesModel Show() + { + return Model.Project == null ? null : Show(Model.Project); + } + + public IAddRemoveReferencesModel Show(ProjectDeclaration project) + { + if (project is null) + { + return null; + } + + Model.Project = project; + _view.ViewModel.Model = Model; + + _view.ShowDialog(); + + Model.NewReferences = _view.ViewModel.ProjectReferences.SourceCollection.OfType().ToList(); + return Model; + } + + public IAddRemoveReferencesModel Model { get; } + } +} diff --git a/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesPresenterFactory.cs b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesPresenterFactory.cs new file mode 100644 index 0000000000..5fe334ad0a --- /dev/null +++ b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesPresenterFactory.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using Rubberduck.AddRemoveReferences; +using Rubberduck.Interaction; +using Rubberduck.Parsing.Symbols; +using Rubberduck.Parsing.VBA; +using Rubberduck.Refactorings; +using Rubberduck.Settings; +using Rubberduck.SettingsProvider; +using Rubberduck.VBEditor.SafeComWrappers.Abstract; + +namespace Rubberduck.UI.AddRemoveReferences +{ + public interface IAddRemoveReferencesPresenterFactory : IRefactoringPresenterFactory + { + AddRemoveReferencesPresenter Create(ProjectDeclaration project); + } + + public class AddRemoveReferencesPresenterFactory : IAddRemoveReferencesPresenterFactory + { + private readonly Logger _logger = LogManager.GetCurrentClassLogger(); + private readonly bool _use64BitPaths = Environment.Is64BitProcess; + + private readonly IVBE _vbe; + private readonly RubberduckParserState _state; + private readonly IConfigProvider _settings; + private readonly IRegisteredLibraryFinderService _finder; + private readonly IMessageBox _messageBox; + + public AddRemoveReferencesPresenterFactory(IVBE vbe, + RubberduckParserState state, + IConfigProvider generalSettingsProvider, + IRegisteredLibraryFinderService finder, + IMessageBox messageBox) + { + _vbe = vbe; + _state = state; + _settings = generalSettingsProvider; + _finder = finder; + _messageBox = messageBox; + } + + public AddRemoveReferencesPresenter Create(ProjectDeclaration project) + { + if (project is null) + { + return null; + } + + var refs = new Dictionary(); + // Iterating the returned libraries here instead of just .ToDictionary() using because we can't trust that the registry doesn't contain errors. + foreach (var reference in _finder.FindRegisteredLibraries()) + { + if (refs.ContainsKey(reference.UniqueId)) + { + _logger.Warn($"Duplicate registry definition for {reference.Guid} version {reference.Version}."); + continue; + } + refs.Add(reference.UniqueId, reference); + } + + var models = new Dictionary(); + using (var references = project.Project?.References) + { + if (references is null) + { + return null; + } + var priority = 1; + foreach (var reference in references) + { + var libraryId = new RegisteredLibraryKey(new Guid(reference.Guid), reference.Major, reference.Minor); + if (refs.ContainsKey(libraryId)) + { + // TODO: If for some reason the VBA reference is broken, we could technically use this to repair it. Just a thought... + models.Add(libraryId, new ReferenceModel(refs[libraryId], reference, priority++)); + } + else // These should all be either VBA projects or irreparably broken. + { + models.Add(libraryId, new ReferenceModel(reference, priority++)); + } + reference.Dispose(); + } + } + + foreach (var reference in refs.Where(library => + (_use64BitPaths || library.Value.Has32BitVersion) && + !models.ContainsKey(library.Key))) + { + models.Add(reference.Key, new ReferenceModel(reference.Value)); + } + + var settings = _settings.Create(); + var model = new AddRemoveReferencesModel(project, models.Values, settings); + + return new AddRemoveReferencesPresenter(new AddRemoveReferencesDialog(new AddRemoveReferencesViewModel(model, _messageBox))); + } + + public AddRemoveReferencesPresenter Create() + { + using (var pane = _vbe.ActiveCodePane) + { + var selected = (ProjectDeclaration)Declaration.GetProjectParent(_state.DeclarationFinder.FindSelectedDeclaration(pane)); + return selected is null ? null : Create(selected); + } + } + } +} diff --git a/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesViewModel.cs b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesViewModel.cs new file mode 100644 index 0000000000..b2960d2278 --- /dev/null +++ b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesViewModel.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows.Input; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Windows.Data; +using NLog; +using Rubberduck.AddRemoveReferences; +using Rubberduck.Interaction; +using Rubberduck.Resources; +using Rubberduck.UI.Command; +using Rubberduck.VBEditor.SafeComWrappers; + +namespace Rubberduck.UI.AddRemoveReferences +{ + public enum ReferenceFilter + { + Recent, + Pinned, + ComTypes, + Projects + } + + public class AddRemoveReferencesViewModel : ViewModelBase + { + private readonly IMessageBox _messageBox; + + private readonly ObservableCollection _available; + private readonly ObservableCollection _project; + + public AddRemoveReferencesViewModel(IAddRemoveReferencesModel model, IMessageBox messageBox) + { + Model = model; + _messageBox = messageBox; + + foreach (var reference in model.References + .Where(item => Model.Settings.PinnedReferences + .Contains(item.Type == ReferenceKind.TypeLibrary ? item.Guid.ToString() : item.FullPath))) + { + reference.IsPinned = true; + } + + foreach (var reference in model.References + .Where(item => Model.Settings.RecentReferences + .Contains(item.Type == ReferenceKind.TypeLibrary ? item.Guid.ToString() : item.FullPath))) + { + reference.IsRecent = true; + } + + _available = new ObservableCollection(model.References + .Where(reference => !reference.IsReferenced).OrderBy(reference => reference.Description)); + _project = new ObservableCollection(model.References + .Where(reference => reference.IsReferenced).OrderBy(reference => reference.Priority)); + + AddCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecuteAddCommand); + RemoveCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecuteRemoveCommand); + BrowseCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecuteBrowseCommand); + MoveUpCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecuteMoveUpCommand); + MoveDownCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecuteMoveDownCommand); + PinLibraryCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecutePinLibraryCommand); + PinReferenceCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecutePinReferenceCommand); + } + + public IAddRemoveReferencesModel Model { get; set; } + + public ICommand AddCommand { get; } + + public ICommand RemoveCommand { get; } + /// + /// Prompts user for a .tlb, .dll, or .ocx file, and attempts to append it to . + /// + public ICommand BrowseCommand { get; } + + /// + /// Applies all changes to project references. + /// + public ICommand ApplyCommand { get; } + + /// + /// Moves the up on the 'Priority' tab. + /// + public ICommand MoveUpCommand { get; } + + /// + /// Moves the down on the 'Priority' tab. + /// + public ICommand MoveDownCommand { get; } + + public ICommand PinLibraryCommand { get; } + + public ICommand PinReferenceCommand { get; } + + private void ExecuteAddCommand(object parameter) + { + if (SelectedLibrary == null) + { + return; + } + + SelectedLibrary.Priority = _project.Count + 1; + _project.Add(SelectedLibrary); + ProjectReferences.Refresh(); + _available.Remove(SelectedLibrary); + } + + private void ExecuteRemoveCommand(object parameter) + { + if (SelectedReference == null) + { + return; + } + + var priority = SelectedReference.Priority; + SelectedReference.Priority = null; + _available.Add(SelectedReference); + _project.Remove(SelectedReference); + + foreach (var reference in _project.Where(lib => lib.Priority > priority).ToList()) + { + reference.Priority--; + } + ProjectReferences.Refresh(); + } + + private static readonly List FileFilters = new List + { + RubberduckUI.References_BrowseFilterExecutable, + RubberduckUI.References_BrowseFilterExcel, + RubberduckUI.References_BrowseFilterTypes, + RubberduckUI.References_BrowseFilterActiveX, + RubberduckUI.References_BrowseFilterAllFiles, + }; + + private void ExecuteBrowseCommand(object parameter) + { + using (var dialog = new OpenFileDialog + { + Filter = string.Join("|", FileFilters), + Title = RubberduckUI.References_BrowseCaption + }) + { + dialog.ShowDialog(); + if (string.IsNullOrEmpty(dialog.FileName)) + { + return; + } + + var existing = _available.FirstOrDefault(library => + library.FullPath.Equals(dialog.FileName, StringComparison.OrdinalIgnoreCase)); + + var project = Model.Project.Project; + using (var references = project.References) + { + try + { + using (var reference = references.AddFromFile(dialog.FileName)) + { + if (reference is null) + { + return; + } + + _project.Add(existing ?? new ReferenceModel(reference, _project.Count + 1)); + ProjectReferences.Refresh(); + if (existing is null) + { + return; + } + + existing.Priority = _project.Count + 1; + _available.Remove(existing); + AvailableReferences.Refresh(); + } + } + catch (COMException ex) + { + _messageBox.NotifyWarn(ex.Message, RubberduckUI.References_AddFailedCaption); + } + } + } + } + + private void ExecuteMoveUpCommand(object parameter) + { + if (SelectedReference == null || SelectedReference.IsBuiltIn || SelectedReference.Priority == 1) + { + return; + } + + var swap = _project.SingleOrDefault(reference => reference.Priority == SelectedReference.Priority - 1); + + if (swap is null || swap.IsBuiltIn) + { + return; + } + + swap.Priority = SelectedReference.Priority; + SelectedReference.Priority--; + ProjectReferences.Refresh(); + } + + private void ExecuteMoveDownCommand(object parameter) + { + if (SelectedReference == null || SelectedReference.IsBuiltIn || SelectedReference.Priority == _project.Count) + { + return; + } + + var swap = _project.SingleOrDefault(reference => reference.Priority == SelectedReference.Priority + 1); + + if (swap is null || swap.IsBuiltIn) + { + return; + } + + swap.Priority = SelectedReference.Priority; + SelectedReference.Priority++; + ProjectReferences.Refresh(); + } + + private void ExecutePinLibraryCommand(object parameter) + { + if (SelectedLibrary == null) + { + return; + } + SelectedLibrary.IsPinned = !SelectedLibrary.IsPinned; + AvailableReferences.Refresh(); + } + + private void ExecutePinReferenceCommand(object parameter) + { + if (SelectedReference == null || SelectedReference.IsBuiltIn) + { + return; + } + SelectedReference.IsPinned = !SelectedReference.IsPinned; + ProjectReferences.Refresh(); + } + + public ICollectionView ProjectReferences + { + get + { + var projects = CollectionViewSource.GetDefaultView(_project); + projects.SortDescriptions.Add(new SortDescription("Priority", ListSortDirection.Ascending)); + return projects; + } + } + + public ICollectionView AvailableReferences + { + get + { + var available = CollectionViewSource.GetDefaultView(_available); + available.Filter = reference => Filter((ReferenceModel)reference); + return available; + } + } + + private string _filter; + public string SelectedFilter + { + get => _filter; + set + { + _filter = value; + AvailableReferences.Refresh(); + } + } + + private bool Filter(ReferenceModel reference) + { + var filtered = false; + Enum.TryParse(SelectedFilter, out var filter); + switch (filter) + { + case ReferenceFilter.Recent: + filtered = reference.IsRecent; + break; + case ReferenceFilter.Pinned: + filtered = reference.IsPinned; + break; + case ReferenceFilter.ComTypes: + filtered = reference.Type == ReferenceKind.TypeLibrary; + break; + case ReferenceFilter.Projects: + filtered = reference.Type == ReferenceKind.Project; + break; + } + + var searched = string.IsNullOrEmpty(Search) + || reference.Name.IndexOf(Search, StringComparison.OrdinalIgnoreCase) >= 0 + || reference.Description.IndexOf(Search, StringComparison.OrdinalIgnoreCase) >= 0 + || reference.FullPath.IndexOf(Search, StringComparison.OrdinalIgnoreCase) >= 0; + + return filtered && searched; + } + + private string _search = string.Empty; + public string Search + { + get => _search; + set + { + _search = value; + AvailableReferences.Refresh(); + } + } + + private ReferenceModel _selection; + public ReferenceModel CurrentSelection + { + get => _selection; + set + { + _selection = value; + OnPropertyChanged(); + } + } + + private ReferenceModel _reference; + public ReferenceModel SelectedReference + { + get => _reference; + set + { + _reference = value; + CurrentSelection = _reference; + } + } + + private ReferenceModel _library; + public ReferenceModel SelectedLibrary + { + get => _library; + set + { + _library = value; + CurrentSelection = _library; + } + } + } +} diff --git a/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesWindow.xaml b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesWindow.xaml new file mode 100644 index 0000000000..00bbb46441 --- /dev/null +++ b/Rubberduck.Core/UI/AddRemoveReferences/AddRemoveReferencesWindow.xaml @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +