Skip to content

Lean lsp highlighting#330

Open
Tammo0987 wants to merge 13 commits intomappingfrom
lean-lsp-highlighting
Open

Lean lsp highlighting#330
Tammo0987 wants to merge 13 commits intomappingfrom
lean-lsp-highlighting

Conversation

@Tammo0987
Copy link
Copy Markdown

@Tammo0987 Tammo0987 commented Mar 24, 2026

Description

Implements the basics of semantic highlighting and removes static syntax highlighting for lean.

Testing this PR

Use the waterproof-genre repository to test it. But currently a specific version is required.

Related to: impermeable/waterproof-editor#83

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds basic Lean semantic highlighting by forwarding LSP semantic tokens to the Waterproof editor webview, and removes the previous static (syntax) highlighting configuration for Lean.

Changes:

  • Request and forward LSP semantic tokens from the Lean LSP client to the webview (debounced on file progress).
  • Add a new semanticTokens webview message and wire the editor to apply tokens + set theme-dependent semantic token colors via CSS variables.
  • Remove Lean’s static highlight config and clean up unused CSS mappings.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/lsp-client/lean/client.ts Requests semantic tokens from the VS Code semantic token provider and forwards them to the webview.
shared/Messages.ts Adds a new MessageType.semanticTokens message shape for token forwarding.
editor/src/index.ts Applies semantic token CSS vars on theme changes and forwards semantic tokens into the editor instance.
editor/src/semanticColors.ts Introduces theme-dependent CSS variable values for semantic token coloring.
editor/src/vscodemapping.css Removes terminal ANSI color mappings (no longer used).
.zed/settings.json Adds Zed editor settings file to the repo.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +248 to +293
private requestSemanticTokensDebounced(document: TextDocument) {
if (this.semanticTokenTimer) {
clearTimeout(this.semanticTokenTimer);
}
this.semanticTokenTimer = setTimeout(() => {
this.requestAndForwardSemanticTokens(document).catch(err => {
wpl.debug(`Semantic token request failed: ${err}`)
});
}, 300);
}

private async requestAndForwardSemanticTokens(document: TextDocument): Promise<void> {
if (!this.client.isRunning() || !this.webviewManager) return;

const feature = this.client.getFeature(SemanticTokensRegistrationType.method);
if (!feature) return;

const provider = feature.getProvider(document);
if (!provider?.full) return;

const cts = new CancellationTokenSource();
const tokens = await Promise.resolve(
provider.full.provideDocumentSemanticTokens(document, cts.token)
).catch(() => undefined).finally(() => cts.dispose());

if (!tokens?.data?.length) return;

const tokenLegend = this.client.initializeResult?.capabilities.semanticTokensProvider?.legend;
if (!tokenLegend) return;

const offsetTokens: Array<OffsetSemanticToken> = LeanLspClient.decodeLspTokens(tokens.data).flatMap(t => {
const tokenType = LeanLspClient.mapLspTokenType(tokenLegend.tokenTypes[t.tokenTypeIndex]);
if (tokenType === undefined) return [];

const startOffset = document.offsetAt(new Position(t.line, t.char));
return [{
startOffset,
endOffset: startOffset + t.length,
type: tokenType,
}];
});

this.webviewManager.postMessage(document.uri.toString(), {
type: MessageType.semanticTokens,
body: {tokens: offsetTokens},
});
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

Semantic token requests can overlap (debounce timer + async provider call), and results may arrive out-of-order; the last-resolving request will overwrite highlights even if it corresponds to an older document version. Consider tracking a monotonically increasing request generation / document.version and discarding stale results (and/or reusing a single CancellationTokenSource that you cancel when scheduling a new request).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Should be very rare in practice and will recover on the next request, I think its not worth adding that.

// The payload is forwarded to an InfoView instance, so its type does not concern us
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| MessageBase<MessageType.infoviewRpc, { payload: any }>
| MessageBase<MessageType.semanticTokens, { tokens: Array<OffsetSemanticToken> }>
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The semanticTokens message payload does not include a document version, unlike diagnostics/init which are versioned. Because semantic token computation is async, the webview can receive stale tokens after newer edits. Consider adding a version field (e.g., document.version) and having the editor ignore tokens that don't match the current document version.

Suggested change
| MessageBase<MessageType.semanticTokens, { tokens: Array<OffsetSemanticToken> }>
| MessageBase<MessageType.semanticTokens, { tokens: Array<OffsetSemanticToken>, version: number }>

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Should be very rare in practice and will recover on the next request, I think its not worth adding that.

Comment on lines +296 to +351
private static decodeLspTokens(data: Uint32Array): Array<{ line: number; char: number; length: number; tokenTypeIndex: number }> {
if (data.length % 5 !== 0) {
wpl.debug(`[SemanticTokens] Malformed token data: length ${data.length} is not a multiple of 5`);
}
const tokens = [];
let line = 0;
let char = 0;
for (let i = 0; i + 4 < data.length; i += 5) {
const deltaLine = data[i];
const deltaStartChar = data[i + 1];
if (deltaLine > 0) {
line += deltaLine;
char = deltaStartChar;
} else {
char += deltaStartChar;
}
tokens.push({
line,
char,
length: data[i + 2],
tokenTypeIndex: data[i + 3],
// data[i + 4] is tokenModifiers (unused)
});
}
return tokens;
}

private static mapLspTokenType(lspType: string): SemanticTokenType | undefined {
switch (lspType) {
case "keyword": return SemanticTokenType.Keyword;
case "variable": return SemanticTokenType.Variable;
case "property": return SemanticTokenType.Property;
case "function": return SemanticTokenType.Function;
case "namespace": return SemanticTokenType.Namespace;
case "type": return SemanticTokenType.Type;
case "class": return SemanticTokenType.Class;
case "enum": return SemanticTokenType.Enum;
case "interface": return SemanticTokenType.Interface;
case "struct": return SemanticTokenType.Struct;
case "typeParameter": return SemanticTokenType.TypeParameter;
case "parameter": return SemanticTokenType.Parameter;
case "enumMember": return SemanticTokenType.EnumMember;
case "event": return SemanticTokenType.Event;
case "method": return SemanticTokenType.Method;
case "macro": return SemanticTokenType.Macro;
case "modifier": return SemanticTokenType.Modifier;
case "comment": return SemanticTokenType.Comment;
case "string": return SemanticTokenType.String;
case "number": return SemanticTokenType.Number;
case "regexp": return SemanticTokenType.Regexp;
case "operator": return SemanticTokenType.Operator;
case "decorator": return SemanticTokenType.Decorator;
case "leanSorryLike": return SemanticTokenType.LeanSorryLike;
default: return undefined;
}
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

New semantic-token decoding/mapping logic is non-trivial and currently untested. There are already Jest tests for LeanLspClient in tests/lsp-client/lean; consider adding focused unit tests for decodeLspTokens (delta encoding) and mapLspTokenType to prevent regressions.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think its not worth it to test it, because the decoding is based on the specification of the lsp and the mapping function is trivial.

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.

2 participants