dotnet / dotnet/fsharp

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

Open Beginner friendly
#20,445 0 comments 0 reactions 0 assignees View on GitHub
Needs-Triage
Dominant language
F#
Stars
4.3k
Forks
876
Avg merge
4d 22h
Merged PRs (30d)
144

Description

## Summary

In `FSharpClassificationService.AddSemanticClassificationsAsync`, the open-document branch **reads** from `openedDocumentsSemanticClassificationCache` but **writes** to `unopenedDocumentsSemanticClassificationCache`. As a result `openedDocumentsSemanticClassificationCache` is written nowhere, its lookup always misses, and every semantic classification request for an open document goes through a full `GetFSharpParseAndCheckResultsAsync` + `GetSemanticClassification`.

`vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs`:

| Line | Branch | Operation | Cache |
|---|---|---|---|
| 146 | — | declaration | `unopenedDocumentsSemanticClassificationCache` (TTL 5.0) |
| 149 | — | declaration | `openedDocumentsSemanticClassificationCache` (TTL 2.0) |
| 227 | `not isOpenDocument` | read | `unopened…` |
| 258 | `not isOpenDocument` | write | `unopened…` ✔ |
| 262 | open document | read | `opened…` |
| 301 | open document | write | `unopened…` ✘ |

## Suggested fix

```diff
if classificationData.Length > 0 then
let classificationDataLookup = itemToSemanticClassificationLookup classificationData
- do! unopenedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup)
+ do! openedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup)
```

## Origin

Introduced in #15954 ("More VS cleanup", merged 2023-10-10), which split a single semantic classification cache into two. In that diff all four lines are additions: the read in the open-document branch was pointed at the new `opened…` cache, while the write was kept as a copy of the sibling branch and still names `unopened…`.

`git blame` currently attributes line 301 to #18653 ("Move LSP development to the main branch"), but that is a red herring — that commit only wrapped the method in the `shouldProduceClassification` gate and re-indented the block. The version of the file at its parent commit already has the same wrong cache name.

## Impact

Functionally the classification is still correct — it is a pure cache miss, so the effect is repeated full re-checking rather than wrong colors. It becomes visible when the recomputation stops fitting between requests: each new classification request cancels the previous one, and the semantic pass ends with

```fsharp
|> CancellableTask.ifCanceledReturn ()
```

which completes *successfully with an empty `result`*, so the editor drops the classification it had. Observed as semantic colors appearing for a moment and then disappearing.

## How it was observed

Debugging `FSharp.Editor` in an experimental hive against a large F# solution (VS 18.10, ~40 F# projects):

- breakpoint at line 299 (`if classificationData.Length > 0 then`) hits with `classificationData.Length = 57` — the checker does produce data, and colors do appear;
- the Debug output window for the session contained ~1600 `OperationCanceledException` / `TaskCanceledException`, a large share of them from `FSharp.Editor.dll`, and no other exceptions besides FCS-internal control-flow ones (`UndefinedName`, `CannotRefute`, `IndeterminateType`);
- with the cache never hit, every request re-enters the full check path.

## Secondary observation (not part of the fix above)

`ifCanceledReturn ()` at line 305 makes a cancelled semantic pass indistinguishable from "there are no classifications here". Preserving previously reported classifications on cancellation would be a separate, more invasive change, and is filed here only for reference.

Contributor guide

Open the contributing guide

Research direction

Start in vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs at AddSemanticClassificationsAsync, comparing the open-document cache read around line 262 with the write around line 301. Done means the open-document branch populates openedDocumentsSemanticClassificationCache so later requests can use the cached classifications instead of repeating the full check.

Written by the indexing model from the issue text.

Assessment

Tech stack
fsharp
Domain
tooling
Issue type
Bug
Difficulty
1/5
Estimated time
Under an hour
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
90/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.