This repository has been archived by the owner on Jan 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 169
/
EditorState.cs
592 lines (510 loc) · 32 KB
/
EditorState.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
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Build.Execution;
using Microsoft.Quantum.QsCompiler.CompilationBuilder;
using Microsoft.Quantum.QsCompiler.ReservedKeywords;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.Quantum.QsLanguageServer
{
/// <summary>
/// NOTE: This class intentionally does not give access to any FileContentManager that it manages,
/// since it is responsible for coording access to (any routine of) the FileContentManager.
/// </summary>
internal class EditorState : IDisposable
{
private readonly ProjectManager projects;
private readonly ProjectLoader? projectLoader;
public void Dispose() => this.projects.Dispose();
private readonly Action<PublishDiagnosticParams> publish;
private readonly SendTelemetryHandler? sendTelemetry;
/// <summary>
/// needed to determine if the reality of a source file that has changed on disk is indeed given by the content on disk,
/// or whether its current state as it is in the editor needs to be preserved
/// </summary>
private readonly ConcurrentDictionary<Uri, FileContentManager> openFiles = new();
private FileContentManager? GetOpenFile(Uri key) => this.openFiles.TryGetValue(key, out var file) ? file : null;
/// <summary>
/// any edits in the editor to the listed files (keys) are ignored, while changes on disk are still being processed
/// </summary>
private readonly ConcurrentDictionary<Uri, byte> ignoreEditorUpdatesForFiles;
internal void IgnoreEditorUpdatesFor(Uri uri) => this.ignoreEditorUpdatesForFiles.TryAdd(uri, default);
private static bool ValidFileUri(Uri? file) => file != null && file.IsFile && file.IsAbsoluteUri;
private bool IgnoreFile(Uri? file) => file == null || this.ignoreEditorUpdatesForFiles.ContainsKey(file) || file.LocalPath.ToLowerInvariant().Contains("vctmp");
/// <summary>
/// Calls the given publishDiagnostics Action with the changed diagnostics whenever they have changed,
/// calls the given onException Action whenever the compiler encounters an internal error, and
/// does nothing if the a given action is null.
/// </summary>
internal EditorState(
ProjectLoader? projectLoader,
Action<PublishDiagnosticParams>? publishDiagnostics,
SendTelemetryHandler? sendTelemetry,
Action<string, MessageType>? log,
Action<Exception>? onException)
{
this.ignoreEditorUpdatesForFiles = new ConcurrentDictionary<Uri, byte>();
this.sendTelemetry = sendTelemetry;
this.publish = param =>
{
var onProjFile = param.Uri.AbsolutePath.EndsWith(".csproj", StringComparison.InvariantCultureIgnoreCase);
if (!param.Diagnostics.Any() || this.openFiles.ContainsKey(param.Uri) || onProjFile)
{
// Some editors (e.g. Visual Studio) will actually ignore diagnostics for .csproj files.
// Since all errors on project loading are associated with the corresponding project file for publishing,
// we need to replace the project file ending before publishing. This issue is naturally resolved once we have our own project files...
var parentDir = Path.GetDirectoryName(param.Uri.AbsolutePath) ?? "";
var projFileWithoutExtension = Path.GetFileNameWithoutExtension(param.Uri.AbsolutePath);
if (onProjFile && Uri.TryCreate(Path.Combine(parentDir, $"{projFileWithoutExtension}.qsproj"), UriKind.Absolute, out var parentFolder))
{
param.Uri = parentFolder;
}
publishDiagnostics?.Invoke(param);
}
};
this.projectLoader = projectLoader;
this.projects = new ProjectManager(onException, log, this.publish, this.sendTelemetry);
}
/// <summary>
/// Extracts the EvaluatedInclude for all items of the given type in the given project instance,
/// and returns the combined path of the project directory and the evaluated include.
/// </summary>
private static IEnumerable<string> GetItemsByType(ProjectInstance project, string itemType) =>
project.Items
.Where(item => item.ItemType.Equals(itemType, StringComparison.OrdinalIgnoreCase) && item.EvaluatedInclude != null)
.Select(item => Path.Combine(project.Directory, item.EvaluatedInclude));
/// <summary>
/// If the given uri corresponds to a C# project file,
/// determines if the project is consistent with a recognized Q# project using the ProjectLoader.
/// Returns the project information containing the outputPath of the project
/// along with the Q# source files as well as all project and dll references as out parameter if it is.
/// Returns null if it isn't, or if the project file itself has been listed as to be ignored.
/// Calls SendTelemetry with suitable data if the project is a recognized Q# project.
/// </summary>
internal bool QsProjectLoader(Uri projectFile, [NotNullWhen(true)] out ProjectInformation? info)
{
info = null;
if (this.projectLoader is null || projectFile == null || !ValidFileUri(projectFile) || this.IgnoreFile(projectFile))
{
return false;
}
var projectInstance = this.projectLoader.TryGetQsProjectInstance(projectFile.LocalPath);
if (projectInstance == null)
{
return false;
}
/* project item groups */
var sourceFiles = GetItemsByType(projectInstance, "QSharpCompile");
var projectReferences = GetItemsByType(projectInstance, "ProjectReference");
// we need to normalize paths here -
// see also https://stackoverflow.com/questions/1266674/how-can-one-get-an-absolute-or-normalized-file-path-in-net
var decompositions =
GetItemsByType(projectInstance, "ResolvedTargetSpecificDecompositions").Select(Path.GetFullPath);
var references = GetItemsByType(projectInstance, "Reference").Except(decompositions);
/* project properties */
void AddProperty(IDictionary<string, string?> props, string property, params string[] alternativeNames)
{
var propVal = projectInstance.GetPropertyValue(property)?.Trim();
for (var i = 0; string.IsNullOrWhiteSpace(propVal) && i < alternativeNames.Length; ++i)
{
propVal = projectInstance.GetPropertyValue(alternativeNames[i])?.Trim();
}
props.Add(property, propVal);
}
var buildProperties = ImmutableDictionary.CreateBuilder<string, string?>();
AddProperty(buildProperties, MSBuildProperties.TargetPath);
AddProperty(buildProperties, MSBuildProperties.ResolvedProcessorArchitecture);
AddProperty(buildProperties, MSBuildProperties.QuantumSdkPath);
AddProperty(buildProperties, MSBuildProperties.QuantumSdkVersion);
AddProperty(buildProperties, MSBuildProperties.QsharpLangVersion);
#pragma warning disable CS0618 // Type or member is obsolete
AddProperty(buildProperties, MSBuildProperties.ResolvedTargetCapability, MSBuildProperties.ResolvedRuntimeCapabilities);
#pragma warning restore CS0618 // Type or member is obsolete
AddProperty(buildProperties, MSBuildProperties.WarningsAsErrors);
AddProperty(buildProperties, MSBuildProperties.ResolvedQsharpOutputType);
AddProperty(buildProperties, MSBuildProperties.ExposeReferencesViaTestNames);
AddProperty(buildProperties, MSBuildProperties.QsFmtExe);
info = new ProjectInformation(
sourceFiles: sourceFiles,
projectReferences: projectReferences,
references: references,
buildProperties);
/* telemetry data */
var telemetryMeas = new Dictionary<string, int>();
telemetryMeas["sources"] = sourceFiles.Count();
telemetryMeas["csharpfiles"] = GetItemsByType(projectInstance, "Compile").Where(file => !file.EndsWith(".g.cs")).Count();
static bool GeneratePackageInfo(string packageName) =>
packageName.StartsWith("microsoft.quantum.", StringComparison.InvariantCultureIgnoreCase);
string? GetVersion(ProjectItemInstance item) => item.Metadata
.FirstOrDefault(data => data.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.EvaluatedValue;
var packageRefs = projectInstance.Items
.Where(item => item.ItemType.Equals("PackageReference", StringComparison.OrdinalIgnoreCase))
.Where(item => GeneratePackageInfo(item.EvaluatedInclude))
.Select(item => (item.EvaluatedInclude, GetVersion(item)));
var telemetryProps = new Dictionary<string, string?>();
telemetryProps["projectNameHash"] = ProjectLoader.GetProjectNameHash(projectFile.LocalPath);
foreach (var (package, version) in packageRefs)
{
telemetryProps[$"pkgref.{package}"] = version;
}
telemetryProps["qsharplangversion"] = buildProperties[MSBuildProperties.QsharpLangVersion];
telemetryProps["quantumSdkVersion"] = buildProperties[MSBuildProperties.QuantumSdkVersion];
telemetryProps["defaultSimulator"] = projectInstance.GetPropertyValue("DefaultSimulator")?.Trim();
telemetryProps["processorArchitecture"] = buildProperties[MSBuildProperties.ResolvedProcessorArchitecture];
telemetryProps["targetCapability"] = buildProperties[MSBuildProperties.ResolvedTargetCapability];
this.sendTelemetry?.Invoke("project-load", telemetryProps, telemetryMeas); // does not send anything unless the corresponding flag is defined upon compilation
return true;
}
/// <summary>
/// For each given uri, loads the corresponding project if the uri contains the project file for a Q# project,
/// and publishes suitable diagnostics for it.
/// </summary>
public Task LoadProjectsAsync(IEnumerable<Uri> projects) =>
this.projectLoader is not null ?
this.projects.LoadProjectsAsync(projects, this.QsProjectLoader, this.GetOpenFile) :
Task.CompletedTask;
/// <summary>
/// If the given uri corresponds to the project file for a Q# project,
/// updates that project in the list of tracked projects or adds it if needed, and publishes suitable diagnostics for it.
/// </summary>
public Task ProjectDidChangeOnDiskAsync(Uri project) =>
this.projectLoader is not null ?
this.projects.ProjectChangedOnDiskAsync(project, this.QsProjectLoader, this.GetOpenFile) :
Task.CompletedTask;
/// <summary>
/// Updates all tracked Q# projects that reference the assembly with the given uri
/// either directly or indirectly via a reference to another Q# project, and publishes suitable diagnostics.
/// </summary>
public Task AssemblyDidChangeOnDiskAsync(Uri dllPath) =>
this.projects.AssemblyChangedOnDiskAsync(dllPath);
/// <summary>
/// To be used whenever a .qs file is changed on disk.
/// Reloads the file from disk and updates the project(s) which list it as souce file
/// if the file is not listed as currently being open in the editor, publishing suitable diagnostics.
/// If the file is listed as being open in the editor, updates all load diagnostics for the file,
/// but does not update the file content, since the editor manages that one.
/// </summary>
public Task SourceFileDidChangeOnDiskAsync(Uri sourceFile) =>
this.projects.SourceFileChangedOnDiskAsync(sourceFile, this.GetOpenFile);
// routines related to tracking the editor state
/// <summary>
/// To be called whenever a file is opened in the editor.
/// Does nothing if the given file is listed as to be ignored.
/// Otherwise publishes suitable diagnostics for it.
/// Invokes the given Action showError with a suitable message if the given file cannot be loaded.
/// Invokes the given Action logError with a suitable message if the given file cannot be associated with a compilation unit,
/// or if the given file is already listed as being open in the editor.
/// </summary>
/// <exception cref="ArgumentException">The URI of <paramref name="textDocument"/> is not an absolute file URI.</exception>
internal Task OpenFileAsync(
TextDocumentItem textDocument,
Action<string, MessageType>? showError = null,
Action<string, MessageType>? logError = null)
{
if (!ValidFileUri(textDocument.Uri))
{
throw new ArgumentException("invalid text document identifier");
}
_ = this.projects.ManagerTaskAsync(textDocument.Uri, (manager, associatedWithProject) =>
{
if (this.IgnoreFile(textDocument.Uri))
{
return;
}
var newManager = CompilationUnitManager.InitializeFileManager(textDocument.Uri, textDocument.Text, this.publish, ex =>
{
showError?.Invoke($"Failed to load file '{textDocument.Uri.LocalPath}'", MessageType.Error);
manager.LogException(ex);
});
// Currently it is not possible to handle both the behavior of VS and VS Code for changes on disk in a manner that will never fail.
// To mitigate the impact of failures we choose to just log them as info.
var file = this.openFiles.GetOrAdd(textDocument.Uri, newManager);
// this may be the case (depending on the editor) e.g. when opening a version control diff ...
if (file != newManager)
{
showError?.Invoke(
$"Version control and opening multiple versions of the same file in the editor are currently not supported. \n" +
$"Intellisense has been disable for the file '{textDocument.Uri.LocalPath}'. An editor restart is required to enable intellisense again.",
MessageType.Error);
#if DEBUG
if (showError == null)
{
logError?.Invoke("Attempting to open a file that is already open in the editor.", MessageType.Error);
}
#endif
this.IgnoreEditorUpdatesFor(textDocument.Uri);
this.openFiles.TryRemove(textDocument.Uri, out FileContentManager _);
if (!associatedWithProject)
{
_ = manager.TryRemoveSourceFileAsync(textDocument.Uri);
}
this.publish(new PublishDiagnosticParams { Uri = textDocument.Uri, Diagnostics = Array.Empty<Diagnostic>() });
return;
}
if (!associatedWithProject)
{
logError?.Invoke(
$"The file {textDocument.Uri.LocalPath} is not associated with a compilation unit. Only syntactic diagnostics are generated.",
MessageType.Info);
}
// When opening a file, the initial LSP version might have already changed.
file.Version = textDocument.Version;
_ = manager.AddOrUpdateSourceFileAsync(file);
});
// reloading from disk in case we encountered a file already open error above
return this.projects.SourceFileChangedOnDiskAsync(textDocument.Uri, this.GetOpenFile); // NOTE: relies on that the manager task is indeed executed first!
}
/// <summary>
/// To be called whenever a file is changed within the editor (i.e. changes are not necessarily reflected on disk).
/// Does nothing if the given file is listed as to be ignored.
/// </summary>
/// <exception cref="ArgumentException">The URI of the text document identifier in the given parameter is not an absolute file URI.</exception>
internal Task DidChangeAsync(DidChangeTextDocumentParams param)
{
if (!ValidFileUri(param.TextDocument.Uri))
{
throw new ArgumentException("invalid text document identifier");
}
return this.projects.ManagerTaskAsync(param.TextDocument.Uri, (manager, __) =>
{
if (this.IgnoreFile(param.TextDocument.Uri))
{
return;
}
// Currently it is not possible to handle both the behavior of VS and VS Code for changes on disk in a manner that will never fail.
// To mitigate the impact of failures we choose to ignore them silently.
if (!this.openFiles.ContainsKey(param.TextDocument.Uri))
{
return;
}
_ = manager.SourceFileDidChangeAsync(param); // independent on whether the file does or doesn't belong to a project
});
}
/// <summary>
/// Used to reload the file content when a file is saved.
/// Does nothing if the given file is listed as to be ignored.
/// Expects to get the entire content of the file at the time of saving as argument.
/// </summary>
/// <exception cref="ArgumentException">The URI of <paramref name="textDocument"/> is not an absolute file URI.</exception>
internal Task SaveFileAsync(TextDocumentIdentifier textDocument, string fileContent)
{
if (!ValidFileUri(textDocument.Uri))
{
throw new ArgumentException("invalid text document identifier");
}
return this.projects.ManagerTaskAsync(textDocument.Uri, (manager, __) =>
{
if (this.IgnoreFile(textDocument.Uri))
{
return;
}
// Currently it is not possible to handle both the behavior of VS and VS Code for changes on disk in a manner that will never fail.
// To mitigate the impact of failures we choose to ignore them silently and do our best to recover.
if (!this.openFiles.TryGetValue(textDocument.Uri, out var file))
{
file = CompilationUnitManager.InitializeFileManager(textDocument.Uri, fileContent, this.publish, manager.LogException);
this.openFiles.TryAdd(textDocument.Uri, file);
_ = manager.AddOrUpdateSourceFileAsync(file);
}
else
{
_ = manager.AddOrUpdateSourceFileAsync(file, fileContent); // let's reload the file content on saving
}
});
}
/// <summary>
/// To be called whenever a file is closed in the editor.
/// Does nothing if the given file is listed as to be ignored.
/// Otherwise the file content is reloaded from disk (in case changes in the editor are discarded without closing), and the diagnostics are updated.
/// Invokes the given Action onError with a suitable message if the given file is not listed as being open in the editor.
/// </summary>
/// <exception cref="ArgumentException">The URI of <paramref name="textDocument"/> is null or not an absolute file URI.</exception>
internal Task CloseFileAsync(TextDocumentIdentifier textDocument, Action<string, MessageType>? onError = null)
{
if (textDocument is null || !ValidFileUri(textDocument.Uri))
{
throw new ArgumentException("invalid text document identifier");
}
_ = this.projects.ManagerTaskAsync(textDocument.Uri, (manager, associatedWithProject) => // needs to be *first* (due to the modification of OpenFiles)
{
if (this.IgnoreFile(textDocument.Uri))
{
return;
}
// Currently it is not possible to handle both the behavior of VS and VS Code for changes on disk in a manner that will never fail.
// To mitigate the impact of failures we choose to ignore them silently.
var removed = this.openFiles.TryRemove(textDocument.Uri, out _);
#if DEBUG
if (!removed)
{
onError?.Invoke($"Attempting to close file '{textDocument.Uri.LocalPath}' that is not currently listed as open in the editor.", MessageType.Error);
}
#endif
if (!associatedWithProject)
{
_ = manager.TryRemoveSourceFileAsync(textDocument.Uri);
}
this.publish(new PublishDiagnosticParams { Uri = textDocument.Uri, Diagnostics = Array.Empty<Diagnostic>() });
});
// When edits are made in a file, but those are discarded by closing the file and hitting "no, don't save",
// no notification is sent for the now discarded changes;
// hence we reload the file content from disk upon closing.
return this.projects.SourceFileChangedOnDiskAsync(textDocument.Uri, this.GetOpenFile); // NOTE: relies on that the manager task is indeed executed first!
}
// routines related to providing information for editor commands
/// <summary>
/// Returns the workspace edit that describes the changes to be done if the symbol at the given position - if any - is renamed to the given name.
/// Returns null if no symbol exists at the specified position,
/// or if the specified uri is not a valid file uri.
/// or if some parameters are unspecified (null) or inconsistent with the tracked editor state.
/// </summary>
public WorkspaceEdit? Rename(RenameParams? param, bool versionedChanges = false) =>
ValidFileUri(param?.TextDocument?.Uri) && !this.IgnoreFile(param?.TextDocument?.Uri) ? this.projects.Rename(param, versionedChanges) : null;
/// <summary>
/// Returns the edits to format the file according to the specified settings.
/// Returns null if the specified uri is not a valid file uri,
/// or the given file is listed as to be ignored,
/// or if some parameters are unspecified (null).
/// </summary>
public TextEdit[]? Formatting(DocumentFormattingParams param) =>
ValidFileUri(param?.TextDocument?.Uri) && !this.IgnoreFile(param?.TextDocument?.Uri) ? this.projects.Formatting(param) : null;
/// <summary>
/// Returns the source file and position where the item at the given position is declared at,
/// if such a declaration exists, and returns the given position and file otherwise.
/// Returns null if the given file is listed as to be ignored or if the information cannot be determined at this point.
/// </summary>
public Location? DefinitionLocation(TextDocumentPositionParams? param) =>
ValidFileUri(param?.TextDocument?.Uri) && !this.IgnoreFile(param?.TextDocument?.Uri) ? this.projects.DefinitionLocation(param) : null;
/// <summary>
/// Returns the signature help information for a call expression if there is such an expression at the specified position.
/// Returns null if the given file is listed as to be ignored,
/// or if some parameters are unspecified (null),
/// or if the specified position is not a valid position within the currently processed file content
/// or if no call expression exists at the specified position at this time
/// or if no signature help information can be provided for the call expression at the specified position.
/// </summary>
public SignatureHelp? SignatureHelp(TextDocumentPositionParams param, MarkupKind format = MarkupKind.PlainText) =>
ValidFileUri(param?.TextDocument?.Uri) && !this.IgnoreFile(param?.TextDocument?.Uri) ? this.projects.SignatureHelp(param, format) : null;
/// <summary>
/// Returns information about the item at the specified position as Hover information.
/// Returns null if the given file is listed as to be ignored,
/// or if some parameters are unspecified (null),
/// or if the specified position is not a valid position within the currently processed file content,
/// or if no token exists at the specified position at this time.
/// </summary>
public Hover? HoverInformation(TextDocumentPositionParams param, MarkupKind format = MarkupKind.PlainText) =>
ValidFileUri(param?.TextDocument?.Uri) && !this.IgnoreFile(param?.TextDocument?.Uri) ? this.projects.HoverInformation(param, format) : null;
/// <summary>
/// Returns an array with all usages of the identifier at the given position (if any) as DocumentHighlights.
/// Returns null if the given file is listed as to be ignored,
/// or if some parameters are unspecified (null),
/// or if the specified position is not a valid position within the currently processed file content,
/// or if no identifier exists at the specified position at this time.
/// </summary>
public DocumentHighlight[]? DocumentHighlights(TextDocumentPositionParams param) =>
ValidFileUri(param?.TextDocument?.Uri) && !this.IgnoreFile(param?.TextDocument?.Uri) ? this.projects.DocumentHighlights(param) : null;
/// <summary>
/// Returns an array with all locations where the symbol at the given position - if any - is referenced.
/// Returns null if the given file is listed as to be ignored,
/// or if some parameters are unspecified (null),
/// or if the specified position is not a valid position within the currently processed file content,
/// or if no symbol exists at the specified position at this time.
/// </summary>
public Location[]? SymbolReferences(ReferenceParams param) =>
ValidFileUri(param?.TextDocument?.Uri) && !this.IgnoreFile(param?.TextDocument?.Uri) ? this.projects.SymbolReferences(param) : null;
/// <summary>
/// Returns the SymbolInformation for each namespace declaration, type declaration, and function or operation declaration.
/// Returns null if the given file is listed as to be ignored, or if the given parameter or its uri is null.
/// </summary>
public SymbolInformation[]? DocumentSymbols(DocumentSymbolParams param) =>
ValidFileUri(param?.TextDocument?.Uri) && !this.IgnoreFile(param?.TextDocument?.Uri) ? this.projects.DocumentSymbols(param) : null;
/// <summary>
/// Returns a look-up of workspace edits suggested by the compiler for the given location and context.
/// The key of the look-up is a suitable title for the corresponding edits that can be presented to the user.
/// Returns null if the given file is listed as to be ignored, or if the given parameter or its uri is null.
/// </summary>
public IEnumerable<CodeAction>? CodeActions(CodeActionParams param) =>
ValidFileUri(param?.TextDocument?.Uri) && !this.IgnoreFile(param?.TextDocument?.Uri) ? this.projects.CodeActions(param) : null;
/// <summary>
/// Returns a list of suggested completion items for the given location.
/// <para/>
/// Returns null if the given file is listed as to be ignored, or if the given parameter or its uri or position is null.
/// </summary>
public CompletionList? Completions(TextDocumentPositionParams param) =>
ValidFileUri(param?.TextDocument?.Uri) && !this.IgnoreFile(param?.TextDocument?.Uri)
? this.projects.Completions(param)
: null;
/// <summary>
/// Resolves additional information for the given completion item. Returns the original completion item if no
/// additional information is available, or if the completion item is no longer valid or accurate.
/// <para/>
/// Returns null if any parameter is null or the file given in the original completion request is invalid or
/// ignored.
/// </summary>
internal CompletionItem? ResolveCompletion(CompletionItem? item, CompletionItemData data, MarkupKind format) =>
item != null && ValidFileUri(data?.TextDocument?.Uri) && !this.IgnoreFile(data?.TextDocument?.Uri)
? this.projects.ResolveCompletion(item, data, format)
: null;
// utils to query the editor state server for testing purposes
// -> explicitly part of this class because any access to the resources may need to be coordinated as well
/// <summary>
/// Waits for all currently running or queued tasks to finish before getting the file content in memory.
/// -> Method to be used for testing/diagnostic purposes only!
/// </summary>
internal string[]? FileContentInMemory(TextDocumentIdentifier textDocument) =>
this.projects.FileContentInMemory(textDocument);
/// <summary>
/// Waits for all currently running or queued tasks to finish before getting the diagnostics for the given file.
/// -> Method to be used for testing purposes only!
/// </summary>
internal Diagnostic[]? FileDiagnostics(TextDocumentIdentifier textDocument)
{
var allDiagnostics = this.projects.GetDiagnostics(textDocument?.Uri);
return allDiagnostics?.Length == 1 ? allDiagnostics.Single().Diagnostics : null; // count is > 1 if the given uri corresponds to a project file
}
/// <summary>
/// Waits for all currently running or queued tasks to finish before getting the project information.
/// -> Method to be used for testing/diagnostic purposes only!
/// </summary>
internal string? ProjectInformation(TextDocumentIdentifier textDocument)
{
var projectInfo = this.projects.GetProjectInformation(textDocument);
if (projectInfo is null)
{
return null;
}
var stringWriter = new StringWriter();
var writer = System.Xml.XmlWriter.Create(stringWriter);
void WriteElementGroup(string groupName, IEnumerable<string> paths)
{
writer.WriteStartElement(groupName);
foreach (var path in paths)
{
writer.WriteStartElement("File");
writer.WriteAttributeString("Path", path);
writer.WriteEndElement();
}
writer.WriteEndElement();
}
writer.WriteStartDocument();
writer.WriteStartElement("ProjectInfo");
writer.WriteAttributeString("OutputPath", projectInfo.Properties.DllOutputPath);
writer.WriteAttributeString("TargetCapability", projectInfo.Properties.TargetCapability.Name);
writer.WriteAttributeString("ProcessorArchitecture", projectInfo.Properties.ProcessorArchitecture);
WriteElementGroup("Sources", projectInfo.SourceFiles);
WriteElementGroup("ProjectReferences", projectInfo.ProjectReferences);
WriteElementGroup("References", projectInfo.References);
writer.WriteEndElement();
writer.WriteEndDocument();
writer.Flush();
return stringWriter.ToString();
}
}
}