Skip to content

Fall back to a full result when previousResult gets an unknown id - #1858

Open
Ngo Quoc Viet (NgoQuocViet2001) wants to merge 1 commit into
microsoft:mainfrom
NgoQuocViet2001:fix-semantic-tokens-unknown-previous-result
Open

Fall back to a full result when previousResult gets an unknown id#1858
Ngo Quoc Viet (NgoQuocViet2001) wants to merge 1 commit into
microsoft:mainfrom
NgoQuocViet2001:fix-semantic-tokens-unknown-previous-result

Conversation

@NgoQuocViet2001

Copy link
Copy Markdown

Problem

previousResult is the builder's only channel for learning which token set the client currently holds:

public previousResult(id: string) {
    if (this.id === id) {
        this._prevData = this.getFinalDataDelta();
    }
    this.initialize();
}

There is no else. On a mismatch _prevData keeps whatever generation it was left holding — initialize() resets _id, _data, _dataNonDelta, _dataLen and the _prevLine/_prevChar cursors, but deliberately not _prevData.

canBuildEdits() then reports true even though the builder has no baseline for the id it was given, and buildEdits() returns edits diffed against that older generation under a freshly generated resultId.

Effect

Measured against the built SemanticTokensBuilder (public API via server/src/common/api.ts):

gen1 resultId : ... data [0,0,5,1,0]
gen2 canBuildEdits: true -> delta {"resultId":"...","edits":[{"start":3,"deleteCount":1,"data":[2]}]}

after an unknown previousResultId:
  canBuildEdits() : true               (expected false)
  buildEdits()    : {"resultId":"...","edits":[{"start":3,"deleteCount":1,"data":[3]}]}
  is a full result: false

A SemanticTokensDelta has no field naming its baseline — the client takes it to be the previousResultId it sent — so the client applies edits computed against a different array. Token offsets, lengths and types drift out of alignment, and because each later delta chains off the already-wrong state, the highlighting stays wrong until something forces a full request. There is no error and no log line, and a server author who checks canBuildEdits() first is told the wrong thing by the very guard meant to prevent this.

The one-generation-behind case happens to line up by luck, which is presumably why it has gone unnoticed — the damage starts once the client is two or more generations behind, e.g. after a cancelled delta request or a workspace/semanticTokens/refresh.

The documented usage pattern passes the client id straight through, as this repo's own testbed does:

connection.languages.semanticTokens.onDelta((params) => {
    const builder = getTokenBuilder(document);
    builder.previousResult(params.previousResultId);
    buildTokens(builder, document);
    return builder.buildEdits();
});

Fix

Clear _prevData when the id does not match. buildEdits() already returns this.build() when _prevData is undefined, so the fallback path needs no change and canBuildEdits() becomes truthful for free.

Test plan

  • Added An unknown previousResultId falls back to a full result to server/src/node/test/sematicTokens.test.ts.
  • Ran the server suite → 14 passing.
  • Checked: reverting only semanticTokens.ts fails the new test.

No protocol type changes, so protocol/metaModel.json and its schema are untouched.

previousResult() only assigns _prevData when the id matches, and initialize()
deliberately leaves _prevData alone, so a mismatch left the builder holding an
older generation's data. canBuildEdits() then reported true and buildEdits()
returned edits computed against that generation, labelled with a freshly
generated resultId.

A SemanticTokensDelta carries no field naming its baseline -- the client takes
it to be the previousResultId it sent -- so those edits are applied to a
different array and the highlighting drifts, silently and persistently.

Clear _prevData on a mismatch; buildEdits() already returns a full result in
that case.
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant