-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
Copy pathMiscellaneousFilesWorkspace.cs
332 lines (276 loc) · 14.6 KB
/
MiscellaneousFilesWorkspace.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
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Features.Workspaces;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
[Export(typeof(MiscellaneousFilesWorkspace))]
internal sealed partial class MiscellaneousFilesWorkspace : Workspace, IOpenTextBufferEventListener
{
private readonly IThreadingContext _threadingContext;
private readonly IVsService<IVsTextManager> _textManagerService;
private readonly OpenTextBufferProvider _openTextBufferProvider;
private readonly IMetadataAsSourceFileService _fileTrackingMetadataAsSourceService;
private readonly Dictionary<Guid, LanguageInformation> _languageInformationByLanguageGuid = [];
/// <summary>
/// <see cref="WorkspaceRegistration"/> instances for all open buffers being tracked by by this object
/// for possible inclusion into this workspace.
/// </summary>
private IBidirectionalMap<string, WorkspaceRegistration> _monikerToWorkspaceRegistration = BidirectionalMap<string, WorkspaceRegistration>.Empty;
/// <summary>
/// The mapping of all monikers in the RDT and the <see cref="ProjectId"/> of the project and <see cref="SourceTextContainer"/> of the open
/// file we have created for that open buffer. An entry should only be in here if it's also already in <see cref="_monikerToWorkspaceRegistration"/>.
/// </summary>
private readonly Dictionary<string, (ProjectId projectId, SourceTextContainer textContainer)> _monikersToProjectIdAndContainer = new Dictionary<string, (ProjectId, SourceTextContainer)>();
private readonly ImmutableArray<MetadataReference> _metadataReferences;
private IVsTextManager _textManager;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public MiscellaneousFilesWorkspace(
IThreadingContext threadingContext,
IVsService<SVsTextManager, IVsTextManager> textManagerService,
OpenTextBufferProvider openTextBufferProvider,
IMetadataAsSourceFileService fileTrackingMetadataAsSourceService,
VisualStudioWorkspace visualStudioWorkspace)
: base(visualStudioWorkspace.Services.HostServices, WorkspaceKind.MiscellaneousFiles)
{
_threadingContext = threadingContext;
_textManagerService = textManagerService;
_openTextBufferProvider = openTextBufferProvider;
_fileTrackingMetadataAsSourceService = fileTrackingMetadataAsSourceService;
_metadataReferences = ImmutableArray.CreateRange(CreateMetadataReferences());
_openTextBufferProvider.AddListener(this);
}
public async Task InitializeAsync()
{
await TaskScheduler.Default;
_textManager = await _textManagerService.GetValueAsync().ConfigureAwait(false);
}
void IOpenTextBufferEventListener.OnOpenDocument(string moniker, ITextBuffer textBuffer, IVsHierarchy _) => TrackOpenedDocument(moniker, textBuffer);
void IOpenTextBufferEventListener.OnCloseDocument(string moniker) => TryUntrackClosingDocument(moniker);
void IOpenTextBufferEventListener.OnRenameDocument(string newMoniker, string oldMoniker, ITextBuffer buffer)
{
// We want to consider this file to be added in one of two situations:
//
// 1) the old file already was a misc file, at which point we might just be doing a rename from
// one name to another with the same extension
// 2) the old file was a different extension that we weren't tracking, which may have now changed
if (TryUntrackClosingDocument(oldMoniker) || TryGetLanguageInformation(oldMoniker) == null)
{
// Add the new one, if appropriate.
TrackOpenedDocument(newMoniker, buffer);
}
}
/// <summary>
/// Not relevant to the misc workspace.
/// </summary>
void IOpenTextBufferEventListener.OnRefreshDocumentContext(string moniker, IVsHierarchy hierarchy) { }
/// <summary>
/// Not relevant to the misc workspace.
/// </summary>
void IOpenTextBufferEventListener.OnDocumentOpenedIntoWindowFrame(string moniker, IVsWindowFrame windowFrame) { }
/// <summary>
/// Not relevant to the misc workspace.
/// </summary>
void IOpenTextBufferEventListener.OnSaveDocument(string moniker) { }
public void RegisterLanguage(Guid languageGuid, string languageName, string scriptExtension)
=> _languageInformationByLanguageGuid.Add(languageGuid, new LanguageInformation(languageName, scriptExtension));
private LanguageInformation TryGetLanguageInformation(string filename)
{
LanguageInformation languageInformation = null;
if (ErrorHandler.Succeeded(_textManager.MapFilenameToLanguageSID(filename, out var fileLanguageGuid)))
{
_languageInformationByLanguageGuid.TryGetValue(fileLanguageGuid, out languageInformation);
}
return languageInformation;
}
private IEnumerable<MetadataReference> CreateMetadataReferences()
{
var manager = this.Services.GetService<VisualStudioMetadataReferenceManager>();
var searchPaths = VisualStudioMetadataReferenceManager.GetReferencePaths();
return from fileName in new[] { "mscorlib.dll", "System.dll", "System.Core.dll" }
let fullPath = FileUtilities.ResolveRelativePath(fileName, basePath: null, baseDirectory: null, searchPaths: searchPaths, fileExists: File.Exists)
where fullPath != null
select manager.CreateMetadataReferenceSnapshot(fullPath, MetadataReferenceProperties.Assembly);
}
private void TrackOpenedDocument(string moniker, ITextBuffer textBuffer)
{
_threadingContext.ThrowIfNotOnUIThread();
var languageInformation = TryGetLanguageInformation(moniker);
if (languageInformation == null)
{
// We can never put this document in a workspace, so just bail
return;
}
// We don't want to realize the document here unless it's already initialized. Document initialization is watched in
// OnAfterAttributeChangeEx and will retrigger this if it wasn't already done.
if (!_monikerToWorkspaceRegistration.ContainsKey(moniker))
{
var registration = Workspace.GetWorkspaceRegistration(textBuffer.AsTextContainer());
registration.WorkspaceChanged += Registration_WorkspaceChanged;
_monikerToWorkspaceRegistration = _monikerToWorkspaceRegistration.Add(moniker, registration);
if (!IsClaimedByAnotherWorkspace(registration))
{
AttachToDocument(moniker, textBuffer);
}
}
}
private void Registration_WorkspaceChanged(object sender, EventArgs e)
{
// We may or may not be getting this notification from the foreground thread if another workspace
// is raising events on a background. Let's send it back to the UI thread since we can't talk
// to the RDT in the background thread. Since this is all asynchronous a bit more asynchrony is fine.
if (!_threadingContext.JoinableTaskContext.IsOnMainThread)
{
ScheduleTask(() => Registration_WorkspaceChanged(sender, e));
return;
}
_threadingContext.ThrowIfNotOnUIThread();
var workspaceRegistration = (WorkspaceRegistration)sender;
// Since WorkspaceChanged notifications may be asynchronous and happened on a different thread,
// we might have already unsubscribed for this synchronously from the RDT while we were in the process of sending this
// request back to the UI thread.
if (!_monikerToWorkspaceRegistration.TryGetKey(workspaceRegistration, out var moniker))
{
return;
}
// It's also theoretically possible that we are getting notified about a workspace change to a document that has
// been simultaneously removed from the RDT but we haven't gotten the notification. In that case, also bail.
if (!_openTextBufferProvider.IsFileOpen(moniker))
{
return;
}
if (workspaceRegistration.Workspace == null)
{
if (_monikersToProjectIdAndContainer.TryGetValue(moniker, out var projectIdAndSourceTextContainer))
{
// The workspace was taken from us and released and we have only asynchronously found out now.
// We already have the file open in our workspace, but the global mapping of source text container
// to the workspace that owns it needs to be updated once more.
RegisterText(projectIdAndSourceTextContainer.textContainer);
}
else
{
// We should now try to claim this. The moniker we have here is the moniker after the rename if we're currently processing
// a rename. It's possible in that case that this is being closed by the other workspace due to that rename. If the rename
// is changing or removing the file extension, we wouldn't want to try attaching, which is why we have to re-check
// the moniker. Once we observe the rename later in OnAfterAttributeChangeEx we'll completely disconnect.
if (TryGetLanguageInformation(moniker) != null)
{
if (_openTextBufferProvider.TryGetBufferFromFilePath(moniker, out var buffer))
{
AttachToDocument(moniker, buffer);
}
}
}
}
else if (IsClaimedByAnotherWorkspace(workspaceRegistration))
{
// It's now claimed by another workspace, so we should unclaim it
if (_monikersToProjectIdAndContainer.ContainsKey(moniker))
{
DetachFromDocument(moniker);
}
}
}
/// <summary>
/// Stops tracking a document in the RDT for whether we should attach to it.
/// </summary>
/// <returns>true if we were previously tracking it.</returns>
private bool TryUntrackClosingDocument(string moniker)
{
_threadingContext.ThrowIfNotOnUIThread();
var unregisteredRegistration = false;
// Remove our registration changing handler before we call DetachFromDocument. Otherwise, calling DetachFromDocument
// causes us to set the workspace to null, which we then respond to as an indication that we should
// attach again.
if (_monikerToWorkspaceRegistration.TryGetValue(moniker, out var registration))
{
registration.WorkspaceChanged -= Registration_WorkspaceChanged;
_monikerToWorkspaceRegistration = _monikerToWorkspaceRegistration.RemoveKey(moniker);
unregisteredRegistration = true;
}
DetachFromDocument(moniker);
return unregisteredRegistration;
}
private static bool IsClaimedByAnotherWorkspace(WorkspaceRegistration registration)
{
// Currently, we are also responsible for pushing documents to the metadata as source workspace,
// so we count that here as well
return registration.Workspace != null && registration.Workspace.Kind != WorkspaceKind.MetadataAsSource && registration.Workspace.Kind != WorkspaceKind.MiscellaneousFiles;
}
private void AttachToDocument(string moniker, ITextBuffer textBuffer)
{
_threadingContext.ThrowIfNotOnUIThread();
if (_fileTrackingMetadataAsSourceService.TryAddDocumentToWorkspace(moniker, textBuffer.AsTextContainer(), out var _))
{
// We already added it, so we will keep it excluded from the misc files workspace
return;
}
var projectInfo = CreateProjectInfoForDocument(moniker);
OnProjectAdded(projectInfo);
var sourceTextContainer = textBuffer.AsTextContainer();
OnDocumentOpened(projectInfo.Documents.Single().Id, sourceTextContainer);
_monikersToProjectIdAndContainer.Add(moniker, (projectInfo.Id, sourceTextContainer));
}
/// <summary>
/// Creates the <see cref="ProjectInfo"/> that can be added to the workspace for a newly opened document.
/// </summary>
private ProjectInfo CreateProjectInfoForDocument(string filePath)
{
// This should always succeed since we only got here if we already confirmed the moniker is acceptable
var languageInformation = TryGetLanguageInformation(filePath);
Contract.ThrowIfNull(languageInformation);
var checksumAlgorithm = SourceHashAlgorithms.Default;
var fileLoader = new WorkspaceFileTextLoader(Services.SolutionServices, filePath, defaultEncoding: null);
return MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument(
this, filePath, fileLoader, languageInformation, checksumAlgorithm, Services.SolutionServices, _metadataReferences);
}
private void DetachFromDocument(string moniker)
{
_threadingContext.ThrowIfNotOnUIThread();
if (_fileTrackingMetadataAsSourceService.TryRemoveDocumentFromWorkspace(moniker))
{
return;
}
if (_monikersToProjectIdAndContainer.TryGetValue(moniker, out var projectIdAndContainer))
{
OnProjectRemoved(projectIdAndContainer.projectId);
_monikersToProjectIdAndContainer.Remove(moniker);
return;
}
}
public override bool CanApplyChange(ApplyChangesKind feature)
=> feature == ApplyChangesKind.ChangeDocument;
protected override void ApplyDocumentTextChanged(DocumentId documentId, SourceText newText)
{
foreach (var (projectId, textContainer) in _monikersToProjectIdAndContainer.Values)
{
if (projectId == documentId.ProjectId)
{
TextEditApplication.UpdateText(newText, textContainer.GetTextBuffer(), EditOptions.DefaultMinimalChange);
break;
}
}
}
}