or is the only way to get the editor font.
+ // HOWEVER, they both have downside. is a block element, and is styled with top padding, so looks awful.
+ // And the hover widget override's 's background color so if we use it for the signature, it doesn't match styling of other hovers.
+ if(supportsMarkdown && supportsTagCode)
+ {
+ //sb.Append("");
+ sb.Append("");
+ //sb.Append("");
+ }
+
+ /*
+ TODO can we use ```msbuild with a hacky match in the grammar?
+ {
+ "match": "\u200c\u200c([a-zA-Z-]+) ([a-zA-Z-]+)(?: \: ([a-zA-Z-]+))?\u200c\u200c",
+ "captures": {
+ "1": { "name": "keyword" },
+ "2": { "name": "variable.other" },
+ "3": { "name": "entity.name.type" }
+ }
+ },
+ */
+ }
+
+ void EndSignatureBlock()
+ {
+ if(supportsMarkdown && supportsTagCode)
+ {
+ //sb.Append("");
+ sb.Append("
");
+ //sb.Append("
");
+ }
+ }
+
+ bool WriteNameElement(ISymbol info)
+ {
+ var label = DescriptionFormatter.GetTitle(info);
+ if(label.kind == null) {
+ return false;
+ }
+
+ StartSignatureBlock();
+
+ // the icon is a font so put it inside the so size matches signature text
+ if (supportsIcons && info.GetGlyph(false) is MSBuildGlyph glyph)
+ {
+ AppendIcon(glyph);
+ }
+
+ AppendColorSpan(KnownColor.Keyword, label.kind);
+ sb.Append(' ');
+ AppendColorSpan(KnownColor.Identifier, label.name);
+
+ if(info is FunctionInfo fi) {
+ if(!fi.IsProperty) {
+ AppendColorSpan(KnownColor.Punctuation, "(");
+
+ bool first = true;
+ foreach(var p in fi.Parameters) {
+ if(first) {
+ first = false;
+ } else {
+ AppendColorSpan(KnownColor.Punctuation, ",");
+ sb.Append(' ');
+ }
+
+ AppendColorSpan(KnownColor.Parameter, p.Name);
+ sb.Append(' ');
+ AppendColorSpan(KnownColor.Punctuation, ":");
+ sb.Append(' ');
+ AppendColorSpan(KnownColor.Type, p.Type);
+ }
+ AppendColorSpan(KnownColor.Punctuation, ")");
+ }
+ }
+
+ if(info is ITypedSymbol typedSymbol) {
+ var tdesc = typedSymbol.GetTypeDescription();
+ if(tdesc.Count > 0) {
+ var typeInfo = string.Join(" ", tdesc);
+ sb.Append(' ');
+ AppendColorSpan(KnownColor.Punctuation, ":");
+ sb.Append(' ');
+ AppendColorSpan(KnownColor.Type, typeInfo);
+ }
+ }
+
+ EndSignatureBlock();
+
+ return true;
+ }
+
+ /*
+ ContainerElement GetSeenInElement (ITextBuffer buffer, MSBuildResolveResult rr, ISymbol info, MSBuildRootDocument doc)
+ {
+ var seenIn = doc.GetDescendedDocumentsReferencingSymbol (info).ToList ();
+ if (seenIn.Count == 0) {
+ return null;
+ }
+
+ Func shorten = null;
+
+ var elements = new List ();
+
+ int count = 0;
+ foreach (var s in seenIn) {
+ if (count == 5) {
+ elements.Add (new ClassifiedTextElement (
+ new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, "["),
+ new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, "more in Find References", () => {
+ NavigationService.FindReferences (buffer, rr);
+ }),
+ new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, "]")
+ ));
+ break;
+ }
+ count++;
+
+ // collapse any .. segments
+ string path = System.IO.Path.GetFullPath (s);
+
+ //factor out some common prefixes into variables
+ //we do this instead of using the original string, as the result is simpler
+ //and easier to understand
+ shorten ??= CreateFilenameShortener (doc.Environment);
+ var replacement = shorten (path);
+ if (!replacement.HasValue) {
+ elements.Add (
+ new ClassifiedTextElement (
+ new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, path, () => OpenFile (path), path)
+ )
+ );
+ continue;
+ }
+
+ elements.Add (new ClassifiedTextElement (
+ new ClassifiedTextRun (PredefinedClassificationTypeNames.SymbolReference, replacement.Value.prefix),
+ new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, replacement.Value.remaining, () => OpenFile (path), path)
+ ));
+ }
+
+ if (elements.Count == 0) {
+ return null;
+ }
+
+ elements.Insert (0, new ClassifiedTextElement (new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, "Seen in:")));
+ return new ContainerElement (ContainerElementStyle.Stacked, elements);
+ }
+ */
+
+ void AddBreak() => sb.AppendLine("
");
+
+ public string GetResolvedPathElement(List navs)
+ {
+ sb.Clear();
+
+ if(navs.Count == 1) {
+ sb.Append("Resolved path: ");
+ AddFileLink(navs[0].Path);
+ return sb.ToString();
+ }
+
+ sb.Append("Resolved paths:");
+
+ int i = 0;
+ foreach(var location in navs) {
+ AddBreak();
+ AddFileLink(location.Path);
+ if(i == 5) {
+ AddBreak();
+ // TODO: make this a link
+ sb.Append("[More in Go to Definition]");
+ break;
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Shortens filenames by extracting common prefixes into MSBuild properties. Returns null if the name could not be shortened in this way.
+ ///
+ public Func CreateFilenameShortener(IMSBuildEnvironment environment)
+ {
+ var prefixes = GetPrefixes(environment);
+ return s => GetLongestReplacement(s, prefixes);
+ }
+
+ static List<(string prefix, string subst)> GetPrefixes(IMSBuildEnvironment environment)
+ {
+ var list = new List<(string prefix, string subst)>();
+ if(environment.ToolsPath is string toolsPath) {
+ list.Add((toolsPath, $"$({ReservedPropertyNames.binPath})"));
+ }
+
+ if(environment.ToolsetProperties != null) {
+ var wellKnownPathProperties = new[] { WellKnownProperties.MSBuildSDKsPath, WellKnownProperties.MSBuildExtensionsPath, WellKnownProperties.MSBuildExtensionsPath32, WellKnownProperties.MSBuildExtensionsPath64 };
+ foreach(var propName in wellKnownPathProperties) {
+ if(environment.ToolsetProperties.TryGetValue(propName, out var propVal)) {
+ list.Add((propVal, $"$({propName})"));
+ }
+ }
+ }
+
+ return list;
+ }
+
+ static (string prefix, string remaining)? GetLongestReplacement(string val, List<(string prefix, string subst)> replacements)
+ {
+ (string prefix, string subst)? longestReplacement = null;
+ foreach(var replacement in replacements) {
+ if(val.StartsWith(replacement.prefix, StringComparison.OrdinalIgnoreCase)) {
+ if(!longestReplacement.HasValue || longestReplacement.Value.prefix.Length < replacement.prefix.Length) {
+ longestReplacement = replacement;
+ }
+ }
+ }
+
+ if(longestReplacement.HasValue) {
+ return (longestReplacement.Value.subst, val.Substring(longestReplacement.Value.prefix.Length));
+ }
+
+ return null;
+ }
+
+ Task GetDocsXml(IRoslynSymbol symbol, CancellationToken token)
+ {
+ return Task.Run(() => {
+ try {
+ // MSBuild uses property getters directly but they don't typically have docs.
+ // Use the docs from the property instead.
+ // FIXME: this doesn't seem to work for the indexer string[]get_Chars, at least on Mono
+ if(symbol is IMethodSymbol method && method.MethodKind == MethodKind.PropertyGet) {
+ symbol = method.AssociatedSymbol ?? symbol;
+ }
+ return symbol.GetDocumentationCommentXml(expandIncludes: true, cancellationToken: token);
+ } catch(Exception ex) when(!(ex is OperationCanceledException && token.IsCancellationRequested)) {
+ LogDocsLoadingError(logger, ex);
+ }
+ return null;
+ }, token);
+ }
+
+ // roslyn's IDocumentationCommentFormattingService seems to be basically unusable
+ // without internals access, so do some basic formatting ourselves
+ bool RenderDocsXmlSummaryElement(string docs)
+ {
+ var docsXml = XDocument.Parse(docs);
+ var summaryEl = docsXml.Root?.Element("summary");
+ if(summaryEl == null) {
+ return false;
+ }
+
+ AddBreak();
+
+ foreach(var node in summaryEl.Nodes()) {
+ switch(node) {
+ case XText text:
+ // TODO: escaping
+ sb.Append(text.Value);
+ break;
+ case XElement el:
+ switch(el.Name.LocalName) {
+ case "see":
+ AddTypeNameFromCref(el, logger);
+ continue;
+ case "attribution":
+ continue;
+ case "para":
+ NewBlock();
+ RenderXmlDocsPara(el, logger);
+ continue;
+ default:
+ LogDocsUnexpectedElement(logger, "summary", el.Name.ToString());
+ continue;
+ }
+ default:
+ LogDocsUnexpectedNode(logger, "summary", node.NodeType);
+ continue;
+ }
+ }
+
+ return true;
+ }
+
+ void AddTypeNameFromCref(XElement el, ILogger logger)
+ {
+ if(el.Attribute("cref") is { } att && att.Value is string cref) {
+ var colonIdx = cref.IndexOf(':');
+ if(colonIdx > -1) {
+ cref = cref.Substring(colonIdx + 1);
+ }
+ if(!string.IsNullOrEmpty(cref)) {
+ AppendColorSpan(KnownColor.Type, cref);
+ }
+ } else {
+ LogDocsMissingAttribute(logger, "see", "cref");
+ }
+ }
+
+ void RenderXmlDocsPara(XElement para, ILogger logger)
+ {
+ foreach(var node in para.Nodes()) {
+ switch(node) {
+ case XText text:
+ sb.Append(text.Value);
+ continue;
+ case XElement el:
+ switch(el.Name.LocalName) {
+ case "see":
+ AddTypeNameFromCref(el, logger);
+ continue;
+ default:
+ LogDocsUnexpectedElement(logger, "para", el.Name.ToString());
+ continue;
+ }
+ default:
+ LogDocsUnexpectedNode(logger, "para", node.NodeType);
+ continue;
+ }
+ }
+ }
+
+ public string GetPackageInfoTooltip(string packageId, IPackageInfo? package)
+ {
+ sb.Clear();
+
+ StartSignatureBlock();
+
+ // TODO: GetImageElement (feedKind),
+ AppendColorSpan(KnownColor.Keyword, "package");
+ sb.Append(" ");
+ AppendColorSpan(KnownColor.Type, package?.Id ?? packageId);
+
+ EndSignatureBlock();
+
+ NewBlock();
+
+ if(package is null)
+ {
+ AppendColorSpan(KnownColor.Comment, "Could not load package information");
+ return sb.ToString();
+ }
+
+ var description = !string.IsNullOrWhiteSpace(package.Description) ? package.Description : package.Summary;
+ if(string.IsNullOrWhiteSpace(description)) {
+ description = package.Summary;
+ }
+ if(!string.IsNullOrWhiteSpace(description)) {
+ // TODO: VS Code sanitizes this but we should too
+ sb.Append(description);
+ } else {
+ AppendColorSpan(KnownColor.Comment, "[no description]");
+ }
+
+ if(!supportsMarkdown)
+ {
+ return sb.ToString();
+ }
+
+ var nugetOrgUrl = package.GetNuGetOrgUrl();
+ if(nugetOrgUrl != null) {
+ NewBlock();
+ AddLink(nugetOrgUrl, "Go to package on NuGet.org");
+ }
+
+ var projectUrl = package.ProjectUrl != null && Uri.TryCreate(package.ProjectUrl, UriKind.Absolute, out var parsedUrl) && parsedUrl.Scheme == Uri.UriSchemeHttps
+ ? package.ProjectUrl : null;
+ if(projectUrl != null) {
+ NewBlock();
+ AddLink(projectUrl, "Go to project URL");
+ }
+
+ return sb.ToString();
+ }
+
+ void AddLink(string url, string linkText)
+ {
+ if(!supportsMarkdown)
+ {
+ throw new NotSupportedException("Cannot add link when markdown is not supported");
+ }
+ sb.Append($"[{Escape(linkText)}]({url})");
+ }
+
+ void AddFileLink(string filePath, string? linkText = null)
+ {
+ if(!supportsMarkdown)
+ {
+ sb.Append(filePath);
+ return;
+ }
+ var fullPath = Path.GetFullPath(filePath);
+ var uriString = ProtocolConversions.GetAbsoluteUriString(fullPath);
+ sb.Append($"[{Escape(linkText ?? filePath)}]({uriString})");
+ }
+
+ static string Escape(string s) => ProtocolConversions.EscapeMarkdown(s);
+
+ //public object GetDiagnosticTooltip (MSBuildDiagnostic diagnostic) => GetDiagnosticElement (diagnostic.Descriptor.Severity, diagnostic.GetFormattedMessage () ?? diagnostic.GetFormattedTitle ());
+
+ void AppendIcon(MSBuildGlyph glyph)
+ {
+ if(!supportsIcons)
+ {
+ return;
+ }
+ var iconName = glyph.ToVSCodeImage().ToVSCodeImageId();
+ sb.Append($"$({iconName}) "); // icons are text so add a trailing space to separate from following text
+ }
+
+ void StartDiagnosticElement(MSBuildDiagnosticSeverity severity)
+ {
+ AppendIcon(severity.ToGlyph());
+
+ /*
+ var imageId = severity switch {
+ MSBuildDiagnosticSeverity.Error => KnownImages.StatusError,
+ MSBuildDiagnosticSeverity.Warning => KnownImages.StatusWarning,
+ _ => KnownImages.StatusInformation
+ };
+ */
+
+ // should we show the title as well as the description? it's not possible to align the image cleanly if we do that
+ //var titleElement = new ClassifiedTextElement (new ClassifiedTextRun (PredefinedClassificationTypeNames.NaturalLanguage, diagnostic.GetFormattedTitle (), ClassifiedTextRunStyle.Bold));
+
+ /*
+ var messageElements = FormatDescriptionText (message);
+ var imageElement = GetImageElement (imageId);
+
+ return new ContainerElement (
+ ContainerElementStyle.Wrapped | ContainerElementStyle.VerticalPadding,
+ imageElement,
+ new ClassifiedTextElement (messageElements)
+ );
+ */
+ }
+
+ void EndDiagnosticElement() { }
+
+ [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = "XML docs element '{elementName}' has unexpected element '{childElementName}'")]
+ static partial void LogDocsUnexpectedElement(ILogger logger, string elementName, UserIdentifiable childElementName);
+
+ [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = "XML docs element '{elementName}' has unexpected attribute '{attributeName}'")]
+ static partial void LogDocsUnexpectedAttribute(ILogger logger, string elementName, UserIdentifiable attributeName);
+
+ [LoggerMessage(EventId = 2, Level = LogLevel.Debug, Message = "XML docs element '{elementName}' has unexpected attribute '{attributeName}'")]
+ static partial void LogDocsMissingAttribute(ILogger logger, string elementName, UserIdentifiable attributeName);
+
+ [LoggerMessage(EventId = 3, Level = LogLevel.Debug, Message = "XML docs element '{elementName}' has unexpected node of type {nodeType}")]
+ static partial void LogDocsUnexpectedNode(ILogger logger, string elementName, XmlNodeType nodeType);
+
+ [LoggerMessage(EventId = 4, Level = LogLevel.Warning, Message = "Error loading XML docs")]
+ static partial void LogDocsLoadingError(ILogger logger, Exception ex);
+
+ [LoggerMessage(EventId = 5, Level = LogLevel.Warning, Message = "Error rendering XML docs")]
+ static partial void LogDocsRenderingError(ILogger logger, Exception ex);
+}
diff --git a/MSBuildLanguageServer/GlobalSuppressions.cs b/MSBuildLanguageServer/GlobalSuppressions.cs
new file mode 100644
index 00000000..64a7b4ab
--- /dev/null
+++ b/MSBuildLanguageServer/GlobalSuppressions.cs
@@ -0,0 +1,8 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("MicrosoftCodeAnalysisCorrectness", "RS1024:Symbols should be compared for equality", Justification = "", Scope = "member", Target = "~M:Roslyn.Utilities.GeneratedCodeUtilities.IsGeneratedSymbolWithGeneratedCodeAttribute(Microsoft.CodeAnalysis.ISymbol,Microsoft.CodeAnalysis.INamedTypeSymbol)~System.Boolean")]
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionClientCapabilities.cs b/MSBuildLanguageServer/Handler/Completion/CompletionClientCapabilities.cs
new file mode 100644
index 00000000..955d713c
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionClientCapabilities.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+///
+/// Helper that makes reading client completion capabilities simpler and more performant.
+///
+class CompletionClientCapabilities
+{
+ public static CompletionClientCapabilities Create(ClientCapabilities clientCapabilities) => new(clientCapabilities);
+
+ CompletionClientCapabilities(ClientCapabilities clientCapabilities)
+ {
+ var completionSetting = clientCapabilities.TextDocument?.Completion;
+
+ ContextSupport = completionSetting?.ContextSupport ?? false;
+
+ SupportedItemDefaults = completionSetting?.CompletionListSetting?.ItemDefaults?.ToHashSet(StringComparer.OrdinalIgnoreCase) ?? [];
+
+ SupportedItemKinds = completionSetting?.CompletionItemKind?.ValueSet?.ToHashSet() ?? [];
+
+ DefaultInsertTextMode = completionSetting?.InsertTextMode;
+
+ LabelDetailsSupport = completionSetting?.CompletionItem?.LabelDetailsSupport ?? false;
+
+ CommitCharactersSupport = completionSetting?.CompletionItem?.CommitCharactersSupport ?? false;
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ DeprecatedSupport = completionSetting?.CompletionItem?.DeprecatedSupport ?? false;
+#pragma warning restore CS0618 // Type or member is obsolete
+
+ DocumentationFormat = completionSetting?.CompletionItem?.DocumentationFormat?.ToHashSet() ?? [];
+
+ InsertReplaceSupport = completionSetting?.CompletionItem?.InsertReplaceSupport ?? false;
+
+ InsertTextModeSupport = completionSetting?.CompletionItem?.InsertTextModeSupport?.ValueSet.ToHashSet() ?? [];
+
+ PreselectSupport = completionSetting?.CompletionItem?.PreselectSupport ?? false;
+
+ ResolveSupport = completionSetting?.CompletionItem?.ResolveSupport?.Properties.ToHashSet(StringComparer.OrdinalIgnoreCase) ?? [];
+
+ SnippetSupport = completionSetting?.CompletionItem?.SnippetSupport ?? false;
+
+ TagSupport = completionSetting?.CompletionItem?.TagSupport?.ValueSet.ToHashSet() ?? [];
+ }
+
+ ///
+ /// Whether the client supports providing additional context via the property
+ ///
+ public bool ContextSupport { get; }
+
+ ///
+ /// The property names that the client supports on the collection
+ ///
+ public HashSet SupportedItemDefaults { get; }
+
+ ///
+ /// The values supported by the client
+ ///
+ public HashSet SupportedItemKinds { get; }
+
+ ///
+ /// The client's default insertion behavior for items that do not specify a value
+ ///
+ public InsertTextMode? DefaultInsertTextMode { get; }
+
+ ///
+ /// Whether the client supports the property
+ ///
+ public bool LabelDetailsSupport { get; }
+
+ ///
+ /// Whether the client supports the property
+ ///
+ public bool CommitCharactersSupport { get; }
+
+ ///
+ /// Whether the client supports the deprecated property
+ ///
+ public bool DeprecatedSupport { get; }
+
+ ///
+ /// Which formats the client supports for documentation
+ ///
+ public HashSet DocumentationFormat { get; }
+
+ ///
+ /// Whether the client supports values on the property
+ ///
+ public bool InsertReplaceSupport { get; }
+
+ ///
+ /// Whether the client supports the property
+ ///
+ public HashSet InsertTextModeSupport { get; }
+
+ ///
+ /// Whether the client supports the property
+ ///
+ public bool PreselectSupport { get; }
+
+ ///
+ /// Which properties the client supports resolving
+ ///
+ public HashSet ResolveSupport { get; }
+
+ ///
+ /// Whether the client supports treating as a snippet
+ /// when is set to .
+ ///
+ public bool SnippetSupport { get; }
+
+ ///
+ /// The values supported by the client
+ ///
+ public HashSet TagSupport { get; }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionHandler.cs b/MSBuildLanguageServer/Handler/Completion/CompletionHandler.cs
new file mode 100644
index 00000000..eff48d8c
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionHandler.cs
@@ -0,0 +1,429 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Composition;
+
+using Microsoft.CodeAnalysis.LanguageServer;
+using Microsoft.CodeAnalysis.LanguageServer.Handler;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.CommonLanguageServerProtocol.Framework;
+using Microsoft.Extensions.Logging;
+
+using MonoDevelop.MSBuild.Editor.Completion;
+using MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+using MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+using MonoDevelop.MSBuild.Editor.LanguageServer.Parser;
+using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace;
+using MonoDevelop.MSBuild.Editor.NuGetSearch;
+using MonoDevelop.MSBuild.Language;
+using MonoDevelop.MSBuild.Language.Expressions;
+using MonoDevelop.MSBuild.Language.Typesystem;
+using MonoDevelop.MSBuild.PackageSearch;
+using MonoDevelop.MSBuild.Schema;
+using MonoDevelop.Xml.Dom;
+using MonoDevelop.Xml.Editor.Completion;
+using MonoDevelop.Xml.Parser;
+
+using ProjectFileTools.NuGetSearch.Contracts;
+using ProjectFileTools.NuGetSearch.Feeds;
+
+using Roslyn.LanguageServer.Protocol;
+
+using static MonoDevelop.MSBuild.Language.ExpressionCompletion;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler;
+
+[ExportCSharpVisualBasicStatelessLspService(typeof(CompletionHandler)), Shared]
+[Method(Methods.TextDocumentCompletionName)]
+[method: ImportingConstructor]
+sealed class CompletionHandler([Import(AllowDefault = true)] IMSBuildFileSystem fileSystem)
+ : ILspServiceDocumentRequestHandler?>
+{
+ readonly IMSBuildFileSystem fileSystem = fileSystem ?? DefaultMSBuildFileSystem.Instance;
+
+ public bool MutatesSolutionState => false;
+
+ public bool RequiresLSPSolution => true;
+
+ public TextDocumentIdentifier GetTextDocumentIdentifier(CompletionParams request) => request.TextDocument;
+
+ readonly XmlRootState stateMachine = new();
+
+ public async Task?> HandleRequestAsync(CompletionParams request, RequestContext context, CancellationToken cancellationToken)
+ {
+ // HELP: CompletionTriggerKind maps really poorly
+ //
+ // VS has CompletionTriggerReason.Insertion|Backspace|Invoke|InvokeAndCommitIfUnique and a few others
+ // and XmlEditor maps this to its internal type XmlTriggerReason.TypedChar|Backspace|Invocation.
+ //
+ // However, in LSP the distinction is between
+ // * CompletionTriggerKind.TypedCharacter, which means a character we *explicitly* marked as a trigger char was typed
+ // * CompletionTriggerKind.Invoked, which means completion was manually invoked or any character was typed
+ //
+ // CompletionTriggerKind.TypedCharacter isn't useful to us, and so we only handle Invoked. However, this
+ // maps to both XmlTriggerReason.TypedChar and XmlTriggerReason.Invocation, which both handle subtly
+ // different behaviors for implicit vs explicit completion invocation. For now, we will just map it
+ // to Invocation and review the details later.
+
+ switch(request.Context?.TriggerKind ?? CompletionTriggerKind.Invoked)
+ {
+ // user typed [a-zA-Z] or explicitly triggered completion
+ case CompletionTriggerKind.Invoked:
+ // user typed a char in ServerCapabilities.CompletionOptions.TriggerCharacters
+ case CompletionTriggerKind.TriggerCharacter:
+ // we marked a completion list as incomplete and user typed another char
+ case CompletionTriggerKind.TriggerForIncompleteCompletions:
+ break;
+ // unknown, ignore
+ default:
+ return null;
+ }
+
+ var xmlReason = XmlTriggerReason.Invocation;
+ var msbuildReason = ExpressionTriggerReason.Invocation;
+
+ var document = context.GetRequiredDocument();
+ var sourceText = document.Text.Text;
+ var textSource = sourceText.GetTextSource();
+ var position = ProtocolConversions.PositionToLinePosition(request.Position);
+ int offset = position.ToOffset(sourceText);
+ var typedCharacter = offset <= 0 ? '\0' : sourceText[offset - 1];
+
+ var msbuildParserService = context.GetRequiredService();
+ var xmlParserService = context.GetRequiredService();
+ var logger = context.GetRequiredService();
+ var extLogger = logger.ToILogger();
+
+ // get a spine, reusing anything we can from the last completed parse
+ // TODO: port the caching spine parser?
+ xmlParserService.TryGetCompletedParseResult(document.CurrentState, out var lastXmlParse);
+ var spine = LspXmlParserService.GetSpineParser(stateMachine, lastXmlParse, position, sourceText, cancellationToken);
+
+ // TODO : replace this with collection of schemas
+ async Task GetRootDocument()
+ {
+ if(GetMSBuildParseResult(msbuildParserService, document.CurrentState, cancellationToken) is { } t)
+ {
+ return (await t).MSBuildDocument;
+ } else
+ {
+ return MSBuildRootDocument.Empty;
+ }
+ }
+
+ var functionTypeProvider = context.GetRequiredService().FunctionTypeProvider;
+
+ //FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards
+ await functionTypeProvider.EnsureInitialized(cancellationToken);
+
+ var msbuildTrigger = MSBuildCompletionTrigger.TryCreate(spine, textSource, msbuildReason, offset, typedCharacter, extLogger, functionTypeProvider, null, cancellationToken);
+ if(msbuildTrigger is not null)
+ {
+ MSBuildRootDocument doc = await GetRootDocument();
+ return await GetExpressionCompletionList(request, context, doc, msbuildTrigger, extLogger, sourceText, functionTypeProvider, fileSystem, cancellationToken).ConfigureAwait(false);
+ }
+
+ (XmlCompletionTrigger kind, int spanStart, int spanLength)? xmlTrigger = null;
+ xmlTrigger = XmlCompletionTriggering.GetTriggerAndSpan(spine, xmlReason, typedCharacter, textSource, cancellationToken: cancellationToken);
+ if(xmlTrigger.Value.kind != XmlCompletionTrigger.None)
+ {
+ // ignore the case where the method returns false when the deepest node's name cannot be completed, we will try to provide completion anyways
+ spine.TryGetNodePath(textSource, out var nodePath, cancellationToken: cancellationToken);
+
+ // if we're completing an existing element, remove it from the path
+ // so we don't get completions for its children instead
+ if((xmlTrigger.Value.kind == XmlCompletionTrigger.ElementName || xmlTrigger.Value.kind == XmlCompletionTrigger.Tag) && nodePath?.Count > 0)
+ {
+ if(nodePath[nodePath.Count - 1] is XElement leaf && leaf.Name.Length == xmlTrigger.Value.spanLength)
+ {
+ nodePath.RemoveAt(nodePath.Count - 1);
+ }
+ }
+
+ MSBuildRootDocument doc = await GetRootDocument();
+
+ var clientCapabilities = context.GetRequiredClientCapabilities();
+ var clientInfo = context.GetRequiredService().TryGetInitializeParams()?.ClientInfo;
+
+ var editRange = sourceText.GetLspRange(xmlTrigger.Value.spanStart, xmlTrigger.Value.spanLength);
+
+ var rr = MSBuildResolver.Resolve(spine.Clone(), textSource, MSBuildRootDocument.Empty, null, extLogger, cancellationToken);
+ var docsProvider = MSBuildCompletionDocsProvider.Create(extLogger, clientCapabilities, clientInfo, doc, sourceText, rr);
+ var xmlCompletionContext = new MSBuildXmlCompletionContext(spine, xmlTrigger.Value.kind, textSource, nodePath, editRange, rr, doc, docsProvider, sourceText);
+ var dataSource = new MSBuildXmlCompletionDataSource();
+ return await GetXmlCompletionListAsync(context, dataSource, xmlCompletionContext, request.TextDocument, cancellationToken).ConfigureAwait(false);
+ }
+
+ return null;
+ }
+
+ async static Task GetXmlCompletionListAsync(RequestContext context, MSBuildXmlCompletionDataSource dataSource, MSBuildXmlCompletionContext xmlCompletionContext, TextDocumentIdentifier textDocument, CancellationToken cancellationToken)
+ {
+ var completionTasks = dataSource.GetCompletionTasks(xmlCompletionContext, cancellationToken);
+
+ var subLists = await Task.WhenAll(completionTasks).ConfigureAwait(false);
+
+ var allItems = subLists.SelectMany(s => s is null ? [] : s);
+
+ return await CompletionRenderer.RenderCompletionItems(context, textDocument, allItems, xmlCompletionContext.EditRange, cancellationToken).ConfigureAwait(false);
+ }
+
+ static Task? GetMSBuildParseResult(LspMSBuildParserService msbuildParserService, EditorDocumentState documentState, CancellationToken cancellationToken)
+ {
+ // if we determined we are triggering, now we can look up the current parsed document
+ // FIXME: do we need it to be current or will a stale one do? it's really just used for schemas
+ if(msbuildParserService.TryGetCompletedParseResult(documentState, out var parseResult))
+ {
+ return Task.FromResult(parseResult);
+ } else if(msbuildParserService.TryGetParseResult(documentState, out Task? parseTask, cancellationToken))
+ {
+ return parseTask;
+ } else
+ {
+ return null;
+ }
+ }
+
+ async static Task GetExpressionCompletionList(
+ CompletionParams request, RequestContext context,
+ MSBuildRootDocument doc, MSBuildCompletionTrigger trigger,
+ ILogger logger, SourceText sourceText,
+ IFunctionTypeProvider functionTypeProvider, IMSBuildFileSystem fileSystem,
+ CancellationToken cancellationToken)
+ {
+ var rr = trigger.ResolveResult;
+
+ var clientCapabilities = context.GetRequiredClientCapabilities();
+ var clientInfo = context.GetRequiredService().TryGetInitializeParams()?.ClientInfo;
+ var docsProvider = MSBuildCompletionDocsProvider.Create(logger, clientCapabilities, clientInfo, doc, sourceText, rr);
+
+ var valueSymbol = rr.GetElementOrAttributeValueInfo(doc);
+ if(valueSymbol is null || valueSymbol.ValueKind == MSBuildValueKind.Nothing)
+ {
+ return null;
+ }
+
+ var kindWithModifiers = valueSymbol.ValueKind;
+
+ if(!ValidateListPermitted(trigger.ListKind, kindWithModifiers))
+ {
+ return null;
+ }
+
+ var kind = kindWithModifiers.WithoutModifiers();
+
+ if(kind == MSBuildValueKind.Data || kind == MSBuildValueKind.Nothing)
+ {
+ return null;
+ }
+
+ // FIXME: This is a temporary hack so we have completion for imported XSD schemas with missing type info.
+ // It is not needed for inferred schemas, as they have already performed the inference.
+ if(kind == MSBuildValueKind.Unknown)
+ {
+ kind = MSBuildInferredSchema.InferValueKindFromName(valueSymbol);
+ }
+
+ var items = await GetExpressionCompletionItems(doc, trigger, logger, functionTypeProvider, fileSystem, rr, docsProvider, valueSymbol, kind, cancellationToken).ConfigureAwait(false);
+
+ bool isIncomplete = false;
+
+ // TODO: use request.PartialResultToken to report these as they come in
+ if(trigger.TriggerState == TriggerState.Value)
+ {
+ switch(kind)
+ {
+ case MSBuildValueKind.NuGetID:
+ {
+ isIncomplete = true;
+ if(trigger.Expression is ExpressionText t)
+ {
+ var packageSearchManager = context.GetRequiredLspService();
+ var packageType = valueSymbol.CustomType?.Values[0].Name;
+ var packageNameItems = await GetPackageNameCompletions(t.Value, packageType, doc, packageSearchManager, docsProvider, cancellationToken);
+ if(packageNameItems != null)
+ {
+ items.AddRange(packageNameItems);
+ }
+ }
+ break;
+ }
+ case MSBuildValueKind.NuGetVersion:
+ {
+ isIncomplete = true;
+ var packageSearchManager = context.GetRequiredLspService();
+ var packageVersionItems = await GetPackageVersionCompletion(doc, rr, packageSearchManager, docsProvider, cancellationToken);
+ if(packageVersionItems != null)
+ {
+ items.AddRange(packageVersionItems);
+ }
+ break;
+ }
+ }
+ }
+
+ var renderedList = await CompletionRenderer.RenderCompletionItems(
+ context,
+ request.TextDocument,
+ items,
+ sourceText.GetLspRange(trigger.SpanStart, trigger.SpanLength),
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ if (renderedList is not null)
+ {
+ renderedList.IsIncomplete = isIncomplete;
+ }
+
+ return renderedList;
+ }
+
+ private static async Task> GetExpressionCompletionItems(
+ MSBuildRootDocument doc, MSBuildCompletionTrigger trigger, ILogger logger,
+ IFunctionTypeProvider functionTypeProvider, IMSBuildFileSystem fileSystem,
+ MSBuildResolveResult rr, MSBuildCompletionDocsProvider docsProvider,
+ ITypedSymbol valueSymbol, MSBuildValueKind kind,
+ CancellationToken cancellationToken)
+ {
+ var items = new List();
+
+ bool isValue = trigger.TriggerState == TriggerState.Value;
+
+ if(trigger.ComparandVariables != null && isValue)
+ {
+ foreach(var ci in GetComparandCompletions(doc, fileSystem, trigger.ComparandVariables, logger))
+ {
+ items.Add(new MSBuildCompletionItem(ci, XmlCommitKind.AttributeValue, docsProvider));
+ }
+ }
+
+ if(isValue)
+ {
+ switch(kind)
+ {
+ case MSBuildValueKind.Sdk:
+ case MSBuildValueKind.SdkWithVersion:
+ {
+ items.AddRange(SdkCompletion.GetSdkCompletions(doc, logger, cancellationToken).Select(s => new MSBuildSdkCompletionItem(s, docsProvider)));
+ break;
+ }
+ case MSBuildValueKind.Lcid:
+ items.AddRange(CultureHelper.GetKnownCultures().Select(c => new MSBuildLcidCompletionItem(c, docsProvider)));
+ break;
+ case MSBuildValueKind.Culture:
+ items.AddRange(CultureHelper.GetKnownCultures().Select(c => new MSBuildCultureCompletionItem(c, docsProvider)));
+ break;
+ }
+
+ if(kind == MSBuildValueKind.Guid || valueSymbol.CustomType is CustomTypeInfo { BaseKind: MSBuildValueKind.Guid, AllowUnknownValues: true })
+ {
+ items.Add(new MSBuildNewGuidCompletionItem());
+ }
+ }
+
+ //TODO: better metadata support
+ // NOTE: can't just check CustomTypeInfo isn't null, must check kind, as NuGetID stashes the dependency type in the CustomTypeInfo
+ if(kind == MSBuildValueKind.CustomType && valueSymbol.CustomType != null && valueSymbol.CustomType.Values.Count > 0 && isValue)
+ {
+ bool addDescriptionHint = CompletionHelpers.ShouldAddHintForCompletions(valueSymbol);
+ foreach(var value in valueSymbol.CustomType.Values)
+ {
+ items.Add(new MSBuildCompletionItem(value, XmlCommitKind.AttributeValue, docsProvider, addDescriptionHint: addDescriptionHint));
+ }
+
+ } else
+ {
+ //FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards
+ await functionTypeProvider.EnsureInitialized(cancellationToken);
+ if(GetCompletionInfos(rr, trigger.TriggerState, valueSymbol, trigger.Expression, trigger.SpanLength, doc, functionTypeProvider, fileSystem, logger, kindIfUnknown: kind) is IEnumerable completionInfos)
+ {
+ bool addDescriptionHint = CompletionHelpers.ShouldAddHintForCompletions(valueSymbol);
+ foreach(var ci in completionInfos)
+ {
+ items.Add(new MSBuildCompletionItem(ci, XmlCommitKind.AttributeValue, docsProvider, addDescriptionHint: addDescriptionHint));
+ }
+ }
+ }
+
+ bool allowExpressions = valueSymbol.ValueKind.AllowsExpressions();
+
+ if(allowExpressions && isValue || trigger.TriggerState == TriggerState.BareFunctionArgumentValue)
+ {
+ items.Add(new MSBuildReferenceExpressionCompletionItem("$(", "Property reference", MSBuildCompletionItemKind.PropertySyntax));
+ }
+
+ if(allowExpressions && isValue)
+ {
+ items.Add(new MSBuildReferenceExpressionCompletionItem("@(", "Item reference", MSBuildCompletionItemKind.ItemSyntax));
+ if(CompletionHelpers.IsMetadataAllowed(trigger.Expression, rr))
+ {
+ items.Add(new MSBuildReferenceExpressionCompletionItem("%(", "Metadata reference", MSBuildCompletionItemKind.MetadataSyntax));
+ }
+ }
+
+ return items;
+ }
+
+ static async Task?> GetPackageVersionCompletion(MSBuildRootDocument doc, MSBuildResolveResult resolveResult, IPackageSearchManager packageSearchManager, MSBuildCompletionDocsProvider docsProvider, CancellationToken cancellationToken)
+ {
+ if(!PackageCompletion.TryGetPackageVersionSearchJob(resolveResult, doc, packageSearchManager, out var packageSearchJob, out string? packageId, out string? targetFrameworkSearchParameter))
+ {
+ return null;
+ }
+
+ var results = await packageSearchJob.ToTask(cancellationToken);
+
+ var packageDocsProvider = new PackageCompletionDocsProvider(packageSearchManager, docsProvider, targetFrameworkSearchParameter);
+
+ // FIXME should we deduplicate?
+ // FIXME: this index sort hack won't work when we are returning the results in parts as they come in from the different sources
+ var items = new List();
+ var index = results.Count;
+ foreach(var result in results)
+ {
+ items.Add(new OrderedPackageVersionCompletionItem (index--, packageId, result, packageDocsProvider));
+ }
+
+ return items;
+ }
+
+ static async Task> GetPackageNameCompletions(string searchString, string? packageType, MSBuildRootDocument doc, IPackageSearchManager packageSearchManager, MSBuildCompletionDocsProvider docsProvider, CancellationToken cancellationToken)
+ {
+ // NOTE: empty search string is still valid, it populates the list with some basic/placeholder packages
+
+ var targetFrameworkSearchParameter = doc.GetTargetFrameworkNuGetSearchParameter();
+
+ var packageDocsProvider = new PackageCompletionDocsProvider(packageSearchManager, docsProvider, targetFrameworkSearchParameter);
+
+ var results = await packageSearchManager.SearchPackageNames(searchString.ToLower(), targetFrameworkSearchParameter, packageType).ToTask(cancellationToken);
+
+ return CreateNuGetItemsFromSearchResults(results, packageDocsProvider);
+ }
+
+ static List CreateNuGetItemsFromSearchResults(IReadOnlyList> results, PackageCompletionDocsProvider packageDocsProvider)
+ {
+ var items = new List();
+ var dedup = new HashSet();
+
+ // dedup, preferring nuget -> myget -> local
+ AddItems(FeedKind.NuGet);
+ AddItems(FeedKind.MyGet);
+ AddItems(FeedKind.Local);
+
+ void AddItems(FeedKind kind)
+ {
+ foreach(var result in results)
+ {
+ if(result.Item2 == kind)
+ {
+ if(dedup.Add(result.Item1))
+ {
+ items.Add(new PackageNameCompletionItem (result, packageDocsProvider));
+ }
+ }
+ }
+ }
+
+ return items;
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCompletionItem.cs
new file mode 100644
index 00000000..5aede805
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCompletionItem.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using MonoDevelop.MSBuild.Language;
+using MonoDevelop.MSBuild.Schema;
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+
+class MSBuildCompletionItem(
+ ISymbol symbol, XmlCommitKind xmlCommitKind,
+ MSBuildCompletionDocsProvider docsProvider,
+ string? prefix = null, string? annotation = null, string? sortText = null, bool addDescriptionHint = false
+ ) : ILspCompletionItem
+{
+ string label => prefix is not null ? prefix + symbol.Name : symbol.Name;
+
+ public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal);
+
+ public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken)
+ {
+ var item = new CompletionItem { Label = label, SortText = sortText };
+
+ if(settings.IncludeDeprecatedPropertyOrTag && symbol.IsDeprecated())
+ {
+ settings.SetDeprecated(item);
+ }
+
+ if(settings.IncludeItemKind)
+ {
+ item.Kind = symbol.GetCompletionItemKind();
+ }
+
+ if(annotation is not null)
+ {
+ item.FilterText = $"{symbol.Name} {annotation}";
+ if(settings.IncludeLabelDetails)
+ {
+ item.LabelDetails = new CompletionItemLabelDetails { Description = annotation };
+ }
+ } else if(addDescriptionHint)
+ {
+ if(settings.IncludeLabelDetails)
+ {
+ var descriptionHint = DescriptionFormatter.GetCompletionHint(symbol);
+ item.LabelDetails = new CompletionItemLabelDetails { Description = descriptionHint };
+ }
+ }
+
+ // TODO: generate completion edit based on the xmlCompletionItemKind
+
+ if(settings.IncludeDocumentation)
+ {
+ var tooltipContent = await docsProvider.GetDocumentation(symbol, cancellationToken);
+ if(tooltipContent is not null)
+ {
+ //Value = "$(symbol-keyword) keyword Choose
\r\n\r\nGroups When and Otherwise elements", // tooltipContent,
+ item.Documentation = tooltipContent;
+ }
+ }
+
+ return item;
+ }
+}
\ No newline at end of file
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCultureCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCultureCompletionItem.cs
new file mode 100644
index 00000000..fbf25914
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCultureCompletionItem.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using MonoDevelop.MSBuild.Language;
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+
+class MSBuildCultureCompletionItem(KnownCulture culture, MSBuildCompletionDocsProvider docsProvider) : ILspCompletionItem
+{
+ string label => culture.Name;
+
+ public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal);
+
+ public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken)
+ {
+ var item = new CompletionItem { Label = label };
+
+ if(settings.IncludeItemKind)
+ {
+ item.Kind = MSBuildCompletionItemKind.Culture;
+ }
+
+ // add the culture name to the filter text so ppl can just type the actual language/country instead of looking up the code
+ item.FilterText = $"{culture.Name} {culture.DisplayName}";
+
+ if(settings.IncludeLabelDetails)
+ {
+ item.LabelDetails = new CompletionItemLabelDetails { Description = culture.DisplayName };
+ }
+
+ if(settings.IncludeDocumentation)
+ {
+ item.Documentation = await docsProvider.GetDocumentation(culture.CreateCultureSymbol(), cancellationToken);
+ }
+
+ return item;
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildLcidCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildLcidCompletionItem.cs
new file mode 100644
index 00000000..72b95f5e
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildLcidCompletionItem.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using MonoDevelop.MSBuild.Language;
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+
+class MSBuildLcidCompletionItem(KnownCulture culture, MSBuildCompletionDocsProvider docsProvider) : ILspCompletionItem
+{
+ readonly string label = culture.Lcid.ToString();
+
+ public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal);
+
+ public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken)
+ {
+ var item = new CompletionItem { Label = label };
+
+ if(settings.IncludeItemKind)
+ {
+ item.Kind = MSBuildCompletionItemKind.Culture;
+ }
+
+ // add the culture name to the filter text so ppl can just type the actual language/country instead of looking up the code
+ item.FilterText = $"{label} {culture.DisplayName}";
+
+ if(settings.IncludeLabelDetails)
+ {
+ item.LabelDetails = new CompletionItemLabelDetails { Description = culture.DisplayName };
+ }
+
+ if(settings.IncludeDocumentation)
+ {
+ item.Documentation = await docsProvider.GetDocumentation(culture.CreateCultureSymbol(), cancellationToken);
+ }
+
+ return item;
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildNewGuidCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildNewGuidCompletionItem.cs
new file mode 100644
index 00000000..b7bfd8d6
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildNewGuidCompletionItem.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+
+class MSBuildNewGuidCompletionItem() : ILspCompletionItem
+{
+ const string label = "New GUID";
+
+ public bool IsMatch(CompletionItem request) => string.Equals(label, request.Label, StringComparison.Ordinal);
+
+ public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken)
+ {
+ var item = new CompletionItem { Label = label };
+
+ if(settings.IncludeItemKind)
+ {
+ item.Kind = MSBuildCompletionItemKind.NewGuid;
+ }
+
+ if(settings.IncludeDocumentation)
+ {
+ item.Documentation = new MarkupContent {
+ Value = "Inserts a new GUID",
+ Kind = MarkupKind.Markdown
+ };
+ }
+
+ if(settings.IncludeInsertText)
+ {
+ item.InsertText = Guid.NewGuid().ToString("B").ToUpper();
+ }
+
+ return item;
+ }
+}
\ No newline at end of file
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildReferenceExpressionCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildReferenceExpressionCompletionItem.cs
new file mode 100644
index 00000000..783f2608
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildReferenceExpressionCompletionItem.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+
+class MSBuildReferenceExpressionCompletionItem(string text, string description, CompletionItemKind itemKind) : ILspCompletionItem
+{
+ public bool IsMatch(CompletionItem request) => string.Equals(text, request.Label, StringComparison.Ordinal);
+
+ public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken)
+ {
+ var item = new CompletionItem { Label = text };
+
+ if(settings.IncludeItemKind)
+ {
+ item.Kind = itemKind;
+ }
+
+ if(settings.IncludeDocumentation)
+ {
+ item.Documentation = new MarkupContent {
+ Value = description,
+ Kind = MarkupKind.Markdown
+ };
+ }
+
+ if (settings.SupportSnippetFormat && settings.IncludeInsertTextFormat && settings.IncludeTextEdit)
+ {
+ if (settings.IncludeInsertTextFormat)
+ {
+ item.InsertTextFormat = InsertTextFormat.Snippet;
+ }
+ // TODO: calculate whether this would unbalance the expression
+ item.TextEdit = new TextEdit {
+ NewText = $"{text}$0)",
+ Range = ctx.EditRange
+ };
+ }
+
+ //TODO: custom commit support. we should be retriggering completion and enabling overtype support for the paren.
+ //See MSBuildCompletionCommitManager. TryCommitItemKind
+
+ return item;
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildSdkCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildSdkCompletionItem.cs
new file mode 100644
index 00000000..b175b32f
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildSdkCompletionItem.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using MonoDevelop.MSBuild.SdkResolution;
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+
+class MSBuildSdkCompletionItem(SdkInfo info, MSBuildCompletionDocsProvider docsProvider) : ILspCompletionItem
+{
+ string label => info.Name;
+
+ public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal);
+
+ public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken)
+ {
+ var item = new CompletionItem { Label = label };
+
+ if(settings.IncludeItemKind)
+ {
+ item.Kind = MSBuildCompletionItemKind.Sdk;
+ }
+
+ if(settings.IncludeDocumentation && info.Path is string sdkPath)
+ {
+ // FIXME: better docs
+ item.Documentation = new MarkupContent { Kind = MarkupKind.Markdown, Value = $"`{sdkPath}`" };
+ }
+
+ return item;
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/OrderedPackageVersionCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/OrderedPackageVersionCompletionItem.cs
new file mode 100644
index 00000000..20d0e086
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/OrderedPackageVersionCompletionItem.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using ProjectFileTools.NuGetSearch.Feeds;
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+
+class OrderedPackageVersionCompletionItem(
+ int index,
+ string packageId,
+ Tuple packageVersionAndKind,
+ PackageCompletionDocsProvider docsProvider
+ ) : ILspCompletionItem
+{
+ string packageVersion => packageVersionAndKind.Item1;
+ FeedKind packageKind => packageVersionAndKind.Item2;
+
+ public bool IsMatch(CompletionItem request) => string.Equals(request.Label, packageVersion, StringComparison.Ordinal);
+
+ public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken)
+ {
+ var item = new CompletionItem {
+ Label = packageVersion,
+ SortText = $"_{index:D5}"
+ };
+
+ if(settings.IncludeItemKind)
+ {
+ item.Kind = packageKind.GetCompletionItemKind();
+ }
+
+ // TODO: generate completion edit
+
+ if(settings.IncludeDocumentation)
+ {
+ item.Documentation = await docsProvider.GetPackageDocumentation(packageId, packageVersion, packageKind, cancellationToken);
+ }
+
+ return item;
+ }
+}
\ No newline at end of file
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/PackageCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/PackageCompletionItem.cs
new file mode 100644
index 00000000..2483192a
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/PackageCompletionItem.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using ProjectFileTools.NuGetSearch.Feeds;
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+
+class PackageNameCompletionItem(
+ Tuple packageNameAndKind,
+ PackageCompletionDocsProvider docsProvider
+ ) : ILspCompletionItem
+{
+ string packageId => packageNameAndKind.Item1;
+ FeedKind packageKind => packageNameAndKind.Item2;
+
+ public bool IsMatch(CompletionItem request) => string.Equals(request.Label, packageId, StringComparison.Ordinal);
+
+ public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken)
+ {
+ var item = new CompletionItem { Label = packageId };
+
+ if(settings.IncludeItemKind)
+ {
+ item.Kind = packageKind.GetCompletionItemKind();
+ }
+
+ // TODO: generate completion edit
+
+ if(settings.IncludeDocumentation)
+ {
+ item.Documentation = await docsProvider.GetPackageDocumentation(packageId, null, packageKind, cancellationToken);
+ }
+
+ return item;
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlClosingTagCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlClosingTagCompletionItem.cs
new file mode 100644
index 00000000..488279dd
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlClosingTagCompletionItem.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using MonoDevelop.Xml.Dom;
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+
+class XmlClosingTagCompletionItem(bool includeBracket, string name, XElement element, int dedupCount) : ILspCompletionItem
+{
+ readonly string label = (includeBracket ? "" : "/") + name;
+
+ public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal);
+
+ // TODO: custom insert text, including for multiple closing tags
+ public ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken)
+ {
+ // force these to sort last, they're not very interesting values to browse as these tags are usually already closed
+ string sortText = "ZZZZZZ" + label;
+
+ var item = new CompletionItem { Label = label, Kind = XmlToLspCompletionItemKind.ClosingTag, SortText = sortText };
+
+ if(settings.IncludeDocumentation)
+ {
+ item.Documentation = GetClosingTagDocumentation(element, dedupCount > 1);
+ };
+
+ return new(item);
+ }
+
+
+ static MarkupContent GetClosingTagDocumentation(XElement element, bool isMultiple)
+ => CreateMarkdown(
+ isMultiple
+ ? $"Closing tag for element `{element.Name}`, closing all intermediate elements"
+ : $"Closing tag for element `{element.Name}`"
+ );
+
+ static MarkupContent CreateMarkdown(string markdown) => new() { Kind = MarkupKind.Markdown, Value = markdown };
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlCompletionItem.cs
new file mode 100644
index 00000000..dad38c98
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlCompletionItem.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+
+class XmlCompletionItem(string label, CompletionItemKind kind, string markdownDocumentation, XmlCommitKind commitKind) : ILspCompletionItem
+{
+ public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal);
+
+ public ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken)
+ {
+ var item = new CompletionItem { Label = label, Kind = kind };
+
+ if(settings.IncludeDocumentation)
+ {
+ // TODO: strip markdown if client only supports text
+ item.Documentation = markdownDocumentation;
+ }
+
+ // TODO: generate completion edit based on CommitKind
+
+ return new(item);
+ }
+
+ static MarkupContent CreateMarkdown(string markdown)
+ => new() { Kind = MarkupKind.Markdown, Value = markdown };
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlEntityCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlEntityCompletionItem.cs
new file mode 100644
index 00000000..55e61199
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlEntityCompletionItem.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+
+class XmlEntityCompletionItem(string name, string character) : ILspCompletionItem
+{
+ readonly string label = $"&{name};";
+
+ public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal);
+
+
+ //TODO: need to tweak semicolon insertion for XmlCompletionItemKind.Entity
+ public ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken)
+ {
+ var item = new CompletionItem { Label = label, FilterText = name, Kind = XmlToLspCompletionItemKind.Entity };
+
+ if(settings.IncludeDocumentation)
+ {
+ item.Documentation = $"Escaped '{character}'";
+ };
+
+ return new(item);
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionRenderSettings.cs b/MSBuildLanguageServer/Handler/Completion/CompletionRenderSettings.cs
new file mode 100644
index 00000000..cf7ca211
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionRenderSettings.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+///
+/// Provides with information about which properties it should include
+/// when creating instances for either the completion list for for resolving individual items.
+///
+class CompletionRenderSettings
+{
+ public CompletionRenderSettings(CompletionClientCapabilities clientCapabilities, bool fullRender)
+ {
+ ClientCapabilities = clientCapabilities;
+ FullRender = fullRender;
+
+ IncludeLabelDetails = ClientCapabilities.LabelDetailsSupport && (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.LabelDetails)));
+ IncludeItemKind = fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.Kind));
+
+ bool supportsDeprecatedTag = clientCapabilities.TagSupport.Contains(CompletionItemTag.Deprecated);
+ IncludeDeprecatedTag = supportsDeprecatedTag && (fullRender || !ClientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.Tags)));
+#pragma warning disable CS0618 // Type or member is obsolete
+ IncludeDeprecatedProperty = !supportsDeprecatedTag && ClientCapabilities.DeprecatedSupport && (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.Deprecated)));
+#pragma warning restore CS0618 // Type or member is obsolete
+
+ IncludeTextEdit = (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.TextEdit)));
+ IncludeInsertText = (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.InsertText)));
+ IncludeTextEditText = (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.TextEditText)));
+ IncludeInsertTextFormat = (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.InsertTextFormat)));
+
+ SupportsDataDefault = clientCapabilities.SupportedItemDefaults.Contains(nameof(CompletionListItemDefaults.Data));
+ SupportsEditRange = clientCapabilities.SupportedItemDefaults.Contains(nameof(CompletionListItemDefaults.EditRange));
+ }
+
+ public CompletionClientCapabilities ClientCapabilities { get; }
+
+ public bool FullRender { get; }
+
+ public bool IncludeDocumentation => FullRender;
+
+ public bool IncludeItemKind { get; }
+
+ public bool IncludeLabelDetails { get; }
+
+ public bool IncludeTextEdit { get; }
+
+ public bool IncludeTextEditText { get; }
+
+ public bool IncludeInsertText { get; }
+
+ public bool IncludeInsertTextFormat { get; }
+
+ public bool SupportsInsertReplaceEdit => ClientCapabilities.InsertReplaceSupport;
+
+ public bool SupportsDataDefault { get; }
+
+ public bool SupportsEditRange { get; }
+
+ ///
+ /// Whether the client supports treating as a snippet
+ /// when is set to .
+ ///
+ public bool SupportSnippetFormat => ClientCapabilities.SnippetSupport;
+
+ public bool IncludeDeprecatedProperty { get; }
+
+ public bool IncludeDeprecatedTag { get; }
+
+ public bool IncludeDeprecatedPropertyOrTag => IncludeDeprecatedProperty || IncludeDeprecatedTag;
+
+ static readonly CompletionItemTag[] DeprecatedTag = [CompletionItemTag.Deprecated];
+
+ ///
+ /// Marks an item as deprecated using whichever method the client supports.
+ ///
+ public void SetDeprecated(CompletionItem item)
+ {
+ if(IncludeDeprecatedTag)
+ {
+ if(item.Tags is not null)
+ {
+ throw new ArgumentException("LSP protocol only defines one tag");
+ }
+ item.Tags = DeprecatedTag;
+ } else if(IncludeDeprecatedProperty)
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ item.Deprecated = true;
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionRenderer.cs b/MSBuildLanguageServer/Handler/Completion/CompletionRenderer.cs
new file mode 100644
index 00000000..18bff9e5
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionRenderer.cs
@@ -0,0 +1,96 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis.LanguageServer.Handler;
+using Microsoft.CodeAnalysis.PooledObjects;
+
+using MonoDevelop.MSBuild.Editor.LanguageServer.Services;
+
+using Roslyn.LanguageServer.Protocol;
+using LSP = Roslyn.LanguageServer.Protocol;
+
+using CompletionResolveData = Microsoft.CodeAnalysis.LanguageServer.Handler.Completion.CompletionResolveData;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+static class CompletionRenderer
+{
+ public static async Task RenderCompletionItems(
+ RequestContext context,
+ TextDocumentIdentifier textDocument,
+ IEnumerable items,
+ LSP.Range editRange,
+ CancellationToken cancellationToken)
+ {
+ var clientCapabilities = context.GetRequiredClientCapabilities();
+
+ var completionCapabilities = CompletionClientCapabilities.Create(clientCapabilities);
+ var renderSettings = new CompletionRenderSettings(completionCapabilities, false);
+
+ var completionListCache = context.GetRequiredService();
+
+ var rawItems = new List();
+ var renderContext = new CompletionRenderContext(editRange);
+ var resultId = completionListCache.UpdateCache(new CompletionListCacheEntry(rawItems, renderContext));
+ var resolveData = new CompletionResolveData(resultId, textDocument);
+
+ using var _ = ArrayBuilder.GetInstance(out var renderedBuilder);
+
+ // If the client doesn't support EditRange, and the item doesn't define a TextEdit, we must add a computed one.
+ // If the client doesn't support resolving the TextEdit in the resovle handler, then we must do this upfront.
+ bool includeEditRangeTextEdit = !renderSettings.SupportsEditRange && renderSettings.IncludeTextEdit;
+
+ // could consider parallelizing this
+ // however, the common case should be that Render returns a completed ValueTask, so it's faster than it looks
+ foreach(var item in items)
+ {
+ rawItems.Add(item);
+
+ var renderedItem = await item.Render(renderSettings, renderContext, cancellationToken).ConfigureAwait(false);
+
+ if(!renderSettings.SupportsDataDefault)
+ {
+ renderedItem.Data = resolveData;
+ }
+
+ if(includeEditRangeTextEdit)
+ {
+ renderedItem.AddEditRangeTextEdit(renderSettings, renderContext);
+ }
+
+ renderedBuilder.Add(renderedItem);
+ }
+
+ var completionList = new CompletionList {
+ Items = renderedBuilder.ToArray()
+ };
+
+
+ if(renderSettings.SupportsDataDefault)
+ {
+ (completionList.ItemDefaults ??= new()).Data = resolveData;
+ }
+
+
+ if(renderSettings.SupportsEditRange)
+ {
+ (completionList.ItemDefaults ??= new()).EditRange = editRange;
+ }
+
+ return completionList;
+ }
+
+ ///
+ /// If the item doesn't define a TextEdit, then compute one based on the EditRange in the
+ ///
+ public static void AddEditRangeTextEdit(this CompletionItem item, CompletionRenderSettings settings, CompletionRenderContext ctx)
+ {
+ if (item.TextEdit is null)
+ {
+ item.TextEdit = new TextEdit {
+ NewText = item.TextEditText ?? item.InsertText ?? item.Label,
+ Range = ctx.EditRange
+ };
+ }
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionResolveHandler.cs b/MSBuildLanguageServer/Handler/Completion/CompletionResolveHandler.cs
new file mode 100644
index 00000000..d3f8d795
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/CompletionResolveHandler.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Composition;
+using System.Text.Json;
+
+using Microsoft.CodeAnalysis.LanguageServer;
+using Microsoft.CodeAnalysis.LanguageServer.Handler;
+
+using MonoDevelop.MSBuild.Editor.LanguageServer.Services;
+
+using Roslyn.LanguageServer.Protocol;
+
+using CompletionResolveData = Microsoft.CodeAnalysis.LanguageServer.Handler.Completion.CompletionResolveData;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+[ExportCSharpVisualBasicStatelessLspService(typeof(CompletionResolveHandler)), Shared]
+[Method(Methods.TextDocumentCompletionResolveName)]
+sealed class CompletionResolveHandler : ILspServiceRequestHandler
+{
+ public bool MutatesSolutionState => false;
+ public bool RequiresLSPSolution => false;
+
+ public async Task HandleRequestAsync(CompletionItem request, RequestContext context, CancellationToken cancellationToken)
+ {
+ if(request.Data is not JsonElement data)
+ {
+ throw new InvalidOperationException("Completion item is missing data");
+ }
+ var resolveData = data.Deserialize(ProtocolConversions.LspJsonSerializerOptions);
+ if(resolveData?.ResultId == null)
+ {
+ throw new InvalidOperationException("Completion item is missing resultId");
+ }
+
+ var completionListCache = context.GetRequiredService();
+
+ var cacheEntry = completionListCache.GetCachedEntry(resolveData.ResultId);
+ if(cacheEntry == null)
+ {
+ throw new InvalidOperationException("Did not find cached completion list");
+ }
+
+ var clientCapabilities = context.GetRequiredClientCapabilities();
+ var completionCapabilities = CompletionClientCapabilities.Create(clientCapabilities);
+ var renderSettings = new CompletionRenderSettings(completionCapabilities, true);
+
+ foreach(var item in cacheEntry.Items)
+ {
+ if(item.IsMatch(request))
+ {
+ var renderedItem = await item.Render(renderSettings, cacheEntry.Context, cancellationToken).ConfigureAwait(false);
+
+ // avoid ambiguity if there are multiple items with the same label and IsMatch didn't distinguish
+ if(!string.Equals(renderedItem.SortText, request.SortText) || !string.Equals(renderedItem.FilterText, request.FilterText))
+ {
+ continue;
+ }
+
+ // if the client doesn't support EditRange, and the item doesn't define a TextEdit, we must add a computed one
+ if(!renderSettings.SupportsEditRange)
+ {
+ renderedItem.AddEditRangeTextEdit(renderSettings, cacheEntry.Context);
+ }
+ return renderedItem;
+ };
+ }
+
+ throw new InvalidOperationException("Did not find completion item in cached list");
+ }
+}
\ No newline at end of file
diff --git a/MSBuildLanguageServer/Handler/Completion/ILspCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/ILspCompletionItem.cs
new file mode 100644
index 00000000..59fc47c7
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/ILspCompletionItem.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Roslyn.LanguageServer.Protocol;
+using LSP = Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+///
+/// An object that can be resolved to a completion item
+///
+interface ILspCompletionItem
+{
+ ///
+ /// Resolve this object to a
+ ///
+ /// Provides information about which properties should be included
+ /// A populated
+ ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken);
+
+ ///
+ /// Whether this is a match for resolving the requested item.
+ ///
+ bool IsMatch(CompletionItem request);
+}
+
+///
+/// Information common to rendering many/all items that may be used when rendering the
+/// items upfront or cached and provided later when the item is resolved.
+///
+///
+record struct CompletionRenderContext(LSP.Range EditRange);
\ No newline at end of file
diff --git a/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionDocsProvider.cs b/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionDocsProvider.cs
new file mode 100644
index 00000000..910a7caa
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionDocsProvider.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.Extensions.Logging;
+
+using MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+using MonoDevelop.MSBuild.Language;
+
+using ProjectFileTools.NuGetSearch.Contracts;
+using ProjectFileTools.NuGetSearch.Feeds;
+using MonoDevelop.MSBuild.PackageSearch;
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+class MSBuildCompletionDocsProvider(DisplayElementRenderer renderer, MSBuildRootDocument document, SourceText sourceText, MSBuildResolveResult resolveResult)
+{
+ internal static MSBuildCompletionDocsProvider Create(ILogger logger, ClientCapabilities clientCapabilities, ClientInfo? clientInfo, MSBuildRootDocument document, SourceText sourceText, MSBuildResolveResult rr)
+ {
+ var documentationFormat = clientCapabilities.TextDocument?.Completion?.CompletionItem?.DocumentationFormat;
+ var renderer = new DisplayElementRenderer(logger, clientCapabilities, clientInfo, documentationFormat);
+ return new MSBuildCompletionDocsProvider(renderer, document, sourceText, rr);
+ }
+
+ public async Task GetDocumentation(ISymbol symbol, CancellationToken cancellationToken)
+ {
+ var tooltipContent = await renderer.GetInfoTooltipElement(sourceText, document, symbol, resolveResult, false, cancellationToken);
+ if(tooltipContent is not null)
+ {
+ return new MarkupContent {
+ //Value = "$(symbol-keyword) keyword Choose
\r\n\r\nGroups When and Otherwise elements", // tooltipContent,
+ Value = tooltipContent,
+ Kind = MarkupKind.Markdown
+ };
+ }
+ return null;
+ }
+
+ public async Task GetPackageDocumentation(IPackageSearchManager packageSearchManager, string packageId, string? packageVersion, FeedKind feedKind, string? targetFrameworkSearchParameter, CancellationToken cancellationToken)
+ {
+ var packageInfos = await packageSearchManager.SearchPackageInfo(packageId, null, targetFrameworkSearchParameter).ToTask(cancellationToken);
+ var packageInfo = packageInfos.FirstOrDefault(p => p.SourceKind == feedKind) ?? packageInfos.FirstOrDefault();
+
+ var tooltipContent = renderer.GetPackageInfoTooltip(packageId, packageInfo);
+ if(tooltipContent is not null)
+ {
+ return new MarkupContent {
+ Value = tooltipContent,
+ Kind = MarkupKind.Markdown
+ };
+ }
+ return null;
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionItemKind.cs b/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionItemKind.cs
new file mode 100644
index 00000000..2cd6ce96
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionItemKind.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using MonoDevelop.MSBuild.Language;
+using MonoDevelop.MSBuild.Language.Typesystem;
+
+using ProjectFileTools.NuGetSearch.Feeds;
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+///
+/// Maps MSBuild completion items to LSP
+///
+static class MSBuildCompletionItemKind
+{
+ public const CompletionItemKind Property = CompletionItemKind.Property;
+ public const CompletionItemKind Item = CompletionItemKind.Class;
+ public const CompletionItemKind Metadata = CompletionItemKind.Property;
+ public const CompletionItemKind Function = CompletionItemKind.Function;
+
+ public const CompletionItemKind Constant = CompletionItemKind.Constant;
+
+ public const CompletionItemKind PropertySyntax = CompletionItemKind.Property;
+ public const CompletionItemKind ItemSyntax = CompletionItemKind.Class;
+ public const CompletionItemKind MetadataSyntax = CompletionItemKind.Property;
+
+ public const CompletionItemKind Culture = CompletionItemKind.Constant;
+ public const CompletionItemKind Sdk = CompletionItemKind.Module;
+
+ public const CompletionItemKind NewGuid = CompletionItemKind.Macro;
+
+ // TODO: icons
+ public const CompletionItemKind PackageNuGet = CompletionItemKind.Module;
+ public const CompletionItemKind PackageMyGet = CompletionItemKind.Module;
+ public const CompletionItemKind PackageLocal = CompletionItemKind.Module;
+
+ internal static CompletionItemKind GetCompletionItemKind(this ISymbol symbol)
+ => symbol switch {
+ PropertyInfo => Property,
+ ItemInfo => Item,
+ MetadataInfo => Metadata,
+ ConstantSymbol => Constant,
+ _ => CompletionItemKind.Element
+ };
+
+ internal static CompletionItemKind GetCompletionItemKind(this FeedKind feedKind)
+ => feedKind switch {
+ FeedKind.MyGet => PackageMyGet,
+ FeedKind.Local => PackageLocal,
+ _ => PackageNuGet
+ };
+}
\ No newline at end of file
diff --git a/MSBuildLanguageServer/Handler/Completion/MSBuildXmlCompletionDataSource.cs b/MSBuildLanguageServer/Handler/Completion/MSBuildXmlCompletionDataSource.cs
new file mode 100644
index 00000000..6a84b15c
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/MSBuildXmlCompletionDataSource.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis.Text;
+
+using MonoDevelop.MSBuild.Editor.Completion;
+using MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+using MonoDevelop.MSBuild.Language;
+using MonoDevelop.MSBuild.Language.Syntax;
+using MonoDevelop.MSBuild.Language.Typesystem;
+using MonoDevelop.MSBuild.Schema;
+using MonoDevelop.Xml.Dom;
+using MonoDevelop.Xml.Editor.Completion;
+using MonoDevelop.Xml.Parser;
+
+using LSP = Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+record class MSBuildXmlCompletionContext
+ (
+ XmlSpineParser SpineParser, XmlCompletionTrigger XmlTriggerKind, ITextSource TextSource, List NodePath, LSP.Range EditRange,
+ MSBuildResolveResult ResolveResult, MSBuildRootDocument Document, MSBuildCompletionDocsProvider DocsProvider, SourceText SourceText
+ )
+ : XmlCompletionContext
+ (
+ SpineParser, XmlTriggerKind, TextSource, NodePath, EditRange
+ )
+{
+}
+
+class MSBuildXmlCompletionDataSource : XmlCompletionDataSource
+{
+ protected override Task?> GetElementCompletionsAsync(MSBuildXmlCompletionContext context, bool includeBracket, CancellationToken token)
+ {
+ var doc = context.Document;
+
+ var nodePath = context.NodePath;
+ if (!CompletionHelpers.TryGetElementSyntaxForElementCompletion(nodePath, out MSBuildElementSyntax? languageElement, out string? elementName)) {
+ return TaskCompleted(null);
+ }
+
+ var items = new List();
+
+ foreach(var el in doc.GetElementCompletions(languageElement, elementName))
+ {
+ if(el is ItemInfo)
+ {
+ items.Add(new MSBuildCompletionItem(el, XmlCommitKind.SelfClosingElement, context.DocsProvider, includeBracket ? "<" : null));
+ } else
+ {
+ items.Add(new MSBuildCompletionItem(el, XmlCommitKind.Element, context.DocsProvider, includeBracket ? "<" : null));
+ }
+ }
+
+ bool allowCData = languageElement != null && languageElement.ValueKind != MSBuildValueKind.Nothing;
+
+ return TaskCompleted(items);
+ }
+
+ protected override Task?> GetAttributeCompletionsAsync(MSBuildXmlCompletionContext context, IAttributedXObject attributedObject, Dictionary existingAttributes, CancellationToken token)
+ {
+ var rr = context.ResolveResult;
+ var doc = context.Document;
+
+ if(rr?.ElementSyntax == null)
+ {
+ return TaskCompleted(null);
+ }
+
+ var items = new List();
+
+ foreach(var att in rr.GetAttributeCompletions(doc, doc.ToolsVersion))
+ {
+ if(!existingAttributes.ContainsKey(att.Name))
+ {
+ items.Add(new MSBuildCompletionItem(att, XmlCommitKind.Attribute, context.DocsProvider));
+ }
+ }
+
+ return TaskCompleted(items);
+ }
+
+ protected override bool AllowTextContentInElement(MSBuildXmlCompletionContext context)
+ {
+ // when completing a tag name this is used to determine whether to include CDATA
+ // so we need to base it off the same MSBuildElementSyntax used for completion
+ // TODO: eliminate the duplicate TryGetElementSyntaxForElementCompletion call
+ if(context.XmlTriggerKind == XmlCompletionTrigger.ElementName || context.XmlTriggerKind == XmlCompletionTrigger.Tag)
+ {
+ var nodePath = context.NodePath;
+ if(!CompletionHelpers.TryGetElementSyntaxForElementCompletion(nodePath, out MSBuildElementSyntax? languageElement, out _))
+ {
+ return true;
+ }
+ return languageElement is null || languageElement.ValueKind != MSBuildValueKind.Nothing;
+ }
+
+ // otherwise, it's for entity completion, and the resolveResult is fine
+ if(context.ResolveResult?.ElementSyntax is { } elementSyntax)
+ {
+ return elementSyntax.ValueKind != MSBuildValueKind.Nothing;
+ }
+
+ return true;
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/PackageCompletionDocsProvider.cs b/MSBuildLanguageServer/Handler/Completion/PackageCompletionDocsProvider.cs
new file mode 100644
index 00000000..dba94892
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/PackageCompletionDocsProvider.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using ProjectFileTools.NuGetSearch.Contracts;
+using ProjectFileTools.NuGetSearch.Feeds;
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+record class PackageCompletionDocsProvider(IPackageSearchManager PackageSearchManager, MSBuildCompletionDocsProvider docsProvider, string? TargetFrameworkSearchParameter)
+{
+ public Task GetPackageDocumentation(string packageId, string? packageVersion, FeedKind feedKind, CancellationToken cancellationToken)
+ => docsProvider.GetPackageDocumentation(PackageSearchManager, packageId, packageVersion, feedKind, TargetFrameworkSearchParameter, cancellationToken);
+}
\ No newline at end of file
diff --git a/MSBuildLanguageServer/Handler/Completion/XmlCommitKind.cs b/MSBuildLanguageServer/Handler/Completion/XmlCommitKind.cs
new file mode 100644
index 00000000..fc4cecd1
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/XmlCommitKind.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+///
+/// Controls how XML completion items are committed
+///
+enum XmlCommitKind
+{
+ Element,
+ SelfClosingElement,
+ Attribute,
+ AttributeValue,
+ CData,
+ Comment,
+ Prolog,
+ Entity,
+ ClosingTag,
+ MultipleClosingTags
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/XmlCompletionDataSource.cs b/MSBuildLanguageServer/Handler/Completion/XmlCompletionDataSource.cs
new file mode 100644
index 00000000..2be86711
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/XmlCompletionDataSource.cs
@@ -0,0 +1,160 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems;
+using MonoDevelop.Xml.Dom;
+using MonoDevelop.Xml.Editor.Completion;
+using MonoDevelop.Xml.Parser;
+
+using LSP = Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+record class XmlCompletionContext(XmlSpineParser SpineParser, XmlCompletionTrigger XmlTriggerKind, ITextSource TextSource, List NodePath, LSP.Range EditRange)
+{
+}
+
+class XmlCompletionDataSource where TContext : XmlCompletionContext
+{
+ public IEnumerable?>> GetCompletionTasks(TContext triggerContext, CancellationToken cancellationToken)
+ {
+ switch(triggerContext.XmlTriggerKind)
+ {
+ case XmlCompletionTrigger.ElementValue:
+ yield return GetElementValueCompletionsAsync(triggerContext, cancellationToken);
+ goto case XmlCompletionTrigger.Tag;
+
+ case XmlCompletionTrigger.Tag:
+ case XmlCompletionTrigger.ElementName:
+ //TODO: if it's on the first or second line and there's no DTD declaration, add the DTDs, or at least (maxDepth: 1) is not IAttributedXObject attributedOb)
+ {
+ throw new InvalidOperationException("Did not find IAttributedXObject in stack for XmlCompletionTrigger.Attribute");
+ }
+ triggerContext.SpineParser.Clone().AdvanceUntilEnded((XObject)attributedOb, triggerContext.TextSource, 1000);
+ var attributes = attributedOb.Attributes.ToDictionary(StringComparer.OrdinalIgnoreCase);
+ yield return GetAttributeCompletionsAsync(triggerContext, attributedOb, attributes, cancellationToken);
+ break;
+
+ case XmlCompletionTrigger.AttributeValue:
+ if(triggerContext.SpineParser.Spine.TryPeek(out XAttribute? att) && triggerContext.SpineParser.Spine.TryPeek(1, out IAttributedXObject? attributedObject))
+ {
+ yield return GetAttributeValueCompletionsAsync(triggerContext, attributedObject, att, cancellationToken);
+ }
+ break;
+
+ case XmlCompletionTrigger.Entity:
+ bool isElement = triggerContext.NodePath.Count > 0 && triggerContext.NodePath[^1] is XElement;
+ if(!isElement || AllowTextContentInElement(triggerContext))
+ {
+ yield return GetEntityCompletionsAsync(triggerContext, cancellationToken);
+ }
+ break;
+
+ case XmlCompletionTrigger.DocType:
+ case XmlCompletionTrigger.DeclarationOrCDataOrComment:
+ yield return GetDeclarationCompletionsAsync(triggerContext, cancellationToken);
+ break;
+ }
+ }
+
+ protected virtual Task?> GetElementCompletionsAsync(TContext context, bool includeBracket, CancellationToken token)
+ => TaskCompleted(null);
+
+ protected virtual Task?> GetElementValueCompletionsAsync(TContext context, CancellationToken token)
+ => TaskCompleted(null);
+
+ protected virtual Task?> GetAttributeCompletionsAsync(TContext context, IAttributedXObject attributedObject, Dictionary existingAttributes, CancellationToken token)
+ => TaskCompleted(null);
+
+ protected virtual Task?> GetAttributeValueCompletionsAsync(TContext context, IAttributedXObject attributedObject, XAttribute attribute, CancellationToken token)
+ => TaskCompleted(null);
+
+ protected virtual Task?> GetEntityCompletionsAsync(TContext context, CancellationToken token)
+ => TaskCompleted(entityItems);
+
+ protected virtual Task?> GetDeclarationCompletionsAsync(TContext context, CancellationToken token)
+ => TaskCompleted(
+ AllowTextContentInElement(context)
+ ? [cdataItemWithBracket, commentItemWithBracket]
+ : [commentItemWithBracket]
+ );
+
+ protected virtual bool AllowTextContentInElement(TContext context) => true;
+
+ protected static Task?> TaskCompleted(IList? items) => Task.FromResult(items);
+
+ Task?> GetMiscellaneousTagsAsync(TContext triggerContext, bool includeBracket, CancellationToken cancellationToken)
+ => Task.Run(() => (IList?)GetMiscellaneousTags(triggerContext, includeBracket).ToList(), cancellationToken);
+
+ ///
+ /// Gets completion items for closing tags, comments, CDATA etc.
+ ///
+ IEnumerable GetMiscellaneousTags(TContext context, bool includeBracket)
+ {
+ if(context.NodePath.Count == 0 & context.EditRange.Start.Line == 0)
+ {
+ yield return includeBracket ? prologItemWithBracket : prologItem;
+ }
+
+ if(AllowTextContentInElement(context))
+ {
+ yield return includeBracket ? cdataItemWithBracket : cdataItem;
+ }
+
+ yield return includeBracket ? commentItemWithBracket : commentItem;
+
+ foreach(var closingTag in GetClosingTags(context.NodePath, includeBracket))
+ {
+ yield return closingTag;
+ }
+ }
+
+ IEnumerable GetClosingTags(List nodePath, bool includeBracket)
+ {
+ var dedup = new HashSet();
+
+ //FIXME: search forward to see if tag's closed already
+ for(int i = nodePath.Count - 1; i >= 0; i--)
+ {
+ var ob = nodePath[i];
+ if(!(ob is XElement el))
+ continue;
+ if(!el.IsNamed || el.IsClosed)
+ yield break;
+
+ string name = el.Name.FullName!;
+ if(!dedup.Add(name))
+ {
+ continue;
+ }
+
+ yield return new XmlClosingTagCompletionItem(includeBracket, name, el, dedup.Count);
+ }
+ }
+
+ readonly XmlCompletionItem cdataItem = new("![CDATA[", XmlToLspCompletionItemKind.CData, "XML character data", XmlCommitKind.CData);
+ readonly XmlCompletionItem cdataItemWithBracket = new(""
+ readonly XmlCompletionItem prologItem = new("?xml", XmlToLspCompletionItemKind.Prolog, "XML prolog", XmlCommitKind.Prolog);
+ readonly XmlCompletionItem prologItemWithBracket = new(""),
+ new ("amp", "&"),
+ ];
+}
diff --git a/MSBuildLanguageServer/Handler/Completion/XmlToLspCompletionItemKind.cs b/MSBuildLanguageServer/Handler/Completion/XmlToLspCompletionItemKind.cs
new file mode 100644
index 00000000..a80cc3b4
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Completion/XmlToLspCompletionItemKind.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion;
+
+///
+/// Central location for mapping XML item kinds to values
+///
+class XmlToLspCompletionItemKind
+{
+ public const CompletionItemKind ClosingTag = CompletionItemKind.CloseElement;
+ public const CompletionItemKind Comment = CompletionItemKind.TagHelper;
+ public const CompletionItemKind CData = CompletionItemKind.TagHelper;
+ public const CompletionItemKind Prolog = CompletionItemKind.TagHelper;
+ public const CompletionItemKind Entity = CompletionItemKind.TagHelper;
+}
diff --git a/MSBuildLanguageServer/Handler/DocumentDiagnosticsHandler.cs b/MSBuildLanguageServer/Handler/DocumentDiagnosticsHandler.cs
new file mode 100644
index 00000000..914c97fe
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/DocumentDiagnosticsHandler.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Composition;
+
+using Microsoft.CodeAnalysis.LanguageServer.Handler;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.CommonLanguageServerProtocol.Framework;
+
+using MonoDevelop.MSBuild.Analysis;
+using MonoDevelop.MSBuild.Editor.LanguageServer.Parser;
+using MonoDevelop.MSBuild.Language;
+using MonoDevelop.Xml.Analysis;
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler;
+
+
+[ExportCSharpVisualBasicStatelessLspService(typeof(DocumentDiagnosticsHandler)), Shared]
+[Method(Methods.TextDocumentDiagnosticName)]
+sealed class DocumentDiagnosticsHandler : ILspServiceDocumentRequestHandler?>
+{
+ public bool MutatesSolutionState => false;
+
+ public bool RequiresLSPSolution => true;
+
+ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentDiagnosticParams request) => request.TextDocument;
+
+ public async Task?> HandleRequestAsync(DocumentDiagnosticParams request, RequestContext context, CancellationToken cancellationToken)
+ {
+ var msbuildParserService = context.GetRequiredService();
+ var xmlParserService = context.GetRequiredService();
+ var logger = context.GetRequiredService();
+ var extLogger = logger.ToILogger();
+
+ var document = context.GetRequiredDocument();
+
+ if(!msbuildParserService.TryGetParseResult(document.CurrentState, out Task? parseTask, cancellationToken))
+ {
+ return null;
+ }
+
+ var result = await parseTask!; // not sure why we need the ! here, TryGetParseResult has NotNullWhen(true)
+
+
+ var msbuildDoc = result.MSBuildDocument;
+ var sourceText = result.XmlParseResult.Text;
+
+ IEnumerable? msbuildDiagnostics = msbuildDoc.Diagnostics;
+
+ if(msbuildDoc.ProjectElement is not null)
+ {
+ try
+ {
+ // FIXME move this to a service
+ var analyzerDriver = new MSBuildAnalyzerDriver(context.GetRequiredService ().ToILogger ());
+ analyzerDriver.AddBuiltInAnalyzers();
+ msbuildDiagnostics = await Task.Run(() => analyzerDriver.Analyze(msbuildDoc, true, cancellationToken), cancellationToken);
+ } catch(Exception ex)
+ {
+ logger.LogException(ex, "Error in analyzer service");
+ }
+ }
+
+ var convertedXmlDiagnostics = result.XmlParseResult.Diagnostics?.Select(d => ConvertDiagnostic(d, sourceText)) ?? [];
+ var convertedMSBuildDiagnostics = msbuildDiagnostics.Select(d => ConvertDiagnostic(d, sourceText));
+
+ return new FullDocumentDiagnosticReport {
+ Items = convertedMSBuildDiagnostics.Concat(convertedXmlDiagnostics).ToArray()
+ };
+ }
+
+ const string sourceName = "msbuild";
+
+ static Diagnostic ConvertDiagnostic(XmlDiagnostic xmlDiagnostic, SourceText sourceText)
+ {
+ return new Diagnostic {
+ Range = xmlDiagnostic.Span.ToLspRange(sourceText),
+ Message = xmlDiagnostic.GetFormattedMessageWithTitle(),
+ Severity = ConvertSeverity(xmlDiagnostic.Descriptor.Severity),
+ Source = sourceName
+ // don't use ID and code, it's generally too long and not useful
+ // Code = xmlDiagnostic.Descriptor.Id,
+ };
+ }
+
+ static DiagnosticSeverity ConvertSeverity(XmlDiagnosticSeverity severity) => severity switch {
+ XmlDiagnosticSeverity.Suggestion => DiagnosticSeverity.Information,
+ XmlDiagnosticSeverity.Warning => DiagnosticSeverity.Warning,
+ XmlDiagnosticSeverity.Error => DiagnosticSeverity.Error,
+ _ => throw new ArgumentException($"Unsupported XmlDiagnosticSeverity '{0}'")
+ };
+
+ static Diagnostic ConvertDiagnostic(MSBuildDiagnostic msbuildDiagnostic, SourceText sourceText)
+ {
+ DiagnosticTag[]? diagnosticTags = msbuildDiagnostic.Descriptor.Id switch {
+ CoreDiagnostics.DeprecatedWithMessage_Id => [DiagnosticTag.Deprecated],
+ CoreDiagnostics.RedundantMinimumVersion_Id => [DiagnosticTag.Unnecessary],
+ _ => null
+ };
+
+ return new Diagnostic {
+ Range = msbuildDiagnostic.Span.ToLspRange(sourceText),
+ Message = msbuildDiagnostic.GetFormattedMessageWithTitle(),
+ Severity = ConvertSeverity(msbuildDiagnostic.Descriptor.Severity),
+ Tags = diagnosticTags,
+ Source = sourceName
+ // don't use ID and code, it's generally too long and not useful
+ //Code = msbuildDiagnostic.Descriptor.Id,
+ };
+ }
+
+ static DiagnosticSeverity ConvertSeverity(MSBuildDiagnosticSeverity severity) => severity switch {
+ MSBuildDiagnosticSeverity.Suggestion => DiagnosticSeverity.Information,
+ MSBuildDiagnosticSeverity.Warning => DiagnosticSeverity.Warning,
+ MSBuildDiagnosticSeverity.Error => DiagnosticSeverity.Error,
+ _ => throw new ArgumentException($"Unsupported MSBuildDiagnosticSeverity '{0}'")
+ };
+}
diff --git a/MSBuildLanguageServer/Handler/HoverHandler.cs b/MSBuildLanguageServer/Handler/HoverHandler.cs
new file mode 100644
index 00000000..3849702e
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/HoverHandler.cs
@@ -0,0 +1,176 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Composition;
+
+using Microsoft.CodeAnalysis.LanguageServer;
+using Microsoft.CodeAnalysis.LanguageServer.Handler;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.CommonLanguageServerProtocol.Framework;
+
+using MonoDevelop.MSBuild.Editor.LanguageServer.Parser;
+using MonoDevelop.MSBuild.Editor.NuGetSearch;
+using MonoDevelop.MSBuild.Language;
+using MonoDevelop.MSBuild.Language.Expressions;
+using MonoDevelop.MSBuild.Language.Typesystem;
+using MonoDevelop.MSBuild.PackageSearch;
+using MonoDevelop.MSBuild.Schema;
+
+using ProjectFileTools.NuGetSearch.Contracts;
+
+using Roslyn.LanguageServer.Protocol;
+
+using Range = Roslyn.LanguageServer.Protocol.Range;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler;
+
+// partly based on MonoDevelop.MSBuild.Editor/QuickInfo/MSBuildQuickInfoSource.cs
+
+[ExportCSharpVisualBasicStatelessLspService(typeof(HoverHandler)), Shared]
+[Method(Methods.TextDocumentHoverName)]
+internal sealed class HoverHandler : ILspServiceDocumentRequestHandler
+{
+ public bool MutatesSolutionState => false;
+
+ public bool RequiresLSPSolution => true;
+
+ public TextDocumentIdentifier GetTextDocumentIdentifier(TextDocumentPositionParams request) => request.TextDocument;
+
+ public async Task HandleRequestAsync(TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
+ {
+ var logger = context.GetRequiredService();
+ var extLogger = logger.ToILogger();
+
+ DisplayElementRenderer CreateRenderer()
+ {
+ var capabilities = context.GetRequiredClientCapabilities();
+ var clientInfo = context.GetRequiredService().TryGetInitializeParams()?.ClientInfo;
+ return new DisplayElementRenderer(extLogger, capabilities, clientInfo, capabilities.TextDocument?.Hover?.ContentFormat);
+ }
+
+ var msbuildParserService = context.GetRequiredService();
+ var xmlParserService = context.GetRequiredService();
+
+ var document = context.GetRequiredDocument();
+
+ if(!msbuildParserService.TryGetParseResult(document.CurrentState, out Task? parseTask, cancellationToken))
+ {
+ return null;
+ }
+
+ var result = await parseTask!; // not sure why we need the ! here, TryGetParseResult has NotNullWhen(true)
+
+ if(result?.MSBuildDocument is not MSBuildRootDocument doc)
+ {
+ return null;
+ }
+
+ var sourceText = result.XmlParseResult.Text;
+ var position = ProtocolConversions.PositionToLinePosition(request.Position);
+ int offset = position.ToOffset (sourceText);
+
+ var spineParser = result.XmlParseResult.GetSpineParser(position, cancellationToken);
+
+ var annotations = MSBuildNavigation.GetAnnotationsAtOffset(doc, offset)?.ToList();
+ if(annotations != null && annotations.Count > 0)
+ {
+ // TODO navigation annotations
+ return CreateNavigationQuickInfo(CreateRenderer(), sourceText, annotations);
+ }
+
+ var functionTypeProvider = context.GetRequiredService().FunctionTypeProvider;
+
+ //FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards
+ await functionTypeProvider.EnsureInitialized(cancellationToken);
+
+ var rr = MSBuildResolver.Resolve(spineParser, result.XmlParseResult.Text.GetTextSource(), result.MSBuildDocument, functionTypeProvider, extLogger, cancellationToken);
+ if (rr is null)
+ {
+ return null;
+ }
+
+ if(rr.ReferenceKind == MSBuildReferenceKind.NuGetID)
+ {
+ var packageSearchManager = context.GetRequiredLspService();
+ return await CreateNuGetQuickInfo(CreateRenderer(), packageSearchManager, logger, sourceText, doc, rr, cancellationToken);
+ }
+
+ var info = rr.GetResolvedReference(doc, functionTypeProvider);
+ if(info is null) {
+ return null;
+ }
+
+ // don't include the deprecation message, as the validator should have added a warning that will be merged into this tooltip
+ var markdown = await CreateRenderer().GetInfoTooltipElement(sourceText, doc, info, rr, true, cancellationToken);
+
+ if(markdown is not null) {
+ return CreateHover(rr.ToLspRange(sourceText), markdown);
+ }
+ return null;
+ }
+
+ static Hover CreateHover(Range range, string markdown)
+ {
+ return new Hover {
+ Range = range,
+ Contents = new MarkupContent {
+ Kind = MarkupKind.Markdown,
+ Value = markdown
+ }
+ };
+ }
+
+ Hover CreateNavigationQuickInfo(DisplayElementRenderer renderer, SourceText sourceText, IEnumerable annotations)
+ {
+ var navs = annotations.ToList();
+ var first = navs.First();
+ var resolvedPathMarkdown = renderer.GetResolvedPathElement(navs);
+
+ return CreateHover(first.Span.ToLspRange (sourceText), resolvedPathMarkdown);
+ }
+
+ //FIXME: can we display some kind of "loading" message while it loads?
+ async Task CreateNuGetQuickInfo(
+ DisplayElementRenderer renderer,
+ IPackageSearchManager packageSearchManager, ILspLogger logger,
+ SourceText sourceText, MSBuildRootDocument doc, MSBuildResolveResult rr, CancellationToken token)
+ {
+ IPackageInfo? info = null;
+ var packageId = rr.GetNuGetIDReference();
+
+ try
+ {
+ var frameworkId = doc.GetTargetFrameworkNuGetSearchParameter();
+
+ //FIXME: can we use the correct version here?
+ var infos = await packageSearchManager.SearchPackageInfo(packageId, null, frameworkId).ToTask(token);
+
+
+ //prefer non-local results as they will have more metadata
+ info = infos
+ .FirstOrDefault(p => p.SourceKind != ProjectFileTools.NuGetSearch.Feeds.FeedKind.Local)
+ ?? infos.FirstOrDefault();
+ }
+ catch(Exception ex) when(!(ex is OperationCanceledException && token.IsCancellationRequested))
+ {
+ logger.LogException(ex);
+ }
+
+ var markdown = renderer.GetPackageInfoTooltip(packageId, info);
+ return CreateHover(rr.ToLspRange (sourceText), markdown);
+ }
+
+ class NullFunctionTypeProvider : IFunctionTypeProvider
+ {
+ public Task EnsureInitialized(CancellationToken token) => Task.CompletedTask;
+ public ClassInfo? GetClassInfo(string name) => null;
+ public IEnumerable GetClassNameCompletions() => [];
+ public ISymbol? GetEnumInfo(string reference) => null;
+ public FunctionInfo? GetItemFunctionInfo(string name) => null;
+ public IEnumerable GetItemFunctionNameCompletions() => [];
+ public FunctionInfo? GetPropertyFunctionInfo(MSBuildValueKind valueKind, string name) => null;
+ public IEnumerable GetPropertyFunctionNameCompletions(ExpressionNode triggerExpression) => [];
+ public FunctionInfo? GetStaticPropertyFunctionInfo(string className, string name) => null;
+ public MSBuildValueKind ResolveType(ExpressionPropertyNode node) => MSBuildValueKind.Unknown;
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/LocationHelpers.cs b/MSBuildLanguageServer/Handler/LocationHelpers.cs
new file mode 100644
index 00000000..ff80d807
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/LocationHelpers.cs
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis.LanguageServer;
+using Microsoft.CodeAnalysis.Text;
+
+using MonoDevelop.MSBuild.Editor.LanguageServer.Parser;
+using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace;
+
+using Roslyn.LanguageServer.Protocol;
+
+using LSP = Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler;
+
+static class LocationHelpers
+{
+ public static SumType? ConvertLocationLinksToLocationsIfNeeded(LocationLink[]? results, bool supportsLocationLink)
+ {
+ if(results is null || results.Length == 0)
+ {
+ return null;
+ }
+
+ if(supportsLocationLink)
+ {
+ return results;
+ }
+
+ if(results.Length == 1)
+ {
+ return LocationLinkToLocation(results[0]);
+ }
+
+ return Array.ConvertAll(results, LocationLinkToLocation);
+ }
+
+ public static Location LocationLinkToLocation(LocationLink ll) => new Location {
+ Uri = ll.TargetUri,
+ Range = ll.TargetRange
+ };
+
+ public static LSP.Range ConvertRangeViaWorkspace(LspEditorWorkspace workspace, string filePath, Xml.Dom.TextSpan? targetSpan)
+ {
+ if(targetSpan is null)
+ {
+ return EmptyRange;
+ }
+
+ var uri = ProtocolConversions.CreateAbsoluteUri(filePath);
+ SourceText? sourceText = null;
+ if(workspace.GetTrackedLspText().TryGetValue(uri, out var tracked))
+ {
+ sourceText = tracked.Text;
+ } else
+ {
+ sourceText = SourceText.From(filePath);
+ }
+
+ return sourceText.GetLspRange(targetSpan.Value.Start, targetSpan.Value.Length);
+ }
+
+ public static LocationLink CreateLocationLink(LSP.Range originRange, string targetPath, LSP.Range? targetRange = null, LSP.Range? targetSelectionRange = null)
+ {
+ targetRange ??= EmptyRange;
+ return new LocationLink {
+ OriginSelectionRange = originRange,
+ TargetUri = ProtocolConversions.CreateAbsoluteUri(targetPath),
+ TargetRange = targetRange,
+ TargetSelectionRange = targetSelectionRange ?? targetRange
+ };
+ }
+
+ public static readonly LSP.Range EmptyRange = new LSP.Range {
+ Start = new LSP.Position { Line = 0, Character = 0 },
+ End = new LSP.Position { Line = 0, Character = 0 }
+ };
+}
diff --git a/MSBuildLanguageServer/Handler/Navigation/FindAllReferencesHandler.cs b/MSBuildLanguageServer/Handler/Navigation/FindAllReferencesHandler.cs
new file mode 100644
index 00000000..e925aee6
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Navigation/FindAllReferencesHandler.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Composition;
+
+using Microsoft.CodeAnalysis.LanguageServer;
+using Microsoft.CodeAnalysis.LanguageServer.Handler;
+using Microsoft.CommonLanguageServerProtocol.Framework;
+using Microsoft.VisualStudio.Composition;
+
+using MonoDevelop.MSBuild.Editor.LanguageServer.Parser;
+using MonoDevelop.MSBuild.Editor.LanguageServer.Services;
+using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace;
+using MonoDevelop.MSBuild.Language;
+
+using Roslyn.LanguageServer.Protocol;
+
+using LSP = Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Navigation;
+
+[ExportCSharpVisualBasicStatelessLspService(typeof(FindAllReferencesHandler)), Shared]
+[Method(Methods.TextDocumentReferencesName)]
+sealed class FindAllReferencesHandler : ILspServiceDocumentRequestHandler
+{
+ public bool MutatesSolutionState => false;
+
+ public bool RequiresLSPSolution => true;
+
+ public TextDocumentIdentifier GetTextDocumentIdentifier(ReferenceParams request) => request.TextDocument;
+
+ public async Task HandleRequestAsync(ReferenceParams request, RequestContext context, CancellationToken cancellationToken)
+ {
+ var logger = context.GetRequiredService();
+ var extLogger = logger.ToILogger();
+
+ var msbuildParserService = context.GetRequiredService();
+
+ var document = context.GetRequiredDocument();
+
+ if(!msbuildParserService.TryGetParseResult(document.CurrentState, out Task? parseTask, cancellationToken))
+ {
+ return null;
+ }
+
+ var parseResult = await parseTask!; // not sure why we need the ! here, TryGetParseResult has NotNullWhen(true)
+
+ if(parseResult?.MSBuildDocument is not MSBuildRootDocument doc)
+ {
+ return null;
+ }
+
+ var sourceText = parseResult.XmlParseResult.Text;
+ var position = ProtocolConversions.PositionToLinePosition(request.Position);
+ int offset = position.ToOffset(sourceText);
+
+ var spineParser = parseResult.XmlParseResult.GetSpineParser(position, cancellationToken);
+
+ var functionTypeProvider = context.GetRequiredService().FunctionTypeProvider;
+
+ //FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards
+ await functionTypeProvider.EnsureInitialized(cancellationToken);
+
+ var rr = MSBuildResolver.Resolve(spineParser, parseResult.XmlParseResult.Text.GetTextSource(), parseResult.MSBuildDocument, functionTypeProvider, extLogger, cancellationToken);
+ if(rr is null)
+ {
+ return null;
+ }
+
+ if(!MSBuildReferenceCollector.CanCreate(rr))
+ {
+ return null;
+ }
+
+ var resultReporter = BufferedProgress.Create(request.PartialResultToken);
+ var navigationService = context.GetRequiredLspService();
+
+ await navigationService.FindReferences(
+ parseResult,
+ (doc, text, logger, reporter) => MSBuildReferenceCollector.Create(doc, text, logger, rr, functionTypeProvider, reporter),
+ resultReporter,
+ request.WorkDoneToken,
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ request.WorkDoneToken?.End();
+
+ return resultReporter.GetFlattenedValues();
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/Navigation/GoToDefinitionHandler.cs b/MSBuildLanguageServer/Handler/Navigation/GoToDefinitionHandler.cs
new file mode 100644
index 00000000..be8882dd
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/Navigation/GoToDefinitionHandler.cs
@@ -0,0 +1,154 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Composition;
+
+using Microsoft.CodeAnalysis.LanguageServer;
+using Microsoft.CodeAnalysis.LanguageServer.Handler;
+using Microsoft.CommonLanguageServerProtocol.Framework;
+
+using MonoDevelop.MSBuild.Editor.LanguageServer.Parser;
+using MonoDevelop.MSBuild.Editor.LanguageServer.Services;
+using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace;
+using MonoDevelop.MSBuild.Editor.Navigation;
+using MonoDevelop.MSBuild.Language;
+
+using Roslyn.LanguageServer.Protocol;
+
+using LSP = Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Navigation;
+
+[ExportCSharpVisualBasicStatelessLspService(typeof(GoToDefinitionHandler)), Shared]
+[Method(Methods.TextDocumentDefinitionName)]
+sealed class GoToDefinitionHandler : ILspServiceDocumentRequestHandler?>
+{
+ public bool MutatesSolutionState => false;
+
+ public bool RequiresLSPSolution => true;
+
+ public TextDocumentIdentifier GetTextDocumentIdentifier(DefinitionParams request) => request.TextDocument;
+
+ public async Task?> HandleRequestAsync(DefinitionParams request, RequestContext context, CancellationToken cancellationToken)
+ {
+ var logger = context.GetRequiredService();
+ var extLogger = logger.ToILogger();
+
+ var msbuildParserService = context.GetRequiredService();
+
+ var document = context.GetRequiredDocument();
+
+ if(!msbuildParserService.TryGetParseResult(document.CurrentState, out Task? parseTask, cancellationToken))
+ {
+ return null;
+ }
+
+ var parseResult = await parseTask!; // not sure why we need the ! here, TryGetParseResult has NotNullWhen(true)
+
+ if(parseResult?.MSBuildDocument is not MSBuildRootDocument doc)
+ {
+ return null;
+ }
+
+ var sourceText = parseResult.XmlParseResult.Text;
+ var position = ProtocolConversions.PositionToLinePosition(request.Position);
+ int offset = position.ToOffset(sourceText);
+
+ var spineParser = parseResult.XmlParseResult.GetSpineParser(position, cancellationToken);
+
+ var functionTypeProvider = context.GetRequiredService().FunctionTypeProvider;
+
+ //FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards
+ await functionTypeProvider.EnsureInitialized(cancellationToken);
+
+ var rr = MSBuildResolver.Resolve(spineParser, parseResult.XmlParseResult.Text.GetTextSource(), parseResult.MSBuildDocument, functionTypeProvider, extLogger, cancellationToken);
+ if(rr is null)
+ {
+ return null;
+ }
+
+ var result = MSBuildNavigation.GetNavigation(doc, offset, rr);
+ if(result is null)
+ {
+ return null;
+ }
+
+ var originRange = parseResult.XmlParseResult.Text.GetLspRange(result.Offset, result.Length);
+
+ var locations = await GetGoToDefinitionLocations(result, parseResult, originRange, request, context, cancellationToken);
+
+ var linkSupport = context.GetRequiredClientCapabilities().TextDocument?.Definition?.LinkSupport ?? false;
+
+ return LocationHelpers.ConvertLocationLinksToLocationsIfNeeded(locations, linkSupport);
+ }
+
+ static async Task GetGoToDefinitionLocations(
+ MSBuildNavigationResult result,
+ MSBuildParseResult originParseResult,
+ LSP.Range originRange,
+ DefinitionParams request,
+ RequestContext context,
+ CancellationToken cancellationToken)
+ {
+ if(result.Paths != null)
+ {
+ if(result.Paths.Length == 1)
+ {
+ return [LocationHelpers.CreateLocationLink(originRange, result.Paths[0])];
+ }
+ if(result.Paths.Length > 1)
+ {
+ return result.Paths.Select(p => LocationHelpers.CreateLocationLink(originRange, p)).ToArray();
+ }
+ }
+
+ if(result.DestFile != null)
+ {
+ var workspace = context.GetRequiredLspService();
+ var targetRange = LocationHelpers.ConvertRangeViaWorkspace(workspace, result.DestFile, result.TargetSpan);
+ return [LocationHelpers.CreateLocationLink(originRange, result.DestFile, targetRange)];
+ }
+
+ Func? resultFilter = null;
+ MSBuildReferenceCollectorFactory collectorFactory;
+
+ switch(result.Kind)
+ {
+ case MSBuildReferenceKind.Target:
+ request.WorkDoneToken?.Begin($"Finding definitions for target '{result.Name}'");
+ collectorFactory = (doc, text, logger, reporter) => new MSBuildTargetDefinitionCollector(doc, text, logger, result.Name!, reporter);
+ break;
+ case MSBuildReferenceKind.Item:
+ request.WorkDoneToken?.Begin($"Finding assignments for item '{result.Name}'");
+ collectorFactory = (doc, text, logger, reporter) => new MSBuildItemReferenceCollector(doc, text, logger, result.Name!, reporter);
+ resultFilter = MSBuildNavigationHelpers.FilterUsageWrites;
+ break;
+ case MSBuildReferenceKind.Property:
+ request.WorkDoneToken?.Begin($"Finding assignments for property '{result.Name}'");
+ collectorFactory = (doc, text, logger, reporter) => new MSBuildPropertyReferenceCollector(doc, text, logger, result.Name!, reporter);
+ resultFilter = MSBuildNavigationHelpers.FilterUsageWrites;
+ break;
+ case MSBuildReferenceKind.NuGetID:
+ // TODO: can we navigate to a package URL?
+ // OpenNuGetUrl(result.Name, EditorHost, logger);
+ default:
+ return null;
+ }
+
+ var resultReporter = BufferedProgress.Create>(request.PartialResultToken, ll => new(ll));
+ var findReferencesService = context.GetRequiredLspService();
+
+ await findReferencesService.FindReferences(
+ originParseResult,
+ originRange,
+ collectorFactory,
+ resultReporter,
+ request.WorkDoneToken,
+ cancellationToken,
+ resultFilter).ConfigureAwait(false);
+
+ request.WorkDoneToken?.End();
+
+ return resultReporter.GetFlattenedValues();
+ }
+}
diff --git a/MSBuildLanguageServer/Handler/WorkDoneProgressExtensions.cs b/MSBuildLanguageServer/Handler/WorkDoneProgressExtensions.cs
new file mode 100644
index 00000000..66e85178
--- /dev/null
+++ b/MSBuildLanguageServer/Handler/WorkDoneProgressExtensions.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Roslyn.LanguageServer.Protocol;
+
+namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler;
+
+static class WorkDoneProgressExtensions
+{
+ public static void Begin(this IProgress? progress, string title, bool? cancellable = null, string? message = null, int? percentage = null)
+ {
+ progress.Report(new WorkDoneProgressBegin {
+ Title = title,
+ Cancellable = cancellable,
+ Message = message,
+ Percentage = percentage
+ });
+ }
+
+ public static void Report(this IProgress progress, bool? cancellable = null, string? message = null, int? percentage = null)
+ {
+ progress.Report(new WorkDoneProgressReport{
+ Cancellable = cancellable,
+ Message = message,
+ Percentage = percentage
+ });
+ }
+
+ public static void End(this IProgress? progress, string? message = null)
+ {
+ progress.Report(new WorkDoneProgressEnd{
+ Message = message
+ });
+ }
+}
+
+/*
+static class ProgressReportingExtensionMethods
+{
+ public static IProgress CreateProgress(this RequestContext context, TParams p)
+ where TParams : IPartialResultParams
+ where TResult : IPartialResult
+ {
+ var manager = context.GetRequiredLspService();
+ return new ProgressReporter(manager, p.PartialResultToken, context.QueueCancellationToken);
+ }
+
+ class ProgressReporter : IProgress
+ where TResult : IPartialResult
+ {
+ readonly IClientLanguageServerManager manager;
+ readonly ProgressToken partialResultToken;
+ readonly CancellationToken queueCancellationToken;
+
+ public ProgressReporter(IClientLanguageServerManager manager, ProgressToken partialResultToken, CancellationToken queueCancellationToken)
+ {
+ this.manager = manager;
+ this.partialResultToken = partialResultToken;
+ this.queueCancellationToken = queueCancellationToken;
+ }
+
+ public void Report(TResult value)
+ {
+ value.PartialResultToken = partialResultToken;
+ manager.SendNotificationAsync(Methods.ProgressNotificationName, queueCancellationToken);
+ }
+ }
+}
+*/
+/*
+
+[ExportCSharpVisualBasicStatelessLspService(typeof(FindAllReferencesHandler)), Shared]
+[Method(Methods.TextDocumentDocumentHighlightName)]
+sealed class DocumentHighlightHandler : ILspServiceDocumentRequestHandler
+{
+ public bool MutatesSolutionState => false;
+
+ public bool RequiresLSPSolution => true;
+
+ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentHighlightParams request) => request.TextDocument;
+
+ public async Task HandleRequestAsync(DocumentHighlightParams request, RequestContext context, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+[ExportCSharpVisualBasicStatelessLspService(typeof(FindAllReferencesHandler)), Shared]
+[Method(Methods.TextDocumentDocumentLinkName)]
+sealed class DocumentLinkHandler : ILspServiceDocumentRequestHandler
+{
+ public bool MutatesSolutionState => false;
+
+ public bool RequiresLSPSolution => true;
+
+ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentLinkParams request) => request.TextDocument;
+
+ public async Task HandleRequestAsync(DocumentLinkParams request, RequestContext context, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+[ExportCSharpVisualBasicStatelessLspService(typeof(FindAllReferencesHandler)), Shared]
+[Method(Methods.DocumentLinkResolveName)]
+sealed class DocumentLinkResolveHandler : ILspServiceDocumentRequestHandler
+{
+ public bool MutatesSolutionState => false;
+
+ public bool RequiresLSPSolution => true;
+
+ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentLink request) => request.TextDocument;
+
+ public async Task HandleRequestAsync(DocumentLink request, RequestContext context, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+*/
\ No newline at end of file
diff --git a/MSBuildLanguageServer/Import/ExportProviderBuilder.cs b/MSBuildLanguageServer/Import/ExportProviderBuilder.cs
new file mode 100644
index 00000000..ce22a286
--- /dev/null
+++ b/MSBuildLanguageServer/Import/ExportProviderBuilder.cs
@@ -0,0 +1,120 @@
+// modified version of
+// https://raw.githubusercontent.com/dotnet/roslyn/12f89683716864af2582b59f9b94395ad8f39910/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ExportProviderBuilder.cs
+// changes annotated inline with // MODIFICATION
+
+// 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.
+
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis.LanguageServer.Logging;
+using Microsoft.CodeAnalysis.LanguageServer.Services;
+using Microsoft.CodeAnalysis.Shared.Collections;
+using Microsoft.Extensions.Logging;
+using Microsoft.VisualStudio.Composition;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.LanguageServer;
+
+internal sealed class ExportProviderBuilder
+{
+ public static async Task CreateExportProviderAsync(
+ ExtensionAssemblyManager extensionManager,
+ IAssemblyLoader assemblyLoader,
+ string? devKitDependencyPath,
+ ILoggerFactory loggerFactory)
+ {
+ var logger = loggerFactory.CreateLogger();
+ var baseDirectory = AppContext.BaseDirectory;
+
+ // BEGIN MODIFICATION
+ /*
+ // Load any Roslyn assemblies from the extension directory
+ var assemblyPaths = Directory.EnumerateFiles(baseDirectory, "Microsoft.CodeAnalysis*.dll");
+ assemblyPaths = assemblyPaths.Concat(Directory.EnumerateFiles(baseDirectory, "Microsoft.ServiceHub*.dll"));
+
+ // DevKit assemblies are not shipped in the main language server folder
+ // and not included in ExtensionAssemblyPaths (they get loaded into the default ALC).
+ // So manually add them to the MEF catalog here.
+ if (devKitDependencyPath != null)
+ {
+ assemblyPaths = assemblyPaths.Concat(devKitDependencyPath);
+ }
+ */
+ IEnumerable assemblyPaths = [
+ typeof (global::MonoDevelop.MSBuild.Editor.Common.ThisAssembly).Assembly.Location,
+ typeof (global::MSBuildLanguageServer.ThisAssembly).Assembly.Location
+ ];
+ // END MODIFICATION
+
+ // Add the extension assemblies to the MEF catalog.
+ assemblyPaths = assemblyPaths.Concat(extensionManager.ExtensionAssemblyPaths);
+
+ logger.LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", assemblyPaths)}.");
+
+ // Create a MEF resolver that can resolve assemblies in the extension contexts.
+ var resolver = new Resolver(assemblyLoader);
+
+ var discovery = PartDiscovery.Combine(
+ resolver,
+ new AttributedPartDiscovery(resolver, isNonPublicSupported: true), // "NuGet MEF" attributes (Microsoft.Composition)
+ new AttributedPartDiscoveryV1(resolver));
+
+ // TODO - we should likely cache the catalog so we don't have to rebuild it every time.
+ var catalog = ComposableCatalog.Create(resolver)
+ .AddParts(await discovery.CreatePartsAsync(assemblyPaths))
+ .WithCompositionService(); // Makes an ICompositionService export available to MEF parts to import
+
+ // Assemble the parts into a valid graph.
+ var config = CompositionConfiguration.Create(catalog);
+
+ // Verify we only have expected errors.
+ ThrowOnUnexpectedErrors(config, logger);
+
+ // Prepare an ExportProvider factory based on this graph.
+ var exportProviderFactory = config.CreateExportProviderFactory();
+
+ // Create an export provider, which represents a unique container of values.
+ // You can create as many of these as you want, but typically an app needs just one.
+ var exportProvider = exportProviderFactory.CreateExportProvider();
+
+ // Immediately set the logger factory, so that way it'll be available for the rest of the composition
+ exportProvider.GetExportedValue().SetFactory(loggerFactory);
+
+ return exportProvider;
+ }
+
+ private static void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ILogger logger)
+ {
+ // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior.
+ // Currently we are expecting the following:
+ // "----- CompositionError level 1 ------
+ // Microsoft.CodeAnalysis.ExternalAccess.Pythia.PythiaSignatureHelpProvider.ctor(implementation): expected exactly 1 export matching constraints:
+ // Contract name: Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api.IPythiaSignatureHelpProviderImplementation
+ // TypeIdentityName: Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api.IPythiaSignatureHelpProviderImplementation
+ // but found 0.
+ // part definition Microsoft.CodeAnalysis.ExternalAccess.Pythia.PythiaSignatureHelpProvider
+ var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? Enumerable.Empty();
+
+ // BEGIN MODIFICATION
+ /*
+ var expectedErroredParts = new string[] { "PythiaSignatureHelpProvider" };
+ */
+ var expectedErroredParts = new string[] { };
+ // END MODIFICATION
+
+ if (erroredParts.Count() != expectedErroredParts.Length || !erroredParts.All(part => expectedErroredParts.Contains(part)))
+ {
+ try
+ {
+ configuration.ThrowOnErrors();
+ }
+ catch (CompositionFailedException ex)
+ {
+ // The ToString for the composition failed exception doesn't output a nice set of errors by default, so log it separately here.
+ logger.LogError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}");
+ throw;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/MSBuildLanguageServer/Import/Extensions.cs b/MSBuildLanguageServer/Import/Extensions.cs
new file mode 100644
index 00000000..2fc6a024
--- /dev/null
+++ b/MSBuildLanguageServer/Import/Extensions.cs
@@ -0,0 +1,278 @@
+// based on
+// https://raw.githubusercontent.com/dotnet/roslyn/b96c245a17de4fba336832b0d4f593c1960b376c/src/LanguageServer/Protocol/Extensions/Extensions.cs
+// with some parts commented out with /* */ comments
+
+// 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.
+
+
+// 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.
+// 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.
+
+using System;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+/*
+using Microsoft.CodeAnalysis.FindUsages;
+*/
+using Microsoft.CodeAnalysis.Shared.Collections;
+using Microsoft.CodeAnalysis.Shared.Extensions;
+using Microsoft.CodeAnalysis.Text;
+using Roslyn.LanguageServer.Protocol;
+using Roslyn.Text.Adornments;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.LanguageServer
+{
+ internal static class Extensions
+ {
+ /*
+ public static Uri GetURI(this TextDocument document)
+ {
+ Contract.ThrowIfNull(document.FilePath);
+ return document is SourceGeneratedDocument
+ ? ProtocolConversions.CreateUriFromSourceGeneratedFilePath(document.FilePath)
+ : ProtocolConversions.CreateAbsoluteUri(document.FilePath);
+ }
+
+ ///
+ /// Generate the Uri of a document by replace the name in file path using the document's name.
+ /// Used to generate the correct Uri when rename a document, because calling doesn't update the file path.
+ ///
+ public static Uri GetUriForRenamedDocument(this TextDocument document)
+ {
+ Contract.ThrowIfNull(document.FilePath);
+ Contract.ThrowIfNull(document.Name);
+ Contract.ThrowIfTrue(document is SourceGeneratedDocument);
+ var directoryName = Path.GetDirectoryName(document.FilePath);
+
+ Contract.ThrowIfNull(directoryName);
+ var path = Path.Combine(directoryName, document.Name);
+ return ProtocolConversions.CreateAbsoluteUri(path);
+ }
+
+ public static Uri CreateUriForDocumentWithoutFilePath(this TextDocument document)
+ {
+ Contract.ThrowIfNull(document.Name);
+ Contract.ThrowIfNull(document.Project.FilePath);
+
+ var projectDirectoryName = Path.GetDirectoryName(document.Project.FilePath);
+ Contract.ThrowIfNull(projectDirectoryName);
+ var path = Path.Combine([projectDirectoryName, .. document.Folders, document.Name]);
+ return ProtocolConversions.CreateAbsoluteUri(path);
+ }
+
+ public static ImmutableArray GetDocuments(this Solution solution, Uri documentUri)
+ => GetDocuments(solution, ProtocolConversions.GetDocumentFilePathFromUri(documentUri));
+
+ public static ImmutableArray GetDocuments(this Solution solution, string documentPath)
+ {
+ var documentIds = solution.GetDocumentIdsWithFilePath(documentPath);
+
+ // We don't call GetRequiredDocument here as the id could be referring to an additional document.
+ var documents = documentIds.Select(solution.GetDocument).WhereNotNull().ToImmutableArray();
+ return documents;
+ }
+
+ ///
+ /// Get all regular and additional s for the given .
+ ///
+ public static ImmutableArray GetTextDocuments(this Solution solution, Uri documentUri)
+ {
+ var documentIds = GetDocumentIds(solution, documentUri);
+
+ var documents = documentIds
+ .Select(solution.GetDocument)
+ .Concat(documentIds.Select(solution.GetAdditionalDocument))
+ .WhereNotNull()
+ .ToImmutableArray();
+ return documents;
+ }
+
+ public static ImmutableArray GetDocumentIds(this Solution solution, Uri documentUri)
+ => solution.GetDocumentIdsWithFilePath(ProtocolConversions.GetDocumentFilePathFromUri(documentUri));
+
+ public static Document? GetDocument(this Solution solution, TextDocumentIdentifier documentIdentifier)
+ {
+ var documents = solution.GetDocuments(documentIdentifier.Uri);
+ return documents.Length == 0
+ ? null
+ : documents.FindDocumentInProjectContext(documentIdentifier, (sln, id) => sln.GetRequiredDocument(id));
+ }
+
+ private static T FindItemInProjectContext(
+ ImmutableArray