This repository was archived by the owner on May 4, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathExpansionClient.cs
More file actions
265 lines (227 loc) · 7.71 KB
/
ExpansionClient.cs
File metadata and controls
265 lines (227 loc) · 7.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
using System;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using Extension.Caching;
using Extension.Logging;
using Extension.SnippetFormats;
using GraphQLClient;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using MSXML;
namespace Extension.AssistantCompletion
{
/// <summary>
/// This class is responsible for the ExpansionSession lifecycle.
/// It initializes a new ExpansionSession and deals with user input during that session.
/// A session ends as soon as the user commits the snippet insertion.
/// </summary>
[Export]
internal class ExpansionClient : IOleCommandTarget, IVsExpansionClient, IDisposable
{
private IVsExpansionSession _currentExpansionSession;
private IOleCommandTarget _nextCommandHandler;
private IVsTextView _currentTextView;
/// <summary>
/// Starts a new snippet insertion based on the caret of the given TextView.
/// </summary>
/// <param name="textView"></param>
/// <param name="snippet"></param>
/// <returns></returns>
public int StartExpansion(IWpfTextView textView, VisualStudioSnippet snippet, bool replaceLine)
{
var caret = textView.Caret;
var code = snippet?.CodeSnippet?.Snippet?.Code?.RawCode;
if (code == null)
return VSConstants.S_FALSE;
// indent code based on caret
var settings = EditorSettingsProvider.GetCurrentIndentationSettings();
var indentedCode = EditorUtils.IndentCodeBlock(code, caret, settings);
snippet.CodeSnippet.Snippet.Code.CodeString = indentedCode;
var currentLine = textView.TextSnapshot.GetLineFromPosition(caret.Position.BufferPosition.Position);
// determine span for insertion, insert at caret or replace whole line
// as the expansion client is a legacy API we have to transform the spans to TextSpan.
TextSpan insertionPosition;
if (replaceLine || (currentLine.GetText().All(c => c == ' ' || c == '\t')))
{
insertionPosition = currentLine.Extent.GetLegacySpan();
}
else
{
insertionPosition = caret.Position.GetLegacyCaretPosition();
}
StartExpansionInternal(textView.ToIVsTextView(), snippet, insertionPosition);
return VSConstants.S_OK;
}
private int StartExpansionInternal(IVsTextView vsTextView, VisualStudioSnippet formattedSnippet, TextSpan position)
{
_currentTextView = vsTextView;
// start listening for incoming commands/keys
vsTextView.AddCommandFilter(this, out _nextCommandHandler);
// create IXMLDOMNode from snippet
IXMLDOMNode snippetXml;
var serializer = new XmlSerializer(typeof(VisualStudioSnippet));
using (var sw = new StringWriter())
{
using var xw = XmlWriter.Create(sw, new XmlWriterSettings { Encoding = Encoding.UTF8 });
serializer.Serialize(xw, formattedSnippet);
var xmlDoc = new DOMDocument();
xmlDoc.loadXML(sw.ToString());
snippetXml = xmlDoc.documentElement.childNodes.nextNode();
}
vsTextView.GetBuffer(out var textLines);
textLines.GetLanguageServiceID(out var languageServiceId);
var expansion = (IVsExpansion)textLines;
expansion.InsertSpecificExpansion(
pSnippet: snippetXml,
tsInsertPos: position,
pExpansionClient: this,
guidLang: languageServiceId,
pszRelativePath: string.Empty,
out _currentExpansionSession);
ReportUsage(formattedSnippet.CodeSnippet.Header.Id);
return VSConstants.S_OK;
}
public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
{
return _nextCommandHandler.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
}
/// <summary>
/// Handle incoming user keys/commands to maily support jumping between user variables during a snippet session.
/// </summary>
/// <param name="pguidCmdGroup"></param>
/// <param name="nCmdID"></param>
/// <param name="nCmdexecopt"></param>
/// <param name="pvaIn"></param>
/// <param name="pvaOut"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
{
try
{
if (_currentExpansionSession == null)
return VSConstants.S_OK;
//make a copy of this so we can look at it after forwarding some commands
var commandID = nCmdID;
var typedChar = char.MinValue;
//make sure the input is a char before getting it
if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR)
{
typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn);
}
//check for a commit character
if (nCmdID == (uint)VSConstants.VSStd2KCmdID.RETURN
|| nCmdID == (uint)VSConstants.VSStd2KCmdID.TAB
|| (char.IsWhiteSpace(typedChar)))
{
if (nCmdID == (uint)VSConstants.VSStd2KCmdID.BACKTAB)
{
_currentExpansionSession.GoToPreviousExpansionField();
return VSConstants.S_OK;
}
else if (nCmdID == (uint)VSConstants.VSStd2KCmdID.TAB)
{
_currentExpansionSession
.GoToNextExpansionField(0); //false to support cycling through all the fields
return VSConstants.S_OK;
}
else if (nCmdID == (uint)VSConstants.VSStd2KCmdID.RETURN ||
nCmdID == (uint)VSConstants.VSStd2KCmdID.CANCEL)
{
if (_currentExpansionSession.EndCurrentExpansion(0) == VSConstants.S_OK)
{
_currentExpansionSession = null;
return VSConstants.S_OK;
}
}
}
//pass along the command so the char is added to the buffer
var result = _nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
return result;
}
catch( Exception e)
{
ExtensionLogger.LogException(e);
Dispose();
return _nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
}
}
public int GetExpansionFunction(IXMLDOMNode xmlFunctionNode, string bstrFieldName,
out IVsExpansionFunction pFunc)
{
pFunc = null;
return VSConstants.S_OK;
}
public int FormatSpan(IVsTextLines pBuffer, TextSpan[] ts)
{
return VSConstants.S_OK;
}
public int EndExpansion()
{
_currentTextView?.RemoveCommandFilter(this);
return VSConstants.S_OK;
}
public int IsValidType(IVsTextLines pBuffer, TextSpan[] ts, string[] rgTypes, int iCountTypes,
out int pfIsValidType)
{
pfIsValidType = 1;
return VSConstants.S_OK;
}
public int IsValidKind(IVsTextLines pBuffer, TextSpan[] ts, string bstrKind, out int pfIsValidKind)
{
pfIsValidKind = 1;
return VSConstants.S_OK;
}
public int OnBeforeInsertion(IVsExpansionSession pSession)
{
return VSConstants.S_OK;
}
public int OnAfterInsertion(IVsExpansionSession pSession)
{
return VSConstants.S_OK;
}
public int PositionCaretForEditing(IVsTextLines pBuffer, TextSpan[] ts)
{
return VSConstants.S_OK;
}
public int OnItemChosen(string pszTitle, string pszPath)
{
return VSConstants.S_OK;
}
/// <summary>
/// Report snippet usage to the Codiga API to update snippet statistics
/// </summary>
/// <param name="id"></param>
private void ReportUsage(long id)
{
var clientProvider = new DefaultCodigaClientProvider();
if(!clientProvider.TryGetClient(out var client))
return;
ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
try
{
await client.RecordRecipeUseAsync(id);
}
catch (CodigaAPIException e)
{
ExtensionLogger.LogException(e);
}
});
}
public void Dispose()
{
_currentTextView?.RemoveCommandFilter(this);
_currentExpansionSession?.EndCurrentExpansion(0);
_currentExpansionSession = null;
}
}
}