Skip to content

Keep the semantic colors of open documents on screen - #20450

Open
xperiandri wants to merge 2 commits into
dotnet:mainfrom
xperiandri:fix-20445-semantic-classification
Open

Keep the semantic colors of open documents on screen#20450
xperiandri wants to merge 2 commits into
dotnet:mainfrom
xperiandri:fix-20445-semantic-classification

Conversation

@xperiandri

Copy link
Copy Markdown
Contributor

Description

Semantic colours of an open F# file appear for a moment and then disappear. Roslyn replaces the semantic tags of a span with whatever IFSharpClassificationService.AddSemanticClassificationsAsync returns, so an empty answer that completes successfully strips the colours the user is looking at. Only an OperationCanceledException carrying Roslyn's own token leaves the previous tags alone.

Three things in the F# service produce such empty answers.

The open-document cache was never populated. Splitting the cache in two (#15954) left the open-document branch reading openedDocumentsSemanticClassificationCache and writing unopenedDocumentsSemanticClassificationCache, so every request for an open file re-ran the checker.

What was cached could not answer the next request. The entry is keyed by document and text version, but it held the classification of the requested span only. A second request for another span of the same version — scrolling, a split view, Roslyn asking around the viewport — would have hit that entry and sliced nothing out of it. The classification of the whole file is cached now and sliced per request, the way the unopened-documents branch already does it.

"I cannot answer" was encoded as a cancellation. WorkspaceExtensions raised OperationCanceledException for "this project has no options yet" (a project loading or reloading, a miscellaneous file, any failure in the options agent) and for an aborted check; ifCanceledReturn () then turned those, and every checker-internal cancellation that the CancellableTask adapter relabels with the caller's token, into an empty success. Those entry points now have Try siblings returning voption, and the classification service re-emits the last classification it had for the document instead of nothing. It does so only while the text that classification was computed from is still the current one — its spans name positions, so against edited text they would colour the wrong characters. A cancellation carrying Roslyn's token still propagates untouched.

The raising members stay as thin wrappers over the Try ones, with the same messages, so the call sites that have not moved over behave exactly as before; migrating them is follow-up work. The four-tuple they hand out became a named record, which is also what ProjectCache stores.

Fixes #20445, including the secondary observation about ifCanceledReturn in that issue.

Left for later, deliberately: concurrent requests for the same version each compute their own whole-file lookup (no in-flight coalescing), and the last-known-good entry of a document is kept until its project goes away.

Checklist

  • Test cases added
  • Performance benchmarks added in case of performance changes
  • Release notes entry updated

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change path Release notes path Description
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md

@xperiandri
xperiandri force-pushed the fix-20445-semantic-classification branch from a44eabe to a048f57 Compare September 4, 2026 17:30
@github-actions github-actions Bot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label Sep 4, 2026
@xperiandri xperiandri changed the title Keep the semantic colours of open documents on screen Keep the semantic colors of open documents on screen Sep 4, 2026
Comment thread vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs Outdated
xperiandri and others added 2 commits September 5, 2026 00:10
Every entry point in WorkspaceExtensions signalled "the project has no
options yet" by raising an OperationCanceledException, which the callers
then had to tell apart from a real cancellation. They cannot, so they
treat both as "cancelled" and return an empty result. For a service
Roslyn asks repeatedly while a solution loads, that is a wrong answer,
not a missing one.

Add `Try` siblings that return ValueNone instead, and keep the raising
members as thin wrappers so the call sites that have not moved over
still get the same exception with the same message.

The four-tuple those members hand out becomes a named record, which is
also what ProjectCache stores, so a cache hit hands back the instance it
holds rather than rebuilding a tuple.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Splitting the semantic classification cache in two (dotnet#15954) left the
open-document branch reading the opened-documents cache and writing the
unopened one, so the opened cache was never populated and every request
for an open file re-ran the checker. What it wrote was also only the
requested span, keyed by document and text version, so a second request
for another span of the same version (scrolling, a split view, Roslyn
asking around the viewport) would have hit that entry and sliced nothing
out of it.

Cache the classification of the whole file instead, and slice it per
request, the way the unopened-documents branch already does.

A check that cannot complete - the project is loading or reloading, or
the check was superseded - must not answer with no classifications
either: Roslyn replaces the tags of a span with whatever comes back, so
an empty answer strips the colours the user is looking at, while a
cancellation carrying Roslyn's own token leaves them alone. Keep the
last classification per document and re-emit it for those requests, but
only while the text it was computed from is still the current one, as
its spans would otherwise land on the wrong characters.

`ifCanceledThen` replaces `ifCanceledReturn ()` for the same reason: the
checker relabels its internal cancellations with the caller's token, and
those must produce the last known colours rather than none.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the fix-20445-semantic-classification branch from a048f57 to 6e5d1f9 Compare September 4, 2026 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

Semantic classification cache for opened documents is never populated (written to the unopened-documents cache)

1 participant