Skip to content

Commit c83caa8

Browse files
authored
Merge pull request #403 from waf/dev/highlight-repl-keywords
Make completion cancellable and handle REPL keywords
2 parents 9c0204b + 4bdf1b9 commit c83caa8

5 files changed

Lines changed: 152 additions & 23 deletions

File tree

CSharpRepl.Services/Completion/AutoCompleteService.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Diagnostics;
77
using System.Linq;
8+
using System.Threading;
89
using System.Threading.Tasks;
910
using CSharpRepl.Services.Extensions;
1011
using CSharpRepl.Services.SyntaxHighlighting;
@@ -35,7 +36,7 @@ public AutoCompleteService(SyntaxHighlighter highlighter, IMemoryCache cache, Co
3536
this.configuration = configuration;
3637
}
3738

38-
public async Task<CompletionItemWithDescription[]> Complete(Document document, string text, int caret)
39+
public async Task<CompletionItemWithDescription[]> Complete(Document document, string text, int caret, CancellationToken cancellationToken)
3940
{
4041
var cacheKey = CacheKeyPrefix + document.Name + text + caret;
4142
if (text != string.Empty && cache.Get<CompletionItemWithDescription[]>(cacheKey) is CompletionItemWithDescription[] cached)
@@ -47,7 +48,7 @@ public async Task<CompletionItemWithDescription[]> Complete(Document document, s
4748
try
4849
{
4950
var completions = await completionService
50-
.GetCompletionsAsync(document, caret)
51+
.GetCompletionsAsync(document, caret, cancellationToken: cancellationToken)
5152
.ConfigureAwait(false);
5253

5354
var completionsWithDescriptions = completions?.ItemsList

CSharpRepl.Services/Roslyn/RoslynServices.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,13 +192,13 @@ public async Task<IReadOnlyList<string>> GetPreviousSubmissionsAsync()
192192
.ToList();
193193
}
194194

195-
public async Task<IReadOnlyCollection<CompletionItemWithDescription>> CompleteAsync(string text, int caret)
195+
public async Task<IReadOnlyCollection<CompletionItemWithDescription>> CompleteAsync(string text, int caret, CancellationToken cancellationToken = default)
196196
{
197197
if (!Initialization.IsCompleted)
198198
return [];
199199

200200
var document = workspaceManager.CurrentDocument.WithText(SourceText.From(text));
201-
return await autocompleteService.Complete(document, text, caret).ConfigureAwait(false);
201+
return await autocompleteService.Complete(document, text, caret, cancellationToken).ConfigureAwait(false);
202202
}
203203

204204
public async Task<SymbolResult> GetSymbolAtIndexAsync(string text, int caret)

CSharpRepl.Tests/CompletionTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,15 @@ public async Task Complete_ItemsFilteringAndOrder(string text, params string[] e
126126
Assert.Equal(expectedItems[i], completions.ElementAt(i).Item.DisplayText);
127127
}
128128
}
129+
130+
[Theory]
131+
[InlineData("he", "help")]
132+
[InlineData("ex", "exit")]
133+
[InlineData("cl", "clear")]
134+
public async Task Complete_ReplKeywords(string source, string item)
135+
{
136+
var completions = await promptCallbacks.GetCompletionItemsCoreAsync(source, source.Length);
137+
var completion = completions.SingleOrDefault(c => c.DisplayText == item);
138+
Assert.NotNull(completion);
139+
}
129140
}

CSharpRepl/CSharpReplPromptCallbacks.cs

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

55
using System;
6+
using System.Buffers;
67
using System.Collections.Generic;
78
using System.Collections.Immutable;
89
using System.Diagnostics;
@@ -31,6 +32,9 @@ namespace CSharpRepl.PrettyPromptConfig;
3132
/// </summary>
3233
internal class CSharpReplPromptCallbacks : PromptCallbacks
3334
{
35+
private const string lowercaseLetters = "abcdefghijklmnopqrstuvwxyz";
36+
private static SearchValues<char> lowercaseSearchValues = SearchValues.Create(lowercaseLetters);
37+
3438
private readonly IConsoleEx console;
3539
private readonly RoslynServices roslyn;
3640
private readonly Configuration configuration;
@@ -90,12 +94,41 @@ protected override Task<bool> ShouldOpenCompletionWindowAsync(string text, int c
9094

9195
protected override async Task<IReadOnlyList<CompletionItem>> GetCompletionItemsAsync(string text, int caret, TextSpan spanToBeReplaced, CancellationToken cancellationToken)
9296
{
93-
var completions = await roslyn.CompleteAsync(text, caret).ConfigureAwait(false);
94-
return completions
95-
.OrderByDescending(i => i.Item.Rules.MatchPriority)
96-
.ThenBy(i => i.Item.SortText)
97-
.Select(CreatePrettyPromptCompletionItem)
97+
return await GetCompletionItemsCoreAsync(text, caret, cancellationToken).ConfigureAwait(false);
98+
}
99+
100+
// Made internal for testing
101+
internal async Task<IReadOnlyList<CompletionItem>> GetCompletionItemsCoreAsync(string text, int caret, CancellationToken cancellationToken = default)
102+
{
103+
var replKeywordCompletions = GetReplKeywordCompletions();
104+
105+
var completions = await roslyn.CompleteAsync(text, caret, cancellationToken).ConfigureAwait(false);
106+
return replKeywordCompletions
107+
.Concat(completions
108+
.OrderByDescending(i => i.Item.Rules.MatchPriority)
109+
.ThenBy(i => i.Item.SortText)
110+
.Select(CreatePrettyPromptCompletionItem))
98111
.ToArray();
112+
113+
IEnumerable<CompletionItem> GetReplKeywordCompletions()
114+
{
115+
var trimmed = text.AsSpan().Trim();
116+
const int largestKeywordLength = 5;
117+
if (trimmed.Length > largestKeywordLength)
118+
{
119+
return [];
120+
}
121+
122+
Span<char> lowercaseBuffer = stackalloc char[largestKeywordLength];
123+
trimmed.ToLowerInvariant(lowercaseBuffer);
124+
var lowercaseTrimmed = lowercaseBuffer.TrimEnd('\0');
125+
if (lowercaseTrimmed.ContainsAnyExcept(lowercaseSearchValues))
126+
{
127+
return [];
128+
}
129+
130+
return ReplKeywordCompletionItems.AllItems;
131+
}
99132
}
100133

101134
internal CompletionItem CreatePrettyPromptCompletionItem(CompletionItemWithDescription r)
@@ -132,10 +165,40 @@ private static ImmutableArray<CharacterSetModificationRule> MergeCommitRules(
132165

133166
protected override async Task<IReadOnlyCollection<FormatSpan>> HighlightCallbackAsync(string text, CancellationToken cancellationToken)
134167
{
168+
var replKeywordSpan = HighlightReplKeyword(text);
169+
if (replKeywordSpan is not null)
170+
{
171+
return [replKeywordSpan.Value];
172+
}
173+
135174
var classifications = await roslyn.SyntaxHighlightAsync(text).ConfigureAwait(false);
136175
return classifications.ToFormatSpans();
137176
}
138177

178+
private static FormatSpan? HighlightReplKeyword(string text)
179+
{
180+
var trimmed = text.Trim().ToLowerInvariant();
181+
switch (trimmed)
182+
{
183+
case ReadEvalPrintLoop.Keywords.HelpText:
184+
case "#help":
185+
return FullSpanWithColor(ReadEvalPrintLoop.Keywords.HelpInfo.Color);
186+
187+
case ReadEvalPrintLoop.Keywords.ExitText:
188+
return FullSpanWithColor(ReadEvalPrintLoop.Keywords.ExitInfo.Color);
189+
190+
case ReadEvalPrintLoop.Keywords.ClearText:
191+
return FullSpanWithColor(ReadEvalPrintLoop.Keywords.ClearInfo.Color);
192+
}
193+
194+
return null;
195+
196+
FormatSpan FullSpanWithColor(AnsiColor color)
197+
{
198+
return EntireWordFormatSpan(text, color);
199+
}
200+
}
201+
139202
protected override async Task<KeyPress> TransformKeyPressAsync(string text, int caret, KeyPress keyPress, CancellationToken cancellationToken)
140203
{
141204
// user submitted the prompt but it's incomplete. Insert a newline automatically with the correct level of indentation.
@@ -286,6 +349,42 @@ protected override Task<bool> ConfirmCompletionCommit(string text, int caret, Ke
286349

287350
return null;
288351
}
352+
353+
private static FormatSpan EntireWordFormatSpan(ReadOnlySpan<char> word, AnsiColor color)
354+
{
355+
return new(0, word.Length, color);
356+
}
357+
358+
private static FormattedString EntireWordFormatString(string word, AnsiColor color)
359+
{
360+
return new(word, EntireWordFormatSpan(word, color));
361+
}
362+
363+
private static FormattedString EntireWordFormatString(ReadEvalPrintLoop.Keywords.KeywordInfo keywordInfo)
364+
{
365+
return EntireWordFormatString(keywordInfo.Text, keywordInfo.Color);
366+
}
367+
368+
private static class ReplKeywordCompletionItems
369+
{
370+
private static readonly FormattedString helpFormattedString = EntireWordFormatString(ReadEvalPrintLoop.Keywords.HelpInfo);
371+
private static readonly FormattedString exitFormattedString = EntireWordFormatString(ReadEvalPrintLoop.Keywords.ExitInfo);
372+
private static readonly FormattedString clearFormattedString = EntireWordFormatString(ReadEvalPrintLoop.Keywords.ClearInfo);
373+
374+
public static CompletionItem Help { get; } = new(
375+
ReadEvalPrintLoop.Keywords.HelpText,
376+
displayText: helpFormattedString);
377+
378+
public static CompletionItem Exit { get; } = new(
379+
ReadEvalPrintLoop.Keywords.ExitText,
380+
displayText: exitFormattedString);
381+
382+
public static CompletionItem Clear { get; } = new(
383+
ReadEvalPrintLoop.Keywords.ClearText,
384+
displayText: clearFormattedString);
385+
386+
public static IReadOnlyList<CompletionItem> AllItems = [Help, Exit, Clear];
387+
}
289388
}
290389

291390
/// <summary>

CSharpRepl/ReadEvalPrintLoop.cs

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -204,18 +204,36 @@ static string KeyPressPatternToString(IEnumerable<KeyPressPattern> patterns)
204204
}
205205
}
206206

207-
private static string Help =>
208-
PromptConfiguration.HasUserOptedOutFromColor
209-
? @"""help"""
210-
: AnsiColor.Green.GetEscapeSequence() + "help" + AnsiEscapeCodes.Reset;
211-
212-
private static string Exit =>
213-
PromptConfiguration.HasUserOptedOutFromColor
214-
? @"""exit"""
215-
: AnsiColor.BrightRed.GetEscapeSequence() + "exit" + AnsiEscapeCodes.Reset;
216-
217-
private static string Clear =>
218-
PromptConfiguration.HasUserOptedOutFromColor
219-
? @"""clear"""
220-
: AnsiColor.BrightBlue.GetEscapeSequence() + "clear" + AnsiEscapeCodes.Reset;
207+
public static string Help => Keywords.Help;
208+
public static string Exit => Keywords.Exit;
209+
public static string Clear => Keywords.Clear;
210+
211+
public static class Keywords
212+
{
213+
public const string HelpText = "help";
214+
public const string ExitText = "exit";
215+
public const string ClearText = "clear";
216+
217+
public static readonly KeywordInfo HelpInfo = new(HelpText, AnsiColor.Green);
218+
public static readonly KeywordInfo ExitInfo = new(ExitText, AnsiColor.BrightRed);
219+
public static readonly KeywordInfo ClearInfo = new(ClearText, AnsiColor.BrightBlue);
220+
221+
public static string Help => GetColoredText(HelpInfo);
222+
public static string Exit => GetColoredText(ExitInfo);
223+
public static string Clear => GetColoredText(ClearInfo);
224+
225+
private static string GetColoredText(KeywordInfo keywordInfo)
226+
{
227+
return GetColoredText(keywordInfo.Text, keywordInfo.Color);
228+
}
229+
230+
private static string GetColoredText(string text, AnsiColor color)
231+
{
232+
return PromptConfiguration.HasUserOptedOutFromColor
233+
? $@"""{text}"""
234+
: color.GetEscapeSequence() + text + AnsiEscapeCodes.Reset;
235+
}
236+
237+
public sealed record KeywordInfo(string Text, AnsiColor Color);
238+
}
221239
}

0 commit comments

Comments
 (0)