diff --git a/src/OmniSharp.Abstractions/Models/FileBasedRequest.cs b/src/OmniSharp.Abstractions/Models/FileBasedRequest.cs
new file mode 100644
index 0000000000..0173d0bea9
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/FileBasedRequest.cs
@@ -0,0 +1,18 @@
+using System.IO;
+
+namespace OmniSharp.Models
+{
+ public class FileBasedRequest : IRequest
+ {
+ private string _fileName;
+
+ ///
+ /// The name of the file this request is based on.
+ ///
+ public string FileName
+ {
+ get => _fileName?.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+ set => _fileName = value;
+ }
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/Request.cs b/src/OmniSharp.Abstractions/Models/Request.cs
index 96663c5626..cd3e8e0fcd 100644
--- a/src/OmniSharp.Abstractions/Models/Request.cs
+++ b/src/OmniSharp.Abstractions/Models/Request.cs
@@ -1,29 +1,15 @@
using System.Collections.Generic;
-using System.IO;
using Newtonsoft.Json;
namespace OmniSharp.Models
{
- public class Request : IRequest
+ public class Request : FileBasedRequest
{
- private string _fileName;
-
[JsonConverter(typeof(ZeroBasedIndexConverter))]
public int Line { get; set; }
[JsonConverter(typeof(ZeroBasedIndexConverter))]
public int Column { get; set; }
public string Buffer { get; set; }
public IEnumerable Changes { get; set; }
- public string FileName
- {
- get
- {
- return _fileName?.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
- }
- set
- {
- _fileName = value;
- }
- }
}
}
diff --git a/src/OmniSharp.Abstractions/Models/v2/Completion/CharacterSetModificationRule.cs b/src/OmniSharp.Abstractions/Models/v2/Completion/CharacterSetModificationRule.cs
new file mode 100644
index 0000000000..7922191e88
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v2/Completion/CharacterSetModificationRule.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace OmniSharp.Models.V2.Completion
+{
+ public class CharacterSetModificationRule
+ {
+ ///
+ /// The kind of modification.
+ ///
+ [JsonConverter(typeof(StringEnumConverter), /*camelCaseText*/ true)]
+ public CharacterSetModificationRuleKind Kind { get; set; }
+
+ ///
+ /// One or more characters.
+ ///
+ public char[] Characters { get; set; }
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v2/Completion/CharacterSetModificationRuleKind.cs b/src/OmniSharp.Abstractions/Models/v2/Completion/CharacterSetModificationRuleKind.cs
new file mode 100644
index 0000000000..f283943267
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v2/Completion/CharacterSetModificationRuleKind.cs
@@ -0,0 +1,9 @@
+namespace OmniSharp.Models.V2.Completion
+{
+ public enum CharacterSetModificationRuleKind
+ {
+ Add,
+ Remove,
+ Replace
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionItem.cs b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionItem.cs
new file mode 100644
index 0000000000..65a2529b03
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionItem.cs
@@ -0,0 +1,22 @@
+namespace OmniSharp.Models.V2.Completion
+{
+ public class CompletionItem
+ {
+ public string DisplayText { get; set; }
+ public string Kind { get; set; }
+ public string FilterText { get; set; }
+ public string SortText { get; set; }
+
+ ///
+ /// Rules that modify the set of characters that can be typed to cause the
+ /// selected item to be commited.
+ ///
+ public CharacterSetModificationRule[] CommitCharacterRules { get; set; }
+
+ // These properties must be resolved via the '/v2/completionItem/resolve'
+ // end point before they are available.
+ public string Description { get; set; }
+ public TextEdit TextEdit { get; set; }
+ public TextEdit[] AdditionalTextEdits { get; set; }
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionItemResolveRequest.cs b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionItemResolveRequest.cs
new file mode 100644
index 0000000000..06966ccc13
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionItemResolveRequest.cs
@@ -0,0 +1,20 @@
+using OmniSharp.Mef;
+
+namespace OmniSharp.Models.V2.Completion
+{
+ [OmniSharpEndpoint(OmniSharpEndpoints.V2.CompletionItemResolve, typeof(CompletionItemResolveRequest), typeof(CompletionItemResolveResponse))]
+ public class CompletionItemResolveRequest : FileBasedRequest
+ {
+ ///
+ /// Zero-based index of the completion item to resolve within the list of items returned
+ /// by the last .
+ ///
+ public int ItemIndex { get; set; }
+
+ ///
+ /// The display text of the completion item to resolve. If set, this is used to help verify
+ /// that the correct completion item is resolved.
+ ///
+ public string DisplayText { get; set; }
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionItemResolveResponse.cs b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionItemResolveResponse.cs
new file mode 100644
index 0000000000..f52e45e3c0
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionItemResolveResponse.cs
@@ -0,0 +1,7 @@
+namespace OmniSharp.Models.V2.Completion
+{
+ public class CompletionItemResolveResponse
+ {
+ public CompletionItem Item { get; set; }
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionRequest.cs b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionRequest.cs
new file mode 100644
index 0000000000..1c72d8ec9a
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionRequest.cs
@@ -0,0 +1,18 @@
+using OmniSharp.Mef;
+
+namespace OmniSharp.Models.V2.Completion
+{
+ [OmniSharpEndpoint(OmniSharpEndpoints.V2.Completion, typeof(CompletionRequest), typeof(CompletionResponse))]
+ public class CompletionRequest : FileBasedRequest
+ {
+ ///
+ /// The zero-based position in the file where completion is requested.
+ ///
+ public int Position { get; set; }
+
+ ///
+ /// The action that started completion.
+ ///
+ public CompletionTrigger Trigger { get; set; }
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionResponse.cs b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionResponse.cs
new file mode 100644
index 0000000000..bb282c65ef
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionResponse.cs
@@ -0,0 +1,25 @@
+namespace OmniSharp.Models.V2.Completion
+{
+ public class CompletionResponse
+ {
+ // Note: We use an expression-bodied property rather than a getter-only auto-property
+ // to ensure that a new instance is created every time, since this class is mutable.
+ public static CompletionResponse Empty => new CompletionResponse();
+
+ ///
+ /// The default set of typed characters that cause a completion item to be committed.
+ ///
+ public char[] DefaultCommitCharacters { get; set; }
+
+ ///
+ /// Returns true if the completion list is "suggestion mode", meaning that it should not
+ /// commit aggressively on characters like ' '.
+ ///
+ public bool IsSuggestionMode { get; set; }
+
+ ///
+ /// The completion items to present to the user.
+ ///
+ public CompletionItem[] Items { get; set; }
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionTrigger.cs b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionTrigger.cs
new file mode 100644
index 0000000000..fab82a4aa6
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionTrigger.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace OmniSharp.Models.V2.Completion
+{
+ public class CompletionTrigger
+ {
+ ///
+ /// The action that started completion.
+ ///
+ [JsonConverter(typeof(StringEnumConverter), /*camelCaseText*/ true)]
+ public CompletionTriggerKind Kind { get; set; }
+
+ ///
+ /// The character associated with the triggering action.
+ ///
+ public string Character { get; set; }
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionTriggerKind.cs b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionTriggerKind.cs
new file mode 100644
index 0000000000..51027cd27f
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v2/Completion/CompletionTriggerKind.cs
@@ -0,0 +1,9 @@
+namespace OmniSharp.Models.V2.Completion
+{
+ public enum CompletionTriggerKind
+ {
+ Invoke,
+ Insertion,
+ Deletion
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v2/TextEdit.cs b/src/OmniSharp.Abstractions/Models/v2/TextEdit.cs
new file mode 100644
index 0000000000..ed6010b744
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v2/TextEdit.cs
@@ -0,0 +1,8 @@
+namespace OmniSharp.Models.V2
+{
+ public class TextEdit
+ {
+ public Range Range { get; set; }
+ public string NewText { get; set; }
+ }
+}
diff --git a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
index 84f7e8633c..167f251d83 100644
--- a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
+++ b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
@@ -45,6 +45,9 @@ public static class OmniSharpEndpoints
public static class V2
{
+ public const string Completion = "/v2/completion";
+ public const string CompletionItemResolve = "/v2/completionItem/resolve";
+
public const string GetCodeActions = "/v2/getcodeactions";
public const string RunCodeAction = "/v2/runcodeaction";
diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/IntellisenseService.cs b/src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/AutoCompleteService.cs
similarity index 98%
rename from src/OmniSharp.Roslyn.CSharp/Services/Intellisense/IntellisenseService.cs
rename to src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/AutoCompleteService.cs
index 2214c8172e..37f702151e 100644
--- a/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/IntellisenseService.cs
+++ b/src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/AutoCompleteService.cs
@@ -13,16 +13,16 @@
using OmniSharp.Options;
using OmniSharp.Roslyn.CSharp.Services.Documentation;
-namespace OmniSharp.Roslyn.CSharp.Services.Intellisense
+namespace OmniSharp.Roslyn.CSharp.Services.IntelliSense
{
[OmniSharpHandler(OmniSharpEndpoints.AutoComplete, LanguageNames.CSharp)]
- public class IntellisenseService : IRequestHandler>
+ public class AutoCompleteService : IRequestHandler>
{
private readonly OmniSharpWorkspace _workspace;
private readonly FormattingOptions _formattingOptions;
[ImportingConstructor]
- public IntellisenseService(OmniSharpWorkspace workspace, FormattingOptions formattingOptions)
+ public AutoCompleteService(OmniSharpWorkspace workspace, FormattingOptions formattingOptions)
{
_workspace = workspace;
_formattingOptions = formattingOptions;
diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs b/src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/CompletionItemExtensions.cs
similarity index 98%
rename from src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs
rename to src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/CompletionItemExtensions.cs
index 8067cbf2a2..fcdefab7ef 100644
--- a/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs
+++ b/src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/CompletionItemExtensions.cs
@@ -8,7 +8,7 @@
using Microsoft.CodeAnalysis.Completion;
using OmniSharp.Utilities;
-namespace OmniSharp.Roslyn.CSharp.Services.Intellisense
+namespace OmniSharp.Roslyn.CSharp.Services.IntelliSense
{
internal static class CompletionItemExtensions
{
diff --git a/src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/V2/CompletionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/V2/CompletionService.cs
new file mode 100644
index 0000000000..c26bcba353
--- /dev/null
+++ b/src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/V2/CompletionService.cs
@@ -0,0 +1,208 @@
+using System;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Completion;
+using Microsoft.Extensions.Logging;
+using OmniSharp.Mef;
+using CSharpCompletionService = Microsoft.CodeAnalysis.Completion.CompletionService;
+
+namespace OmniSharp.Roslyn.CSharp.Services.IntelliSense.V2
+{
+ using Models = OmniSharp.Models.V2;
+ using CompletionModels = OmniSharp.Models.V2.Completion;
+
+ [Shared]
+ [OmniSharpHandler(OmniSharpEndpoints.V2.Completion, LanguageNames.CSharp)]
+ [OmniSharpHandler(OmniSharpEndpoints.V2.CompletionItemResolve, LanguageNames.CSharp)]
+ public class CompletionService :
+ IRequestHandler,
+ IRequestHandler
+ {
+ private readonly OmniSharpWorkspace _workspace;
+ private readonly ILogger _logger;
+
+ private (string fileName, CompletionList completionList) _lastResponse;
+
+ [ImportingConstructor]
+ public CompletionService(OmniSharpWorkspace workspace, ILoggerFactory loggerFactory)
+ {
+ _workspace = workspace;
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ public async Task Handle(CompletionModels.CompletionRequest request)
+ {
+ var fileName = request.FileName;
+ var position = request.Position;
+ var trigger = request.Trigger.ToRoslynCompletionTrigger();
+
+ var document = GetDocument(fileName);
+
+ var text = await document.GetTextAsync();
+ if (position < 0 || position > text.Length)
+ {
+ throw new ArgumentOutOfRangeException($"Invalid position: {position}. Should be within range 0 to {text.Length}");
+ }
+
+ var service = GetService(document);
+
+ var completionList = await service.GetCompletionsAsync(document, position, trigger);
+ if (completionList == null)
+ {
+ return CompletionModels.CompletionResponse.Empty;
+ }
+
+ var isSuggestionMode = completionList.SuggestionModeItem != null;
+
+ int itemCount = completionList.Items.Length;
+ var items = new CompletionModels.CompletionItem[itemCount];
+
+ for (int i = 0; i < itemCount; i++)
+ {
+ var item = completionList.Items[i];
+
+ items[i] = new CompletionModels.CompletionItem
+ {
+ DisplayText = item.DisplayText,
+ Kind = item.GetKind(),
+ FilterText = item.FilterText,
+ SortText = item.SortText,
+ CommitCharacterRules = GetCommitCharacterRulesModels(item.Rules.CommitCharacterRules)
+ };
+ }
+
+ var response = new CompletionModels.CompletionResponse
+ {
+ DefaultCommitCharacters = completionList.Rules.DefaultCommitCharacters.ToArray(),
+ IsSuggestionMode = isSuggestionMode,
+ Items = items
+ };
+
+ _lastResponse = (fileName, completionList);
+
+ return response;
+ }
+
+ private static CompletionModels.CharacterSetModificationRule[] GetCommitCharacterRulesModels(ImmutableArray commitCharacterRules)
+ {
+ var result = commitCharacterRules.Length > 0
+ ? new CompletionModels.CharacterSetModificationRule[commitCharacterRules.Length]
+ : Array.Empty();
+
+ for (int i = 0; i < commitCharacterRules.Length; i++)
+ {
+ var rule = commitCharacterRules[i];
+ result[i] = new CompletionModels.CharacterSetModificationRule
+ {
+ Characters = rule.Characters.ToArray(),
+ Kind = (CompletionModels.CharacterSetModificationRuleKind)rule.Kind
+ };
+ }
+
+ return result;
+ }
+
+ private static CSharpCompletionService GetService(Document document)
+ {
+ var service = CSharpCompletionService.GetService(document);
+ if (service == null)
+ {
+ throw new InvalidOperationException("Could not retrieve Roslyn CompletionService.");
+ }
+
+ return service;
+ }
+
+ private Document GetDocument(string fileName)
+ {
+ var document = _workspace.GetDocument(fileName);
+ if (document == null)
+ {
+ throw new ArgumentException($"Could not find document for {fileName}.");
+ }
+
+ return document;
+ }
+
+ public async Task Handle(CompletionModels.CompletionItemResolveRequest request)
+ {
+ if (_lastResponse.fileName == null || _lastResponse.completionList == null)
+ {
+ throw new InvalidOperationException($"{OmniSharpEndpoints.V2.CompletionItemResolve} end point cannot be called before {OmniSharpEndpoints.V2.Completion}");
+ }
+
+ var previousFileName = _lastResponse.fileName;
+ var previousCompletionList = _lastResponse.completionList;
+
+ var fileName = request.FileName;
+ var itemIndex = request.ItemIndex;
+ var displayText = request.DisplayText;
+
+ if (!string.Equals(_lastResponse.fileName, fileName, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException($"Cannot resolve completion item from '{fileName}' because the last {OmniSharpEndpoints.V2.Completion} request was for '{previousFileName}'");
+ }
+
+ if (itemIndex < 0 || itemIndex >= previousCompletionList.Items.Length)
+ {
+ throw new ArgumentOutOfRangeException($"Invalid item index: {itemIndex}. Should be within range 0 to {previousCompletionList.Items.Length}");
+ }
+
+ var previousItem = previousCompletionList.Items[itemIndex];
+ if (!string.Equals(previousItem.DisplayText, displayText))
+ {
+ throw new ArgumentException($"Cannot resolve completion item. Display text does not match. Expected '{previousItem.DisplayText}' but was '{displayText}'");
+ }
+
+ var document = GetDocument(fileName);
+ var service = GetService(document);
+
+ var description = await service.GetDescriptionAsync(document, previousItem);
+
+ // CompletionService.GetChangeAsync(...) can optionally take the commit character which might be
+ // used to produce a slightly different change. Unfortunately, most editors don't provide this.
+ var change = await service.GetChangeAsync(document, previousItem);
+
+ var text = await document.GetTextAsync();
+
+ var startTextLine = text.Lines.GetLineFromPosition(change.TextChange.Span.Start);
+ var startPoint = new Models.Point
+ {
+ Line = startTextLine.LineNumber,
+ Column = change.TextChange.Span.Start - startTextLine.Start
+ };
+
+ var endTextLine = text.Lines.GetLineFromPosition(change.TextChange.Span.End);
+ var endPoint = new Models.Point
+ {
+ Line = endTextLine.LineNumber,
+ Column = change.TextChange.Span.End - endTextLine.Start
+ };
+
+ return new CompletionModels.CompletionItemResolveResponse
+ {
+ Item = new CompletionModels.CompletionItem
+ {
+ DisplayText = previousItem.DisplayText,
+ Kind = previousItem.GetKind(),
+ FilterText = previousItem.FilterText,
+ SortText = previousItem.SortText,
+ CommitCharacterRules = GetCommitCharacterRulesModels(previousItem.Rules.CommitCharacterRules),
+ Description = description.Text,
+ TextEdit = new Models.TextEdit
+ {
+ NewText = change.TextChange.NewText,
+ Range = new Models.Range
+ {
+ Start = startPoint,
+ End = endPoint
+ }
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/V2/Extensions.cs b/src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/V2/Extensions.cs
new file mode 100644
index 0000000000..5f7aaa6c6d
--- /dev/null
+++ b/src/OmniSharp.Roslyn.CSharp/Services/IntelliSense/V2/Extensions.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis.Completion;
+
+namespace OmniSharp.Roslyn.CSharp.Services.IntelliSense.V2
+{
+ using Models = OmniSharp.Models.V2.Completion;
+
+ internal static class Extensions
+ {
+ public static CompletionTrigger ToRoslynCompletionTrigger(this Models.CompletionTrigger trigger)
+ {
+ if (trigger.Kind == Models.CompletionTriggerKind.Deletion ||
+ trigger.Kind == Models.CompletionTriggerKind.Insertion &&
+ trigger.Character == null)
+ {
+ throw new ArgumentException($"'{trigger.Kind}' completion triggers must provide a Character value.");
+ }
+
+ if (trigger.Character != null &&
+ trigger.Character.Length != 1)
+ {
+ throw new ArgumentException($"Invalid trigger character: {trigger.Character}. Should have a length of 1.");
+ }
+
+ switch (trigger.Kind)
+ {
+ case Models.CompletionTriggerKind.Invoke:
+ return CompletionTrigger.Invoke;
+ case Models.CompletionTriggerKind.Insertion:
+ return CompletionTrigger.CreateInsertionTrigger(trigger.Character[0]);
+ case Models.CompletionTriggerKind.Deletion:
+ return CompletionTrigger.CreateDeletionTrigger(trigger.Character[0]);
+
+ default:
+ throw new ArgumentException($"Invalid completion trigger kind encountered: {trigger.Kind}");
+ }
+ }
+
+ private static readonly ImmutableArray s_kindTags = ImmutableArray.Create(
+ CompletionTags.Class,
+ CompletionTags.Constant,
+ CompletionTags.Delegate,
+ CompletionTags.Enum,
+ CompletionTags.EnumMember,
+ CompletionTags.Event,
+ CompletionTags.ExtensionMethod,
+ CompletionTags.Field,
+ CompletionTags.Interface,
+ CompletionTags.Intrinsic,
+ CompletionTags.Keyword,
+ CompletionTags.Label,
+ CompletionTags.Local,
+ CompletionTags.Method,
+ CompletionTags.Module,
+ CompletionTags.Namespace,
+ CompletionTags.Operator,
+ CompletionTags.Parameter,
+ CompletionTags.Property,
+ CompletionTags.RangeVariable,
+ CompletionTags.Reference,
+ CompletionTags.Structure,
+ CompletionTags.TypeParameter);
+
+ public static string GetKind(this CompletionItem completionItem)
+ {
+ foreach (var tag in s_kindTags)
+ {
+ if (completionItem.Tags.Contains(tag))
+ {
+ return tag;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/AbstractAutoCompleteTestFixture.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/AbstractAutoCompleteTestFixture.cs
index 9910a5435f..75c63ee500 100644
--- a/tests/OmniSharp.Roslyn.CSharp.Tests/AbstractAutoCompleteTestFixture.cs
+++ b/tests/OmniSharp.Roslyn.CSharp.Tests/AbstractAutoCompleteTestFixture.cs
@@ -1,13 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using OmniSharp.Models.AutoComplete;
-using OmniSharp.Roslyn.CSharp.Services.Intellisense;
+using OmniSharp.Roslyn.CSharp.Services.IntelliSense;
using TestUtility;
using Xunit.Abstractions;
namespace OmniSharp.Roslyn.CSharp.Tests
{
- public class AbstractAutoCompleteTestFixture : AbstractSingleRequestHandlerTestFixture
+ public class AbstractAutoCompleteTestFixture : AbstractSingleRequestHandlerTestFixture
{
protected AbstractAutoCompleteTestFixture(ITestOutputHelper output, SharedOmniSharpHostFixture sharedOmniSharpHostFixture)
: base(output, sharedOmniSharpHostFixture)
diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/IntellisenseFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/AutoCompleteFacts.cs
similarity index 98%
rename from tests/OmniSharp.Roslyn.CSharp.Tests/IntellisenseFacts.cs
rename to tests/OmniSharp.Roslyn.CSharp.Tests/AutoCompleteFacts.cs
index d7af3ef07a..7849b96051 100644
--- a/tests/OmniSharp.Roslyn.CSharp.Tests/IntellisenseFacts.cs
+++ b/tests/OmniSharp.Roslyn.CSharp.Tests/AutoCompleteFacts.cs
@@ -10,14 +10,14 @@
namespace OmniSharp.Roslyn.CSharp.Tests
{
- public class IntellisenseFacts : AbstractAutoCompleteTestFixture
+ public class AutoCompleteFacts : AbstractAutoCompleteTestFixture
{
private readonly ILogger _logger;
- public IntellisenseFacts(ITestOutputHelper output, SharedOmniSharpHostFixture sharedOmniSharpHostFixture)
+ public AutoCompleteFacts(ITestOutputHelper output, SharedOmniSharpHostFixture sharedOmniSharpHostFixture)
: base(output, sharedOmniSharpHostFixture)
{
- this._logger = this.LoggerFactory.CreateLogger();
+ this._logger = this.LoggerFactory.CreateLogger();
}
[Theory]
diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs
new file mode 100644
index 0000000000..c334a4858f
--- /dev/null
+++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs
@@ -0,0 +1,375 @@
+using System.Linq;
+using System.Threading.Tasks;
+using OmniSharp.Models.V2.Completion;
+using OmniSharp.Roslyn.CSharp.Services.IntelliSense.V2;
+using TestUtility;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace OmniSharp.Roslyn.CSharp.Tests
+{
+ public class CompletionFacts : AbstractSingleRequestHandlerTestFixture
+ {
+ public CompletionFacts(ITestOutputHelper output)
+ : base(output)
+ {
+ }
+
+ protected override string EndpointName => OmniSharpEndpoints.V2.Completion;
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async void Empty_file_contains_keywords(string fileName)
+ {
+ const string markup = @"$$";
+
+ var (items, _) = await RequestCompletionAsync(fileName, markup);
+
+ Assert.Contains(items, item =>
+ {
+ return item.DisplayText == "class"
+ && item.Kind == "Keyword";
+ });
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async void Attributes_do_not_have_attribute_suffix(string fileName)
+ {
+ const string markup = @"
+class SuperAttribute : System.Attribute { }
+
+[S$$]
+class C
+{
+}";
+
+ var (items, _) = await RequestCompletionAsync(fileName, markup);
+
+ Assert.Contains(items, item => item.DisplayText == "Super");
+ Assert.DoesNotContain(items, item => item.DisplayText == "SuperAttribute");
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task Returns_members_in_object_initializer_context(string fileName)
+ {
+ const string markup = @"
+class B
+{
+ public string Foo { get; set; }
+}
+
+class C
+{
+ void M()
+ {
+ var c = new B
+ {
+ $$
+ }
+ }
+}";
+
+ var (items, _) = await RequestCompletionAsync(fileName, markup);
+ Assert.Contains(items, item => item.DisplayText == "Foo");
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task Returns_named_parameter_inside_a_method_call(string fileName)
+ {
+ const string markup = @"
+class Greeter
+{
+ public void SayHi(string text) { }
+}
+
+class C
+{
+ void M()
+ {
+ var greeter = new Greeter();
+ greeter.SayHi($$
+ }
+}";
+
+ var (items, _) = await RequestCompletionAsync(fileName, markup);
+ Assert.Contains(items, item => item.DisplayText == "text:");
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task Returns_override_signatures(string fileName)
+ {
+ const string markup = @"
+class B
+{
+ public virtual void Test(string text) { }
+ public virtual void Test(string text, string moreText) { }
+}
+
+class C : B
+{
+ override $$
+}";
+
+ var (items, _) = await RequestCompletionAsync(fileName, markup);
+
+ Assert.Contains(items, item => item.DisplayText == "Equals(object obj)");
+ Assert.Contains(items, item => item.DisplayText == "GetHashCode()");
+ Assert.Contains(items, item => item.DisplayText == "Test(string text)");
+ Assert.Contains(items, item => item.DisplayText == "Test(string text, string moreText)");
+ Assert.Contains(items, item => item.DisplayText == "ToString()");
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task Returns_cref_completion(string fileName)
+ {
+ const string markup = @"
+///
+/// A comment. for more details
+///
+public class MyClass
+{
+}
+";
+
+ var (items, _) = await RequestCompletionAsync(fileName, markup);
+ Assert.Contains(items, item => item.DisplayText == "MyClass");
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task Returns_cref_completion_for_generic(string fileName)
+ {
+ const string markup = @"
+///
+/// A comment. for more details
+///
+public class MyClass
+{
+}
+";
+
+ var (items, _) = await RequestCompletionAsync(fileName, markup);
+ Assert.Contains(items, item => item.DisplayText == "List{T}");
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async void Description_has_minimally_qualified_crefs(string fileName)
+ {
+ const string markup = @"
+using System.Text;
+
+/// My Crefs are and .
+class B { }
+
+class C
+{
+ void M()
+ {
+ new B$$
+ }
+}";
+
+ await AssertItemDescriptionAsync(fileName, markup,
+ displayText: "B", expectedDescription:
+ "class B\r\nMy Crefs are StringBuilder and System.IO.Path.");
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task Is_suggestion_mode_true_for_lambda_expression_position1(string fileName)
+ {
+ const string markup = @"
+using System;
+class C
+{
+ int CallMe(int i) => 42;
+
+ void M(Func a) { }
+
+ void M()
+ {
+ M(c$$
+ }
+}";
+
+ var (_, isSuggestionMode) = await RequestCompletionAsync(fileName, markup);
+ Assert.True(isSuggestionMode);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task Is_suggestion_mode_true_for_lambda_expression_position2(string fileName)
+ {
+ const string markup = @"
+using System;
+class C
+{
+ int CallMe(int i) => 42;
+
+ void M()
+ {
+ Func a = c$$
+ }
+}";
+
+ var (_, isSuggestionMode) = await RequestCompletionAsync(fileName, markup);
+ Assert.True(isSuggestionMode);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task Is_suggestion_mode_false_for_normal_position1(string fileName)
+ {
+ const string markup = @"
+using System;
+class C
+{
+ int CallMe(int i) => 42;
+
+ void M(int a) { }
+
+ void M()
+ {
+ M(c$$
+ }
+}";
+
+ var (_, isSuggestionMode) = await RequestCompletionAsync(fileName, markup);
+ Assert.False(isSuggestionMode);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task Is_suggestion_mode_false_for_normal_position2(string fileName)
+ {
+ const string markup = @"
+using System;
+class C
+{
+ int CallMe(int i) => 42;
+
+ void M()
+ {
+ int a = c$$
+ }
+}";
+
+ var (_, isSuggestionMode) = await RequestCompletionAsync(fileName, markup);
+ Assert.False(isSuggestionMode);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task Special_commit_characters_for_using_directives(string fileName)
+ {
+ const string markup = @"using $$";
+
+ var (items, isSuggestionMode) = await RequestCompletionAsync(fileName, markup);
+ Assert.False(isSuggestionMode);
+
+ // In this position, only the '.' and ';' characters commit to make it easier to type
+ // namespace aliases, such as: 'using S = System;'
+
+ var item = items.Single(i => i.DisplayText == "System");
+ var rule = Assert.Single(item.CommitCharacterRules);
+
+ Assert.Equal(CharacterSetModificationRuleKind.Replace, rule.Kind);
+ Assert.Equal(2, rule.Characters.Length);
+ Assert.Contains('.', rule.Characters);
+ Assert.Contains(';', rule.Characters);
+ }
+
+ private async Task AssertItemDescriptionAsync(string fileName, string markup, string displayText, string expectedDescription)
+ {
+ var testFile = new TestFile(fileName, markup);
+
+ using (var host = CreateOmniSharpHost(testFile))
+ {
+ var (items, _) = await RequestCompletionAsync(testFile, host);
+ var item = await ResolveCompletionItemAsync(displayText, items, testFile, host);
+
+ Assert.Equal(expectedDescription, item.Description);
+ }
+ }
+
+ private Task<(CompletionItem[] items, bool isSuggestionMode)> RequestCompletionAsync(string fileName, string markup)
+ {
+ var testFile = new TestFile(fileName, markup);
+
+ using (var host = CreateOmniSharpHost(testFile))
+ {
+ return RequestCompletionAsync(testFile, host);
+ }
+ }
+
+ private async Task<(CompletionItem[] items, bool isSuggestionMode)> RequestCompletionAsync(TestFile testFile, OmniSharpTestHost host)
+ {
+ var handler = GetRequestHandler(host);
+
+ var request = new CompletionRequest
+ {
+ FileName = testFile.FileName,
+ Position = testFile.Content.Position,
+ Trigger = new CompletionTrigger
+ {
+ Kind = CompletionTriggerKind.Invoke
+ }
+ };
+
+ var response = await handler.Handle(request);
+
+ return (response.Items, response.IsSuggestionMode);
+ }
+
+ private async Task ResolveCompletionItemAsync(string displayText, CompletionItem[] items, TestFile testFile, OmniSharpTestHost host)
+ {
+ var itemIndex = GetItemIndex(displayText, items);
+ Assert.True(itemIndex >= 0);
+
+ var handler = GetRequestHandler(host);
+
+ var request = new CompletionItemResolveRequest
+ {
+ DisplayText = displayText,
+ FileName = testFile.FileName,
+ ItemIndex = itemIndex
+ };
+
+ var response = await handler.Handle(request);
+
+ return response.Item;
+ }
+
+ private int GetItemIndex(string displayText, CompletionItem[] items)
+ {
+ for (int i = 0; i < items.Length; i++)
+ {
+ if (items[i].DisplayText == displayText)
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+ }
+}