Skip to content

Commit

Permalink
Improve tooltips on hover.
Browse files Browse the repository at this point in the history
  • Loading branch information
tintoy committed Aug 20, 2017
1 parent 75e97b8 commit f2947e7
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 220 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## v0.1.1

* Use a patched version of `Microsoft.Language.Xml` that behaves correctly in non-windows environments.
* Use a patched version of `Microsoft.Language.Xml` that behaves correctly in non-windows environments (issues with CR vs CRLF line-endings).
* Improve tooltips on hover.

## v0.1.0

Expand Down
256 changes: 256 additions & 0 deletions src/LanguageServer.Engine/Handlers/HoverContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
using Lsp.Models;
using Microsoft.Language.Xml;
using System;
using System.IO;
using System.Text;
using System.Linq;

namespace MSBuildProjectTools.LanguageServer.Handlers
{
using MSBuild;
using Documents;
using Utilities;

/// <summary>
/// Content for tooltips when hovering over nodes in the MSBuild XML.
/// </summary>
public static class HoverContent
{
/// <summary>
/// Get hover content for an <see cref="MSBuildProperty"/>.
/// </summary>
/// <param name="property">
/// The <see cref="MSBuildProperty"/>.
/// </param>
/// <returns>
/// The content.
/// </returns>
public static MarkedStringContainer Property(MSBuildProperty property)
{
if (property == null)
throw new ArgumentNullException(nameof(property));

if (property.IsOverridden)
{
Position overridingDeclarationPosition = property.DeclaringXml.Location.ToNative();

StringBuilder overrideDescription = new StringBuilder();
string declarationFile = property.DeclaringXml.Location.File;
if (declarationFile != property.Property.Xml.Location.File)
{
Uri declarationDocumentUri = UriHelper.CreateDocumentUri(declarationFile);
overrideDescription.AppendLine(
$"Value overridden at {overridingDeclarationPosition} in [{Path.GetFileName(declarationFile)}]({declarationDocumentUri})."
);
}
else
overrideDescription.AppendLine($"Value overridden at {overridingDeclarationPosition} in this file.");

overrideDescription.AppendLine();
overrideDescription.AppendLine();
overrideDescription.AppendLine(
$"Unused value: `{property.DeclaringXml.Value}`"
);
overrideDescription.AppendLine();
overrideDescription.AppendLine(
$"Actual value: `{property.Value}`"
);

return new MarkedStringContainer(
$"Property: `{property.Name}`",
overrideDescription.ToString()
);
}

return new MarkedStringContainer(
$"Property: `{property.Name}`",
$"Value: `{property.Value}`"
);
}

/// <summary>
/// Get hover content for an <see cref="MSBuildUnusedProperty"/>.
/// </summary>
/// <param name="undefinedProperty">
/// The <see cref="MSBuildUnusedProperty"/>.
/// </param>
/// <returns>
/// The content.
/// </returns>
public static MarkedStringContainer UnusedProperty(MSBuildUnusedProperty undefinedProperty, ProjectDocument projectDocument)
{
if (undefinedProperty == null)
throw new ArgumentNullException(nameof(undefinedProperty));

string condition = undefinedProperty.PropertyElement.Condition;
if (String.IsNullOrWhiteSpace(condition))
condition = undefinedProperty.PropertyElement.Parent.Condition; // Condition may be on parent element.

string expandedCondition = projectDocument.MSBuildProject.ExpandString(condition);

return new MarkedStringContainer(
$"Property: `{undefinedProperty.Name}` (condition evaluates to false)",
$"Unused value:\n* `{undefinedProperty.Value}`\n\nCondition:\n* Raw =`{condition}`\n* Evaluated = `{expandedCondition}`"
);
}

/// <summary>
/// Get hover content for an <see cref="MSBuildItemGroup"/>.
/// </summary>
/// <param name="itemGroup">
/// The <see cref="MSBuildItemGroup"/>.
/// </param>
/// <returns>
/// The content.
/// </returns>
public static MarkedStringContainer ItemGroup(MSBuildItemGroup itemGroup)
{
if (itemGroup == null)
throw new ArgumentNullException(nameof(itemGroup));

if (itemGroup.Name == "PackageReference")
{
string packageVersion = itemGroup.GetFirstMetadataValue("Version");

return new MarkedStringContainer(
$"NuGet Package: `{itemGroup.FirstInclude}`",
$"Version: {packageVersion}"
);
}

string[] includes = itemGroup.Includes.ToArray();
StringBuilder itemIncludeContent = new StringBuilder();
itemIncludeContent.AppendLine(
$"Include: `{itemGroup.OriginatingElement.Include}` "
);
itemIncludeContent.AppendLine();
itemIncludeContent.Append(
$"Evaluates to {itemGroup.Items.Count} item"
);
if (!itemGroup.HasSingleItem)
itemIncludeContent.Append("s");
itemIncludeContent.AppendLine(".");

foreach (string include in includes.Take(5))
{
// TODO: Consider making hyperlinks for includes that map to files which exist.
itemIncludeContent.AppendLine(
$"* `{include}`"
);
}
if (includes.Length > 5)
itemIncludeContent.AppendLine("* ...");

return new MarkedStringContainer(
$"Items Group: `{itemGroup.OriginatingElement.ItemType}`",
itemIncludeContent.ToString()
);
}

/// <summary>
/// Get hover content for an attribute of an <see cref="MSBuildItemGroup"/>.
/// </summary>
/// <param name="itemGroup">
/// The <see cref="MSBuildItemGroup"/>.
/// </param>
/// <param name="attribute">
/// The attribute.
/// </param>
/// <returns>
/// The content.
/// </returns>
public static MarkedStringContainer ItemGroup(MSBuildItemGroup itemGroup, XmlAttributeSyntax attribute)
{
if (itemGroup == null)
throw new ArgumentNullException(nameof(itemGroup));

if (itemGroup.Name == "PackageReference")
return ItemGroup(itemGroup);

// TODO: Handle the "Condition" attribute.
if (attribute.Name == "Condition")
return null;

string metadataName = attribute.Name;
if (String.Equals(metadataName, "Include"))
metadataName = "Identity";

StringBuilder metadataValues = new StringBuilder();
metadataValues.AppendLine("Values:");

foreach (string metadataValue in itemGroup.GetMetadataValues(metadataName).Distinct())
{
metadataValues.AppendLine(
$"* `{metadataValue}`"
);
}

return new MarkedStringContainer(
$"Item Metadata: `{itemGroup.Name}.{metadataName}`",
metadataValues.ToString()
);
}

/// <summary>
/// Get hover content for an <see cref="MSBuildTarget"/>.
/// </summary>
/// <param name="target">
/// The <see cref="MSBuildTarget"/>.
/// </param>
/// <returns>
/// The content.
/// </returns>
public static MarkedStringContainer Target(MSBuildTarget target)
{
if (target == null)
throw new ArgumentNullException(nameof(target));

return $"Target: `{target.Name}`";
}

/// <summary>
/// Get hover content for an <see cref="MSBuildImport"/>.
/// </summary>
/// <param name="import">
/// The <see cref="MSBuildImport"/>.
/// </param>
/// <returns>
/// The content.
/// </returns>
public static MarkedStringContainer Import(MSBuildImport import)
{
if (import == null)
throw new ArgumentNullException(nameof(import));

string importedProject = import.ProjectImportElement.Project;
Uri projectFileUri = UriHelper.CreateDocumentUri(import.ImportedProjectRoot.Location.File);

return $"Import: [{importedProject}]({projectFileUri})";
}

/// <summary>
/// Get hover content for an <see cref="MSBuildSdkImport"/>.
/// </summary>
/// <param name="sdkImport">
/// The <see cref="MSBuildSdkImport"/>.
/// </param>
/// <returns>
/// The content.
/// </returns>
public static MarkedStringContainer SdkImport(MSBuildSdkImport sdkImport)
{
if (sdkImport == null)
throw new ArgumentNullException(nameof(sdkImport));

StringBuilder imports = new StringBuilder("Imports:");
imports.AppendLine();
foreach (string projectFile in sdkImport.ImportedProjectFiles)
imports.AppendLine($"* [{Path.GetFileName(projectFile)}]({UriHelper.CreateDocumentUri(projectFile)})");

return new MarkedStringContainer(
$"SDK Import: {sdkImport.Name}",
imports.ToString()
);
}
}
}
Loading

0 comments on commit f2947e7

Please sign in to comment.