dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
18.99k stars 4.03k forks source link

API needed for accessing project context name #63597

Open mgoertz-msft opened 2 years ago

mgoertz-msft commented 2 years ago

Background and Motivation

Multi-targeting projects, such as MAUI projects, include multiple TFMs causing the creation of one Roslyn Project per TFM. The NavigationBar feature provides support for switching the document context between these Projects, which ends up setting the IVsHierarchy property __VSHPROPID8.VSHPROPID_ActiveIntellisenseProjectContext in VisualStudioWorkspaceImpl.

Unfortunately, this there is no API to get the context string from Project or ProjectId, so there is no way for other IDE features, such as the XAML language service to look at a hierarchy's current project context to determine the matching Roslyn Project or to implement our own project context switcher. VisualStudioWorkspaceImpl keeps its own internal map _projectSystemNameToProjectsMap for this purpose.

Thankfully, at least the correct unique project context name is stored as the ProjectId.DebugName and can be extracted via ProjectId.ToString(), which we are going to take advantage of for now, but this is less than ideal and there should really be a dedicated API for this on either Project or ProjectId.

Proposed API

public class Project/ProjectId
{
+   public string ContextName { get; }
    ...
}

Usage Examples

hierarchy.SetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID8.VSHPROPID_ActiveIntellisenseProjectContext, project.ContextName);
if (ErrorHandler.Succeeded(hierarchy.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID8.VSHPROPID_ActiveIntellisenseProjectContext, out object contextObject)) &&
    contextObject is string contextName)
{
    var activeProject = workspace.CurrentSolution.Projects.FirstOrDefault(p => p.ContextName == contextName)
}

Alternative Designs

If changing the signature of Project/ProjectId is not an option, then perhaps a public extension method could be provided:

public static class ProjectExtensions
{
    public static string ContextName(this Project project) => ...
}

Risks

New API, so the risk should be very low.

mgoertz-msft commented 2 years ago

FYI @tmat

CyrusNajmabadi commented 2 years ago

Is this equivalent to to the NameAndFlavor internal property we already have? If not equivalent, could you clarify how they are different?

tmat commented 2 years ago

I think it is the same as NameAndFlavor, though this property is rather hacky. We should do better.

CyrusNajmabadi commented 2 years ago

I'm fine with doing better. I just wanted to know if 'ContextName' was something different. Note: i think we do need more than one property here. Because there is just the 'Name' (which includes both pieces), and then the 'Context/FlavorName'. But then there's still a need to get the name without the flavor/context name.

jasonmalinowski commented 2 years ago

I'm fine with doing better. I just wanted to know if 'ContextName' was something different.

It's different. There are two names, meaningfully, for Visual Studio projects:

  1. The DisplayName that's set here. This is what goes into that NameAndFlavor regex. That's the name that you know and love like "MyProject (netstandard2.0)" or something.
  2. The "project system name", which is a name given to us for various legacy VS APIs. This is not a user-parseable string, and contains GUIDs and other ugly bits. The name is unique.

Rather than exposing the string directly, I'd hope we'd expose APIs more like what is actually needed though. @mgoertz-msft there are already some public APIs for getting the active context and changing context for Roslyn where we'll do all the project system stuff as an implementation detail for you; were those not sufficient here?

CyrusNajmabadi commented 2 years ago

The "project system name", which is a name given to us for various legacy VS APIs.

I would really love to not expose that.

mgoertz-msft commented 2 years ago

@jasonmalinowski OK, so perhaps I haven't found the right APIs then. I need to be able to get and set the active project context's project and listen to an event for when that changes. How would I do that?

jasonmalinowski commented 2 years ago

@mgoertz-msft So one thing that might be a problem here is we generally speak in terms of a document having an active context, not a project. This was because our model is largely built to support the navigation bar and editor-centric features like classification. To make that a bit more concrete, we don't have a way to ask "what is the active context of this CPS project" as a direct question, instead because [a] we don't actually have a concept of "the CPS project" but also [b] for all you know the file that's added to the CPS project is being linked into another project, and you need the running document table to decide which is really active.

What you can do is go from a text buffer to the DocumentId via GetOpenDocumentInCurrentContext which returns the right context already considering everything. For changes we have this event. It seems setting the method is internal though (Workspace.SetDocumentContext); I admit I'm not sure why that's the case.

jasonmalinowski commented 2 years ago

@mgoertz-msft Perhaps what might also help here is to have a bit of the (ahem) context? Do you just need a dropdown for XAML editors similar to what we have, or something else?

mgoertz-msft commented 2 years ago

@jasonmalinowski Correct:

... for other IDE features, such as the XAML language service to look at a hierarchy's current project context to determine the matching Roslyn Project or to implement our own project context switcher

mgoertz-msft commented 2 years ago

So one thing that might be a problem here is we generally speak in terms of a document having an active context, not a project.

@jasonmalinowski Yes, in this case that is a problem because for multi-targeted projects the VisualStudioWorkspaceImpl sets the __VSHPROPID8.VSHPROPID_ActiveIntellisenseProjectContext on the hierarchy, which does represent the entire project and not just a document. And that's really the context we need to be able to associate with a Roslyn project, so we can get the correct compilation from the currently active project context for our type system - at least as long as our language service still runs in proc.

How does another C# document (docB) know if the first document's (docA) context has been changed because of a multi-targeting project context switch or because of docA being a linked file? I suppose in that case docA nav bar would show all TFMs in addition to the projects it's linked into and as long as docB is part of the newly active project then its nav bar would switch to that as well. Is that how it works?

Besides that, while GetDocumentIdInCurrentContext works for XAML in MAUI projects, GetRelatedDocumentIds always returns an empty result for the same XAML text container. So to get all the Roslyn projects I currently need to enumerate all projects with the same project file path.

Long story short, it seems that if we were to make Workspace.SetDocumentContext public and support additional files in GetRelatedDocumentIds then we should be able to support nav bars for additional documents in multi-targeting projects.