diff --git a/.gitignore b/.gitignore index 8eb34baec0..30969a1743 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.sln.docstates *.csproj.user *.csproj.DotSettings +launchSettings.json # External NuGet Packages [Pp]ackages/ diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteBase.cs b/Rubberduck.Core/AutoComplete/AutoCompleteBase.cs deleted file mode 100644 index c294d3721f..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteBase.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Rubberduck.Parsing.VBA; -using Rubberduck.Settings; -using Rubberduck.VBEditor; -using Rubberduck.VBEditor.Events; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; - -namespace Rubberduck.AutoComplete -{ - - public abstract class AutoCompleteBase : IAutoComplete - { - protected AutoCompleteBase(string inputToken, string outputToken) - { - InputToken = inputToken; - OutputToken = outputToken; - } - - public bool IsInlineCharCompletion => InputToken.Length == 1 && OutputToken.Length == 1; - public bool IsEnabled { get; set; } - public string InputToken { get; } - public string OutputToken { get; } - - public virtual bool Execute(AutoCompleteEventArgs e, AutoCompleteSettings settings) - { - var input = e.Character.ToString(); - if (!IsMatch(input)) - { - return false; - } - - var module = e.CodeModule; - using (var pane = module.CodePane) - { - var pSelection = pane.Selection; - var zSelection = pSelection.ToZeroBased(); - - var original = module.GetLines(pSelection); - var nextChar = zSelection.StartColumn == original.Length ? string.Empty : original.Substring(zSelection.StartColumn, 1); - if (input == InputToken && (input != OutputToken || nextChar != OutputToken)) - { - string code; - if (!StripExplicitCallStatement(ref original, ref pSelection)) - { - code = original.Insert(Math.Max(0, zSelection.StartColumn), InputToken + OutputToken); - } - else - { - code = original; - } - module.ReplaceLine(pSelection.StartLine, code); - - var newCode = module.GetLines(pSelection); - if (newCode.Equals(code, StringComparison.OrdinalIgnoreCase)) - { - pane.Selection = new Selection(pSelection.StartLine, pSelection.StartColumn + 1); - } - else - { - // VBE added a space; need to compensate: - pane.Selection = new Selection(pSelection.StartLine, GetPrettifiedCaretPosition(pSelection, code, newCode)); - } - e.Handled = true; - return true; - } - else if (input == OutputToken && nextChar == OutputToken) - { - // just move caret one character to the right & suppress the keypress - pane.Selection = new Selection(pSelection.StartLine, GetPrettifiedCaretPosition(pSelection, original, original) + 1); - e.Handled = true; - return true; - } - return false; - } - } - - private bool StripExplicitCallStatement(ref string code, ref Selection pSelection) - { - // VBE will "helpfully" strip empty parentheses in 'Call Something()' - // ...and there's no way around it. since Call statement is optional and obsolete, - // this function strips it - var pattern = @"\bCall\b\s+"; - if (Regex.IsMatch(code, pattern, RegexOptions.IgnoreCase)) - { - pSelection = new Selection(pSelection.StartLine, pSelection.StartColumn - "Call ".Length); - code = Regex.Replace(code, pattern, string.Empty, RegexOptions.IgnoreCase); - return true; - } - return false; - } - - private int GetPrettifiedCaretPosition(Selection pSelection, string insertedCode, string prettifiedCode) - { - var zSelection = pSelection.ToZeroBased(); - - var outputTokenIndices = new List(); - for (int i = 0; i < insertedCode.Length; i++) - { - var character = insertedCode[i].ToString(); - if (character == OutputToken) - { - outputTokenIndices.Add(i); - } - } - if (!outputTokenIndices.Any()) - { - return pSelection.EndColumn; - } - var firstAfterCaret = outputTokenIndices.Where(i => i > zSelection.StartColumn).Min(); - - var prettifiedTokenIndices = new List(); - for (int i = 0; i < prettifiedCode.Length; i++) - { - var character = prettifiedCode[i].ToString(); - if (character == OutputToken) - { - prettifiedTokenIndices.Add(i); - } - } - - return prettifiedTokenIndices.Any() - ? prettifiedTokenIndices[outputTokenIndices.IndexOf(firstAfterCaret)] + 1 - : prettifiedCode.Length + 2; - } - - public virtual bool IsMatch(string input) => - (IsInlineCharCompletion && !string.IsNullOrEmpty(input) && (input == InputToken || input == OutputToken)); - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteBlockBase.cs b/Rubberduck.Core/AutoComplete/AutoCompleteBlockBase.cs deleted file mode 100644 index a31335505a..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteBlockBase.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.Parsing.VBA; -using Rubberduck.Settings; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; -using Rubberduck.VBEditor; -using Rubberduck.VBEditor.Events; -using Rubberduck.VBEditor.SafeComWrappers.Abstract; -using System.Linq; -using System.Text.RegularExpressions; -using System.Windows.Forms; -using Rubberduck.Parsing.VBA.Extensions; - -namespace Rubberduck.AutoComplete -{ - - public abstract class AutoCompleteBlockBase : AutoCompleteBase - { - /// Used for auto-indenting blocks as per indenter settings. - /// The token that starts the block, i.e. what to detect. - /// The token that closes the block, i.e. what to insert. - protected AutoCompleteBlockBase(IConfigProvider indenterSettings, string inputToken, string outputToken) - :base(inputToken, outputToken) - { - IndenterSettings = indenterSettings; - } - - public bool IsCapturing { get; set; } - - protected virtual bool FindInputTokenAtBeginningOfCurrentLine => false; - protected virtual bool SkipPreCompilerDirective => true; - - protected readonly IConfigProvider IndenterSettings; - - protected virtual bool ExecuteOnCommittedInputOnly => true; - protected virtual bool MatchInputTokenAtEndOfLineOnly => false; - - protected virtual bool IndentBody => true; - - public override bool Execute(AutoCompleteEventArgs e, AutoCompleteSettings settings) - { - var ignoreTab = e.Character == '\t' && !settings.CompleteBlockOnTab; - var ignoreEnter = e.Character == '\r' && !settings.CompleteBlockOnEnter; - if (IsInlineCharCompletion || e.IsDelete || ignoreTab || ignoreEnter) - { - return false; - } - - var module = e.CodeModule; - using (var pane = module.CodePane) - { - var selection = pane.Selection; - var originalCode = module.GetLines(selection); - var code = originalCode.Trim().StripStringLiterals(); - var hasComment = code.HasComment(out int commentStart); - - var isDeclareStatement = Regex.IsMatch(code, $"\\b{Tokens.Declare}\\b", RegexOptions.IgnoreCase); - var isExitStatement = Regex.IsMatch(code, $"\\b{Tokens.Exit}\\b", RegexOptions.IgnoreCase); - var isNamedArg = Regex.IsMatch(code, $"\\b{InputToken}\\:\\=", RegexOptions.IgnoreCase); - - if ((SkipPreCompilerDirective && code.StartsWith("#")) - || isDeclareStatement || isExitStatement || isNamedArg) - { - return false; - } - - if (IsMatch(code) && !IsBlockCompleted(module, selection)) - { - var indent = originalCode.TakeWhile(c => char.IsWhiteSpace(c)).Count(); - var newCode = OutputToken.PadLeft(OutputToken.Length + indent, ' '); - - var stdIndent = IndentBody - ? IndenterSettings.Create().IndentSpaces - : 0; - - module.InsertLines(selection.NextLine.StartLine, "\n" + newCode); - - module.ReplaceLine(selection.NextLine.StartLine, new string(' ', indent + stdIndent)); - pane.Selection = new Selection(selection.NextLine.StartLine, indent + stdIndent + 1); - - e.Handled = true; - return true; - } - return false; - } - } - - public override bool IsMatch(string code) - { - code = code.Trim().StripStringLiterals(); - var pattern = SkipPreCompilerDirective - ? $"\\b{InputToken}\\b" - : $"{InputToken}\\b"; // word boundary marker (\b) would prevent matching the # character - - bool regexOk; - if (MatchInputTokenAtEndOfLineOnly) - { - regexOk = !code.StartsWith(Tokens.Else, System.StringComparison.OrdinalIgnoreCase) && - code.EndsWith(InputToken, System.StringComparison.OrdinalIgnoreCase); - } - else - { - regexOk = Regex.IsMatch(code, pattern, RegexOptions.IgnoreCase); - } - - var hasComment = code.HasComment(out int commentIndex); - return regexOk && (!hasComment || code.IndexOf(InputToken) < commentIndex); - } - - protected bool IsBlockCompleted(ICodeModule module, Selection selection) - { - string content; - var proc = module.GetProcOfLine(selection.StartLine); - if (proc == null) - { - content = module.GetLines(1, module.CountOfDeclarationLines).StripStringLiterals(); - } - else - { - var procKind = module.GetProcKindOfLine(selection.StartLine); - var startLine = module.GetProcStartLine(proc, procKind); - var lineCount = module.GetProcCountLines(proc, procKind); - content = module.GetLines(startLine, lineCount); - } - - var options = RegexOptions.IgnoreCase; - var inputPattern = $"(? 0 && inputMatches == outputMatches; - } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteClosingBrace.cs b/Rubberduck.Core/AutoComplete/AutoCompleteClosingBrace.cs deleted file mode 100644 index 8203785cfd..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteClosingBrace.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteClosingBrace : AutoCompleteBase - { - public AutoCompleteClosingBrace() - : base("{", "}") { } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteClosingBracket.cs b/Rubberduck.Core/AutoComplete/AutoCompleteClosingBracket.cs deleted file mode 100644 index 7eeb2daa30..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteClosingBracket.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteClosingBracket : AutoCompleteBase - { - public AutoCompleteClosingBracket() - : base("[", "]") { } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteClosingParenthese.cs b/Rubberduck.Core/AutoComplete/AutoCompleteClosingParenthese.cs deleted file mode 100644 index aec3ea5af3..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteClosingParenthese.cs +++ /dev/null @@ -1,11 +0,0 @@ - -using Rubberduck.Parsing.Grammar; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteClosingParenthese : AutoCompleteBase - { - public AutoCompleteClosingParenthese() - :base("(", ")") { } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteClosingString.cs b/Rubberduck.Core/AutoComplete/AutoCompleteClosingString.cs deleted file mode 100644 index 6c681eadf2..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteClosingString.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteClosingString : AutoCompleteBase - { - public AutoCompleteClosingString() - : base("\"", "\"") { } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteDoBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompleteDoBlock.cs deleted file mode 100644 index c25d32e340..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteDoBlock.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteDoBlock : AutoCompleteBlockBase - { - public AutoCompleteDoBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.Do}", Tokens.Loop) { } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteEnumBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompleteEnumBlock.cs deleted file mode 100644 index 26baed9512..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteEnumBlock.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteEnumBlock : AutoCompleteBlockBase - { - public AutoCompleteEnumBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.Enum}", $"{Tokens.End} {Tokens.Enum}") { } - - protected override bool IndentBody => IndenterSettings.Create().IndentEnumTypeAsProcedure; - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteForBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompleteForBlock.cs deleted file mode 100644 index 24a88ed041..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteForBlock.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteForBlock : AutoCompleteBlockBase - { - public AutoCompleteForBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.For}", Tokens.Next) { } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteFunctionBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompleteFunctionBlock.cs deleted file mode 100644 index 09943f99ac..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteFunctionBlock.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.Settings; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; -using Rubberduck.VBEditor; -using Rubberduck.VBEditor.Events; -using System.Text.RegularExpressions; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteFunctionBlock : AutoCompleteBlockBase - { - public AutoCompleteFunctionBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.Function}", $"{Tokens.End} {Tokens.Function}") { } - - public override bool Execute(AutoCompleteEventArgs e, AutoCompleteSettings settings) - { - var result = base.Execute(e, settings); - if (result) - { - var module = e.CodeModule; - using (var pane = module.CodePane) - { - var original = module.GetLines(e.CurrentSelection); - var hasAsToken = Regex.IsMatch(original, $"\\)\\s+{Tokens.As}", RegexOptions.IgnoreCase) || - Regex.IsMatch(original, $"{Tokens.Function}\\s+\\(.*\\)\\s+{Tokens.As} ", RegexOptions.IgnoreCase); - var hasAsType = Regex.IsMatch(original, $"{Tokens.Function}\\s+\\w+\\(.*\\)\\s+{Tokens.As}\\s+\\w+", RegexOptions.IgnoreCase); - var asTypeClause = hasAsToken && hasAsType - ? string.Empty - : hasAsToken - ? $" {Tokens.Variant}" - : $" {Tokens.As} {Tokens.Variant}"; - - var code = original + asTypeClause; - module.ReplaceLine(e.CurrentSelection.StartLine, code); - var newCode = module.GetLines(e.CurrentSelection); - if (code == newCode) - { - pane.Selection = new Selection(e.CurrentSelection.StartLine, code.Length - Tokens.Variant.Length + 1, - e.CurrentSelection.StartLine, code.Length + 1); - } - } - } - - return result; - } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteHandlerBase.cs b/Rubberduck.Core/AutoComplete/AutoCompleteHandlerBase.cs new file mode 100644 index 0000000000..7dd5eabfcb --- /dev/null +++ b/Rubberduck.Core/AutoComplete/AutoCompleteHandlerBase.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using Rubberduck.Parsing.VBA.Extensions; +using Rubberduck.Settings; +using Rubberduck.VBEditor; +using Rubberduck.VBEditor.Events; +using Rubberduck.VBEditor.SourceCodeHandling; + +namespace Rubberduck.AutoComplete +{ + public abstract class AutoCompleteHandlerBase + { + protected AutoCompleteHandlerBase(ICodePaneHandler pane) + { + CodePaneHandler = pane; + } + + protected ICodePaneHandler CodePaneHandler { get; } + + public abstract CodeString Handle(AutoCompleteEventArgs e, AutoCompleteSettings settings); + } +} \ No newline at end of file diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteIfBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompleteIfBlock.cs deleted file mode 100644 index d3e77a02a6..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteIfBlock.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.Parsing.VBA; -using Rubberduck.Settings; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; -using Rubberduck.VBEditor; -using Rubberduck.VBEditor.Events; -using System.Linq; -using System.Text.RegularExpressions; -using System.Windows.Forms; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteIfBlock : AutoCompleteBlockBase - { - public AutoCompleteIfBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.If}", $"{Tokens.End} {Tokens.If}") { } - - // matching "If" would trigger erroneous block completion on inline if..then..else syntax. - protected override bool MatchInputTokenAtEndOfLineOnly => true; - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteOnErrorResumeNextBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompleteOnErrorResumeNextBlock.cs deleted file mode 100644 index 3ae42855b7..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteOnErrorResumeNextBlock.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteOnErrorResumeNextBlock : AutoCompleteBlockBase - { - public AutoCompleteOnErrorResumeNextBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.On} {Tokens.Error} {Tokens.Resume} {Tokens.Next}", $"{Tokens.On} {Tokens.Error} {Tokens.GoTo} 0") { } - - protected override bool ExecuteOnCommittedInputOnly => false; - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompletePrecompilerIfBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompletePrecompilerIfBlock.cs deleted file mode 100644 index a766656879..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompletePrecompilerIfBlock.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompletePrecompilerIfBlock : AutoCompleteBlockBase - { - public AutoCompletePrecompilerIfBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"#{Tokens.If}", $"#{Tokens.End} {Tokens.If}") { } - - protected override bool SkipPreCompilerDirective => false; - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompletePropertyBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompletePropertyBlock.cs deleted file mode 100644 index 4940ba1482..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompletePropertyBlock.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.Settings; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; -using Rubberduck.VBEditor; -using Rubberduck.VBEditor.Events; -using System.Text.RegularExpressions; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompletePropertyBlock : AutoCompleteBlockBase - { - public AutoCompletePropertyBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.Property}", $"{Tokens.End} {Tokens.Property}") { } - - public override bool Execute(AutoCompleteEventArgs e, AutoCompleteSettings settings) - { - var result = base.Execute(e, settings); - var module = e.CodeModule; - using (var pane = module.CodePane) - { - var original = module.GetLines(e.CurrentSelection); - var hasAsToken = Regex.IsMatch(original, $@"{Tokens.Property} {Tokens.Get}\s+\(.*\)\s+{Tokens.As}\s?", RegexOptions.IgnoreCase); - var hasAsType = Regex.IsMatch(original, $@"{Tokens.Property} {Tokens.Get}\s+\w+\(.*\)\s+{Tokens.As}\s+(?\w+)", RegexOptions.IgnoreCase); - var asTypeClause = hasAsToken && hasAsType - ? string.Empty - : hasAsToken - ? $" {Tokens.Variant}" - : $" {Tokens.As} {Tokens.Variant}"; - - - if (result && Regex.IsMatch(original, $"{Tokens.Property} {Tokens.Get}")) - { - var code = original + asTypeClause; - module.ReplaceLine(e.CurrentSelection.StartLine, code); - var newCode = module.GetLines(e.CurrentSelection); - if (code == newCode) - { - pane.Selection = new Selection(e.CurrentSelection.StartLine, code.Length - Tokens.Variant.Length + 1, - e.CurrentSelection.StartLine, code.Length + 1); - } - } - } - - return result; - } - - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteProvider.cs b/Rubberduck.Core/AutoComplete/AutoCompleteProvider.cs deleted file mode 100644 index 2787852851..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Rubberduck.Settings; - -namespace Rubberduck.AutoComplete -{ - public interface IAutoCompleteProvider - { - IEnumerable AutoCompletes { get; } - } - - public class AutoCompleteProvider : IAutoCompleteProvider - { - public AutoCompleteProvider(IEnumerable autoCompletes) - { - var defaults = new DefaultSettings().Default; - var defaultKeys = defaults.AutoCompletes.Select(x => x.Key); - var defaultAutoCompletes = autoCompletes.Where(autoComplete => defaultKeys.Contains(autoComplete.GetType().Name)); - - foreach (var autoComplete in defaultAutoCompletes) - { - autoComplete.IsEnabled = defaults.AutoCompletes.First(setting => setting.Key == autoComplete.GetType().Name).IsEnabled; - } - - AutoCompletes = autoCompletes; - } - - public IEnumerable AutoCompletes { get; } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteSelectBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompleteSelectBlock.cs deleted file mode 100644 index a6b5d447ca..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteSelectBlock.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteSelectBlock : AutoCompleteBlockBase - { - public AutoCompleteSelectBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.Select} {Tokens.Case}", $"{Tokens.End} {Tokens.Select}") { } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteService.cs b/Rubberduck.Core/AutoComplete/AutoCompleteService.cs deleted file mode 100644 index 5697818a94..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteService.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Windows.Forms; -using Rubberduck.AutoComplete.SelfClosingPairCompletion; -using Rubberduck.Common; -using Rubberduck.Parsing.VBA.Extensions; -using Rubberduck.Settings; -using Rubberduck.VBEditor; -using Rubberduck.VBEditor.Events; -using Rubberduck.VBEditor.SafeComWrappers.Abstract; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteService : IDisposable - { - private readonly IGeneralConfigService _configService; - private readonly List _selfClosingPairs = new List - { - new SelfClosingPair('(', ')'), - new SelfClosingPair('"', '"'), - new SelfClosingPair('[', ']'), - new SelfClosingPair('{', '}'), - }; - - private readonly SelfClosingPairCompletionService _selfClosingPairCompletion; - - private AutoCompleteSettings _settings; - private bool _popupShown; - private bool _enabled = false; - private bool _initialized; - - public AutoCompleteService(IGeneralConfigService configService, SelfClosingPairCompletionService selfClosingPairCompletion) - { - _selfClosingPairCompletion = selfClosingPairCompletion; - _configService = configService; - InitializeConfig(); - _configService.SettingsChanged += ConfigServiceSettingsChanged; - } - - public void Enable() - { - if (!_initializing) - { - InitializeConfig(); - } - - if (!_enabled) - { - VBENativeServices.KeyDown += HandleKeyDown; - VBENativeServices.IntelliSenseChanged += HandleIntelliSenseChanged; - _enabled = true; - } - } - - private bool _initializing; - private void InitializeConfig() - { - _initializing = true; - // No reason to think this would throw, but if it does, _initializing state needs to be reset. - try - { - if (!_initialized) - { - var config = _configService.LoadConfiguration(); - ApplyAutoCompleteSettings(config); - } - } - finally - { - _initializing = false; - } - } - - public void Disable() - { - if (_enabled && _initialized) - { - VBENativeServices.KeyDown -= HandleKeyDown; - VBENativeServices.IntelliSenseChanged -= HandleIntelliSenseChanged; - _enabled = false; - } - } - - private void HandleIntelliSenseChanged(object sender, IntelliSenseEventArgs e) - { - _popupShown = e.Visible; - } - - private void ConfigServiceSettingsChanged(object sender, ConfigurationChangedEventArgs e) - { - var config = _configService.LoadConfiguration(); - ApplyAutoCompleteSettings(config); - } - - public void ApplyAutoCompleteSettings(Configuration config) - { - _settings = config.UserSettings.AutoCompleteSettings; - if (_settings.IsEnabled) - { - Enable(); - } - else - { - Disable(); - } - _initialized = true; - } - - private void HandleKeyDown(object sender, AutoCompleteEventArgs e) - { - if (e.Character == default && !e.IsDelete) - { - return; - } - - var module = e.CodeModule; - var qualifiedSelection = module.GetQualifiedSelection(); - Debug.Assert(qualifiedSelection != null, nameof(qualifiedSelection) + " != null"); - var pSelection = qualifiedSelection.Value.Selection; - - if (_popupShown || pSelection.LineCount > 1 || e.IsDelete) - { - return; - } - - var currentContent = module.GetLines(pSelection); - if (HandleSmartConcat(e, pSelection, currentContent, module)) - { - return; - } - - HandleSelfClosingPairs(e, module, pSelection); - } - - private void HandleSelfClosingPairs(AutoCompleteEventArgs e, ICodeModule module, Selection pSelection) - { - if (!pSelection.IsSingleCharacter) - { - return; - } - - var currentCode = e.CurrentLine; - var currentSelection = e.CurrentSelection; - //var surroundingCode = GetSurroundingCode(module, currentSelection); // todo: find a way to parse the current instruction - - var original = new CodeString(currentCode, new Selection(0, currentSelection.EndColumn - 1), new Selection(pSelection.StartLine, 1)); - - var prettifier = new CodeStringPrettifier(module); - foreach (var selfClosingPair in _selfClosingPairs) - { - CodeString result; - if (e.Character == '\b' && pSelection.StartColumn > 1) - { - result = _selfClosingPairCompletion.Execute(selfClosingPair, original, '\b'); - } - else - { - result = _selfClosingPairCompletion.Execute(selfClosingPair, original, e.Character, prettifier); - } - - if (result != default) - { - using (var pane = module.CodePane) - { - module.DeleteLines(result.SnippetPosition); - module.InsertLines(result.SnippetPosition.StartLine, result.Code); - pane.Selection = result.SnippetPosition.Offset(result.CaretPosition); - e.Handled = true; - return; - } - } - } - } - - /// - /// Adds a line continuation when {ENTER} is pressed inside a string literal; returns false otherwise. - /// - private bool HandleSmartConcat(AutoCompleteEventArgs e, Selection pSelection, string currentContent, ICodeModule module) - { - var shouldHandle = _settings.EnableSmartConcat && - e.Character == '\r' && - IsInsideStringLiteral(pSelection, ref currentContent); - - var lastIndexLeftOfCaret = currentContent.Length > 2 ? currentContent.Substring(0, pSelection.StartColumn - 1).LastIndexOf('"') : 0; - if (shouldHandle && lastIndexLeftOfCaret > 0) - { - var indent = currentContent.NthIndexOf('"', 1); - var whitespace = new string(' ', indent); - var code = $"{currentContent.Substring(0, pSelection.StartColumn - 1)}\" & _\r\n{whitespace}\"{currentContent.Substring(pSelection.StartColumn - 1)}"; - - if (e.ControlDown) - { - code = $"{currentContent.Substring(0, pSelection.StartColumn - 1)}\" & vbNewLine & _\r\n{whitespace}\"{currentContent.Substring(pSelection.StartColumn - 1)}"; - - } - - module.ReplaceLine(pSelection.StartLine, code); - using (var pane = module.CodePane) - { - pane.Selection = new Selection(pSelection.StartLine + 1, indent + currentContent.Substring(pSelection.StartColumn - 2).Length); - e.Handled = true; - return true; - } - } - - return false; - } - - private string GetSurroundingCode(ICodeModule module, Selection selection) - { - // throws AccessViolationException! - var declarationLines = module.CountOfDeclarationLines; - if (selection.StartLine <= declarationLines) - { - return module.GetLines(1, declarationLines); - } - - var currentProc = module.GetProcOfLine(selection.StartLine); - var procKind = module.GetProcKindOfLine(selection.StartLine); - var procStart = module.GetProcStartLine(currentProc, procKind); - var lineCount = module.GetProcCountLines(currentProc, procKind); - return module.GetLines(procStart, lineCount); - } - - private bool IsInsideStringLiteral(Selection pSelection, ref string currentContent) - { - if (!currentContent.Substring(pSelection.StartColumn - 1).Contains("\"") || - currentContent.StripStringLiterals().HasComment(out _)) - { - return false; - } - - var zSelection = pSelection.ToZeroBased(); - var leftOfCaret = currentContent.Substring(0, zSelection.StartColumn); - var rightOfCaret = currentContent.Substring(Math.Min(zSelection.StartColumn + 1, currentContent.Length - 1)); - if (!rightOfCaret.Contains("\"")) - { - // the string isn't terminated, but VBE would terminate it here. - currentContent += "\""; - rightOfCaret += "\""; - } - - // odd number of double quotes on either side of the caret means we're inside a string literal, right? - return (leftOfCaret.Count(c => c.Equals('"')) % 2) != 0 && - (rightOfCaret.Count(c => c.Equals('"')) % 2) != 0; - } - - public void Dispose() - { - Disable(); - if (_configService != null) - { - _configService.SettingsChanged -= ConfigServiceSettingsChanged; - } - } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteSubBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompleteSubBlock.cs deleted file mode 100644 index 9f1a8ee300..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteSubBlock.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteSubBlock : AutoCompleteBlockBase - { - public AutoCompleteSubBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.Sub}", $"{Tokens.End} {Tokens.Sub}") { } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteTypeBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompleteTypeBlock.cs deleted file mode 100644 index f1ba5af361..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteTypeBlock.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteTypeBlock : AutoCompleteBlockBase - { - public AutoCompleteTypeBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.Type}", $"{Tokens.End} {Tokens.Type}") { } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteWhileBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompleteWhileBlock.cs deleted file mode 100644 index f40b8c15a8..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteWhileBlock.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteWhileBlock : AutoCompleteBlockBase - { - public AutoCompleteWhileBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.While}", Tokens.Wend) { } - } -} diff --git a/Rubberduck.Core/AutoComplete/AutoCompleteWithBlock.cs b/Rubberduck.Core/AutoComplete/AutoCompleteWithBlock.cs deleted file mode 100644 index a16dd7c81e..0000000000 --- a/Rubberduck.Core/AutoComplete/AutoCompleteWithBlock.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Rubberduck.Parsing.Grammar; -using Rubberduck.SettingsProvider; -using Rubberduck.SmartIndenter; - -namespace Rubberduck.AutoComplete -{ - public class AutoCompleteWithBlock : AutoCompleteBlockBase - { - public AutoCompleteWithBlock(IConfigProvider indenterSettings) - : base(indenterSettings, $"{Tokens.With}", $"{Tokens.End} {Tokens.With}") { } - } -} diff --git a/Rubberduck.Core/AutoComplete/IAutoComplete.cs b/Rubberduck.Core/AutoComplete/IAutoComplete.cs deleted file mode 100644 index 5bc8ce344e..0000000000 --- a/Rubberduck.Core/AutoComplete/IAutoComplete.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Rubberduck.Settings; -using Rubberduck.VBEditor.Events; - -namespace Rubberduck.AutoComplete -{ - public interface IAutoComplete - { - string InputToken { get; } - string OutputToken { get; } - bool IsMatch(string code); - bool Execute(AutoCompleteEventArgs e, AutoCompleteSettings settings); - bool IsInlineCharCompletion { get; } - bool IsEnabled { get; set; } - } -} diff --git a/Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/CodeStringPrettifier.cs b/Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/CodeStringPrettifier.cs deleted file mode 100644 index a39e61c394..0000000000 --- a/Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/CodeStringPrettifier.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Rubberduck.Common; -using Rubberduck.VBEditor.SafeComWrappers.Abstract; -using System; - -namespace Rubberduck.AutoComplete.SelfClosingPairCompletion -{ - public class CodeStringPrettifier : ICodeStringPrettifier - { - private readonly ICodeModule _module; - - public CodeStringPrettifier(ICodeModule module) - { - _module = module; - } - - public bool IsSpacingUnchanged(CodeString code, CodeString original) - { - using (var pane = _module.CodePane) - { - using (var window = pane.Window) - { - //window.ScreenUpdating = false; - _module.DeleteLines(code.SnippetPosition); - _module.InsertLines(code.SnippetPosition.StartLine, code.Code); - //window.ScreenUpdating = true; - - pane.Selection = code.SnippetPosition.Offset(code.CaretPosition); - - var lines = _module.GetLines(code.SnippetPosition); - if (lines.Equals(code.Code, StringComparison.InvariantCultureIgnoreCase)) - { - return true; - } - } - - _module.ReplaceLine(code.SnippetPosition.StartLine, original.Code); - } - return false; - } - } -} diff --git a/Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/ICodeStringPrettifier.cs b/Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/ICodeStringPrettifier.cs deleted file mode 100644 index edc998e9ae..0000000000 --- a/Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/ICodeStringPrettifier.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Rubberduck.Common; - -namespace Rubberduck.AutoComplete.SelfClosingPairCompletion -{ - public interface ICodeStringPrettifier - { - /// - /// Evaluates whether the specified renders as intended in the VBE. - /// - /// Returns true if the spacing is unchanged, false if the caret position wouldn't be where it's expected to be. - bool IsSpacingUnchanged(CodeString code, CodeString original); - } -} diff --git a/Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/SelfClosingPairCompletionService.cs b/Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/SelfClosingPairCompletionService.cs deleted file mode 100644 index 82c29daba5..0000000000 --- a/Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/SelfClosingPairCompletionService.cs +++ /dev/null @@ -1,283 +0,0 @@ -using Antlr4.Runtime; -using Antlr4.Runtime.Misc; -using Rubberduck.Common; -using Rubberduck.Parsing; -using Rubberduck.Parsing.Grammar; -using Rubberduck.Parsing.VBA.Parsing; -using Rubberduck.VBEditor; -using System; -using System.Linq; -using System.Windows.Forms; - -namespace Rubberduck.AutoComplete.SelfClosingPairCompletion -{ - public class SelfClosingPairCompletionService - { - private readonly IShowIntelliSenseCommand _showIntelliSense; - - public SelfClosingPairCompletionService(IShowIntelliSenseCommand showIntelliSense) - { - _showIntelliSense = showIntelliSense; - } - - public CodeString Execute(SelfClosingPair pair, CodeString original, char input, ICodeStringPrettifier prettifier = null) - { - if (input == pair.OpeningChar) - { - var result = HandleOpeningChar(pair, original); - if (result != default && prettifier != null) - { - if (prettifier.IsSpacingUnchanged(result, original)) - { - //_showIntelliSense?.Execute(); /* lovely VBE makes a loud "DING!!" if the command has no effect */ - return result; - } - - return default; - } - - return result; - } - - if (input == pair.ClosingChar) - { - return HandleClosingChar(pair, original); - } - - return default; - } - - public CodeString Execute(SelfClosingPair pair, CodeString original, Keys input) - { - if (input == Keys.Back) - { - return HandleBackspace(pair, original); - } - - return default; - } - - private CodeString HandleOpeningChar(SelfClosingPair pair, CodeString original) - { - var nextPosition = original.CaretPosition.ShiftRight(); - var autoCode = new string(new[] { pair.OpeningChar, pair.ClosingChar }); - var lines = original.Code.Split('\n'); - var line = lines[original.CaretPosition.StartLine]; - lines[original.CaretPosition.StartLine] = line.Insert(original.CaretPosition.StartColumn, autoCode); - - return new CodeString(string.Join("\n", lines), nextPosition, original.SnippetPosition); - } - - private CodeString HandleClosingChar(SelfClosingPair pair, CodeString original) - { - if (original.Code.Count(c => c == pair.OpeningChar) == original.Code.Count(c => c == pair.ClosingChar)) - { - var nextPosition = original.CaretPosition.ShiftRight(); - var newCode = original.Code; - - return new CodeString(newCode, nextPosition, original.SnippetPosition); - } - return default; - } - - private CodeString HandleBackspace(SelfClosingPair pair, CodeString original) - { - return DeleteMatchingTokens(pair, original); - } - - private CodeString DeleteMatchingTokens(SelfClosingPair pair, CodeString original) - { - var position = original.CaretPosition; - var lines = original.Lines; - - var previous = Math.Max(0, position.StartColumn - 1); - var next = previous + 1; - - var line = lines[original.CaretPosition.StartLine]; - if (original.CaretPosition.EndColumn < next && line[previous] == pair.OpeningChar && line[next] == pair.ClosingChar) - { - if (line.Length == 2) - { - return new CodeString(string.Empty, default, Selection.Empty.ShiftRight()); - } - lines[original.CaretPosition.StartLine] = line.Length == 2 ? string.Empty : line.Remove(previous, 2); - return new CodeString(string.Join("\n", lines), original.CaretPosition.ShiftLeft(), original.SnippetPosition); - } - - if (previous < line.Length - 1 && line[previous] == pair.OpeningChar) - { - Selection closingTokenPosition; - closingTokenPosition = line[Math.Min(line.Length - 1, next)] == pair.ClosingChar - ? position - : FindMatchingTokenPosition(pair, original); - - if (closingTokenPosition != default) - { - var closingLine = lines[closingTokenPosition.EndLine].Remove(closingTokenPosition.StartColumn, 1); - lines[closingTokenPosition.EndLine] = closingLine; - - if (closingLine == pair.OpeningChar.ToString()) - { - lines[original.CaretPosition.StartLine] = string.Empty; - } - else - { - var openingLine = lines[original.CaretPosition.StartLine].Remove(original.CaretPosition.ShiftLeft().StartColumn, 1); - lines[original.CaretPosition.StartLine] = openingLine; - } - - return new CodeString(string.Join("\n", lines), original.CaretPosition.ShiftLeft(), original.SnippetPosition); - } - } - - return default; - } - - private Selection FindMatchingTokenPosition(SelfClosingPair pair, CodeString original) - { - var code = original.Code; - code = code.EndsWith($"{pair.OpeningChar}{pair.ClosingChar}") - ? code.Substring(0, code.LastIndexOf(pair.ClosingChar) + 1) - : code; - var result = VBACodeStringParser.Parse(code, p => p.startRule()); - if (((ParserRuleContext)result.parseTree).exception != null) - { - result = VBACodeStringParser.Parse(code, p => p.mainBlockStmt()); - if (((ParserRuleContext)result.parseTree).exception != null) - { - return default; - } - } - var visitor = new MatchingTokenVisitor(pair, original); - visitor.Visit(result.parseTree); - return visitor.Result; - } - - - - private class MatchingTokenVisitor : VBAParserBaseVisitor - { - private readonly SelfClosingPair _pair; - private readonly CodeString _code; - - public Selection Result { get; private set; } - - public MatchingTokenVisitor(SelfClosingPair pair, CodeString code) - { - _pair = pair; - _code = code; - } - - public override Selection VisitLiteralExpr([NotNull] VBAParser.LiteralExprContext context) - { - if (context.Start.Text.StartsWith(_pair.OpeningChar.ToString()) - && context.Start.Text.EndsWith(_pair.ClosingChar.ToString())) - { - if (_code.CaretPosition.StartLine == context.Start.Line - 1 - && _code.CaretPosition.StartColumn == context.Start.Column + 1) - { - Result = new Selection(context.Start.Line - 1, context.Stop.Column + context.Stop.Text.Length - 1); - } - } - var inner = context.GetDescendents(); - foreach (var item in inner) - { - if (context != item) - { - var result = Visit(item); - if (result != default) - { - Result = result; - } - } - } - - return base.VisitLiteralExpr(context); - } - - public override Selection VisitIndexExpr([NotNull] VBAParser.IndexExprContext context) - { - if (context.LPAREN()?.Symbol.Text[0] == _pair.OpeningChar - && context.RPAREN()?.Symbol.Text[0] == _pair.ClosingChar) - { - if (_code.CaretPosition.StartLine == context.LPAREN().Symbol.Line - 1 - && _code.CaretPosition.StartColumn == context.RPAREN().Symbol.Column) - { - var token = context.RPAREN().Symbol; - Result = new Selection(token.Line - 1, token.Column); - } - } - var inner = context.GetDescendents(); - foreach (var item in inner) - { - if (context != item) - { - var result = Visit(item); - if (result != default) - { - Result = result; - } - } - } - - return base.VisitIndexExpr(context); - } - - public override Selection VisitArgList([NotNull] VBAParser.ArgListContext context) - { - if (context.Start.Text[0] == _pair.OpeningChar - && context.Stop.Text[0] == _pair.ClosingChar) - { - if (_code.CaretPosition.StartLine == context.Start.Line - 1 - && _code.CaretPosition.StartColumn == context.Start.Column + 1) - { - var token = context.Stop; - Result = new Selection(token.Line - 1, token.Column); - } - } - var inner = context.GetDescendents(); - foreach (var item in inner) - { - if (context != item) - { - var result = Visit(item); - if (result != default) - { - Result = result; - } - } - } - - return base.VisitArgList(context); - } - - public override Selection VisitParenthesizedExpr([NotNull] VBAParser.ParenthesizedExprContext context) - { - if (context.Start.Text[0] == _pair.OpeningChar - && context.Stop.Text[0] == _pair.ClosingChar) - { - if (_code.CaretPosition.StartLine == context.Start.Line - 1 - && _code.CaretPosition.StartColumn == context.Start.Column + 1) - { - var token = context.Stop; - Result = new Selection(token.Line - 1, token.Column); - } - } - var inner = context.GetDescendents(); - foreach (var item in inner) - { - if (context != item) - { - var result = Visit(item); - if (result != default) - { - Result = result; - } - } - } - - return base.VisitParenthesizedExpr(context); - } - } - } -} diff --git a/Rubberduck.Core/AutoComplete/Service/AutoCompleteService.cs b/Rubberduck.Core/AutoComplete/Service/AutoCompleteService.cs new file mode 100644 index 0000000000..a45c93944c --- /dev/null +++ b/Rubberduck.Core/AutoComplete/Service/AutoCompleteService.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using NLog; +using Rubberduck.Settings; +using Rubberduck.VBEditor.Events; + +namespace Rubberduck.AutoComplete.Service +{ + public class AutoCompleteService : IDisposable + { + private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + + private readonly IGeneralConfigService _configService; + private readonly IEnumerable _handlers; + + private AutoCompleteSettings _settings; + private bool _popupShown; + private bool _enabled; + private bool _initialized; + + public AutoCompleteService(IGeneralConfigService configService, IEnumerable handlers) + { + _configService = configService; + _configService.SettingsChanged += ConfigServiceSettingsChanged; + + _handlers = handlers; + InitializeConfig(); + } + + private bool _initializing; + + private void InitializeConfig() + { + _initializing = true; + // No reason to think this would throw, but if it does, _initializing state needs to be reset. + try + { + if (!_initialized) + { + var config = _configService.LoadConfiguration(); + ApplyAutoCompleteSettings(config); + } + } + finally + { + _initializing = false; + } + } + + private void Enable() + { + if (!_initializing) + { + InitializeConfig(); + } + + if (!_enabled) + { + VBENativeServices.KeyDown += HandleKeyDown; + VBENativeServices.IntelliSenseChanged += HandleIntelliSenseChanged; + _enabled = true; + } + } + + private void Disable() + { + if (_enabled && _initialized) + { + VBENativeServices.KeyDown -= HandleKeyDown; + VBENativeServices.IntelliSenseChanged -= HandleIntelliSenseChanged; + _enabled = false; + _popupShown = false; + } + } + + private void HandleIntelliSenseChanged(object sender, IntelliSenseEventArgs e) + { + _popupShown = e.Visible; + } + + private void ConfigServiceSettingsChanged(object sender, ConfigurationChangedEventArgs e) + { + var config = _configService.LoadConfiguration(); + ApplyAutoCompleteSettings(config); + } + + public void ApplyAutoCompleteSettings(Configuration config) + { + _settings = config.UserSettings.AutoCompleteSettings; + if (_settings.IsEnabled) + { + Enable(); + } + else + { + Disable(); + } + _initialized = true; + } + + private bool WillHandle(AutoCompleteEventArgs e) + { + Debug.Assert(_settings != null); + + if (!_enabled) + { + Logger.Warn("KeyDown controller is executing, but auto-completion service is disabled."); + return false; + } + + if (_popupShown || e.Character == default && e.IsDeleteKey) + { + return false; + } + + var module = e.Module; + using (var pane = module.CodePane) + { + if (pane.Selection.LineCount > 1) + { + return false; + } + } + + return true; + } + + private void HandleKeyDown(object sender, AutoCompleteEventArgs e) + { + if (!WillHandle(e)) + { + return; + } + + foreach (var handler in _handlers) + { + var result = handler.Handle(e, _settings); + if (result != null && e.Handled) + { + return; + } + } + } + + public void Dispose() + { + Disable(); + if (_configService != null) + { + _configService.SettingsChanged -= ConfigServiceSettingsChanged; + } + } + } +} diff --git a/Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/SelfClosingPair.cs b/Rubberduck.Core/AutoComplete/Service/SelfClosingPair.cs similarity index 53% rename from Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/SelfClosingPair.cs rename to Rubberduck.Core/AutoComplete/Service/SelfClosingPair.cs index 0ce8b00b17..e2157d98f3 100644 --- a/Rubberduck.Core/AutoComplete/SelfClosingPairCompletion/SelfClosingPair.cs +++ b/Rubberduck.Core/AutoComplete/Service/SelfClosingPair.cs @@ -1,4 +1,4 @@ -namespace Rubberduck.AutoComplete.SelfClosingPairCompletion +namespace Rubberduck.AutoComplete.Service { public class SelfClosingPair { @@ -10,5 +10,10 @@ public SelfClosingPair(char opening, char closing) public char OpeningChar { get; } public char ClosingChar { get; } + + /// + /// True if is the same as . + /// + public bool IsSymetric => OpeningChar == ClosingChar; } } \ No newline at end of file diff --git a/Rubberduck.Core/AutoComplete/Service/SelfClosingPairCompletionService.cs b/Rubberduck.Core/AutoComplete/Service/SelfClosingPairCompletionService.cs new file mode 100644 index 0000000000..e805dfe78a --- /dev/null +++ b/Rubberduck.Core/AutoComplete/Service/SelfClosingPairCompletionService.cs @@ -0,0 +1,436 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using Antlr4.Runtime; +using Antlr4.Runtime.Misc; +using Antlr4.Runtime.Tree; +using Rubberduck.Parsing.Grammar; +using Rubberduck.Parsing.VBA.Parsing; +using Rubberduck.VBEditor; + +namespace Rubberduck.AutoComplete.Service +{ + public class SelfClosingPairCompletionService + { + private readonly IShowIntelliSenseCommand _showIntelliSense; + + public SelfClosingPairCompletionService(IShowIntelliSenseCommand showIntelliSense) + { + _showIntelliSense = showIntelliSense; + } + + public CodeString Execute(SelfClosingPair pair, CodeString original, char input) + { + 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; + + if (pair.IsSymetric && input != '\b' && + original.Code.Length >= 1 && + previousCharIsClosingChar && !nextCharIsClosingChar + || original.IsComment || (original.IsInsideStringLiteral && !nextCharIsClosingChar)) + { + return null; + } + + if (input == pair.OpeningChar) + { + var result = HandleOpeningChar(pair, original); + return result; + } + + if (input == pair.ClosingChar) + { + return HandleClosingChar(pair, original); + } + + if (input == '\b') + { + return Execute(pair, original, Keys.Back); + } + + return null; + } + + public CodeString Execute(SelfClosingPair pair, CodeString original, Keys input) + { + if (original.IsComment) + { + return null; + } + + if (input == Keys.Back) + { + return HandleBackspace(pair, original); + } + + return null; + } + + private CodeString HandleOpeningChar(SelfClosingPair pair, CodeString original) + { + var nextPosition = original.CaretPosition.ShiftRight(); + var autoCode = new string(new[] { pair.OpeningChar, pair.ClosingChar }); + var lines = original.Lines; + var line = lines[original.CaretPosition.StartLine]; + + string newCode; + if (string.IsNullOrEmpty(original.Code)) + { + newCode = autoCode; + } + else if (pair.IsSymetric && original.CaretPosition.StartColumn < line.Length && line[original.CaretPosition.StartColumn] == pair.ClosingChar) + { + newCode = line; + } + else + { + newCode = original.CaretPosition.StartColumn == line.Length + ? line + autoCode + : line.Insert(original.CaretPosition.StartColumn, autoCode); + } + lines[original.CaretPosition.StartLine] = newCode; + + return new CodeString(string.Join("\r\n", lines), nextPosition, new Selection(original.SnippetPosition.StartLine, 1, original.SnippetPosition.EndLine, 1)); + } + + private CodeString HandleClosingChar(SelfClosingPair pair, CodeString original) + { + if (pair.IsSymetric) + { + return null; + } + + var nextIsClosingChar = original.CaretLine.Length > original.CaretCharIndex && + original.CaretLine[original.CaretCharIndex] == pair.ClosingChar; + if (nextIsClosingChar) + { + var nextPosition = original.CaretPosition.ShiftRight(); + var newCode = original.Code; + + return new CodeString(newCode, nextPosition, new Selection(original.SnippetPosition.StartLine, 1, original.SnippetPosition.EndLine, 1)); + } + return null; + } + + private CodeString HandleBackspace(SelfClosingPair pair, CodeString original) + { + return DeleteMatchingTokens(pair, original); + } + + private CodeString DeleteMatchingTokens(SelfClosingPair pair, CodeString original) + { + var position = original.CaretPosition; + var lines = original.Lines; + + var line = lines[original.CaretPosition.StartLine]; + if (line.Length == 0) + { + // nothing to delete at caret position... bail out. + return null; + } + + var previous = Math.Max(0, position.StartColumn - 1); + var next = Math.Min(line.Length - 1, position.StartColumn); + + var previousChar = line[previous]; + var nextChar = line[next]; + + if (original.CaretPosition.StartColumn < next && + previousChar == pair.OpeningChar && + nextChar == pair.ClosingChar) + { + if (line.Length == 2) + { + // entire line consists in the self-closing pair itself. + return 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); + } + + if (previous < line.Length - 1 && previousChar == pair.OpeningChar) + { + return DeleteMatchingTokensMultiline(pair, original); + } + + return null; + } + + private CodeString DeleteMatchingTokensMultiline(SelfClosingPair pair, CodeString original) + { + var position = original.CaretPosition; + var lines = original.Lines; + var line = lines[original.CaretPosition.StartLine]; + var next = Math.Min(line.Length - 1, position.StartColumn); + + Selection closingTokenPosition; + closingTokenPosition = line[Math.Min(line.Length - 1, next)] == pair.ClosingChar + ? position + : FindMatchingTokenPosition(pair, original); + + if (closingTokenPosition == default) + { + // could not locate the closing token... bail out. + return null; + } + + var closingLine = lines[closingTokenPosition.EndLine].Remove(closingTokenPosition.StartColumn, 1); + lines[closingTokenPosition.EndLine] = closingLine; + + if (closingLine == pair.OpeningChar.ToString() || closingLine == pair.OpeningChar + " _" || closingLine == pair.OpeningChar + " & _") + { + lines[closingTokenPosition.EndLine] = string.Empty; + } + else + { + var openingLine = lines[position.StartLine].Remove(position.ShiftLeft().StartColumn, 1); + lines[position.StartLine] = openingLine; + } + + var finalCaretPosition = original.CaretPosition.ShiftLeft(); + + var lastLine = lines[lines.Length - 1]; + if (string.IsNullOrEmpty(lastLine.Trim())) + { + lines = lines.Where((x, i) => i <= position.StartLine || !string.IsNullOrWhiteSpace(x)).ToArray(); + lastLine = lines[lines.Length - 1]; + + if (lastLine.EndsWith(" _") && finalCaretPosition.StartLine == lines.Length - 1) + { + finalCaretPosition = HandleBackspaceContinuations(lines, finalCaretPosition); + } + } + + var caretLine = lines[finalCaretPosition.StartLine]; + if (caretLine.EndsWith(" _") && finalCaretPosition.StartLine == lines.Length - 1) + { + finalCaretPosition = HandleBackspaceContinuations(lines, finalCaretPosition); + } + else if (caretLine.EndsWith("& _") || caretLine.EndsWith("& _")) + { + HandleBackspaceContinuations(lines, finalCaretPosition); + } + + var nonEmptyLines = lines.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + var lastNonEmptyLine = nonEmptyLines.Length > 0 ? nonEmptyLines[nonEmptyLines.Length - 1] : null; + if (lastNonEmptyLine != null) + { + if (position.StartLine > nonEmptyLines.Length - 1) + { + // caret is on a now-empty line, shift one line up. + finalCaretPosition = new Selection(position.StartLine - 1, lastNonEmptyLine.Length - 1); + } + + if (lastNonEmptyLine.EndsWith(" _")) + { + var newPosition = HandleBackspaceContinuations(nonEmptyLines, new Selection(nonEmptyLines.Length - 1, 1)); + if (finalCaretPosition.StartLine == nonEmptyLines.Length - 1) + { + finalCaretPosition = newPosition; + } + } + + lines = nonEmptyLines; + } + + + // 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, + new Selection(original.SnippetPosition.StartLine, 1, original.SnippetPosition.EndLine, 1)); + } + + private static Selection HandleBackspaceContinuations(string[] nonEmptyLines, Selection finalCaretPosition) + { + var lineIndex = Math.Min(finalCaretPosition.StartLine, nonEmptyLines.Length - 1); + var line = nonEmptyLines[lineIndex]; + if (line.EndsWith(" _") && lineIndex == nonEmptyLines.Length - 1) + { + nonEmptyLines[lineIndex] = line.Remove(line.Length - 2); + line = nonEmptyLines[lineIndex]; + } + + if (lineIndex == nonEmptyLines.Length - 1) + { + line = nonEmptyLines[lineIndex]; + } + + if (line.EndsWith("&")) + { + // we're not concatenating anything anymore; remove concat operator too. + var concatOffset = line.EndsWith(" &") ? 2 : 1; + nonEmptyLines[lineIndex] = line.Remove(line.Length - concatOffset); + } + TrimNonEmptyLine(nonEmptyLines, lineIndex, "& vbNewLine"); + TrimNonEmptyLine(nonEmptyLines, lineIndex, "& vbCrLf"); + TrimNonEmptyLine(nonEmptyLines, lineIndex, "& vbCr"); + TrimNonEmptyLine(nonEmptyLines, lineIndex, "& vbLf"); + + // we're keeping the closing quote, but let's put the caret inside: + line = nonEmptyLines[lineIndex]; + var quoteOffset = line.EndsWith("\"") ? 1 : 0; + finalCaretPosition = new Selection(finalCaretPosition.StartLine, line.Length - quoteOffset); + return finalCaretPosition; + } + + private static void TrimNonEmptyLine(string[] nonEmptyLines, int lineIndex, string ending) + { + var line = nonEmptyLines[lineIndex]; + if (line.EndsWith(ending, StringComparison.OrdinalIgnoreCase)) + { + var offset = line.EndsWith(" " + ending, StringComparison.OrdinalIgnoreCase) + ? ending.Length + 1 + : ending.Length; + nonEmptyLines[lineIndex] = line.Remove(line.Length - offset); + } + } + + private Selection FindMatchingTokenPosition(SelfClosingPair pair, CodeString original) + { + var code = string.Join("\r\n", original.Lines) + "\r\n"; + code = code.EndsWith($"{pair.OpeningChar}{pair.ClosingChar}") + ? code.Substring(0, code.LastIndexOf(pair.ClosingChar) + 1) + : code; + + var leftOfCaret = original.CaretLine.Substring(0, original.CaretPosition.StartColumn + 1); + var rightOfCaret = original.CaretLine.Substring(original.CaretPosition.StartColumn); + + if (leftOfCaret.Count(c => c == pair.OpeningChar) == 1 && + rightOfCaret.Count(c => c == pair.ClosingChar) == 1) + { + return new Selection(original.CaretPosition.StartLine, + original.CaretLine.LastIndexOf(pair.ClosingChar)); + } + + var result = VBACodeStringParser.Parse(code, p => p.startRule()); + if (((ParserRuleContext)result.parseTree).exception != null) + { + result = VBACodeStringParser.Parse(code, p => p.mainBlockStmt()); + if (((ParserRuleContext)result.parseTree).exception != null) + { + result = VBACodeStringParser.Parse(code, p => p.blockStmt()); + if (((ParserRuleContext)result.parseTree).exception != null) + { + return default; + } + } + } + var visitor = new MatchingTokenVisitor(pair, original); + var matchingTokenPosition = visitor.Visit(result.parseTree); + return matchingTokenPosition; + } + + + + private class MatchingTokenVisitor : VBAParserBaseVisitor + { + private readonly SelfClosingPair _pair; + private readonly CodeString _code; + + public MatchingTokenVisitor(SelfClosingPair pair, CodeString code) + { + _pair = pair; + _code = code; + } + + protected override bool ShouldVisitNextChild(IRuleNode node, Selection currentResult) + { + return currentResult.Equals(default); + } + + public override Selection VisitLiteralExpr([NotNull] VBAParser.LiteralExprContext context) + { + var innerResult = VisitChildren(context); + if (innerResult != DefaultResult) + { + return innerResult; + } + + if (context.Start.Text.StartsWith(_pair.OpeningChar.ToString()) + && context.Start.Text.EndsWith(_pair.ClosingChar.ToString())) + { + if (_code.CaretPosition.StartLine == context.Start.Line - 1 + && _code.CaretPosition.StartColumn == context.Start.Column + 1) + { + return new Selection(context.Start.Line - 1, context.Stop.Column + context.Stop.Text.Length - 1); + } + } + + return DefaultResult; + } + + public override Selection VisitIndexExpr([NotNull] VBAParser.IndexExprContext context) + { + var innerResult = VisitChildren(context); + if (innerResult != DefaultResult) + { + return innerResult; + } + + if (context.LPAREN()?.Symbol.Text[0] == _pair.OpeningChar + && context.RPAREN()?.Symbol.Text[0] == _pair.ClosingChar) + { + if (_code.CaretPosition.StartLine == context.LPAREN().Symbol.Line - 1 + && _code.CaretPosition.StartColumn == context.LPAREN().Symbol.Column + 1) + { + var token = context.RPAREN().Symbol; + return new Selection(token.Line - 1, token.Column); + } + } + + return DefaultResult; + } + + public override Selection VisitArgList([NotNull] VBAParser.ArgListContext context) + { + var innerResult = VisitChildren(context); + if (innerResult != DefaultResult) + { + return innerResult; + } + + if (context.Start.Text[0] == _pair.OpeningChar + && context.Stop.Text[0] == _pair.ClosingChar) + { + if (_code.CaretPosition.StartLine == context.Start.Line - 1 + && _code.CaretPosition.StartColumn == context.Start.Column + 1) + { + var token = context.Stop; + return new Selection(token.Line - 1, token.Column); + } + } + + return DefaultResult; + } + + public override Selection VisitParenthesizedExpr([NotNull] VBAParser.ParenthesizedExprContext context) + { + var innerResult = VisitChildren(context); + if (innerResult != DefaultResult) + { + return innerResult; + } + + if (context.Start.Text[0] == _pair.OpeningChar + && context.Stop.Text[0] == _pair.ClosingChar) + { + if (_code.CaretPosition.StartLine == context.Start.Line - 1 + && _code.CaretPosition.StartColumn == context.Start.Column + 1) + { + var token = context.Stop; + return new Selection(token.Line - 1, token.Column); + } + } + + return DefaultResult; + } + } + } +} diff --git a/Rubberduck.Core/AutoComplete/Service/SelfClosingPairHandler.cs b/Rubberduck.Core/AutoComplete/Service/SelfClosingPairHandler.cs new file mode 100644 index 0000000000..f50212287f --- /dev/null +++ b/Rubberduck.Core/AutoComplete/Service/SelfClosingPairHandler.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Windows.Forms; +using Rubberduck.Settings; +using Rubberduck.VBEditor; +using Rubberduck.VBEditor.Events; +using Rubberduck.VBEditor.SourceCodeHandling; + +namespace Rubberduck.AutoComplete.Service +{ + public class SelfClosingPairHandler : AutoCompleteHandlerBase + { + private static readonly IEnumerable SelfClosingPairs = new List + { + new SelfClosingPair('(', ')'), + new SelfClosingPair('"', '"'), + new SelfClosingPair('[', ']'), + new SelfClosingPair('{', '}'), + }; + + private readonly SelfClosingPairCompletionService _scpService; + + public SelfClosingPairHandler(ICodePaneHandler pane, SelfClosingPairCompletionService scpService) + : base(pane) + { + _scpService = scpService; + } + + public override CodeString Handle(AutoCompleteEventArgs e, AutoCompleteSettings settings) + { + var original = CodePaneHandler.GetCurrentLogicalLine(e.Module); + foreach (var pair in SelfClosingPairs) + { + var isPresent = original.CaretLine.EndsWith($"{pair.OpeningChar}{pair.ClosingChar}"); + + var result = ExecuteSelfClosingPair(e, original, pair); + if (result == null) + { + continue; + } + + 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. + } + + result = ExecuteSelfClosingPair(e, prettified, pair); + if (result == null) + { + continue; + } + + 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 = result.ReplaceLine(result.CaretPosition.StartLine, currentLine.TrimEnd()); + } + + if (pair.OpeningChar == '(' && e.Character != '\b' && !result.CaretLine.EndsWith($"{pair.OpeningChar}{pair.ClosingChar}")) + { + // VBE eats it. just bail out. + return null; + } + + e.Handled = true; + result = new CodeString(result.Code, result.CaretPosition, new Selection(result.SnippetPosition.StartLine, 1, result.SnippetPosition.EndLine, 1)); + return result; + } + + return null; + } + + private CodeString ExecuteSelfClosingPair(AutoCompleteEventArgs e, CodeString original, SelfClosingPair pair) + { + CodeString result; + if (e.Character == '\b' && original.CaretPosition.StartColumn > 1) + { + result = _scpService.Execute(pair, original, Keys.Back); + } + else + { + result = _scpService.Execute(pair, original, e.Character); + } + + return result; + } + } +} \ No newline at end of file diff --git a/Rubberduck.Core/AutoComplete/ShowIntelliSenseCommand.cs b/Rubberduck.Core/AutoComplete/Service/ShowIntelliSenseCommand.cs similarity index 96% rename from Rubberduck.Core/AutoComplete/ShowIntelliSenseCommand.cs rename to Rubberduck.Core/AutoComplete/Service/ShowIntelliSenseCommand.cs index 4fd2e0d628..4663c29b39 100644 --- a/Rubberduck.Core/AutoComplete/ShowIntelliSenseCommand.cs +++ b/Rubberduck.Core/AutoComplete/Service/ShowIntelliSenseCommand.cs @@ -3,7 +3,7 @@ using Rubberduck.UI.Command; using Rubberduck.VBEditor.SafeComWrappers.Abstract; -namespace Rubberduck.AutoComplete +namespace Rubberduck.AutoComplete.Service { public interface IShowIntelliSenseCommand { diff --git a/Rubberduck.Core/AutoComplete/Service/SmartConcatenationHandler.cs b/Rubberduck.Core/AutoComplete/Service/SmartConcatenationHandler.cs new file mode 100644 index 0000000000..b52ddb4d13 --- /dev/null +++ b/Rubberduck.Core/AutoComplete/Service/SmartConcatenationHandler.cs @@ -0,0 +1,66 @@ +using Rubberduck.Parsing.VBA.Extensions; +using Rubberduck.Settings; +using Rubberduck.VBEditor; +using Rubberduck.VBEditor.Events; +using Rubberduck.VBEditor.SourceCodeHandling; + +namespace Rubberduck.AutoComplete.Service +{ + /// + /// Adds a line continuation when {ENTER} is pressed when inside a string literal. + /// + public class SmartConcatenationHandler : AutoCompleteHandlerBase + { + public SmartConcatenationHandler(ICodePaneHandler pane) + : base(pane) + { + } + + public override CodeString Handle(AutoCompleteEventArgs e, AutoCompleteSettings settings) + { + if (e.Character != '\r' || (!settings?.SmartConcat.IsEnabled ?? true)) + { + return null; + } + + var currentContent = CodePaneHandler.GetCurrentLogicalLine(e.Module); + if (!currentContent.IsInsideStringLiteral) + { + return null; + } + + var lastIndexLeftOfCaret = currentContent.CaretLine.Length > 2 ? currentContent.CaretLine.Substring(0, currentContent.CaretPosition.StartColumn).LastIndexOf('"') : 0; + if (lastIndexLeftOfCaret > 0) + { + var indent = currentContent.CaretLine.NthIndexOf('"', 1); + var whitespace = new string(' ', indent); + + // todo: handle shift modifier? + var concatVbNewLine = settings.SmartConcat.ConcatVbNewLineModifier.HasFlag(ModifierKeySetting.CtrlKey) && e.IsControlKeyDown; + + var autoCode = $"\" {(concatVbNewLine ? "& vbNewLine " : string.Empty)}& _\r\n{whitespace}\""; + var left = currentContent.CaretLine.Substring(0, currentContent.CaretPosition.StartColumn); + var right = currentContent.CaretLine.Substring(currentContent.CaretPosition.StartColumn); + + var caretLine = $"{left}{autoCode}{right}"; + var lines = currentContent.Lines; + lines[currentContent.CaretPosition.StartLine] = caretLine; + var code = string.Join("\r\n", lines); + + var newContent = new CodeString(code, currentContent.CaretPosition, currentContent.SnippetPosition); + var newPosition = new Selection(newContent.CaretPosition.StartLine + 1, indent + 1); + + e.Handled = true; + var 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 null; + } + } +} \ No newline at end of file diff --git a/Rubberduck.Core/Common/CodeString.cs b/Rubberduck.Core/Common/CodeString.cs deleted file mode 100644 index 64d9c49f59..0000000000 --- a/Rubberduck.Core/Common/CodeString.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Rubberduck.VBEditor; -using System; - -namespace Rubberduck.Common -{ - /// - /// Represents a code string that includes caret position. - /// - public struct CodeString : IEquatable - { - /// - /// Creates a new CodeString - /// - /// Code string - /// Zero-based caret position in the code string. - /// One-based selection span of the code string in the containing module. - public CodeString(string code, Selection zPosition, Selection pPosition = default) - { - if (code == null) throw new ArgumentNullException(nameof(code)); - - var lines = code.Split('\n'); - var line = lines[zPosition.StartLine]; - if (line != string.Empty && line[Math.Min(line.Length - 1, zPosition.StartColumn)] == '|') - { - Code = line.Remove(Math.Min(line.Length - 1, zPosition.StartColumn), 1); - } - else - { - Code = code; - } - - SnippetPosition = pPosition == default - ? new Selection(1, 1, lines.Length, lines[lines.Length-1].Length) - : pPosition; - - CaretPosition = zPosition; - } - - public static CodeString FromString(string code) - { - var zPosition = new Selection(); - var lines = (code ?? string.Empty).Split('\n'); - for (int i = 0; i < lines.Length; i++) - { - var line = lines[i]; - var index = line.IndexOf('|'); - if (index >= 0) - { - lines[i] = line.Remove(index, 1); - zPosition = new Selection(i, index); - break; - } - } - - var newCode = string.Join("\n", lines); - return new CodeString(code, zPosition); - } - - - /// - /// The code string. - /// - public string Code { get; } - /// - /// Zero-based caret position in the code string. - /// - public Selection CaretPosition { get; } - /// - /// One-based position of the code string in the containing module. - /// - public Selection SnippetPosition { get; } - - public string[] Lines - { - get - { - return Code?.Split('\n') - ?? new string[] { }; - } - } - - public static bool operator ==(CodeString codeString1, CodeString codeString2) => (codeString1.Code == codeString2.Code && codeString1.CaretPosition == codeString2.CaretPosition); - public static bool operator !=(CodeString codeString1, CodeString codeString2) => !(codeString1 == codeString2); - - public override bool Equals(object obj) - { - if (obj == null) - { - return false; - } - - var other = (CodeString)obj; - return Equals(other); - } - - public override int GetHashCode() - { - return HashCode.Compute(Code, CaretPosition); - } - - public override string ToString() - { - return InsertPseudoCaret(); - } - - private string InsertPseudoCaret() - { - if (string.IsNullOrEmpty(Code)) - { - return string.Empty; - } - - var lines = Code.Split('\n'); - var line = lines[CaretPosition.StartLine]; - lines[CaretPosition.StartLine] = line.Insert(CaretPosition.StartColumn, "|"); - return string.Join("\n", lines); - } - - public bool Equals(CodeString other) - { - if (other == default) - { - return false; - } - return (Code == null && other.Code == null) - || (Code != null && Code.Equals(other.Code) && CaretPosition.Equals(other.CaretPosition)); - } - } -} diff --git a/Rubberduck.Core/Common/RubberduckHooks.cs b/Rubberduck.Core/Common/RubberduckHooks.cs index fea4812b5f..0b3d8f46aa 100644 --- a/Rubberduck.Core/Common/RubberduckHooks.cs +++ b/Rubberduck.Core/Common/RubberduckHooks.cs @@ -8,6 +8,7 @@ using Rubberduck.VBEditor.SafeComWrappers.Abstract; using Rubberduck.VBEditor.WindowsApi; using Rubberduck.AutoComplete; +using Rubberduck.AutoComplete.Service; namespace Rubberduck.Common { diff --git a/Rubberduck.Core/Common/StringExtensions.cs b/Rubberduck.Core/Common/StringExtensions.cs index a954f038c2..495f5819f0 100644 --- a/Rubberduck.Core/Common/StringExtensions.cs +++ b/Rubberduck.Core/Common/StringExtensions.cs @@ -1,31 +1,10 @@ -using Rubberduck.VBEditor; -using System; +using System; using System.Globalization; namespace Rubberduck.Common { public static class StringExtensions { - public static CodeString ToCodeString(this string code) - { - var zPosition = new Selection(); - var lines = (code ?? string.Empty).Split('\n'); - for (int i = 0; i < lines.Length; i++) - { - var line = lines[i]; - var index = line.IndexOf('|'); - if (index >= 0) - { - lines[i] = line.Remove(index, 1); - zPosition = new Selection(i, index); - break; - } - } - - var newCode = string.Join("\n", lines); - return new CodeString(newCode, zPosition); - } - public static string Capitalize(this string input) { var tokens = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); diff --git a/Rubberduck.Core/Properties/Settings.Designer.cs b/Rubberduck.Core/Properties/Settings.Designer.cs index 1fbbf7fbc5..8efb395a32 100644 --- a/Rubberduck.Core/Properties/Settings.Designer.cs +++ b/Rubberduck.Core/Properties/Settings.Designer.cs @@ -314,36 +314,6 @@ public static Settings Default { } } - [global::System.Configuration.ApplicationScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute(@" - - - - - - - - - - - - - - - - - - - - -")] - public global::Rubberduck.Settings.AutoCompleteSettings AutoCompleteSettings { - get { - return ((global::Rubberduck.Settings.AutoCompleteSettings)(this["AutoCompleteSettings"])); - } - } - [global::System.Configuration.ApplicationScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("\r\n\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n " + + " \r\n \r\n \r\n \r\n \r\n")] + public global::Rubberduck.Settings.AutoCompleteSettings AutoCompleteSettings { + get { + return ((global::Rubberduck.Settings.AutoCompleteSettings)(this["AutoCompleteSettings"])); + } + } } } diff --git a/Rubberduck.Core/Properties/Settings.settings b/Rubberduck.Core/Properties/Settings.settings index 757efc9198..b753386290 100644 --- a/Rubberduck.Core/Properties/Settings.settings +++ b/Rubberduck.Core/Properties/Settings.settings @@ -181,30 +181,6 @@ <MinimumLogLevel>6</MinimumLogLevel> <EnableExperimentalFeatures /> </GeneralSettings> - - - <?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" 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> <?xml version="1.0" encoding="utf-16"?> @@ -289,5 +265,29 @@ <RunInspectionsOnSuccessfulParse>true</RunInspectionsOnSuccessfulParse> </CodeInspectionSettings> + + <?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> + \ No newline at end of file diff --git a/Rubberduck.Core/Settings/AutoCompleteConfigProvider.cs b/Rubberduck.Core/Settings/AutoCompleteConfigProvider.cs index bada3fe047..6d6faa9d6c 100644 --- a/Rubberduck.Core/Settings/AutoCompleteConfigProvider.cs +++ b/Rubberduck.Core/Settings/AutoCompleteConfigProvider.cs @@ -1,9 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using Rubberduck.AutoComplete; -using Rubberduck.Parsing.VBA; -using Rubberduck.Parsing.VBA.Extensions; -using Rubberduck.SettingsProvider; +using Rubberduck.SettingsProvider; namespace Rubberduck.Settings { @@ -11,47 +6,16 @@ public class AutoCompleteConfigProvider : IConfigProvider { private readonly IPersistanceService _persister; private readonly AutoCompleteSettings _defaultSettings; - private readonly HashSet _foundAutoCompleteKeys; - public AutoCompleteConfigProvider(IPersistanceService persister, IAutoCompleteProvider provider) + public AutoCompleteConfigProvider(IPersistanceService persister) { _persister = persister; - _foundAutoCompleteKeys = provider.AutoCompletes.Select(e => e.GetType().Name).ToHashSet(); _defaultSettings = new DefaultSettings().Default; - _defaultSettings.AutoCompletes = _defaultSettings.AutoCompletes.Where(setting => _foundAutoCompleteKeys.Contains(setting.Key)).ToHashSet(); - - var defaultKeys = _defaultSettings.AutoCompletes.Select(e => e.Key); - var nonDefaultAutoCompletes = provider.AutoCompletes.Where(e => !defaultKeys.Contains(e.GetType().Name)); - - _defaultSettings.AutoCompletes.UnionWith(nonDefaultAutoCompletes.Select(e => new AutoCompleteSetting(e))); } public AutoCompleteSettings Create() { - var loaded = _persister.Load(_defaultSettings); - if (loaded == null) - { - return _defaultSettings; - } - - // Loaded settings don't contain defaults, so we need to combine user settings with defaults. - var settings = new HashSet(); - - foreach (var loadedSetting in loaded.AutoCompletes.Where(e => !settings.Contains(e) && _foundAutoCompleteKeys.Contains(e.Key))) - { - var matchingDefaultSetting = _defaultSettings.AutoCompletes.FirstOrDefault(e => !loaded.AutoCompletes.Contains(e) && e.Equals(loadedSetting)); - if (matchingDefaultSetting != null) - { - loadedSetting.IsEnabled = matchingDefaultSetting.IsEnabled; - } - - settings.Add(loadedSetting); - } - - settings.UnionWith(_defaultSettings.AutoCompletes.Where(e => !settings.Contains(e))); - loaded.AutoCompletes = settings; - - return loaded; + return _persister.Load(_defaultSettings) ?? _defaultSettings; } public AutoCompleteSettings CreateDefaults() diff --git a/Rubberduck.Core/Settings/AutoCompleteSetting.cs b/Rubberduck.Core/Settings/AutoCompleteSetting.cs deleted file mode 100644 index d61069dabf..0000000000 --- a/Rubberduck.Core/Settings/AutoCompleteSetting.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Xml.Serialization; -using System.Configuration; -using Rubberduck.AutoComplete; -using Rubberduck.UI; - -namespace Rubberduck.Settings -{ - [SettingsSerializeAs(SettingsSerializeAs.Xml)] - public class AutoCompleteSetting : ViewModelBase - { - public AutoCompleteSetting() { /* default ctor required for XML serialization */ } - - public AutoCompleteSetting(IAutoComplete autoComplete) - : this(autoComplete.GetType().Name, autoComplete.IsEnabled) { } - - public AutoCompleteSetting(string key, bool isEnabled) - { - Key = key; - IsEnabled = isEnabled; - } - - [XmlAttribute] - public string Key { get; set; } - - private bool _isEnabled; - [XmlAttribute] - public bool IsEnabled - { - get { return _isEnabled; } - set - { - if (_isEnabled != value) - { - _isEnabled = value; - OnPropertyChanged(); - } - } - } - - [XmlIgnore] - public string Description => Resources.Settings.AutoCompletesPage.ResourceManager.GetString(Key + "Description"); - - public override bool Equals(object obj) - { - var other = obj as AutoCompleteSetting; - return other != null && other.Key == Key; - } - - public override int GetHashCode() - { - return VBEditor.HashCode.Compute(Key); - } - } -} \ No newline at end of file diff --git a/Rubberduck.Core/Settings/AutoCompleteSettings.cs b/Rubberduck.Core/Settings/AutoCompleteSettings.cs index a11496d023..a8fc519e70 100644 --- a/Rubberduck.Core/Settings/AutoCompleteSettings.cs +++ b/Rubberduck.Core/Settings/AutoCompleteSettings.cs @@ -7,69 +7,84 @@ namespace Rubberduck.Settings { + [Flags] + public enum ModifierKeySetting + { + None = 0, + CtrlKey = 1, + ShiftKey = 2, + } + public interface IAutoCompleteSettings { - HashSet AutoCompletes { get; set; } + bool IsEnabled { get; set; } + AutoCompleteSettings.SmartConcatSettings SmartConcat { get; set; } + AutoCompleteSettings.SelfClosingPairSettings SelfClosingPairs { get; set; } + AutoCompleteSettings.BlockCompletionSettings BlockCompletion { get; set; } } [SettingsSerializeAs(SettingsSerializeAs.Xml)] [XmlType(AnonymousType = true)] public class AutoCompleteSettings : IAutoCompleteSettings, IEquatable { - [XmlArrayItem("AutoComplete", IsNullable = false)] - public HashSet AutoCompletes { get; set; } + public AutoCompleteSettings() + { + SmartConcat = new SmartConcatSettings(); + SelfClosingPairs = new SelfClosingPairSettings(); + BlockCompletion = new BlockCompletionSettings(); + } [XmlAttribute] public bool IsEnabled { get; set; } - [XmlAttribute] - public bool CompleteBlockOnTab { get; set; } - - [XmlAttribute] - public bool CompleteBlockOnEnter { get; set; } + public SmartConcatSettings SmartConcat { get; set; } - [XmlAttribute] - public bool EnableSmartConcat { get; set; } + public SelfClosingPairSettings SelfClosingPairs { get; set; } - public AutoCompleteSettings() : this(Enumerable.Empty()) - { - /* default constructor required for XML serialization */ - } + public BlockCompletionSettings BlockCompletion { get; set; } - public AutoCompleteSettings(IEnumerable defaultSettings) + public class SmartConcatSettings : IEquatable { - AutoCompletes = new HashSet(defaultSettings); + public bool IsEnabled { get; set; } + public ModifierKeySetting ConcatVbNewLineModifier { get; set; } + + public bool Equals(SmartConcatSettings other) + => other != null && + other.IsEnabled == IsEnabled && + other.ConcatVbNewLineModifier == ConcatVbNewLineModifier; } - public AutoCompleteSetting GetSetting() where TAutoComplete : IAutoComplete + public class SelfClosingPairSettings : IEquatable { - return AutoCompletes.FirstOrDefault(s => typeof(TAutoComplete).Name.Equals(s.Key)) - ?? GetSetting(typeof(TAutoComplete)); + [XmlAttribute] + public bool IsEnabled { get; set; } + + public bool Equals(SelfClosingPairSettings other) + => other != null && + other.IsEnabled == IsEnabled; } - public AutoCompleteSetting GetSetting(Type autoCompleteType) + public class BlockCompletionSettings : IEquatable { - try - { - var existing = AutoCompletes.FirstOrDefault(s => autoCompleteType.Name.Equals(s.Key)); - if (existing != null) - { - return existing; - } - var proto = Convert.ChangeType(Activator.CreateInstance(autoCompleteType, new object[] { null }), autoCompleteType); - var setting = new AutoCompleteSetting(proto as IAutoComplete); - AutoCompletes.Add(setting); - return setting; - } - catch (Exception) - { - return null; - } + [XmlAttribute] + public bool IsEnabled { get; set; } + [XmlAttribute] + public bool CompleteOnEnter { get; set; } + [XmlAttribute] + public bool CompleteOnTab { get; set; } + + public bool Equals(BlockCompletionSettings other) + => other != null && + other.IsEnabled == IsEnabled && + other.CompleteOnEnter == CompleteOnEnter && + other.CompleteOnTab == CompleteOnTab; } public bool Equals(AutoCompleteSettings other) - { - return other != null && AutoCompletes.SequenceEqual(other.AutoCompletes); - } + => other != null && + other.IsEnabled == IsEnabled && + other.BlockCompletion.Equals(BlockCompletion) && + other.SmartConcat.Equals(SmartConcat) && + other.SelfClosingPairs.Equals(SelfClosingPairs); } } diff --git a/Rubberduck.Core/Settings/ConfigurationLoader.cs b/Rubberduck.Core/Settings/ConfigurationLoader.cs index b076e0b676..db476f596a 100644 --- a/Rubberduck.Core/Settings/ConfigurationLoader.cs +++ b/Rubberduck.Core/Settings/ConfigurationLoader.cs @@ -97,10 +97,10 @@ public void SaveConfiguration(Configuration toSerialize) var newInspectionSettings = toSerialize.UserSettings.CodeInspectionSettings.CodeInspections.Select(s => Tuple.Create(s.Name, s.Severity)); var inspectionsChanged = !oldInspectionSettings.SequenceEqual(newInspectionSettings); var inspectOnReparse = toSerialize.UserSettings.CodeInspectionSettings.RunInspectionsOnSuccessfulParse; - var oldAutoCompleteSettings = _autoCompleteProvider.Create().AutoCompletes.Select(s => Tuple.Create(s.Key, s.IsEnabled)); - var newAutoCompleteSettings = toSerialize.UserSettings.AutoCompleteSettings.AutoCompletes.Select(s => Tuple.Create(s.Key, s.IsEnabled)); - var autoCompletesChanged = !oldAutoCompleteSettings.SequenceEqual(newAutoCompleteSettings) || - toSerialize.UserSettings.AutoCompleteSettings.IsEnabled != _autoCompleteProvider.Create().IsEnabled; + + var oldAutoCompleteSettings = _autoCompleteProvider.Create(); + var newAutoCompleteSettings = toSerialize.UserSettings.AutoCompleteSettings; + var autoCompletesChanged = oldAutoCompleteSettings.Equals(newAutoCompleteSettings); _generalProvider.Save(toSerialize.UserSettings.GeneralSettings); _hotkeyProvider.Save(toSerialize.UserSettings.HotkeySettings); diff --git a/Rubberduck.Core/UI/Settings/AutoCompleteSettings.xaml b/Rubberduck.Core/UI/Settings/AutoCompleteSettings.xaml index e7d9c813d6..21f1545bbf 100644 --- a/Rubberduck.Core/UI/Settings/AutoCompleteSettings.xaml +++ b/Rubberduck.Core/UI/Settings/AutoCompleteSettings.xaml @@ -53,10 +53,8 @@ - - + - - - - - - - - - - - - - - - - - - - - - + + - diff --git a/Rubberduck.Core/UI/Settings/AutoCompleteSettingsViewModel.cs b/Rubberduck.Core/UI/Settings/AutoCompleteSettingsViewModel.cs index 27bb1c150e..4097748e7b 100644 --- a/Rubberduck.Core/UI/Settings/AutoCompleteSettingsViewModel.cs +++ b/Rubberduck.Core/UI/Settings/AutoCompleteSettingsViewModel.cs @@ -3,10 +3,6 @@ using Rubberduck.Settings; using Rubberduck.SettingsProvider; using Rubberduck.UI.Command; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System; namespace Rubberduck.UI.Settings { @@ -19,20 +15,6 @@ public AutoCompleteSettingsViewModel(Configuration config) ImportButtonCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), _ => ImportSettings()); } - private ObservableCollection _settings; - public ObservableCollection Settings - { - get { return _settings; } - set - { - if (_settings != value) - { - _settings = value; - OnPropertyChanged(); - } - } - } - public void SetToDefaults(Configuration config) { TransferSettingsToView(config.UserSettings.AutoCompleteSettings); @@ -41,19 +23,30 @@ public void SetToDefaults(Configuration config) public void UpdateConfig(Configuration config) { config.UserSettings.AutoCompleteSettings.IsEnabled = IsEnabled; - config.UserSettings.AutoCompleteSettings.CompleteBlockOnTab = CompleteBlockOnTab; - config.UserSettings.AutoCompleteSettings.CompleteBlockOnEnter = CompleteBlockOnEnter; - config.UserSettings.AutoCompleteSettings.EnableSmartConcat = EnableSmartConcat; - config.UserSettings.AutoCompleteSettings.AutoCompletes = new HashSet(_settings); + + config.UserSettings.AutoCompleteSettings.SelfClosingPairs.IsEnabled = EnableSelfClosingPairs; + + config.UserSettings.AutoCompleteSettings.SmartConcat.IsEnabled = EnableSmartConcat; + config.UserSettings.AutoCompleteSettings.SmartConcat.ConcatVbNewLineModifier = + ConcatVbNewLine ? ModifierKeySetting.CtrlKey : ModifierKeySetting.None; + + config.UserSettings.AutoCompleteSettings.BlockCompletion.IsEnabled = EnableBlockCompletion; + config.UserSettings.AutoCompleteSettings.BlockCompletion.CompleteOnTab = CompleteBlockOnTab; + config.UserSettings.AutoCompleteSettings.BlockCompletion.CompleteOnEnter = CompleteBlockOnEnter; } private void TransferSettingsToView(Rubberduck.Settings.AutoCompleteSettings toLoad) { IsEnabled = toLoad.IsEnabled; - CompleteBlockOnTab = toLoad.CompleteBlockOnTab; - CompleteBlockOnEnter = toLoad.CompleteBlockOnEnter; - EnableSmartConcat = toLoad.EnableSmartConcat; - Settings = new ObservableCollection(toLoad.AutoCompletes); + + EnableSelfClosingPairs = toLoad.SelfClosingPairs.IsEnabled; + + EnableSmartConcat = toLoad.SmartConcat.IsEnabled; + ConcatVbNewLine = toLoad.SmartConcat.ConcatVbNewLineModifier == ModifierKeySetting.CtrlKey; + + EnableBlockCompletion = toLoad.BlockCompletion.IsEnabled; + CompleteBlockOnTab = toLoad.BlockCompletion.CompleteOnTab; + CompleteBlockOnEnter = toLoad.BlockCompletion.CompleteOnEnter; } private bool _isEnabled; @@ -71,21 +64,17 @@ public bool IsEnabled } } - private bool _completeBlockOnTab; - public bool CompleteBlockOnTab + private bool _enableSelfClosingPairs; + + public bool EnableSelfClosingPairs { - get { return _completeBlockOnTab; } + get { return _enableSelfClosingPairs; } set { - if (_completeBlockOnTab != value) + if (_enableSelfClosingPairs != value) { - _completeBlockOnTab = value; + _enableSelfClosingPairs = value; OnPropertyChanged(); - if (!_completeBlockOnTab && !_completeBlockOnEnter) - { - // one must be enabled... - CompleteBlockOnEnter = true; - } } } } @@ -104,6 +93,36 @@ public bool EnableSmartConcat } } + private bool _concatVbNewLine; + + public bool ConcatVbNewLine + { + get { return _concatVbNewLine; } + set + { + if (_concatVbNewLine != value) + { + _concatVbNewLine = value; + OnPropertyChanged(); + } + } + } + + private bool _enableBlockCompletion; + + public bool EnableBlockCompletion + { + get { return _enableBlockCompletion; } + set + { + if (_enableBlockCompletion != value) + { + _enableBlockCompletion = value; + OnPropertyChanged(); + } + } + } + private bool _completeBlockOnEnter; public bool CompleteBlockOnEnter { @@ -123,26 +142,21 @@ public bool CompleteBlockOnEnter } } - private bool? _selectAll; - public bool? SelectAll + private bool _completeBlockOnTab; + public bool CompleteBlockOnTab { - get - { - return _selectAll; - } + get { return _completeBlockOnTab; } set { - if (_selectAll != value) + if (_completeBlockOnTab != value) { - _selectAll = value; - foreach (var setting in Settings) + _completeBlockOnTab = value; + OnPropertyChanged(); + if (!_completeBlockOnTab && !_completeBlockOnEnter) { - if (setting.IsEnabled != (value ?? false)) - { - setting.IsEnabled = value ?? false; - } + // one must be enabled... + CompleteBlockOnEnter = true; } - OnPropertyChanged(); } } } @@ -176,8 +190,23 @@ private void ExportSettings() var service = new XmlPersistanceService { FilePath = dialog.FileName }; service.Save(new Rubberduck.Settings.AutoCompleteSettings { - CompleteBlockOnTab = this.CompleteBlockOnTab, - AutoCompletes = new HashSet(Settings), + IsEnabled = IsEnabled, + BlockCompletion = new Rubberduck.Settings.AutoCompleteSettings.BlockCompletionSettings + { + CompleteOnEnter = CompleteBlockOnEnter, + CompleteOnTab = CompleteBlockOnTab, + IsEnabled = EnableBlockCompletion + }, + SelfClosingPairs = new Rubberduck.Settings.AutoCompleteSettings.SelfClosingPairSettings + { + IsEnabled = EnableSelfClosingPairs + }, + SmartConcat = new Rubberduck.Settings.AutoCompleteSettings.SmartConcatSettings + { + ConcatVbNewLineModifier = + ConcatVbNewLine ? ModifierKeySetting.CtrlKey : ModifierKeySetting.None, + IsEnabled = EnableSmartConcat + } }); } } diff --git a/Rubberduck.Core/UI/UnitTesting/TestExplorerViewModel.cs b/Rubberduck.Core/UI/UnitTesting/TestExplorerViewModel.cs index 7794e69aca..7dc2276436 100644 --- a/Rubberduck.Core/UI/UnitTesting/TestExplorerViewModel.cs +++ b/Rubberduck.Core/UI/UnitTesting/TestExplorerViewModel.cs @@ -99,7 +99,7 @@ private void TestEngineTestCompleted(object sender, TestCompletedEventArgs e) public INavigateSource SelectedItem => SelectedTest; private TestMethodViewModel _selectedTest; - internal TestMethodViewModel SelectedTest + public TestMethodViewModel SelectedTest { get => _selectedTest; set diff --git a/Rubberduck.Core/app.config b/Rubberduck.Core/app.config index 420d811cae..b01edd1d62 100644 --- a/Rubberduck.Core/app.config +++ b/Rubberduck.Core/app.config @@ -233,33 +233,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Rubberduck.Main/Root/RubberduckIoCInstaller.cs b/Rubberduck.Main/Root/RubberduckIoCInstaller.cs index 4dda7dbdee..c572621dc6 100644 --- a/Rubberduck.Main/Root/RubberduckIoCInstaller.cs +++ b/Rubberduck.Main/Root/RubberduckIoCInstaller.cs @@ -13,52 +13,43 @@ using Rubberduck.Common; using Rubberduck.Common.Hotkeys; using Rubberduck.Inspections.Rubberduck.Inspections; -using Rubberduck.Navigation.CodeExplorer; using Rubberduck.Parsing; using Rubberduck.Parsing.ComReflection; using Rubberduck.Parsing.Inspections.Abstract; using Rubberduck.Parsing.PreProcessing; -using Rubberduck.Parsing.Symbols; using Rubberduck.Parsing.Symbols.DeclarationLoaders; using Rubberduck.Parsing.VBA; using Rubberduck.Settings; using Rubberduck.SettingsProvider; using Rubberduck.SmartIndenter; using Rubberduck.UI; -using Rubberduck.UI.CodeExplorer; -using Rubberduck.UI.CodeExplorer.Commands; using Rubberduck.UI.Command; using Rubberduck.UI.Command.MenuItems; using Rubberduck.UI.Command.MenuItems.CommandBars; using Rubberduck.UI.Command.MenuItems.ParentMenus; -using Rubberduck.UI.Command.Refactorings; using Rubberduck.UI.Controls; -using Rubberduck.UI.Inspections; using Rubberduck.UI.Refactorings; using Rubberduck.UI.Refactorings.Rename; -using Rubberduck.UI.ToDoItems; using Rubberduck.UI.UnitTesting; using Rubberduck.UnitTesting; using Rubberduck.VBEditor.SafeComWrappers.Abstract; using Component = Castle.MicroKernel.Registration.Component; -using Rubberduck.UI.CodeMetrics; using Rubberduck.VBEditor.ComManagement; using Rubberduck.Parsing.Common; using Rubberduck.VBEditor.ComManagement.TypeLibsAPI; using Rubberduck.VBEditor.Events; using Rubberduck.VBEditor.Utility; using Rubberduck.AutoComplete; +using Rubberduck.AutoComplete.Service; using Rubberduck.CodeAnalysis.CodeMetrics; using Rubberduck.Parsing.Rewriter; using Rubberduck.Parsing.VBA.ComReferenceLoading; using Rubberduck.Parsing.VBA.DeclarationResolving; -using Rubberduck.Parsing.VBA.Extensions; using Rubberduck.Parsing.VBA.Parsing; using Rubberduck.Parsing.VBA.ReferenceManagement; using Rubberduck.VBEditor; using Rubberduck.VBEditor.ComManagement.TypeLibs; using Rubberduck.VBEditor.SourceCodeHandling; -using Rubberduck.Interaction.Navigation; using Rubberduck.Parsing.VBA.DeclarationCaching; using Rubberduck.Parsing.VBA.Parsing.ParsingExceptions; @@ -271,6 +262,9 @@ private void RegisterFactories(IWindsorContainer container, Assembly[] assemblie private void RegisterSpecialFactories(IWindsorContainer container) { + container.Register(Component.For() + .ImplementedBy() + .LifestyleSingleton()); container.Register(Component.For() .ImplementedBy() .LifestyleSingleton()); @@ -322,7 +316,7 @@ private void RegisterAutoCompletes(IWindsorContainer container, Assembly[] assem { container.Register(Classes.FromAssembly(assembly) .IncludeNonPublicTypes() - .BasedOn() + .BasedOn() .If(type => type.NotDisabledOrExperimental(_initialSettings)) .WithService.Base() .LifestyleTransient()); diff --git a/Rubberduck.Parsing/Grammar/VBAParser.g4 b/Rubberduck.Parsing/Grammar/VBAParser.g4 index b0a466dcef..a9b91491c2 100644 --- a/Rubberduck.Parsing/Grammar/VBAParser.g4 +++ b/Rubberduck.Parsing/Grammar/VBAParser.g4 @@ -681,8 +681,7 @@ builtInType : ; // 5.6.13.1 Argument Lists -argumentList : - whiteSpace? (argument? (whiteSpace? COMMA whiteSpace? argument)*)?? +argumentList : whiteSpace? (argument? (whiteSpace? COMMA whiteSpace? argument)*)?? whiteSpace? ; requiredArgument : argument; diff --git a/Rubberduck.Resources/Settings/AutoCompletesPage.resx b/Rubberduck.Resources/Settings/AutoCompletesPage.resx index f1a063c905..4d08f1258b 100644 --- a/Rubberduck.Resources/Settings/AutoCompletesPage.resx +++ b/Rubberduck.Resources/Settings/AutoCompletesPage.resx @@ -117,61 +117,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Close curly braces '{' - - - Close square brackets '[' - - - Close parentheses '(' - - - Close string literals '"' - - - Close 'Do [Until|While]...Loop' loop blocks - - - Close 'Enum' blocks - - - Close 'For [Each]...Next' loop blocks - - - Close 'If' blocks - - - Treat 'On Error Resume Next...GoTo 0' as a block - - - Close precompiler '#If' blocks - - - Close 'Select' blocks - - - Close 'Type' blocks - - - Close 'While...Wend' loop blocks - - - Close 'With' blocks - - - Override 'Sub' member block completion - - - Override 'Function' member block completion - - - Override 'Property' member block completion - - + Autocomplete blocks on TAB - + Autocomplete blocks on ENTER @@ -180,7 +129,28 @@ Configure which Rubberduck autocompletions are enabled. + + Block Completion + + + Concatenate 'vbNewLine' on Ctrl+Enter + + + Enable autocompletion features + + + Enable block completion + + + Enable self-closing pairs + - Enable smart concatenation + Enable smart-concatenation + + + Self-Closing Pairs + + + Smart-Concatenation \ No newline at end of file diff --git a/Rubberduck.Resources/Settings/SettingsUI.resx b/Rubberduck.Resources/Settings/SettingsUI.resx index 48d8f5ef5b..16d76bce28 100644 --- a/Rubberduck.Resources/Settings/SettingsUI.resx +++ b/Rubberduck.Resources/Settings/SettingsUI.resx @@ -120,9 +120,6 @@ Reset settings to default configuration? - - Enable autocompletion. Feature isn't fully completed and may behave in unintended ways. - Export diff --git a/Rubberduck.VBEEditor/CodeString.cs b/Rubberduck.VBEEditor/CodeString.cs new file mode 100644 index 0000000000..041e13039a --- /dev/null +++ b/Rubberduck.VBEEditor/CodeString.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Rubberduck.VBEditor +{ + public class TestCodeString : CodeString + { + public static readonly char PseudoCaret = '|'; + + public TestCodeString(CodeString codeString) + : this(codeString.Code, codeString.CaretPosition, codeString.SnippetPosition) + { } + + public TestCodeString(string code, Selection zPosition, Selection pPosition = default) + : base(code, zPosition, pPosition) + { } + + public override string ToString() + { + return InsertPseudoCaret(); + } + + private string InsertPseudoCaret() + { + if (string.IsNullOrEmpty(Code)) + { + return string.Empty; + } + + var lines = Lines; + var line = lines[CaretPosition.StartLine]; + lines[CaretPosition.StartLine] = line.Insert(Math.Min(CaretPosition.StartColumn, line.Length), PseudoCaret.ToString()); + return string.Join("\r\n", lines); + } + } + + /// + /// Represents a code string that includes caret position. + /// + public class CodeString : IEquatable + { + /// + /// Creates a new CodeString + /// + /// Code string + /// Zero-based caret position in the code string. + /// One-based selection span of the code string in the containing module. + public CodeString(string code, Selection zPosition, Selection pPosition = default) + { + Code = code ?? throw new ArgumentNullException(nameof(code)); + CaretPosition = zPosition; + + var lines = Lines; + SnippetPosition = pPosition == default + ? new Selection(1, 1, lines.Length, lines[lines.Length-1].Length) + : pPosition; + } + + /// + /// The code string. + /// + public string Code { get; } + /// + /// Zero-based caret position in the code string. + /// + public Selection CaretPosition { get; } + /// + /// Gets the 0-based index of the caret position in the flattened string. + /// + public int CaretCharIndex + { + get + { + var i = 0; + for (var line = 0; line <= CaretPosition.StartLine; line++) + { + if (line < CaretPosition.StartLine) + { + i += Lines[line].Length; + } + else + { + i += CaretPosition.StartColumn; + return i; + } + + i += 2; // "\r\n" + } + + return i; + } + } + /// + /// One-based position of the code string in the containing module. + /// + public Selection SnippetPosition { get; } + /// + /// Gets the individual string lines. + /// + public string[] Lines => Code?.Replace("\r", string.Empty).Split('\n') ?? new string[] { }; + /// + /// Gets the contents of the line that is immediately before the line that contains the caret. + /// + public string PreviousLine => CaretPosition.StartLine == 0 ? null : Lines[CaretPosition.StartLine - 1]; + /// + /// Gets the contents of the line that is immediately after the line that contains the caret. + /// + public string NextLine => CaretPosition.StartLine == Lines.Length ? null : Lines[CaretPosition.StartLine + 1]; + + /// + /// Gets the contents of the line that contains the caret. + /// + public string CaretLine => Lines[CaretPosition.StartLine]; + + public CodeString ReplaceLine(int index, string content) + { + var lines = Lines; + Debug.Assert(index >= 0 && index < lines.Length); + + lines[index] = content; + var code = string.Join("\r\n", lines); + return new CodeString(code, CaretPosition, SnippetPosition); + } + + private static readonly IReadOnlyList ValidRemCommentMarkers = + new [] + { + "Rem" + ' ', + "Rem" + '?', + "Rem" + '<', + "Rem" + '>', + "Rem" + '{', + "Rem" + '}', + "Rem" + '~', + "Rem" + '`', + "Rem" + '!', + "Rem" + '/', + "Rem" + '*', + "Rem" + '(', + "Rem" + ')', + "Rem" + '-', + "Rem" + '=', + "Rem" + '+', + "Rem" + '\\', + "Rem" + '|', + "Rem" + ';', + "Rem" + ':', + "Rem" + '\'', + "Rem" + '"', + "Rem" + ',', + "Rem" + '.', + }; + + public bool IsComment + { + get + { + var noIndent = CaretLine.TrimStart(); + if (noIndent.StartsWith("'") || noIndent.StartsWith("rem ", StringComparison.InvariantCultureIgnoreCase)) + { + // no-brainer comment + return true; + } + + var stripped = StripBracketedExpressions(StripStringLiterals(Code)); + var length = stripped.Length; + var leftOfCaret = stripped.Substring(0, Math.Max(0, Math.Min(length - 1, CaretCharIndex))); + if (leftOfCaret.IndexOf('\'') >= 0) + { + // single-quote comment + return true; + } + else + { + // Rem comment + var instructions = leftOfCaret.Split(':'); + return ValidRemCommentMarkers.Any(marker => instructions.Any(instruction => instruction.TrimStart().StartsWith(marker))); + } + + } + } + + private string StripStringLiterals(string line) + { + return Regex.Replace(line, "\"[^\"]*\"", match => new string(' ', match.Length)); + } + + private string StripBracketedExpressions(string line) + { + return Regex.Replace(line, "\\[[^\\]]*\\]", match => new string(' ', match.Length)); + } + + public bool IsInsideStringLiteral + { + get + { + if (string.IsNullOrWhiteSpace(CaretLine) || !CaretLine.Substring(0, CaretPosition.StartColumn).Contains('"') || IsComment) + { + return false; + } + + var stringStart = CaretLine.IndexOf('"'); + var escaped = CaretLine.Substring(0, stringStart + 1) + + CaretLine.Substring(stringStart + 1).Replace("\"\"", "__"); + + var leftOfCaret = escaped.Substring(0, CaretPosition.StartColumn); + var rightOfCaret = escaped.Substring(Math.Min(CaretPosition.StartColumn + 1, CaretLine.Length - 1)); + if (!rightOfCaret.Contains('"') || CaretPosition.StartColumn + 1 > CaretLine.Length) + { + // the string isn't terminated, but VBE would terminate it here. + rightOfCaret += '"'; + } + + // odd number of double quotes on either side of the caret means we're inside a string literal: + return (leftOfCaret.Count(c => c.Equals('"')) % 2) != 0 && + (rightOfCaret.Count(c => c.Equals('"')) % 2) != 0; + } + } + + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + + var other = (CodeString)obj; + return Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Compute(Code, CaretPosition); + } + + public override string ToString() + { + return Code; + } + + public bool Equals(CodeString other) + { + if (other == null) + { + return false; + } + return (Code == null && other.Code == null) + || (Code != null && Code.Equals(other.Code) && CaretPosition.Equals(other.CaretPosition)); + } + } +} diff --git a/Rubberduck.VBEEditor/Events/AutoCompleteEventArgs.cs b/Rubberduck.VBEEditor/Events/AutoCompleteEventArgs.cs index b43075e77c..a2092c30f4 100644 --- a/Rubberduck.VBEEditor/Events/AutoCompleteEventArgs.cs +++ b/Rubberduck.VBEEditor/Events/AutoCompleteEventArgs.cs @@ -6,26 +6,24 @@ namespace Rubberduck.VBEditor.Events public class AutoCompleteEventArgs : EventArgs { public AutoCompleteEventArgs(ICodeModule module, KeyPressEventArgs e) + : this(module, e.Character, e.ControlDown, e.IsDelete) { } + + public AutoCompleteEventArgs(ICodeModule module, char character, bool isControlKeyDown, bool isDeleteKey) { - Character = e.Character; - CodeModule = module; - CurrentSelection = module.GetQualifiedSelection().Value.Selection; - CurrentLine = module.GetLines(CurrentSelection); - ControlDown = e.ControlDown; - IsDelete = e.IsDelete; + Module = module; + IsControlKeyDown = isControlKeyDown; + Character = character; + IsDeleteKey = isDeleteKey; } + public ICodeModule Module { get; } + /// /// true if the character has been handled, i.e. written to the code pane. /// Set to true to swallow the character and prevent the WM message from reaching the code pane. /// public bool Handled { get; set; } - /// - /// The CodeModule wrapper for the module being edited. - /// - public ICodeModule CodeModule { get; } - /// /// The character whose key was pressed (Enter is always '\r'). Default value if Delete was pressed. /// @@ -34,21 +32,11 @@ public AutoCompleteEventArgs(ICodeModule module, KeyPressEventArgs e) /// /// true if the left control key was down on the keypress. /// - public bool ControlDown { get; } + public bool IsControlKeyDown { get; } /// /// true if the Delete key generated the event. /// - public bool IsDelete { get; } - - /// - /// The current location of the caret. - /// - public Selection CurrentSelection { get; } - - /// - /// The contents of the current line of code. - /// - public string CurrentLine { get; } + public bool IsDeleteKey { get; } } } diff --git a/Rubberduck.VBEEditor/Events/KeyPressEventArgs.cs b/Rubberduck.VBEEditor/Events/KeyPressEventArgs.cs index 03f4ecd7e3..87b0c999c1 100644 --- a/Rubberduck.VBEEditor/Events/KeyPressEventArgs.cs +++ b/Rubberduck.VBEEditor/Events/KeyPressEventArgs.cs @@ -15,8 +15,19 @@ public KeyPressEventArgs(IntPtr hwnd, IntPtr wParam, IntPtr lParam, bool keydown if (keydown) { - // Why \r and not \n? Because it really doesn't matter... - Character = ((Keys)wParam & Keys.KeyCode) == Keys.Enter? '\r' : default; + if (((Keys) wParam & Keys.KeyCode) == Keys.Enter) + { + // Why \r and not \n? Because it really doesn't matter... + Character = '\r'; + } + else if (((Keys) wParam & Keys.KeyCode) == Keys.Back) + { + Character = '\b'; + } + else + { + Character = default; + } } else { diff --git a/Rubberduck.VBEEditor/Events/VBENativeServices.cs b/Rubberduck.VBEEditor/Events/VBENativeServices.cs index 5b563a6762..c2bf5017dc 100644 --- a/Rubberduck.VBEEditor/Events/VBENativeServices.cs +++ b/Rubberduck.VBEEditor/Events/VBENativeServices.cs @@ -166,7 +166,6 @@ private static void OnKeyDown(KeyPressEventArgs e) { using (var module = pane.CodeModule) { - // bug: Keys.Enter == Keys.M var args = new AutoCompleteEventArgs(module, e); Suspend = true; diff --git a/Rubberduck.VBEEditor/SourceCodeHandling/CodePaneSourceCodeHandler.cs b/Rubberduck.VBEEditor/SourceCodeHandling/CodePaneSourceCodeHandler.cs index 9b7aa124e5..911f570885 100644 --- a/Rubberduck.VBEEditor/SourceCodeHandling/CodePaneSourceCodeHandler.cs +++ b/Rubberduck.VBEEditor/SourceCodeHandling/CodePaneSourceCodeHandler.cs @@ -1,9 +1,12 @@ -using Rubberduck.VBEditor.ComManagement; +using System; +using System.Collections.Generic; +using System.Linq; +using Rubberduck.VBEditor.ComManagement; using Rubberduck.VBEditor.SafeComWrappers.Abstract; namespace Rubberduck.VBEditor.SourceCodeHandling { - public class CodePaneSourceCodeHandler : ISourceCodeHandler + public class CodePaneSourceCodeHandler : ICodePaneHandler { private readonly IProjectsProvider _projectsProvider; @@ -26,6 +29,48 @@ public string SourceCode(QualifiedModuleName module) } } + public void SetSelection(QualifiedModuleName module, Selection selection) + { + var component = _projectsProvider.Component(module); + if (component == null) + { + return; + } + + using (var codeModule = component.CodeModule) + { + SetSelection(codeModule, selection); + } + } + + public void SetSelection(ICodeModule module, Selection selection) + { + using (var pane = module.CodePane) + { + pane.Selection = selection; + } + } + + public void SubstituteCode(ICodeModule module, CodeString newCode) + { + module.DeleteLines(newCode.SnippetPosition); + module.InsertLines(newCode.SnippetPosition.StartLine, newCode.Code); + } + + public void SubstituteCode(QualifiedModuleName module, CodeString newCode) + { + var component = _projectsProvider.Component(module); + if (component == null) + { + return; + } + + using (var codeModule = component.CodeModule) + { + SubstituteCode(codeModule, newCode); + } + } + public void SubstituteCode(QualifiedModuleName module, string newCode) { var component = _projectsProvider.Component(module); @@ -40,5 +85,171 @@ public void SubstituteCode(QualifiedModuleName module, string newCode) codeModule.InsertLines(1, newCode); } } + + public CodeString Prettify(ICodeModule module, CodeString original) + { + var originalCode = original.Code.Replace("\r", string.Empty).Split('\n'); + var originalPosition = original.CaretPosition.StartColumn; + var originalNonWhitespaceCharacters = 0; + for (var i = 0; i <= Math.Min(originalPosition - 1, originalCode[original.CaretPosition.StartLine].Length - 1); i++) + { + if (originalCode[original.CaretPosition.StartLine][i] != ' ') + { + originalNonWhitespaceCharacters++; + } + } + + var indent = originalCode[original.CaretPosition.StartLine].TakeWhile(c => c == ' ').Count(); + + module.DeleteLines(original.SnippetPosition.StartLine, original.SnippetPosition.LineCount); + module.InsertLines(original.SnippetPosition.StartLine, string.Join("\r\n", originalCode)); + + var prettifiedCode = module.GetLines(original.SnippetPosition) + .Replace("\r", string.Empty) + .Split('\n'); + + var prettifiedNonWhitespaceCharacters = 0; + var prettifiedCaretCharIndex = 0; + for (var i = 0; i < prettifiedCode[original.CaretPosition.StartLine].Length; i++) + { + if (prettifiedCode[original.CaretPosition.StartLine][i] != ' ') + { + prettifiedNonWhitespaceCharacters++; + if (prettifiedNonWhitespaceCharacters == originalNonWhitespaceCharacters + || i == prettifiedCode[original.CaretPosition.StartLine].Length - 1) + { + prettifiedCaretCharIndex = i; + break; + } + } + } + + var prettifiedPosition = new Selection( + original.SnippetPosition.StartLine - 1 + original.CaretPosition.StartLine, + prettifiedCode[original.CaretPosition.StartLine].Trim().Length == 0 + ? indent + : Math.Min(prettifiedCode[original.CaretPosition.StartLine].Length, prettifiedCaretCharIndex + 1)) + .ToOneBased(); + + SetSelection(module, prettifiedPosition); + + return GetPrettifiedCodeString(original, prettifiedPosition, prettifiedCode); + } + + public CodeString Prettify(QualifiedModuleName module, CodeString original) + { + var component = _projectsProvider.Component(module); + if (component == null) + { + return original; + } + + using (var codeModule = component.CodeModule) + { + return Prettify(codeModule, original); + } + } + + private static CodeString GetPrettifiedCodeString(CodeString original, Selection prettifiedPosition, string[] prettifiedCode) + { + var caretPosition = new Selection(original.CaretPosition.StartLine, + prettifiedPosition.StartColumn - 1); // caretPosition is zero-based + + var snippetPosition = new Selection(original.SnippetPosition.StartLine, + original.SnippetPosition.StartColumn, original.SnippetPosition.EndLine, + prettifiedCode[prettifiedCode.Length - 1].Length); + + var result = new CodeString(string.Join("\r\n", prettifiedCode), caretPosition, snippetPosition); + return result; + } + + public CodeString GetCurrentLogicalLine(ICodeModule module) + { + const string lineContinuation = " _"; + + Selection pSelection; + using (var pane = module.CodePane) + { + pSelection = pane.Selection; + } + + var currentLineIndex = pSelection.StartLine; + var currentLine = module.GetLines(currentLineIndex, 1); + + var caretLine = (currentLineIndex, currentLine); + var lines = new List<(int Line, string Content)> {caretLine}; + + while (currentLineIndex >= 1) + { + currentLineIndex--; + if (currentLineIndex >= 1) + { + currentLine = module.GetLines(currentLineIndex, 1); + if (currentLine.EndsWith(lineContinuation)) + { + lines.Insert(0, (currentLineIndex, currentLine)); + } + else + { + break; + } + } + } + + currentLineIndex = pSelection.StartLine; + currentLine = caretLine.currentLine; + while (currentLineIndex <= module.CountOfLines && currentLine.EndsWith(lineContinuation)) + { + currentLineIndex++; + if (currentLineIndex <= module.CountOfLines) + { + currentLine = module.GetLines(currentLineIndex, 1); + lines.Add((currentLineIndex, currentLine)); + } + else + { + break; + } + } + + var logicalLine = string.Join("\r\n", lines.Select(e => e.Content)); + var zCaretLine = lines.IndexOf(caretLine); + var zCaretColumn = pSelection.StartColumn - 1; + + var startLine = lines[0].Line; + var endLine = lines[lines.Count - 1].Line; + + var result = new CodeString( + logicalLine, + new Selection(zCaretLine, zCaretColumn), + new Selection(startLine, 1, endLine, 1)); + + return result; + + } + + public CodeString GetCurrentLogicalLine(QualifiedModuleName module) + { + var component = _projectsProvider.Component(module); + if (component == null) + { + return null; + } + + using (var codeModule = component.CodeModule) + { + return GetCurrentLogicalLine(codeModule); + } + } + + public Selection GetSelection(QualifiedModuleName module) + { + using (var component = _projectsProvider.Component(module)) + using (var codeModule = component.CodeModule) + using (var pane = codeModule.CodePane) + { + return pane.Selection; + } + } } } diff --git a/Rubberduck.VBEEditor/SourceCodeHandling/ISourceCodeHandler.cs b/Rubberduck.VBEEditor/SourceCodeHandling/ISourceCodeHandler.cs index 5c10c0170e..857983aa72 100644 --- a/Rubberduck.VBEEditor/SourceCodeHandling/ISourceCodeHandler.cs +++ b/Rubberduck.VBEEditor/SourceCodeHandling/ISourceCodeHandler.cs @@ -1,7 +1,31 @@ -namespace Rubberduck.VBEditor.SourceCodeHandling +using Rubberduck.VBEditor.SafeComWrappers.Abstract; + +namespace Rubberduck.VBEditor.SourceCodeHandling { public interface ISourceCodeHandler : ISourceCodeProvider { + /// + /// Replaces the entire module's contents with the specified code. + /// void SubstituteCode(QualifiedModuleName module, string newCode); } + + public interface ICodePaneHandler : ISourceCodeHandler + { + /// + /// Replaces one or more specific line(s) in the specified module. + /// + void SubstituteCode(QualifiedModuleName module, CodeString newCode); + /// + /// Replaces one or more specific line(s) in the specified module. + /// + void SubstituteCode(ICodeModule module, CodeString newCode); + void SetSelection(ICodeModule module, Selection selection); + void SetSelection(QualifiedModuleName module, Selection selection); + CodeString Prettify(QualifiedModuleName module, CodeString original); + CodeString Prettify(ICodeModule module, CodeString original); + CodeString GetCurrentLogicalLine(ICodeModule module); + CodeString GetCurrentLogicalLine(QualifiedModuleName module); + Selection GetSelection(QualifiedModuleName module); + } } diff --git a/Rubberduck.VBEEditor/WindowsApi/CodePaneSubclass.cs b/Rubberduck.VBEEditor/WindowsApi/CodePaneSubclass.cs index 3eacc3ea19..fda7191850 100644 --- a/Rubberduck.VBEEditor/WindowsApi/CodePaneSubclass.cs +++ b/Rubberduck.VBEEditor/WindowsApi/CodePaneSubclass.cs @@ -27,7 +27,7 @@ public override int SubClassProc(IntPtr hWnd, IntPtr msg, IntPtr wParam, IntPtr { case WM.CHAR: args = new KeyPressEventArgs(hWnd, wParam, lParam); - if (args.Character != '\r' && args.Character != '\n') + if (args.Character != '\r' && args.Character != '\n' && args.Character != '\b') { OnKeyDown(args); if (args.Handled) { return 0; } @@ -35,9 +35,9 @@ public override int SubClassProc(IntPtr hWnd, IntPtr msg, IntPtr wParam, IntPtr break; case WM.KEYDOWN: args = new KeyPressEventArgs(hWnd, wParam, lParam, true); - // The only keydown we care about that doesn't generate a WM_CHAR is Delete, and the VBE handles Enter in WM_KEYDOWN, - // so we need to handle it first (otherwise it will already be in code when the managed event is handled). - if (args.IsDelete || args.Character == '\r') + // The only keydown we care about that doesn't generate a WM_CHAR is Delete, and the VBE handles Enter & backspace in WM_KEYDOWN, + // so we need to handle them first (otherwise it will already be in the code pane when the managed event is handled). + if (args.IsDelete || args.Character == '\r' || args.Character == '\b') { OnKeyDown(args); if (args.Handled) { return 0; } diff --git a/Rubberduck.VBEditor.VB6/SafeComWrappers/VB/CodeModule.cs b/Rubberduck.VBEditor.VB6/SafeComWrappers/VB/CodeModule.cs index f588b2d30b..1e0a9c85e5 100644 --- a/Rubberduck.VBEditor.VB6/SafeComWrappers/VB/CodeModule.cs +++ b/Rubberduck.VBEditor.VB6/SafeComWrappers/VB/CodeModule.cs @@ -1,7 +1,5 @@ +using System; using System.Diagnostics.CodeAnalysis; -using System.Security.Cryptography; -using System.Text; -using Rubberduck.VBEditor.Extensions; using Rubberduck.VBEditor.SafeComWrappers.Abstract; using VB = Microsoft.Vbe.Interop.VB6; @@ -122,26 +120,47 @@ public void AddFromFile(string path) public void InsertLines(int line, string content) { - if (IsWrappingNullReference) return; - Target.InsertLines(line, content); + if (IsWrappingNullReference) return; + + try + { + Target.InsertLines(line, content); + } + catch (Exception e) + { + _logger.Error(e); + } } public void DeleteLines(int startLine, int count = 1) { - if (IsWrappingNullReference) return; - Target.DeleteLines(startLine, count); + if (IsWrappingNullReference) return; + + try + { + Target.DeleteLines(startLine, count); + } + catch (Exception e) + { + _logger.Error(e); + } } public void ReplaceLine(int line, string content) { if (IsWrappingNullReference) return; - if (Target.CountOfLines == 0) + + try { - Target.AddFromString(content); + Target.ReplaceLine(line, content); + if (Target.CountOfLines == 0) + { + Target.AddFromString(content); + } } - else + catch (Exception e) { - Target.ReplaceLine(line, content); + _logger.Error(e); } } diff --git a/Rubberduck.VBEditor.VBA/SafeComWrappers/VB/CodeModule.cs b/Rubberduck.VBEditor.VBA/SafeComWrappers/VB/CodeModule.cs index 97f935748f..7bb0ecc6d3 100644 --- a/Rubberduck.VBEditor.VBA/SafeComWrappers/VB/CodeModule.cs +++ b/Rubberduck.VBEditor.VBA/SafeComWrappers/VB/CodeModule.cs @@ -1,7 +1,5 @@ +using System; using System.Diagnostics.CodeAnalysis; -using System.Security.Cryptography; -using System.Text; -using Rubberduck.VBEditor.Extensions; using Rubberduck.VBEditor.SafeComWrappers.Abstract; using VB = Microsoft.Vbe.Interop; @@ -122,8 +120,17 @@ public void AddFromFile(string path) public void InsertLines(int line, string content) { - if (IsWrappingNullReference) return; - Target.InsertLines(line, content); + if (IsWrappingNullReference) return; + try + { + Target.InsertLines(line, content); + + } + catch (Exception e) + { + // "too many line continuations" is one possible cause for a COMException here. + _logger.Error(e); + } } public void DeleteLines(int startLine, int count = 1) @@ -131,20 +138,30 @@ public void DeleteLines(int startLine, int count = 1) if (IsWrappingNullReference) return; if (Target.CountOfLines > 0) { - Target.DeleteLines(startLine, count); + + try + { + Target.DeleteLines(startLine, count); + + } + catch (Exception e) + { + // "too many line continuations" is one possible cause for a COMException here. + _logger.Error(e); + } } } public void ReplaceLine(int line, string content) { if (IsWrappingNullReference) return; - if (Target.CountOfLines == 0) - { - Target.AddFromString(content); - } - else + try { - try + if (Target.CountOfLines == 0) + { + Target.AddFromString(content); + } + else { using (var pane = CodePane) { @@ -153,7 +170,11 @@ public void ReplaceLine(int line, string content) pane.Selection = selection; } } - catch { /* "too many line continuations" is one possible cause */ } + } + catch (Exception e) + { + // "too many line continuations" is one possible cause for a COMException here. + _logger.Error(e); } } diff --git a/RubberduckTests/AutoComplete/CodePaneHandlerTests.cs b/RubberduckTests/AutoComplete/CodePaneHandlerTests.cs new file mode 100644 index 0000000000..a47c025c20 --- /dev/null +++ b/RubberduckTests/AutoComplete/CodePaneHandlerTests.cs @@ -0,0 +1,113 @@ +using Moq; +using NUnit.Framework; +using Rubberduck.VBEditor; +using Rubberduck.VBEditor.ComManagement; +using Rubberduck.VBEditor.SafeComWrappers; +using Rubberduck.VBEditor.SafeComWrappers.Abstract; +using Rubberduck.VBEditor.SourceCodeHandling; +using RubberduckTests.Mocks; + +namespace RubberduckTests.AutoComplete +{ + [TestFixture] + public class CodePaneHandlerTests + { + [Test] + [Category("AutoComplete")] + public void ActuallyDeletesAndInsertsOriginalLine() + { + var code = "MsgBox|".ToCodeString(); + + var sut = InitializeSut(code, code, out var module, out _); + sut.Prettify(module.Object, code); + + module.Verify(m => m.DeleteLines(code.SnippetPosition.StartLine, code.SnippetPosition.LineCount), Times.Once); + module.Verify(m => m.InsertLines(code.SnippetPosition.StartLine, code.Code), Times.Once); + } + + [Test] + [Category("AutoComplete")] + public void GivenSamePrettifiedCode_YieldsSameCodeString() + { + var original = "MsgBox (|".ToCodeString(); + + var sut = InitializeSut(original, original, out var module, out _); + var actual = new TestCodeString(sut.Prettify(module.Object, original)); + + Assert.AreEqual(original, actual); + } + + [Test] + [Category("AutoComplete")] + public void GivenLeadingWhitespace_YieldsSameCodeString() + { + var original = " MsgBox|".ToCodeString(); + + var sut = InitializeSut(original, original, out var module, out _); + var actual = new TestCodeString(sut.Prettify(module.Object, original)); + + Assert.AreEqual(original, actual); + } + + [Test] + [Category("AutoComplete")] + public void GivenTrailingWhitespace_IsTrimmedAndPrettifiedCaretIsAtLastCharacter() + { + var original = "MsgBox |".ToCodeString(); + var prettified = "MsgBox".ToCodeString(); + var expected = "MsgBox|".ToCodeString(); + + var sut = InitializeSut(original, prettified, out var module, out _); + var actual = new TestCodeString(sut.Prettify(module.Object, original)); + + Assert.AreEqual(expected, actual); + } + + [Test] + [Category("AutoComplete")] + public void GivenExtraWhitespace_PrettifiedCaretStillAtSameToken() + { + var original = "MsgBox (\"test|\")".ToCodeString(); + var prettified = "MsgBox (\"test\")".ToCodeString(); + var expected = "MsgBox (\"test|\")".ToCodeString(); + + var sut = InitializeSut(original, prettified, out var module, out _); + var actual = new TestCodeString(sut.Prettify(module.Object, original)); + + Assert.AreEqual(expected, actual); + } + + [Test] + [Category("AutoComplete")] + public void GivenMultilineLogicalLine_StillTracksCaret() + { + var original = @" +MsgBox ""test"" & vbNewLine & _ + ""|"")".ToCodeString(); + + var sut = InitializeSut(original, original, out var module, out _); + var actual = new TestCodeString(sut.Prettify(module.Object, original)); + + Assert.AreEqual(original, actual); + } + + private static ICodePaneHandler InitializeSut(TestCodeString original, TestCodeString prettified, out Mock module, out Mock pane) + { + var builder = new MockVbeBuilder(); + var project = builder.ProjectBuilder("TestProject1", ProjectProtection.Unprotected) + .AddComponent("Module1", ComponentType.StandardModule, ""); + var vbe = builder.AddProject(project.Build()).Build(); + + module = new Mock(); + pane = new Mock(); + pane.SetupProperty(m => m.Selection); + module.Setup(m => m.DeleteLines(original.SnippetPosition.StartLine, original.SnippetPosition.LineCount)); + module.Setup(m => m.InsertLines(original.SnippetPosition.StartLine, original.Code)); + module.Setup(m => m.CodePane).Returns(pane.Object); + module.Setup(m => m.GetLines(original.SnippetPosition)).Returns(prettified.Code); + + var sut = new CodePaneSourceCodeHandler(new ProjectsRepository(vbe.Object)); + return sut; + } + } +} \ No newline at end of file diff --git a/RubberduckTests/AutoComplete/CodeStringTests.cs b/RubberduckTests/AutoComplete/CodeStringTests.cs index a583de51a5..277246f111 100644 --- a/RubberduckTests/AutoComplete/CodeStringTests.cs +++ b/RubberduckTests/AutoComplete/CodeStringTests.cs @@ -9,28 +9,25 @@ namespace RubberduckTests.AutoComplete public class CodeStringTests { [Test] - public void ToStringIncludesCaretPipe() + public void TestCodeString_ToStringIncludesCaretPipe() { - var input = @"foo = MsgBox(|)"; - var sut = new CodeString(input, new Selection(0, input.IndexOf('|'))); - + var input = "foo = MsgBox(|)"; + var sut = input.ToCodeString(); Assert.AreEqual(input, sut.ToString()); } [Test] public void CodeExcludesCaretPipe() { - var input = @"foo = MsgBox(|)"; - var expected = @"foo = MsgBox()"; - var sut = new CodeString(input, new Selection(0, input.IndexOf('|'))); - + var sut = "foo = MsgBox(|)".ToCodeString(); + var expected = "foo = MsgBox()"; Assert.AreEqual(expected, sut.Code); } [Test] - public void SnippetPositionIsL1C1ifUnspecified() + public void SnippetPositionIsL1C1IfUnspecified() { - var sut = new CodeString("|", new Selection()); + var sut = new TestCodeString(TestCodeString.PseudoCaret.ToString(), new Selection()); Assert.AreEqual(Selection.Home, sut.SnippetPosition); } @@ -39,5 +36,82 @@ public void NullCodeStringArgThrows() { Assert.Throws(() => new CodeString(null, Selection.Empty)); } + + [Test] + public void IsInsideStringLiteral_TrueGivenCaretInsideSimpleString() + { + var sut = "foo = \"str|ing\"".ToCodeString(); + Assert.IsTrue(sut.IsInsideStringLiteral); + } + + [Test] + public void IsInsideStringLiteral_FalseGivenCaretOutsideSimpleString() + { + var sut = "foo = |\"string\"".ToCodeString(); + Assert.IsFalse(sut.IsInsideStringLiteral); + } + + [Test] + public void IsInsideStringLiteral_FalseGivenComment() + { + var sut = "'foo = \"|\"".ToCodeString(); + Assert.IsFalse(sut.IsInsideStringLiteral); + } + + [Test] + public void IsInsideStringLiteral_TrueGivenCaretInsideStringWithEscapedQuotes() + { + var sut = "foo = \"\"\"string|\"\"\"".ToCodeString(); + Assert.IsTrue(sut.IsInsideStringLiteral); + } + + [Test] + public void IsInsideStringLiteral_TrueGivenCaretInsideStringBetweenEscapedQuotes() + { + var sut = "foo = \"\"|\"string\"\"\"".ToCodeString(); + Assert.IsTrue(sut.IsInsideStringLiteral); + } + + [Test] + public void IsInsideStringLiteral_TrueGivenUnfinishedString() + { + var sut = "foo = \"unfinished string|".ToCodeString(); + Assert.IsTrue(sut.IsInsideStringLiteral); + } + + [Test] + public void IsComment_TrueGivenIsTrivialSingleQuoteComment() + { + var sut = "'\"not a string literal|, just a comment\"".ToCodeString(); + Assert.IsTrue(sut.IsComment); + } + + [Test] + public void IsComment_TrueGivenIsRemComment() + { + var sut = "Rem \"not a string literal|, just a comment\"".ToCodeString(); + Assert.IsTrue(sut.IsComment); + } + + [Test] + public void IsComment_TrueGivenIsRemCommentInSecondInstruction() + { + var sut = "foo = 2 + 2 : Rem \"not a string literal|, just a comment\"".ToCodeString(); + Assert.IsTrue(sut.IsComment); + } + + [Test] + public void IsComment_TrueGivenIsCommentStartingInPreviousPhysicalLine() + { + var sut = "' _\r\n\"not a string literal|, just a comment\"".ToCodeString(); + Assert.IsTrue(sut.IsComment); + } + + [Test] + public void IsComment_TrueGivenSingleQuoteCommentLine() + { + var sut = "'\"not a string literal|, just a comment\"".ToCodeString(); + Assert.IsTrue(sut.IsComment); + } } } diff --git a/RubberduckTests/AutoComplete/SelfClosingPairCompletionTests.cs b/RubberduckTests/AutoComplete/SelfClosingPairCompletionTests.cs index 46d136dc11..84cf38b42f 100644 --- a/RubberduckTests/AutoComplete/SelfClosingPairCompletionTests.cs +++ b/RubberduckTests/AutoComplete/SelfClosingPairCompletionTests.cs @@ -1,24 +1,61 @@ using NUnit.Framework; -using Rubberduck.AutoComplete.SelfClosingPairCompletion; -using Rubberduck.Common; using System.Windows.Forms; +using Rubberduck.AutoComplete.Service; +using Rubberduck.VBEditor; namespace RubberduckTests.AutoComplete { - [TestFixture] public class SelfClosingPairCompletionTests { - private CodeString Run(SelfClosingPair pair, CodeString original, char input) + private TestCodeString Run(SelfClosingPair pair, CodeString original, char input) { var sut = new SelfClosingPairCompletionService(null); - return sut.Execute(pair, original, input); + var result = sut.Execute(pair, original, input); + return result != null ? new TestCodeString(result) : null; } - private CodeString Run(SelfClosingPair pair, CodeString original, Keys input) - { + private TestCodeString Run(SelfClosingPair pair, CodeString original, Keys input) + { var sut = new SelfClosingPairCompletionService(null); - return sut.Execute(pair, original, input); + var result = sut.Execute(pair, original, input); + return result != null ? new TestCodeString(result) : null; + } + + [Test] + public void PlacesCaretBetweenOpeningAndClosingChars() + { + var pair = new SelfClosingPair('"', '"'); + var input = pair.OpeningChar; + var original = "foo = MsgBox |".ToCodeString(); + var expected = "foo = MsgBox \"|\"".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + + [Test] + public void PlacesCaretBetweenOpeningAndClosingChars_NestedPair() + { + var pair = new SelfClosingPair('"', '"'); + var input = pair.OpeningChar; + var original = "MsgBox (|)".ToCodeString(); + var expected = "MsgBox (\"|\")".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + + [Test] + public void PlacesCaretBetweenOpeningAndClosingChars_PreservesPosition() + { + var pair = new SelfClosingPair('(', ')'); + var input = pair.OpeningChar; + var original = "foo = |".ToCodeString(); + var expected = "foo = (|)".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); } [Test] @@ -62,11 +99,33 @@ public void DeletingOpeningCharRemovesPairedClosingChar_GivenOnlyThatOnTheLine() { var pair = new SelfClosingPair('"', '"'); var input = Keys.Back; - var original = $"{pair.OpeningChar}{pair.ClosingChar}".ToCodeString(); - var expected = string.Empty.ToCodeString(); + var original = $"{pair.OpeningChar}|{pair.ClosingChar}".ToCodeString(); + var expected = string.Empty; var result = Run(pair, original, input); - Assert.AreEqual(expected, result); + Assert.AreEqual(expected, result?.Code); + } + + [Test] + public void BackspacingInsideComment_BailsOut() + { + var pair = new SelfClosingPair('(', ')'); + var input = Keys.Back; + var original = "' _\r\n (|)".ToCodeString(); + + var result = Run(pair, original, input); + Assert.IsNull(result); + } + + [Test] + public void CanTypeClosingChar() + { + var pair = new SelfClosingPair('(', ')'); + var input = pair.ClosingChar; + var original = "foo = |".ToCodeString(); + + var result = Run(pair, original, input); + Assert.IsNull(result); } [Test] @@ -81,21 +140,70 @@ public void DeletingOpeningCharRemovesPairedClosingChar_NestedParens() Assert.AreEqual(expected, result); } + [Test] + public void DeletingOpeningChar_CallStmtArgList() + { + var pair = new SelfClosingPair('(', ')'); + var input = Keys.Back; + var original = "Call xy(|z)".ToCodeString(); + var expected = "Call xy|z".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + + [Test] + public void DeletingOpeningChar_IndexExpr() + { + var pair = new SelfClosingPair('(', ')'); + var input = Keys.Back; + var original = "foo = CInt(|z)".ToCodeString(); + var expected = "foo = CInt|z".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + [Test] public void DeletingOpeningCharRemovesPairedClosingChar_NestedParensMultiline() { var pair = new SelfClosingPair('(', ')'); var input = Keys.Back; - var original = @" -foo = (| _ - (2 + 2) + 42 _ -) + var original = @"foo = (| _ + (2 + 2) + 42) ".ToCodeString(); - var expected = @" -foo = | _ - (2 + 2) + 42 _ + var expected = @"foo = | _ + (2 + 2) + 42".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + + [Test] // fixme: passes, but in-editor behavior seems different. + public void DeletingPairInLogicalLine_SelectionRemainsOnThatLineIfNonEmpty() + { + var pair = new SelfClosingPair('"', '"'); + var input = Keys.Back; + var original = @"foo = ""abc"" & _ + ""|"" & ""a""".ToCodeString(); + var expected = @"foo = ""abc"" & _ + | & ""a""".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + [Test] + public void DeletingMatchingPair_RemovesTrailingEmptyContinuatedLine() + { + var pair = new SelfClosingPair('(', ')'); + var input = Keys.Back; + var original = @"foo = (| _ + (2 + 2) + 42 _ +) ".ToCodeString(); + var expected = @"foo = | _ + (2 + 2) + 42".ToCodeString(); var result = Run(pair, original, input); Assert.AreEqual(expected, result); @@ -125,6 +233,91 @@ public void WhenCaretBetweenOpeningAndClosingChars_BackspaceRemovesBoth() Assert.AreEqual(expected, result); } + [Test][Ignore("todo: figure out how to make this pass without breaking something else.")] + public void BackspacingWorksWhenCaretIsNotOnLastNonEmptyLine_ConcatOnSameLine() + { + var pair = new SelfClosingPair('"', '"'); + var input = Keys.Back; + var original = "foo = \"\" & _\r\n \"\" & _\r\n \"|\" & _\r\n \"\"".ToCodeString(); + var expected = "foo = \"\" & _\r\n \"|\" & _\r\n \"\"".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + + [Test] + [Ignore("todo: figure out how to make this pass without breaking something else.")] + public void BackspacingWorksWhenCaretIsNotOnLastNonEmptyLine_ConcatOnNextLine() + { + var pair = new SelfClosingPair('"', '"'); + var input = Keys.Back; + var original = "foo = \"\" _\r\n & \"\" _\r\n & \"|\" _\r\n & \"\"".ToCodeString(); + var expected = "foo = \"\" _\r\n & \"|\" _\r\n & \"\"".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + + [Test] + public void WhenBackspacingClearsLineContinuatedCaretLine_PlacesCaretInsideStringOnPreviousLine() + { + var pair = new SelfClosingPair('"', '"'); + var input = Keys.Back; + var original = "foo = \"test\" & _\r\n \"|\"".ToCodeString(); + var expected = "foo = \"test|\"".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + + [Test] + public void WhenBackspacingClearsLineContinuatedCaretLine_WithConcatenatedVbNewLine_PlacesCaretInsideStringOnPreviousLine() + { + var pair = new SelfClosingPair('"', '"'); + var input = Keys.Back; + var original = "foo = \"test\" & vbNewLine & _\r\n \"|\"".ToCodeString(); + var expected = "foo = \"test|\"".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + + [Test] + public void WhenBackspacingClearsLineContinuatedCaretLine_WithConcatenatedVbCrLf_PlacesCaretInsideStringOnPreviousLine() + { + var pair = new SelfClosingPair('"', '"'); + var input = Keys.Back; + var original = "foo = \"test\" & vbCrLf & _\r\n \"|\"".ToCodeString(); + var expected = "foo = \"test|\"".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + + [Test] + public void WhenBackspacingClearsLineContinuatedCaretLine_WithConcatenatedVbCr_PlacesCaretInsideStringOnPreviousLine() + { + var pair = new SelfClosingPair('"', '"'); + var input = Keys.Back; + var original = "foo = \"test\" & vbCr & _\r\n \"|\"".ToCodeString(); + var expected = "foo = \"test|\"".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + + [Test] + public void WhenBackspacingClearsLineContinuatedCaretLine_WithConcatenatedVbLf_PlacesCaretInsideStringOnPreviousLine() + { + var pair = new SelfClosingPair('"', '"'); + var input = Keys.Back; + var original = "foo = \"test\" & vbLf & _\r\n \"|\"".ToCodeString(); + var expected = "foo = \"test|\"".ToCodeString(); + + var result = Run(pair, original, input); + Assert.AreEqual(expected, result); + } + [Test] public void WhenCaretBetweenOpeningAndClosingChars_BackspaceRemovesBoth_Indented() { @@ -138,14 +331,14 @@ public void WhenCaretBetweenOpeningAndClosingChars_BackspaceRemovesBoth_Indented } [Test] - public void DeleteKey_ReturnsDefault() + public void DeleteKey_ReturnsNull() { var pair = new SelfClosingPair('(', ')'); var input = Keys.A; var original = @"MsgBox (|)".ToCodeString(); var result = Run(pair, original, input); - Assert.IsTrue(result == default); + Assert.IsNull(result); } [Test] @@ -156,7 +349,103 @@ public void UnhandledKey_ReturnsDefault() var original = @"MsgBox |".ToCodeString(); var result = Run(pair, original, input); - Assert.IsTrue(result == default); + Assert.AreEqual(result, default); + } + + [Test] + public void GivenClosingCharForUnmatchedOpeningChar_AsymetricPairBailsOut() + { + var pair = new SelfClosingPair('(', ')'); + if (pair.IsSymetric) + { + Assert.Inconclusive("Pair symetry is inconsistent with the purpose of the test."); + } + + var input = pair.ClosingChar; + var original = "MsgBox (|".ToCodeString(); + + var result = Run(pair, original, input); + Assert.IsNull(result); + } + + [Test] + public void GivenClosingCharForUnmatchedOpeningChar_SymetricPairBailsOut() + { + var pair = new SelfClosingPair('"', '"'); + if (!pair.IsSymetric) + { + Assert.Inconclusive("Pair symetry is inconsistent with the purpose of the test."); + } + + var input = pair.ClosingChar; + var original = "MsgBox \"|".ToCodeString(); + + var result = Run(pair, original, input); + Assert.IsNull(result); + } + + [Test] + public void GivenClosingCharForUnmatchedOpeningCharNonConsecutive_SymetricPairBailsOut() + { + var pair = new SelfClosingPair('"', '"'); + if (!pair.IsSymetric) + { + Assert.Inconclusive("Pair symetry is inconsistent with the purpose of the test."); + } + + var input = pair.ClosingChar; + var original = "MsgBox \"foo|".ToCodeString(); + + var result = Run(pair, original, input); + Assert.IsNull(result); + } + + [Test] + public void GivenOpeningCharInsideTerminatedStringLiteral_BailsOut() + { + var pair = new SelfClosingPair('(', ')'); + if (pair.IsSymetric) + { + Assert.Inconclusive("Pair symetry is inconsistent with the purpose of the test."); + } + + var input = pair.OpeningChar; + var original = "MsgBox \"foo|\"".ToCodeString(); + + var result = Run(pair, original, input); + Assert.IsNull(result); + } + + [Test] + public void GivenOpeningCharInsideNonTerminatedStringLiteral_BailsOut() + { + var pair = new SelfClosingPair('(', ')'); + if (pair.IsSymetric) + { + Assert.Inconclusive("Pair symetry is inconsistent with the purpose of the test."); + } + + var input = pair.OpeningChar; + var original = "MsgBox \"foo|".ToCodeString(); + + var result = Run(pair, original, input); + Assert.IsNull(result); + } + + [Test] + public void GivenClosingCharForUnmatchedOpeningChar_SingleChar_SymetricPairBailsOut() + { + var pair = new SelfClosingPair('"', '"'); + if (!pair.IsSymetric) + { + Assert.Inconclusive("Pair symetry is inconsistent with the purpose of the test."); + } + + var input = pair.ClosingChar; + var original = "\"|".ToCodeString(); + + var result = Run(pair, original, input); + Assert.IsNull(result); } } } diff --git a/RubberduckTests/AutoComplete/SmartConcatCompletionTests.cs b/RubberduckTests/AutoComplete/SmartConcatCompletionTests.cs new file mode 100644 index 0000000000..823bbade48 --- /dev/null +++ b/RubberduckTests/AutoComplete/SmartConcatCompletionTests.cs @@ -0,0 +1,121 @@ +using System.Linq; +using Moq; +using NUnit.Framework; +using Rubberduck.AutoComplete.Service; +using Rubberduck.Settings; +using Rubberduck.VBEditor; +using Rubberduck.VBEditor.ComManagement; +using Rubberduck.VBEditor.Events; +using Rubberduck.VBEditor.SafeComWrappers; +using Rubberduck.VBEditor.SafeComWrappers.Abstract; +using Rubberduck.VBEditor.SourceCodeHandling; +using RubberduckTests.Mocks; + +namespace RubberduckTests.AutoComplete +{ + [TestFixture] + public class SmartConcatCompletionTests + { + [Test] + public void MaintainsIndent() + { + var original = "foo = \"a|\"".ToCodeString(); + var expected = original.Lines[0].IndexOf('"'); + + var result = Run(original, '\r'); + if (result.Lines.Length != original.Lines.Length + 1) + { + Assert.Inconclusive(); + } + var actual = result.Lines[1].IndexOf('"'); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void CtrlEnterAddsVbNewLineToken() + { + var original = "foo = \"a|\"".ToCodeString(); + var expected = "foo = \"a\" & vbNewLine & _"; + + var result = Run(original, '\r', true); + var actual = result.Lines[0]; + + Assert.AreEqual(expected, actual); + } + + [Test] + public void PlacesCaretOnNextLineBetweenStringDelimiters() + { + var original = "foo = \"a|\"".ToCodeString(); + var expected = "foo = \"a\" & _\r\n \"|\"".ToCodeString(); + + var actual = Run(original, '\r'); + Assert.AreEqual(expected, actual); + } + + [Test] + public void WorksGivenCaretOnSecondPhysicalCodeLine() + { + var original = "foo = \"a\" & _\r\n \"|\"".ToCodeString(); + var expected = "foo = \"a\" & _\r\n \"\" & _\r\n \"|\"".ToCodeString(); + + var actual = Run(original, '\r'); + Assert.AreEqual(expected, actual); + } + + [Test] + public void SplittingExistingString_PutsCaretAtSameRelativePosition() + { + var original = "foo = \"ab|cd\"".ToCodeString(); + var expected = "foo = \"ab\" & _\r\n \"|cd\"".ToCodeString(); + + var actual = Run(original, '\r'); + Assert.AreEqual(expected, actual); + } + + private static TestCodeString Run(TestCodeString original, char input, bool isCtrlDown = false, bool isDeleteKey = false) + { + var sut = InitializeSut(original, out var module, out var settings); + var args = new AutoCompleteEventArgs(module.Object, input, isCtrlDown, isDeleteKey); + + var result = sut.Handle(args, settings); + return result == null ? null : new TestCodeString(result); + } + + private static SmartConcatenationHandler InitializeSut(TestCodeString code, out Mock module, out AutoCompleteSettings settings) + { + return InitializeSut(code, code, out module, out _, out settings); + } + + private static SmartConcatenationHandler InitializeSut(TestCodeString original, TestCodeString prettified, out Mock module, out Mock pane, out AutoCompleteSettings settings) + { + var builder = new MockVbeBuilder(); + var project = builder.ProjectBuilder("TestProject1", ProjectProtection.Unprotected) + .AddComponent("Module1", ComponentType.StandardModule, ""); + var vbe = builder.AddProject(project.Build()).Build(); + + module = new Mock(); + pane = new Mock(); + pane.SetupProperty(m => m.Selection); + pane.Object.Selection = new Selection(original.SnippetPosition.StartLine, 1, original.SnippetPosition.EndLine, 1).Offset(original.CaretPosition); + module.Setup(m => m.DeleteLines(original.SnippetPosition.StartLine, original.SnippetPosition.LineCount)); + module.Setup(m => m.InsertLines(original.SnippetPosition.StartLine, original.Code)); + module.Setup(m => m.CodePane).Returns(pane.Object); + for (var i = 0; i < original.SnippetPosition.LineCount; i++) + { + var index = i; + module.Setup(m => m.GetLines(index + 1, 1)).Returns(original.Lines[index]); + } + module.Setup(m => m.GetLines(original.SnippetPosition)).Returns(prettified.Code); + + settings = new AutoCompleteSettings {IsEnabled = true}; + settings.SmartConcat.IsEnabled = true; + settings.SmartConcat.ConcatVbNewLineModifier = ModifierKeySetting.CtrlKey; + + var handler = new CodePaneSourceCodeHandler(new ProjectsRepository(vbe.Object)); + var sut = new SmartConcatenationHandler(handler); + return sut; + } + } +} \ No newline at end of file diff --git a/RubberduckTests/CodeStringExtensions.cs b/RubberduckTests/CodeStringExtensions.cs index 58e08baeac..d70f753538 100644 --- a/RubberduckTests/CodeStringExtensions.cs +++ b/RubberduckTests/CodeStringExtensions.cs @@ -1,5 +1,4 @@ -using System; -using Rubberduck.Common; +using Rubberduck.Common; using Rubberduck.VBEditor; namespace RubberduckTests @@ -7,18 +6,18 @@ namespace RubberduckTests public static class CodeStringExtensions { /// - /// Creates a code string that encapsulates the caret position, indicated by a single pipe ("|") character. + /// Creates a code string that encapsulates the caret position, indicated by the character. /// - /// The code snippet string. Use a single pipe ("|") to indicate caret position. + /// The code snippet string. Use to indicate caret position. /// Returns a struct that encapsulates a snippet of code and a cursor/caret position relative to that snippet. - public static CodeString ToCodeString(this string code) + public static TestCodeString ToCodeString(this string code) { var zPosition = new Selection(); var lines = (code ?? string.Empty).Split('\n'); - for (int i = 0; i < lines.Length; i++) + for (var i = 0; i < lines.Length; i++) { var line = lines[i]; - var index = line.IndexOf('|'); + var index = line.IndexOf(TestCodeString.PseudoCaret); if (index >= 0) { lines[i] = line.Remove(index, 1); @@ -28,15 +27,7 @@ public static CodeString ToCodeString(this string code) } var newCode = string.Join("\n", lines); - return new CodeString(newCode, zPosition); - } - - public static CodeString InsertPseudoCaret(this string code, Selection zPosition) - { - var lines = (code ?? string.Empty).Split('\n'); - var line = lines[zPosition.StartLine]; - lines[zPosition.StartLine] = line.Insert(zPosition.StartColumn, "|"); - return new CodeString(string.Join("\n", lines), zPosition); + return new TestCodeString(newCode, zPosition); } } } diff --git a/RubberduckTests/Settings/AutoCompleteSettingsTests.cs b/RubberduckTests/Settings/AutoCompleteSettingsTests.cs index 01b31fd17e..a8c02a0a7f 100644 --- a/RubberduckTests/Settings/AutoCompleteSettingsTests.cs +++ b/RubberduckTests/Settings/AutoCompleteSettingsTests.cs @@ -15,29 +15,21 @@ private static Configuration GetDefaultConfig() var autoCompleteSettings = new AutoCompleteSettings { IsEnabled = false, - CompleteBlockOnTab = true, - CompleteBlockOnEnter = true, - EnableSmartConcat = true, - AutoCompletes = new HashSet(new[] + BlockCompletion = new AutoCompleteSettings.BlockCompletionSettings { - new AutoCompleteSetting("AutoCompleteClosingBrace", true), - new AutoCompleteSetting("AutoCompleteClosingBracket", true), - new AutoCompleteSetting("AutoCompleteClosingParenthese", true), - new AutoCompleteSetting("AutoCompleteClosingString", true), - new AutoCompleteSetting("AutoCompleteDoBlock", true), - new AutoCompleteSetting("AutoCompleteEnumBlock", true), - new AutoCompleteSetting("AutoCompleteForBlock", true), - new AutoCompleteSetting("AutoCompleteFunctionBlock", true), - new AutoCompleteSetting("AutoCompleteIfBlock", true), - new AutoCompleteSetting("AutoCompleteOnErrorResumeNextBlock", true), - new AutoCompleteSetting("AutoCompletePrecompilerIfBlock", true), - new AutoCompleteSetting("AutoCompletePropertyBlock", true), - new AutoCompleteSetting("AutoCompleteSelectBlock", true), - new AutoCompleteSetting("AutoCompleteSubBlock", true), - new AutoCompleteSetting("AutoCompleteTypeBlock", true), - new AutoCompleteSetting("AutoCompleteWhileBlock", true), - new AutoCompleteSetting("AutoCompleteWithBlock", true) - }) + CompleteOnTab = true, + CompleteOnEnter = true, + IsEnabled = true + }, + SmartConcat = new AutoCompleteSettings.SmartConcatSettings + { + IsEnabled = true, + ConcatVbNewLineModifier = ModifierKeySetting.CtrlKey + }, + SelfClosingPairs = new AutoCompleteSettings.SelfClosingPairSettings + { + IsEnabled = true + } }; var userSettings = new UserSettings(null, null, autoCompleteSettings, null, null, null, null, null); @@ -49,29 +41,22 @@ private static Configuration GetNonDefaultConfig() var autoCompleteSettings = new AutoCompleteSettings { IsEnabled = true, - CompleteBlockOnTab = false, - CompleteBlockOnEnter = false, - EnableSmartConcat = false, - AutoCompletes = new HashSet(new[] + BlockCompletion = new AutoCompleteSettings.BlockCompletionSettings + { + CompleteOnTab = false, + CompleteOnEnter = false, + IsEnabled = false + }, + SmartConcat = new AutoCompleteSettings.SmartConcatSettings { - new AutoCompleteSetting("AutoCompleteClosingBrace", false), - new AutoCompleteSetting("AutoCompleteClosingBracket", false), - new AutoCompleteSetting("AutoCompleteClosingParenthese", false), - new AutoCompleteSetting("AutoCompleteClosingString", false), - new AutoCompleteSetting("AutoCompleteDoBlock", false), - new AutoCompleteSetting("AutoCompleteEnumBlock", false), - new AutoCompleteSetting("AutoCompleteForBlock", false), - new AutoCompleteSetting("AutoCompleteFunctionBlock", false), - new AutoCompleteSetting("AutoCompleteIfBlock", false), - new AutoCompleteSetting("AutoCompleteOnErrorResumeNextBlock", false), - new AutoCompleteSetting("AutoCompletePrecompilerIfBlock", false), - new AutoCompleteSetting("AutoCompletePropertyBlock", false), - new AutoCompleteSetting("AutoCompleteSelectBlock", false), - new AutoCompleteSetting("AutoCompleteSubBlock", false), - new AutoCompleteSetting("AutoCompleteTypeBlock", false), - new AutoCompleteSetting("AutoCompleteWhileBlock", false), - new AutoCompleteSetting("AutoCompleteWithBlock", false) - }) + IsEnabled = false, + ConcatVbNewLineModifier = ModifierKeySetting.CtrlKey + }, + SelfClosingPairs = new AutoCompleteSettings.SelfClosingPairSettings + { + IsEnabled = false + } + }; var userSettings = new UserSettings(null, null, autoCompleteSettings, null, null, null, null, null); @@ -100,10 +85,9 @@ public void SaveConfigWorks() Assert.Multiple(() => { Assert.AreEqual(config.UserSettings.AutoCompleteSettings.IsEnabled, viewModel.IsEnabled); - Assert.AreEqual(config.UserSettings.AutoCompleteSettings.CompleteBlockOnTab, viewModel.CompleteBlockOnTab); - Assert.AreEqual(config.UserSettings.AutoCompleteSettings.CompleteBlockOnEnter, viewModel.CompleteBlockOnEnter); - Assert.AreEqual(config.UserSettings.AutoCompleteSettings.EnableSmartConcat, viewModel.EnableSmartConcat); - Assert.IsTrue(config.UserSettings.AutoCompleteSettings.AutoCompletes.SequenceEqual(viewModel.Settings)); + Assert.AreEqual(config.UserSettings.AutoCompleteSettings.BlockCompletion.CompleteOnTab, viewModel.CompleteBlockOnTab); + Assert.AreEqual(config.UserSettings.AutoCompleteSettings.BlockCompletion.CompleteOnEnter, viewModel.CompleteBlockOnEnter); + Assert.AreEqual(config.UserSettings.AutoCompleteSettings.SmartConcat.IsEnabled, viewModel.EnableSmartConcat); }); } @@ -119,10 +103,9 @@ public void SetDefaultsWorks() Assert.Multiple(() => { Assert.AreEqual(defaultConfig.UserSettings.AutoCompleteSettings.IsEnabled, viewModel.IsEnabled); - Assert.AreEqual(defaultConfig.UserSettings.AutoCompleteSettings.CompleteBlockOnTab, viewModel.CompleteBlockOnTab); - Assert.AreEqual(defaultConfig.UserSettings.AutoCompleteSettings.CompleteBlockOnEnter, viewModel.CompleteBlockOnEnter); - Assert.AreEqual(defaultConfig.UserSettings.AutoCompleteSettings.EnableSmartConcat, viewModel.EnableSmartConcat); - Assert.IsTrue(defaultConfig.UserSettings.AutoCompleteSettings.AutoCompletes.SequenceEqual(viewModel.Settings)); + Assert.AreEqual(defaultConfig.UserSettings.AutoCompleteSettings.BlockCompletion.CompleteOnTab, viewModel.CompleteBlockOnTab); + Assert.AreEqual(defaultConfig.UserSettings.AutoCompleteSettings.BlockCompletion.CompleteOnEnter, viewModel.CompleteBlockOnEnter); + Assert.AreEqual(defaultConfig.UserSettings.AutoCompleteSettings.SmartConcat.IsEnabled, viewModel.EnableSmartConcat); }); } }