-
Notifications
You must be signed in to change notification settings - Fork 107
/
ToolWindowWithEditor.cs
411 lines (345 loc) · 19.7 KB
/
ToolWindowWithEditor.cs
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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
using System;
using System.Runtime.InteropServices;
using System.Windows.Controls;
using GitScc.Diff;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
namespace GitScc
{
public class ToolWindowWithEditor<T> : ToolWindowPane //, IOleCommandTarget, IVsFindTarget
where T : Control
{
#region Constants
private const int WM_KEYFIRST = 0x0100;
private const int WM_KEYLAST = 0x0109;
#endregion
#region Private Fields
protected T control;
private IOleCommandTarget cachedEditorCommandTarget;
private IVsTextView textView;
private IVsCodeWindow codeWindow;
private IVsInvisibleEditor invisibleEditor;
private IVsFindTarget cachedEditorFindTarget;
private Microsoft.VisualStudio.OLE.Interop.IServiceProvider cachedOleServiceProvider;
#endregion
public ToolWindowWithEditor()
: base(null)
{
}
#region Public Methods
/// <summary>
/// Method to call to cause us to place the file at the given path into our hosted editor.
/// </summary>
public Tuple<Control, IVsTextView> SetDisplayedFile(string filePath)
{
ClearEditor();
try
{
//Get an invisible editor over the file, this makes it much easier than having to manually figure out the right content type,
//language service, and it will automatically associate the document with its owning project, meaning we will get intellisense
//in our editor with no extra work.
IVsInvisibleEditorManager invisibleEditorManager = (IVsInvisibleEditorManager)GetService(typeof(SVsInvisibleEditorManager));
ErrorHandler.ThrowOnFailure(invisibleEditorManager.RegisterInvisibleEditor(filePath,
pProject: null,
dwFlags: (uint)_EDITORREGFLAGS.RIEF_ENABLECACHING,
pFactory: null,
ppEditor: out this.invisibleEditor));
//The doc data is the IVsTextLines that represents the in-memory version of the file we opened in our invisibe editor, we need
//to extract that so that we can create our real (visible) editor.
IntPtr docDataPointer = IntPtr.Zero;
Guid guidIVSTextLines = typeof(IVsTextLines).GUID;
ErrorHandler.ThrowOnFailure(this.invisibleEditor.GetDocData(fEnsureWritable: 1, riid: ref guidIVSTextLines, ppDocData: out docDataPointer));
try
{
IVsTextLines docData = (IVsTextLines)Marshal.GetObjectForIUnknown(docDataPointer);
//Get the component model so we can request the editor adapter factory which we can use to spin up an editor instance.
IComponentModel componentModel = (IComponentModel)GetService(typeof(SComponentModel));
IVsEditorAdaptersFactoryService editorAdapterFactoryService = componentModel.GetService<IVsEditorAdaptersFactoryService>();
//Create a code window adapter.
this.codeWindow = editorAdapterFactoryService.CreateVsCodeWindowAdapter(OleServiceProvider);
//Disable the splitter control on the editor as leaving it enabled causes a crash if the user
//tries to use it here :(
IVsCodeWindowEx codeWindowEx = (IVsCodeWindowEx)this.codeWindow;
INITVIEW[] initView = new INITVIEW[1];
codeWindowEx.Initialize((uint)_codewindowbehaviorflags.CWB_DISABLESPLITTER,
VSUSERCONTEXTATTRIBUTEUSAGE.VSUC_Usage_Filter,
szNameAuxUserContext: "",
szValueAuxUserContext: "",
InitViewFlags: 0,
pInitView: initView);
//docData.SetStateFlags((uint)BUFFERSTATEFLAGS.BSF_USER_READONLY); //set read only
//Associate our IVsTextLines with our new code window.
ErrorHandler.ThrowOnFailure(this.codeWindow.SetBuffer((IVsTextLines)docData));
//Get our text view for our editor which we will use to get the WPF control that hosts said editor.
ErrorHandler.ThrowOnFailure(this.codeWindow.GetPrimaryView(out this.textView));
//Get our WPF host from our text view (from our code window).
IWpfTextViewHost textViewHost = editorAdapterFactoryService.GetWpfTextViewHost(this.textView);
textViewHost.TextView.Options.SetOptionValue(GitTextViewOptions.DiffMarginId, false);
textViewHost.TextView.Options.SetOptionValue(DefaultTextViewHostOptions.ChangeTrackingId, false);
textViewHost.TextView.Options.SetOptionValue(DefaultTextViewHostOptions.GlyphMarginId, false);
textViewHost.TextView.Options.SetOptionValue(DefaultTextViewHostOptions.LineNumberMarginId, false);
textViewHost.TextView.Options.SetOptionValue(DefaultTextViewHostOptions.OutliningMarginId, false);
textViewHost.TextView.Options.SetOptionValue(DefaultTextViewOptions.ViewProhibitUserInputId, true);
return Tuple.Create<Control, IVsTextView>(textViewHost.HostControl, this.textView);
//Debug.Assert(contentControl != null);
//contentControl.Content = textViewHost.HostControl;
}
finally
{
if (docDataPointer != IntPtr.Zero)
{
//Release the doc data from the invisible editor since it gave us a ref-counted copy.
Marshal.Release(docDataPointer);
}
}
}
catch { }
return null;
}
#endregion
#region Protected Overrides
/// <summary>
/// Preprocess input (keyboard) messages in order to translate them to editor commands if they map. Since our tool window is NOT an
/// editor the shell won't consider the editor keybindings when doing its usual input pre-translation. We could either set our
/// window frames InheritKeyBindings property to point to the std editor factory GUID OR we can do this. I chose this method as
/// it allows us to have the editor keybindings active ONLY when the focus is in the editor, that way we won't have editor
/// keybindings active in our window UNLESS the editor has focus, which is what we want.
/// </summary>
//protected override bool PreProcessMessage(ref System.Windows.Forms.Message m)
//{
// //Only try and pre-process keyboard input messages, all others are not interesting to us.
// if (m.Msg >= WM_KEYFIRST && m.Msg <= WM_KEYLAST)
// {
// //Only attempt to do the input -> command mapping if focus is inside our hosted editor.
// if (this.control.IsKeyboardFocusWithin)
// {
// IVsFilterKeys2 filterKeys = (IVsFilterKeys2)GetService(typeof(SVsFilterKeys));
// MSG oleMSG = new MSG() { hwnd = m.HWnd, lParam = m.LParam, wParam = m.WParam, message = (uint)m.Msg };
// //Ask the shell to do the command mapping for us and fire off the command if it succeeds with that mapping. We pass no 'custom' scopes
// //(third and fourth argument) because we pass VSTAEXF_UseTextEditorKBScope to indicate we want the shell to apply the text editor
// //command scope to this call.
// Guid cmdGuid;
// uint cmdId;
// int fTranslated;
// int fStartsMultiKeyChord;
// int res = filterKeys.TranslateAcceleratorEx(new MSG[] { oleMSG },
// (uint)(__VSTRANSACCELEXFLAGS.VSTAEXF_UseTextEditorKBScope),
// 0 /*scope count*/,
// new Guid[0] /*scopes*/,
// out cmdGuid,
// out cmdId,
// out fTranslated,
// out fStartsMultiKeyChord);
// if (fStartsMultiKeyChord == 0)
// {
// //HACK: Work around a bug in TranslateAcceleratorEx that will report it DIDN'T do the command mapping
// //when in fact it did :( Problem has been fixed (since I found it while writing this code), but in the
// //mean time we need to successfully eat keystrokes that have been mapped to commands and dispatched,
// //we DON'T want them to continue on to Translate/Dispatch. "Luckily" asking TranslateAcceleratorEx to
// //do the mapping WITHOUT firing the command will give us the right result code to indicate if the command
// //mapped or not, unfortunately we can't always do this as it would break key-chords as it causes the shell
// //to not remember the first input match of a multi-part chord, hence the reason we ONLY hit this block if
// //it didn't tell us the input IS part of key-chord.
// res = filterKeys.TranslateAcceleratorEx(new MSG[] { oleMSG },
// (uint)(__VSTRANSACCELEXFLAGS.VSTAEXF_NoFireCommand | __VSTRANSACCELEXFLAGS.VSTAEXF_UseTextEditorKBScope),
// 0,
// new Guid[0],
// out cmdGuid,
// out cmdId,
// out fTranslated,
// out fStartsMultiKeyChord);
// return (res == VSConstants.S_OK);
// }
// //We return true (that we handled the input message) if we managed to map it to a command OR it was the
// //beginning of a multi-key chord, anything else should continue on with normal processing.
// return ((res == VSConstants.S_OK) || (fStartsMultiKeyChord != 0));
// }
// }
// return base.PreProcessMessage(ref m);
//}
#endregion
#region IOleCommandTarget Members
/// <summary>
/// When our tool window is active it will be the 'focus command target' of the shell's command route, as such we need to handle any
/// commands we want here and forward the rest to the editor (since most all typing is translated into a command for the editor to
/// deal with).
/// </summary>
//int IOleCommandTarget.Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
//{
// if (this.control.IsKeyboardFocusWithin && (EditorCommandTarget != null))
// {
// int res = EditorCommandTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
// return res;
// }
// return (int)Microsoft.VisualStudio.OLE.Interop.Constants.OLECMDERR_E_NOTSUPPORTED;
//}
/// <summary>
/// When our tool window is active it will be the 'focus command target' of the shell's command route, as such we need to set the state
/// of any commands we want here and forward the rest to the editor (since most all typing is translated into a command for the editor to
/// deal with).
/// </summary>
//int IOleCommandTarget.QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
//{
// if (this.control.IsKeyboardFocusWithin && (EditorCommandTarget != null))
// {
// return EditorCommandTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
// }
// return (int)Microsoft.VisualStudio.OLE.Interop.Constants.OLECMDERR_E_NOTSUPPORTED;
//}
#endregion
#region IVsFindTarget Members
public int Find(string pszSearch, uint grfOptions, int fResetStartPoint, IVsFindHelper pHelper, out uint pResult)
{
pResult = 0;
return EditorFindTarget == null ? 0
: EditorFindTarget.Find(pszSearch, grfOptions, fResetStartPoint, pHelper, out pResult);
}
public int GetCapabilities(bool[] pfImage, uint[] pgrfOptions)
{
return EditorFindTarget == null ? 0
: EditorFindTarget.GetCapabilities(pfImage, pgrfOptions);
}
public int GetCurrentSpan(TextSpan[] pts)
{
return EditorFindTarget == null ? 0
: EditorFindTarget.GetCurrentSpan(pts);
}
public int GetFindState(out object ppunk)
{
ppunk = null;
return EditorFindTarget == null ? 0
: EditorFindTarget.GetFindState(out ppunk);
}
public int GetMatchRect(RECT[] prc)
{
return EditorFindTarget == null ? 0
: EditorFindTarget.GetMatchRect(prc);
}
public int GetProperty(uint propid, out object pvar)
{
pvar = null;
return EditorFindTarget == null ? 0
: EditorFindTarget.GetProperty(propid, out pvar);
}
public int GetSearchImage(uint grfOptions, IVsTextSpanSet[] ppSpans, out IVsTextImage ppTextImage)
{
ppTextImage = null;
return EditorFindTarget == null ? 0
: EditorFindTarget.GetSearchImage(grfOptions, ppSpans, out ppTextImage);
}
public int MarkSpan(TextSpan[] pts)
{
return EditorFindTarget == null ? 0
: EditorFindTarget.MarkSpan(pts);
}
public int NavigateTo(TextSpan[] pts)
{
return EditorFindTarget == null ? 0
: EditorFindTarget.NavigateTo(pts);
}
public int NotifyFindTarget(uint notification)
{
return EditorFindTarget == null ? 0
: EditorFindTarget.NotifyFindTarget(notification);
}
public int Replace(string pszSearch, string pszReplace, uint grfOptions, int fResetStartPoint, IVsFindHelper pHelper, out int pfReplaced)
{
pfReplaced = 0;
return EditorFindTarget == null ? 0
: EditorFindTarget.Replace(pszSearch, pszReplace, grfOptions, fResetStartPoint, pHelper, out pfReplaced);
}
public int SetFindState(object pUnk)
{
return EditorFindTarget == null ? 0
: EditorFindTarget.SetFindState(pUnk);
}
#endregion
#region Private Properties
/// <summary>
/// The IOleCommandTarget for the editor that our tool window will forward all command requests to when it is the active tool window
/// and the editor we are hosting has keyboard focus.
/// </summary>
private IOleCommandTarget EditorCommandTarget
{
get
{
return (this.cachedEditorCommandTarget ?? (this.cachedEditorCommandTarget = this.textView as IOleCommandTarget));
}
}
/// <summary>
/// The IVsFindTarget for the editor that our tool window will forward all find releated requests to when it is the active tool window
/// and the editor we are hosting has keyboard focus.
/// </summary>
private IVsFindTarget EditorFindTarget
{
get
{
return (this.cachedEditorFindTarget ?? (this.cachedEditorFindTarget = this.textView as IVsFindTarget));
}
}
/// <summary>
/// The shell's service provider as an OLE service provider (needed to create the editor bits).
/// </summary>
private Microsoft.VisualStudio.OLE.Interop.IServiceProvider OleServiceProvider
{
get
{
if (this.cachedOleServiceProvider == null)
{
//ServiceProvider.GlobalProvider is a System.IServiceProvider, but the editor pieces want an OLE.IServiceProvider, luckily the
//global provider is also IObjectWithSite and we can use that to extract its underlying (OLE) IServiceProvider object.
IObjectWithSite objWithSite = (IObjectWithSite)ServiceProvider.GlobalProvider;
Guid interfaceIID = typeof(Microsoft.VisualStudio.OLE.Interop.IServiceProvider).GUID;
IntPtr rawSP;
objWithSite.GetSite(ref interfaceIID, out rawSP);
try
{
if (rawSP != IntPtr.Zero)
{
//Get an RCW over the raw OLE service provider pointer.
this.cachedOleServiceProvider = (Microsoft.VisualStudio.OLE.Interop.IServiceProvider)Marshal.GetObjectForIUnknown(rawSP);
}
}
finally
{
if (rawSP != IntPtr.Zero)
{
//Release the raw pointer we got from IObjectWithSite so we don't cause leaks.
Marshal.Release(rawSP);
}
}
}
return this.cachedOleServiceProvider;
}
}
#endregion
#region Private Methods
/// <summary>
/// Cleans up an existing editor if we are about to put a new one in place, used to close down the old editor bits as well as
/// nulling out any cached objects that we have that came from the now dead editor.
/// </summary>
internal void ClearEditor()
{
if (this.codeWindow != null)
{
this.codeWindow.Close();
this.codeWindow = null;
}
if (this.textView != null)
{
this.textView.CloseView();
this.textView = null;
}
this.cachedEditorCommandTarget = null;
this.cachedEditorFindTarget = null;
this.invisibleEditor = null;
}
#endregion
}
}