Skip to content

File-based program directive diagnostics in editor #79421

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 29, 2025

Conversation

RikkiGibson
Copy link
Member

@RikkiGibson RikkiGibson commented Jul 16, 2025

#78688

  • Creates a new IDiagnosticSourceProvider, which produces IDiagnosticSource with IsLiveSource() => false.
  • When VirtualProjectXmlProvider gets a virtual project, it caches the diagnostics from the run-api result in a dictionary
  • The IDiagnosticSource has a reference to the VirtualProjectXmlProvider and accesses the latest diagnostics for the project from it
  • Right now this works well enough to display directive diagnostics when a file is first opened.
  • TODO: When GetVirtualProjectContentAsync gets a new project back from run-api, it needs to signal that the new diagnostics are available, and the associated IDiagnosticSource should be queried again. I'm not sure how to do this. Edit: resolved by using IDiagnosticRefresher.
  • Looking for feedback on the location to keep the cached diagnostics, etc. Possibly the diagnostics should be "unloaded" if the associated project is unloaded. Possibly even these should be cached in the FileBasedProgramsProjectSystem and looked up somehow.
    • Since IDiagnosticSourceProvider needs to be registered through MEF the current factoring felt fairly straightforward. However we could imagine wanting to also surface the design time build diagnostics through this provider somehow. In which case we would need the project system to cache diagnostics instead, in some location where the IDiagnosticSource could access them.
    • Maybe the IDiagnosticSourceProvider could be a parameter to the FileBasedProgramsProjectSystem and when the project system has diagnostics it wants cached/potentially surfaced it could just call into the diagnostic source provider.

using (await _gate.DisposableWaitAsync(cancellationToken))
{
_diagnosticsByFilePath[documentFilePath] = project.Diagnostics;
}
return (project.Content, project.Diagnostics);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the staleness issue semes real here. it looks like we could return incorrect diagnostics for a solution snapshot. and it's unclear what would snap the ide out of that state.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is updated each time a design time build occurs, which for the moment occurs whenever the file-based program is saved to disk.

The cached diagnostics would probably need to be updated in the other code paths also. e.g. if run-api completely fails, the stale diagnostics we already had in the cache are likely not useful and should be cleared out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like we could return incorrect diagnostics for a solution snapshot. and it's unclear what would snap the ide out of that state.

I don't think I fully grasp this concern or what should be done to address it.

@CyrusNajmabadi
Copy link
Member

So, when the "Save" happens, what ensures that the entire pipeline even knows to run again to then get these cached diagnostics?

For context, saving a file doesn't not change our content checksums, and we often (but not always) drive these experiences off of content checksums. So we'd need something else to mix into the system to even know to get diagnostics agian. And importantly, that would either have to run after the diagnostics were definitely computed and cached, or it would have to wait on those being computed when it did its work in order for the user to get the correct diagnostics results.

All this is possible. We just have to be careful and clear (and the code should be evident as to how it is ccomplishing this). Thanks! :)

@RikkiGibson
Copy link
Member Author

So, when the "Save" happens, what ensures that the entire pipeline even knows to run again to then get these cached diagnostics?

A file watcher will kick off a design time build when the file-based program is saved.

For the pipeline to run again, the TODO from PR description needs to be addressed.

TODO: When GetVirtualProjectContentAsync gets a new project back from run-api, it needs to signal that the new diagnostics are available, and the associated IDiagnosticSource should be queried again. I'm not sure how to do this.

@CyrusNajmabadi
Copy link
Member

TODO: When GetVirtualProjectContentAsync gets a new project back from run-api, it needs to signal that the new diagnostics are available, and the associated IDiagnosticSource should be queried again. I'm not sure how to do this.

Answered offline.

@RikkiGibson
Copy link
Member Author

Added use of IDiagnosticRefresher. Seems decently responsive for the moment. We should still track down making this work with in-memory-only changes in the near-to-medium term so that long design time builds, autosave being turned off, etc. will not mess up the user experience.

fbp-directive-diagnostics.mp4

Also, if we break things so badly that run-api doesn't even give back a virtual project, we should probably clear out any stale diagnostics from the last run.


// check for difference, and signal to host to update if so.
if (previousCachedDiagnostics.IsDefault || !project.Diagnostics.SequenceEqual(previousCachedDiagnostics))
diagnosticRefresher.RequestWorkspaceRefresh();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks correct. generally the guidance I have on using the refresher is to use it as sparingly as possible because it refreshes all diagnostics. It looks like you're already checking to make sure there are changes which is good.

But the less that goes through this the better. Ideally we can get most diagnostics from the actual Roslyn snapshots, and then the normal on-type updates handle most changes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we can get most diagnostics from the actual Roslyn snapshots, and then the normal on-type updates handle most changes.

What would it mean to do this? Does that require having a "live source" for diagnostics? e.g. getting the diagnostics through analyzer, or running run-api on the "snapshot" of the file-based program?

_diagnosticsByFilePath[documentFilePath] = project.Diagnostics;

// check for difference, and signal to host to update if so.
if (previousCachedDiagnostics.IsDefault || !project.Diagnostics.SequenceEqual(previousCachedDiagnostics))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we remove diagnostics? It looks really bad when we have stale diagnostics, so we definitely need to ensure we're clearing them when the file based project is deleted / renamed / otherwise changes contexts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also probably need to remove them from the _diagnosticsByFilePath map if the file/project is deleted, otherwise we have a small leak

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wrapping this method to try and ensure that the cache is always dealt with appropriately, whatever the response from run-api.

@RikkiGibson
Copy link
Member Author

Handled unloading. Quite possibly the caching should be in FileBasedProgramsProjectSystem rather than VirtualProjectXmlProvider. I don't have a strong opinion on it.

@RikkiGibson RikkiGibson marked this pull request as ready for review July 25, 2025 01:52
@RikkiGibson RikkiGibson requested a review from a team as a code owner July 25, 2025 01:52
@RikkiGibson RikkiGibson requested a review from dibarbet July 28, 2025 17:10
@RikkiGibson
Copy link
Member Author

@dibarbet @jasonmalinowski for review

@CyrusNajmabadi
Copy link
Member

@dibarbet @jasonmalinowski for review

Can you do a final pass through feedback plz :)

@@ -153,4 +153,9 @@ public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri)
Preferred: buildHostKind,
Actual: buildHostKind);
}

protected override async ValueTask OnProjectUnloadedAsync(string projectFilePath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to figure out the semantics of OnProjectUnloadedAsync

  1. Does this get called when the file is closed in the editor
  2. Does this get called when the file moves from a loose file, into an actual project?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Yes.
  2. No. I'm adding a comment to explain in more details.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way we can tell when that transition happens? So that we can clear out diagnostics if its not a file based program any longer?

Maybe it would be possible to verify that the workspace kind is still the language server workspace? IIRC there would be a workspace changed event triggering a diagnostics refresh, so there should be a diagnostics requests to the server in that scenario already.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way we can tell when that transition happens? So that we can clear out diagnostics if its not a file based program any longer?

Hypothetically, upon reload, we could see if some of the targets we unloaded were associated with a different ProjectFactory than before, and react to that somehow.

Note also the assert that tracked targets for a given project, are all associated with only one projectFactory at a given time:

Debug.Assert(newProjectTargets.All(target => target.ProjectFactory == projectFactory));

In practice I do not think we are going to need a special behavior here. Each design time build, either of FBP or "rich misc file", is going to result in run-api reporting some set of diagnostics, and we will always surface the latest set for the project. And, we expect run-api is not going to report anything for the "non-FBP" case. It would be hard to observe it reporting in the non-FBP case, since it only reports on bad "app directives" currently, and presence of those will mark the file as an FBP.

Long and short is I do not expect an unwanted/buggy behavior here from failing to take an explicit step to clear the diagnostics out when transitioning between FBP and non-FBP. (I'll verify this manually but I keep having to change between different branches and would like to fix in a follow up in the case we really do have a problem here.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wfm

@RikkiGibson RikkiGibson merged commit afaf319 into dotnet:main Jul 29, 2025
25 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Jul 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants