Skip to content

Commit 22edf92

Browse files
authored
Merge pull request #84 from Gustaf-C/semantic_highlights
Add basic semantic highlighting
2 parents 8389289 + 672e894 commit 22edf92

4 files changed

Lines changed: 458 additions & 3 deletions

File tree

src/indexing/DocumentIndexer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const INDEXING_DELAY = 500 // Delay (in ms) after keystroke before attempting to
1212
*/
1313
export default class DocumentIndexer {
1414
private readonly pendingFilesToIndex = new Map<string, NodeJS.Timeout>()
15+
private onIndexed?: (uri: string) => void
1516

1617
constructor (
1718
private readonly indexer: Indexer,
@@ -42,6 +43,7 @@ export default class DocumentIndexer {
4243
*/
4344
indexDocument (textDocument: TextDocument): void {
4445
void this.indexer.indexDocument(textDocument)
46+
this.onIndexed?.(textDocument.uri)
4547
}
4648

4749
/**
@@ -73,5 +75,11 @@ export default class DocumentIndexer {
7375
if (!this.fileInfoIndex.codeInfoCache.has(uri)) {
7476
await this.indexer.indexDocument(textDocument)
7577
}
78+
79+
this.onIndexed?.(uri)
80+
}
81+
82+
setOnIndexed (callback: (uri: string) => void): void {
83+
this.onIndexed = callback
7684
}
7785
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { SemanticTokens, SemanticTokensParams, TextDocuments, Range, Connection } from 'vscode-languageserver'
2+
import MatlabLifecycleManager from '../../lifecycle/MatlabLifecycleManager'
3+
import { TextDocument } from 'vscode-languageserver-textdocument'
4+
import FileInfoIndex, { MatlabFunctionScopeInfo, MatlabGlobalScopeInfo } from '../../indexing/FileInfoIndex'
5+
import DocumentIndexer from '../../indexing/DocumentIndexer'
6+
7+
interface SemanticToken {
8+
range: Range
9+
typeIndex: number
10+
}
11+
12+
/**
13+
* Handles requests for semantic tokens for a document.
14+
*/
15+
class SemanticTokensProvider {
16+
constructor (
17+
protected readonly matlabLifecycleManager: MatlabLifecycleManager,
18+
protected readonly documentIndexer: DocumentIndexer,
19+
protected readonly fileInfoIndex: FileInfoIndex
20+
) { }
21+
22+
async handleSemanticTokensRequest (
23+
params: SemanticTokensParams,
24+
documentManager: TextDocuments<TextDocument>
25+
): Promise<SemanticTokens | null> {
26+
// This provider will be called constantly, should not connect to MATLAB just because it was called
27+
const matlabConnection = await this.matlabLifecycleManager.getMatlabConnection(false)
28+
// If MATLAB is not connected, fall back to default highlighting
29+
if (matlabConnection == null) return null
30+
31+
const textDocument = documentManager.get(params.textDocument.uri)
32+
if (textDocument == null) return null
33+
34+
const codeInfo = this.fileInfoIndex.codeInfoCache.get(params.textDocument.uri)
35+
if (codeInfo == null) return null
36+
37+
const tokens: SemanticToken[] = []
38+
this.collectSemanticTokens(codeInfo.globalScopeInfo, tokens)
39+
40+
// Sort tokens by their position in the document (line and character)
41+
// This is necessary to encode them using relative positions
42+
tokens.sort((a, b) => {
43+
const lineDiff = a.range.start.line - b.range.start.line
44+
if (lineDiff !== 0) return lineDiff
45+
46+
return a.range.start.character - b.range.start.character
47+
})
48+
49+
const data: number[] = []
50+
let prevLine = 0
51+
let prevStart = 0
52+
53+
// Encode semantic tokens using relative line and character positions
54+
for (const token of tokens) {
55+
const line = token.range.start.line
56+
const start = token.range.start.character
57+
const length = token.range.end.character - token.range.start.character
58+
59+
const deltaLine = line - prevLine
60+
const deltaStart = deltaLine === 0 ? start - prevStart : start
61+
62+
data.push(deltaLine, deltaStart, length, token.typeIndex, 0)
63+
prevLine = line
64+
prevStart = start
65+
}
66+
67+
return { data }
68+
}
69+
70+
/**
71+
* Recursively collects semantic tokens for a given scope and its nested scopes.
72+
* Tokens are appended to 'tokens' in-place.
73+
* @param scope The scope from which semantic tokens should be collected
74+
* @param tokens The array to which collected semantic tokens are appended
75+
*/
76+
private collectSemanticTokens (
77+
scope: MatlabGlobalScopeInfo | MatlabFunctionScopeInfo,
78+
tokens: SemanticToken[]
79+
): void {
80+
// Variables: highlight only the first component as variable
81+
for (const item of scope.variables.values()) {
82+
for (const ref of item.references) {
83+
tokens.push({ range: ref.components[0].range, typeIndex: 1 }) // variable
84+
}
85+
}
86+
87+
// Functions/unbound: highlight only the first component as function
88+
for (const item of scope.functionOrUnboundReferences.values()) {
89+
for (const ref of item.references) {
90+
tokens.push({ range: ref.components[0].range, typeIndex: 0 }) // function
91+
}
92+
}
93+
94+
// Class scope
95+
const classScope = (scope as MatlabGlobalScopeInfo).classScope;
96+
if (classScope != null) {
97+
for (const nestedFunc of classScope.functionScopes.values()) {
98+
if (nestedFunc.functionScopeInfo != null) {
99+
this.collectSemanticTokens(nestedFunc.functionScopeInfo, tokens);
100+
}
101+
}
102+
}
103+
104+
// Function scopes
105+
for (const nestedFunc of scope.functionScopes.values()) {
106+
if (nestedFunc.functionScopeInfo != null) {
107+
this.collectSemanticTokens(nestedFunc.functionScopeInfo, tokens)
108+
}
109+
}
110+
}
111+
}
112+
113+
export const SEMANTIC_TOKEN_TYPES = ['function', 'variable']
114+
export const SEMANTIC_TOKEN_MODIFIERS: string[] = []
115+
export default SemanticTokensProvider
116+
117+
/**
118+
* Wires semantic token invalidation to document indexing.
119+
*
120+
* When indexing completes, this schedules a debounced refresh request
121+
* so the client re-requests semantic tokens and updates highlighting.
122+
*/
123+
export function setupSemanticTokensRefresh (
124+
connection: Connection,
125+
documentIndexer: DocumentIndexer
126+
): void {
127+
let refreshTimer: NodeJS.Timeout | undefined
128+
129+
documentIndexer.setOnIndexed(() => {
130+
if (refreshTimer != null) clearTimeout(refreshTimer)
131+
132+
refreshTimer = setTimeout(() => {
133+
void connection.sendRequest('workspace/semanticTokens/refresh')
134+
}, 150)
135+
})
136+
}

src/server.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2022 - 2025 The MathWorks, Inc.
22

33
import { TextDocument } from 'vscode-languageserver-textdocument'
4-
import { ClientCapabilities, InitializeParams, InitializeResult, TextDocuments } from 'vscode-languageserver/node'
4+
import { ClientCapabilities, InitializeParams, InitializeResult, TextDocuments, SemanticTokensRequest, SemanticTokensParams } from 'vscode-languageserver/node'
55
import DocumentIndexer from './indexing/DocumentIndexer'
66
import WorkspaceIndexer from './indexing/WorkspaceIndexer'
77
import ConfigurationManager, { ConnectionTiming } from './lifecycle/ConfigurationManager'
@@ -22,6 +22,7 @@ import PathResolver from './providers/navigation/PathResolver'
2222
import Indexer from './indexing/Indexer'
2323
import RenameSymbolProvider from './providers/rename/RenameSymbolProvider'
2424
import HighlightSymbolProvider from './providers/highlighting/HighlightSymbolProvider'
25+
import SemanticTokensProvider, { SEMANTIC_TOKEN_TYPES, SEMANTIC_TOKEN_MODIFIERS, setupSemanticTokensRefresh } from './providers/semanticTokens/SemanticTokensProvider'
2526
import { RequestType } from './indexing/SymbolSearchService'
2627
import { cacheAndClearProxyEnvironmentVariables } from './utils/ProxyUtils'
2728
import MatlabDebugAdaptorServer from './debug/MatlabDebugAdaptorServer'
@@ -73,6 +74,7 @@ export async function startServer (): Promise<void> {
7374
const navigationSupportProvider = new NavigationSupportProvider(matlabLifecycleManager, fileInfoIndex, indexer, documentIndexer, pathResolver)
7475
const renameSymbolProvider = new RenameSymbolProvider(matlabLifecycleManager, documentIndexer, fileInfoIndex)
7576
const highlightSymbolProvider = new HighlightSymbolProvider(matlabLifecycleManager, documentIndexer, indexer, fileInfoIndex)
77+
const semanticTokensProvider = new SemanticTokensProvider(matlabLifecycleManager, documentIndexer, fileInfoIndex)
7678

7779
let pathSynchronizer: PathSynchronizer | null
7880

@@ -142,7 +144,14 @@ export async function startServer (): Promise<void> {
142144
renameProvider: {
143145
prepareProvider: true
144146
},
145-
documentHighlightProvider: true
147+
documentHighlightProvider: true,
148+
semanticTokensProvider: {
149+
legend: {
150+
tokenTypes: SEMANTIC_TOKEN_TYPES,
151+
tokenModifiers: SEMANTIC_TOKEN_MODIFIERS
152+
},
153+
full: true
154+
}
146155
}
147156
}
148157

@@ -264,7 +273,7 @@ export async function startServer (): Promise<void> {
264273
reportFileOpened(params.document)
265274
void lintingSupportProvider.lintDocument(params.document)
266275
void documentIndexer.indexDocument(params.document)
267-
276+
268277
void navigationSupportProvider.handleDocumentSymbol(params.document.uri, documentManager, RequestType.DocumentSymbol)
269278
})
270279

@@ -361,6 +370,12 @@ export async function startServer (): Promise<void> {
361370
connection.onDocumentHighlight(async params => {
362371
return await highlightSymbolProvider.handleDocumentHighlightRequest(params, documentManager)
363372
})
373+
374+
/** -------------- SEMANTIC TOKENS SUPPORT --------------- **/
375+
connection.onRequest(SemanticTokensRequest.method, async (params: SemanticTokensParams) => {
376+
return await semanticTokensProvider.handleSemanticTokensRequest(params, documentManager)
377+
})
378+
setupSemanticTokensRefresh(connection, documentIndexer)
364379
}
365380

366381
/** -------------------- Helper Functions -------------------- **/

0 commit comments

Comments
 (0)