Skip to content

Commit f3c7918

Browse files
authored
【性能优化】异步完成改为使用 IAsyncCompletionItemManager 进行拼音匹配;修复同步完成导致的C#/F#输入卡顿 (#13)
* 异步完成改为使用 IAsyncCompletionItemManager 进行拼音匹配 * 更多的并行处理;修复同步完成导致的C#/F#输入卡顿 * 修复:光标在 F# 的特殊标志符之后 完成列表筛选错误
1 parent 95ba0e3 commit f3c7918

14 files changed

+405
-25
lines changed

src/ChinesePinyinIntelliSenseExtender.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
5050
</PropertyGroup>
5151
<ItemGroup>
52+
<Compile Include="Intellisense\AsyncCompletion\IdeographAsyncCompletionItemManager.cs" />
53+
<Compile Include="Intellisense\AsyncCompletion\IdeographAsyncCompletionItemManagerProvider.cs" />
5254
<Compile Include="Intellisense\IIdeographCompletionSet.cs" />
5355
<Compile Include="Intellisense\SyncCompletion\IdeographCustomCommitCompletions.cs" />
5456
<Compile Include="Intellisense\SyncCompletion\IdeographCompletionSets.cs" />
@@ -110,6 +112,7 @@
110112
<Compile Include="Ref\StringTrie\ValueRef.cs" />
111113
<Compile Include="Usings.cs" />
112114
<Compile Include="Util\CompletionSetSelectBestMatchHelper.cs" />
115+
<Compile Include="Util\EnumerableExtend.cs" />
113116
<Compile Include="Util\InputTextMatchHelper.cs" />
114117
<Compile Include="Util\StringPreMatchUtil.cs" />
115118
<Compile Include="Util\InputMethodDictionaryLoader.cs" />
@@ -122,6 +125,7 @@
122125
<Compile Include="Util\ConditionalWeakTableExtensions.cs" />
123126
<Compile Include="Util\InputMethodDictionaryGroup.cs" />
124127
<Compile Include="Util\InputMethodDictionaryGroupProvider.cs" />
128+
<Compile Include="Util\ValueStopwatch.cs" />
125129
</ItemGroup>
126130
<ItemGroup>
127131
<Content Include="..\third\rime-pinyin-simp\pinyin_simp.dict.yaml">

src/ChinesePinyinIntelliSenseExtenderPackage.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Runtime.InteropServices;
22
using System.Threading;
33
using System.Windows;
4-
54
using ChinesePinyinIntelliSenseExtender.Options;
65
using ChinesePinyinIntelliSenseExtender.Util;
76

@@ -47,7 +46,7 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke
4746
{
4847
var exception = task.Exception.InnerException?.ToString();
4948
cancellationToken.ThrowIfCancellationRequested();
50-
MessageBox.Show($"Load options failed with \"{exception}\"", PackageName, MessageBoxButton.OK, MessageBoxImage.Error);
49+
MessageBox.Show($"Load options failed with \"{exception}\"", PackageName);
5150
}
5251
}, cancellationToken, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Current);
5352
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
#nullable enable
2+
3+
using System.Collections.Concurrent;
4+
using System.Collections.Immutable;
5+
using System.Threading;
6+
using ChinesePinyinIntelliSenseExtender.Options;
7+
using ChinesePinyinIntelliSenseExtender.Util;
8+
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
9+
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data;
10+
using Microsoft.VisualStudio.Text;
11+
using Microsoft.VisualStudio.Text.PatternMatching;
12+
13+
namespace ChinesePinyinIntelliSenseExtender.Intellisense.AsyncCompletion;
14+
15+
internal class IdeographAsyncCompletionItemManager(IPatternMatcherFactory _patternMatcherFactory) : IAsyncCompletionItemManager
16+
{
17+
//private void write(TimeSpan t, string msg = "", [CallerMemberName] string n = "")
18+
//{
19+
// using var s = File.AppendText("D:\\time.txt");
20+
// s.WriteLine($"[{DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond}] {n}: {t.TotalMilliseconds}ms ({msg})");
21+
//}
22+
23+
private InputMethodDictionaryGroup? _inputMethodDictionaryGroup;
24+
private readonly ConcurrentDictionary<(PreMatchType, StringPreCheckRule, string), string> _filterTextCache = [];
25+
private GeneralOptions? _options;
26+
27+
private async Task<InputMethodDictionaryGroup> GetInputMethodDictionaryGroupAsync()
28+
{
29+
if (_inputMethodDictionaryGroup is null
30+
|| _inputMethodDictionaryGroup.IsDisposed)
31+
{
32+
_inputMethodDictionaryGroup = await InputMethodDictionaryGroupProvider.GetAsync();
33+
}
34+
return _inputMethodDictionaryGroup;
35+
}
36+
37+
private static IReadOnlyList<CompletionItem> SortCompletionItem(IReadOnlyList<CompletionItem> items)
38+
{
39+
bool CheckShouldSort()
40+
{
41+
return !(Parallel.For(1, items.Count, (i, p) =>
42+
{
43+
if (items[i - 1].SortText.CompareTo(items[i].SortText) > 0) p.Stop();
44+
}).IsCompleted);
45+
}
46+
47+
if (CheckShouldSort()) { return items.AsParallel().OrderBy(i => i.SortText).ToArray(); }
48+
return items;
49+
}
50+
51+
public Task<ImmutableArray<CompletionItem>> SortCompletionListAsync(IAsyncCompletionSession session, AsyncCompletionSessionInitialDataSnapshot data, CancellationToken token)
52+
{
53+
//var st = ValueStopwatch.StartNew();
54+
var sortedItems = SortCompletionItem(data.InitialList).ToImmutableArray();
55+
//var t = st.Elapsed;
56+
//write(t);
57+
return Task.FromResult(sortedItems);
58+
}
59+
60+
public async Task<FilteredCompletionModel> UpdateCompletionListAsync(IAsyncCompletionSession session, AsyncCompletionSessionDataSnapshot data, CancellationToken token)
61+
{
62+
//var st = ValueStopwatch.StartNew();
63+
//TimeSpan t;
64+
65+
var view = session.TextView;
66+
// Filter by text
67+
var filterText = session.ApplicableToSpan.GetText(data.Snapshot);
68+
if (string.IsNullOrWhiteSpace(filterText)
69+
// 光标在 F# 的特殊标志符之后: ``aaa!`` |
70+
|| char.IsWhiteSpace(filterText[filterText.Length - 1]))
71+
{
72+
// There is no text filtering. Just apply user filters, sort alphabetically and return.
73+
IReadOnlyList<CompletionItem> listFiltered = data.InitialSortedList;
74+
if (data.SelectedFilters.Any(n => n.IsSelected))
75+
{
76+
listFiltered = listFiltered.ParallelWhere(n => ShouldBeInCompletionList(n, data.SelectedFilters));
77+
}
78+
var listSorted = SortCompletionItem(listFiltered);
79+
var listHighlighted = listSorted.ParallelSelect(n => new CompletionItemWithHighlight(n)).ToImmutableArray();
80+
81+
//t = st.Elapsed;
82+
//write(t, "short");
83+
return new FilteredCompletionModel(listHighlighted, 0, data.SelectedFilters);
84+
}
85+
86+
_options ??= await GeneralOptions.GetLiveInstanceAsync();
87+
var shouldProcessChecker = StringPreMatchUtil.GetPreCheckPredicate(_options.PreMatchType, _options.PreCheckRule);
88+
var inputMethodDictionaryGroup = await GetInputMethodDictionaryGroupAsync();
89+
90+
string GetFilterText(string t)
91+
{
92+
var key = (_options.PreMatchType, _options.PreCheckRule, t);
93+
94+
if (_filterTextCache.TryGetValue(key, out var r)) return r;
95+
if (!shouldProcessChecker.Check(t)) return AddToCacheAndReturn(t);
96+
97+
var spellings = inputMethodDictionaryGroup.FindAll(t);
98+
var res = _options.EnableMultipleSpellings ? string.Join("/", spellings) : spellings[0];
99+
return AddToCacheAndReturn($"{t}/{res}");
100+
101+
string AddToCacheAndReturn(string v)
102+
{
103+
_filterTextCache.TryAdd(key, v);
104+
return v;
105+
}
106+
}
107+
108+
// Pattern matcher not only filters, but also provides a way to order the results by their match quality.
109+
// The relevant CompletionItem is match.Item1, its PatternMatch is match.Item2
110+
var patternMatcher = _patternMatcherFactory.CreatePatternMatcher(
111+
filterText,
112+
new PatternMatcherCreationOptions(System.Globalization.CultureInfo.CurrentCulture, PatternMatcherCreationFlags.IncludeMatchedSpans));
113+
114+
var matches = data.InitialSortedList
115+
// Perform pattern matching
116+
.ParallelChoose(completionItem =>
117+
{
118+
var match = patternMatcher.TryMatch(GetFilterText(completionItem.FilterText));
119+
// Pick only items that were matched, unless length of filter text is 1
120+
return (filterText.Length == 1 || match.HasValue, (completionItem, match));
121+
});
122+
123+
// See which filters might be enabled based on the typed code
124+
var textFilteredFilters = matches.SelectMany(n => n.completionItem.Filters).Distinct();
125+
126+
// When no items are available for a given filter, it becomes unavailable
127+
var updatedFilters = ImmutableArray.CreateRange(data.SelectedFilters.Select(n => n.WithAvailability(textFilteredFilters.Contains(n.Filter))));
128+
129+
// Filter by user-selected filters. The value on availableFiltersWithSelectionState conveys whether the filter is selected.
130+
var filterFilteredList = matches;
131+
if (data.SelectedFilters.Any(n => n.IsSelected))
132+
{
133+
filterFilteredList = matches.Where(n => ShouldBeInCompletionList(n.completionItem, data.SelectedFilters)).ToArray();
134+
}
135+
136+
var bestMatch = filterFilteredList.OrderByDescending(n => n.match.HasValue).ThenBy(n => n.match).FirstOrDefault();
137+
var listWithHighlights = filterFilteredList.Select(n =>
138+
{
139+
ImmutableArray<Span> safeMatchedSpans = ImmutableArray<Span>.Empty;
140+
141+
if (n.completionItem.DisplayText == n.completionItem.FilterText)
142+
{
143+
if (n.match.HasValue)
144+
{
145+
safeMatchedSpans = n.match.Value.MatchedSpans;
146+
}
147+
}
148+
else
149+
{
150+
// Matches were made against FilterText. We are displaying DisplayText. To avoid issues, re-apply matches for these items
151+
var newMatchedSpans = patternMatcher.TryMatch(n.completionItem.DisplayText);
152+
if (newMatchedSpans.HasValue)
153+
{
154+
safeMatchedSpans = newMatchedSpans.Value.MatchedSpans;
155+
}
156+
}
157+
158+
if (safeMatchedSpans.IsDefaultOrEmpty)
159+
{
160+
return new CompletionItemWithHighlight(n.completionItem);
161+
}
162+
else
163+
{
164+
return new CompletionItemWithHighlight(n.completionItem, safeMatchedSpans);
165+
}
166+
}).ToImmutableArray();
167+
168+
int selectedItemIndex = 0;
169+
if (data.DisplaySuggestionItem)
170+
{
171+
selectedItemIndex = -1;
172+
}
173+
else
174+
{
175+
for (int i = 0; i < listWithHighlights.Length; i++)
176+
{
177+
if (listWithHighlights[i].CompletionItem == bestMatch.completionItem)
178+
{
179+
selectedItemIndex = i;
180+
break;
181+
}
182+
}
183+
}
184+
185+
//t = st.Elapsed;
186+
//write(t, "end");
187+
return new FilteredCompletionModel(listWithHighlights, selectedItemIndex, updatedFilters);
188+
}
189+
190+
private static bool ShouldBeInCompletionList(
191+
CompletionItem item,
192+
ImmutableArray<CompletionFilterWithState> filtersWithState)
193+
{
194+
foreach (var filterWithState in filtersWithState.Where(n => n.IsSelected))
195+
{
196+
if (item.Filters.Any(n => n == filterWithState.Filter))
197+
{
198+
return true;
199+
}
200+
}
201+
return false;
202+
}
203+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#nullable enable
2+
3+
using System.ComponentModel.Composition;
4+
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
5+
using Microsoft.VisualStudio.Text.Editor;
6+
using Microsoft.VisualStudio.Text.PatternMatching;
7+
using Microsoft.VisualStudio.Utilities;
8+
9+
namespace ChinesePinyinIntelliSenseExtender.Intellisense.AsyncCompletion;
10+
11+
[Export(typeof(IAsyncCompletionItemManagerProvider))]
12+
[Name(nameof(IdeographAsyncCompletionItemManagerProvider))]
13+
[ContentType("text")]
14+
[ContentType("Roslyn Languages")]
15+
[Order(Before = PredefinedCompletionNames.DefaultCompletionItemManager)]
16+
[TextViewRole(PredefinedTextViewRoles.Editable)]
17+
[method: ImportingConstructor]
18+
internal class IdeographAsyncCompletionItemManagerProvider(IPatternMatcherFactory patternMatcherFactory) : IAsyncCompletionItemManagerProvider
19+
{
20+
private static IdeographAsyncCompletionItemManager? s_instance;
21+
22+
public IAsyncCompletionItemManager GetOrCreate(ITextView textView)
23+
{
24+
return s_instance ??= new IdeographAsyncCompletionItemManager(patternMatcherFactory);
25+
}
26+
}

src/Intellisense/AsyncCompletion/IdeographAsyncCompletionSource.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public IdeographAsyncCompletionSource(IEnumerable<IAsyncCompletionSource> otherA
4747
{
4848
return null;
4949
}
50-
50+
5151
try
5252
{
5353
s_completionContextRecursionTag.Value = true;
@@ -60,7 +60,7 @@ public IdeographAsyncCompletionSource(IEnumerable<IAsyncCompletionSource> otherA
6060

6161
token.ThrowIfCancellationRequested();
6262

63-
var shouldProcessCheckDelegate = StringPreMatchUtil.GetPreCheckPredicate(Options.PreMatchType, Options.PreCheckRule);
63+
var shouldProcessChecker = StringPreMatchUtil.GetPreCheckPredicate(Options.PreMatchType, Options.PreCheckRule);
6464

6565
var allCompletionItems = tasks.SelectMany(static m => m.Status == TaskStatus.RanToCompletion && m.Result?.Items is not null ? m.Result.Items.AsEnumerable() : Array.Empty<CompletionItem>());
6666

@@ -79,7 +79,7 @@ public IdeographAsyncCompletionSource(IEnumerable<IAsyncCompletionSource> otherA
7979
int bufferIndex = 0;
8080
allCompletionItems.AsParallel()
8181
.WithCancellation(token)
82-
.ForAll(m => CreateCompletionItemWithConvertion(m, inputMethodDictionaryGroup, shouldProcessCheckDelegate, itemBuffer, ref bufferIndex));
82+
.ForAll(m => CreateCompletionItemWithConvertion(m, inputMethodDictionaryGroup, shouldProcessChecker, itemBuffer, ref bufferIndex));
8383

8484
if (bufferIndex > 0)
8585
{
@@ -120,11 +120,11 @@ public CompletionStartData InitializeCompletion(CompletionTrigger trigger, Snaps
120120

121121
#region impl
122122

123-
private void CreateCompletionItemWithConvertion(CompletionItem originCompletionItem, InputMethodDictionaryGroup inputMethodDictionaryGroup, Func<string, bool> shouldProcessCheck, CompletionItem[] itemBuffer, ref int bufferIndex)
123+
private void CreateCompletionItemWithConvertion(CompletionItem originCompletionItem, InputMethodDictionaryGroup inputMethodDictionaryGroup, IPreCheckPredicate shouldProcessCheck, CompletionItem[] itemBuffer, ref int bufferIndex)
124124
{
125125
var originInsertText = originCompletionItem.InsertText;
126126

127-
if (!shouldProcessCheck(originInsertText))
127+
if (!shouldProcessCheck.Check(originInsertText))
128128
{
129129
return;
130130
}

src/Intellisense/AsyncCompletion/IdeographAsyncCompletionSourceProvider.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,16 @@
33
using System.ComponentModel.Composition;
44
using System.Diagnostics;
55
using System.Threading;
6-
76
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
87
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data;
98
using Microsoft.VisualStudio.Text;
109
using Microsoft.VisualStudio.Text.Editor;
11-
using Microsoft.VisualStudio.Utilities;
1210

1311
namespace ChinesePinyinIntelliSenseExtender.Intellisense.AsyncCompletion;
1412

15-
[Export(typeof(IAsyncCompletionSourceProvider))]
16-
[Name("表意文字表音补全")]
17-
[ContentType("text")]
13+
//[Export(typeof(IAsyncCompletionSourceProvider))]
14+
//[Name("表意文字表音补全")]
15+
//[ContentType("text")]
1816
internal class IdeographAsyncCompletionSourceProvider : CompletionSourceProviderBase<ITextView, IAsyncCompletionSource>, IAsyncCompletionSourceProvider
1917
{
2018
#region Private 字段

src/Intellisense/CompletionSourceProviderBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ internal abstract class CompletionSourceProviderBase<TCompletionSourceDependence
4444

4545
static CompletionSourceProviderBase()
4646
{
47-
s_providerContentTypeCache.TryAdd(typeof(IdeographAsyncCompletionSourceProvider), Array.Empty<string>());
47+
//s_providerContentTypeCache.TryAdd(typeof(IdeographAsyncCompletionSourceProvider), Array.Empty<string>());
4848
s_providerContentTypeCache.TryAdd(typeof(IdeographCompletionSourceProvider), Array.Empty<string>());
4949
}
5050

src/Intellisense/SyncCompletion/IdeographCompletionCommandHandler.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ internal IdeographCompletionCommandHandler(IVsTextView textViewAdapter, ITextVie
4747
public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
4848
{
4949
//see https://learn.microsoft.com/zh-cn/visualstudio/extensibility/walkthrough-displaying-statement-completion?view=vs-2022&tabs=csharp#tabpanel_19_csharp
50-
50+
5151
if (VsShellUtilities.IsInAutomationFunction(_serviceProvider)
52-
|| !_options.EnableSyncCompletionSupport)
52+
|| !_options.EnableSyncCompletionSupport
53+
// 跳过 C#/F#,防止输入卡顿
54+
|| _textView.TextBuffer.ContentType.BaseTypes.Any(i => i.TypeName == "Roslyn Languages"))
5355
{
5456
return _nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
5557
}

src/Intellisense/SyncCompletion/IdeographCompletionSource.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ public void AugmentCompletionSession(ICompletionSession session, IList<Completio
3636

3737
var inputMethodDictionaryGroup = GetInputMethodDictionaryGroup();
3838

39-
var shouldProcessCheckDelegate = StringPreMatchUtil.GetPreCheckPredicate(Options.PreMatchType, Options.PreCheckRule);
39+
var shouldProcessChecker = StringPreMatchUtil.GetPreCheckPredicate(Options.PreMatchType, Options.PreCheckRule);
4040

4141
var itemBuffer = ArrayPool<Completion>.Shared.Rent(allCompletions.Count * 5); //预留足够多的空间,避免字典过多导致的问题
4242
try
4343
{
4444
int bufferIndex = 0;
4545
foreach (var completion in allCompletions)
4646
{
47-
CreateCompletionWithConvertion(completion, inputMethodDictionaryGroup, shouldProcessCheckDelegate, itemBuffer, ref bufferIndex);
47+
CreateCompletionWithConvertion(completion, inputMethodDictionaryGroup, shouldProcessChecker, itemBuffer, ref bufferIndex);
4848
}
4949

5050
if (bufferIndex > 0)
@@ -146,12 +146,12 @@ private Completion CreateCompletion(Completion originCompletion, string originIn
146146
}
147147
}
148148

149-
private void CreateCompletionWithConvertion(Completion originCompletion, InputMethodDictionaryGroup inputMethodDictionaryGroup, Func<string, bool> shouldProcessCheck, Completion[] itemBuffer, ref int bufferIndex)
149+
private void CreateCompletionWithConvertion(Completion originCompletion, InputMethodDictionaryGroup inputMethodDictionaryGroup, IPreCheckPredicate shouldProcessCheck, Completion[] itemBuffer, ref int bufferIndex)
150150
{
151151
var originInsertText = originCompletion.InsertionText;
152152

153153
if (string.IsNullOrEmpty(originInsertText)
154-
|| !shouldProcessCheck(originInsertText))
154+
|| !shouldProcessCheck.Check(originInsertText))
155155
{
156156
return;
157157
}

0 commit comments

Comments
 (0)