1+ import assert from 'assert'
2+ import sinon from 'sinon'
3+ import quibble from 'quibble'
4+
5+ import { TextDocuments , Range } from 'vscode-languageserver'
6+ import { TextDocument } from 'vscode-languageserver-textdocument'
7+
8+ import MatlabLifecycleManager from '../../../src/lifecycle/MatlabLifecycleManager'
9+ import DocumentIndexer from '../../../src/indexing/DocumentIndexer'
10+ import Indexer from '../../../src/indexing/Indexer'
11+ import FileInfoIndex , { MatlabFunctionScopeInfo , MatlabGlobalScopeInfo } from '../../../src/indexing/FileInfoIndex'
12+ import ClientConnection from '../../../src/ClientConnection'
13+ import getMockConnection from '../../mocks/Connection.mock'
14+ import getMockMvm from '../../mocks/Mvm.mock'
15+ import { dynamicImport } from '../../TestUtils'
16+
17+ describe ( 'SemanticTokenProvider' , ( ) => {
18+ const URI = 'file://test.m'
19+
20+ let matlabLifecycleManager : MatlabLifecycleManager
21+ let fileInfoIndex : FileInfoIndex
22+ let indexer : Indexer
23+ let documentIndexer : DocumentIndexer
24+
25+ let semanticTokensProvider : any
26+ let documentManager : TextDocuments < TextDocument >
27+
28+ const setup = ( ) => {
29+ const mockMvm = getMockMvm ( )
30+
31+ matlabLifecycleManager = new MatlabLifecycleManager ( )
32+ fileInfoIndex = new FileInfoIndex ( )
33+ indexer = new Indexer ( matlabLifecycleManager , mockMvm , fileInfoIndex )
34+ documentIndexer = new DocumentIndexer ( indexer , fileInfoIndex )
35+
36+ documentManager = new TextDocuments ( TextDocument )
37+
38+ sinon . stub ( matlabLifecycleManager , 'getMatlabConnection' ) . resolves ( { } as any )
39+
40+ type SemanticTokensProviderExports = typeof import ( '../../../src/providers/semanticTokens/SemanticTokensProvider' )
41+ const { default : SemanticTokensProvider } = dynamicImport < SemanticTokensProviderExports > (
42+ module , '../../../src/providers/semanticTokens/SemanticTokensProvider' )
43+
44+ semanticTokensProvider = new SemanticTokensProvider (
45+ matlabLifecycleManager ,
46+ documentIndexer ,
47+ fileInfoIndex
48+ )
49+
50+ const doc = TextDocument . create (
51+ URI , 'matlab' , 1 , 'abc'
52+ )
53+
54+ sinon . stub ( documentManager , 'get' ) . returns ( doc )
55+ }
56+
57+ const teardown = ( ) => {
58+ quibble . reset ( )
59+ sinon . restore ( )
60+ }
61+
62+ before ( ( ) => {
63+ ClientConnection . _setConnection ( getMockConnection ( ) )
64+ } )
65+
66+ after ( ( ) => {
67+ ClientConnection . _clearConnection ( )
68+ } )
69+
70+ describe ( '#handleSemanticTokensRequest' , ( ) => {
71+ beforeEach ( ( ) => setup ( ) )
72+ afterEach ( ( ) => teardown ( ) )
73+
74+ it ( 'should return null if there is no MATLAB connection' , async ( ) => {
75+ ( matlabLifecycleManager . getMatlabConnection as sinon . SinonStub ) . resolves ( null )
76+
77+ const res = await semanticTokensProvider . handleSemanticTokensRequest (
78+ { textDocument : { uri : URI } } , documentManager
79+ )
80+
81+ assert . strictEqual ( res , null , 'Result should be null when there is no MATLAB connection' )
82+ } )
83+
84+ it ( 'should return null if there is no document at the given URI' , async ( ) => {
85+ ( documentManager . get as sinon . SinonStub ) . returns ( undefined )
86+
87+ const res = await semanticTokensProvider . handleSemanticTokensRequest (
88+ { textDocument : { uri : URI } } , documentManager
89+ )
90+
91+ assert . strictEqual ( res , null , 'Result should be null when there is no document at the given URI' )
92+ } )
93+
94+ it ( 'should return null if codeinfo is null' , async ( ) => {
95+ fileInfoIndex . codeInfoCache . set ( URI , null as any )
96+
97+ const res = await semanticTokensProvider . handleSemanticTokensRequest (
98+ { textDocument : { uri : URI } } , documentManager
99+ )
100+
101+ assert . strictEqual ( res , null , 'Result should be null when codeinfo is null' )
102+ } )
103+
104+ it ( 'should mark foo as a semantic function' , async ( ) => {
105+ const uri = URI
106+
107+ const range = {
108+ start : { line : 0 , character : 0 } ,
109+ end : { line : 0 , character : 3 } // "foo"
110+ }
111+
112+ fileInfoIndex . codeInfoCache . set (
113+ URI ,
114+ createCodeInfo ( {
115+ functions : [
116+ { name : 'foo' , range : range }
117+ ]
118+ } )
119+ )
120+
121+ const result = await semanticTokensProvider . handleSemanticTokensRequest (
122+ { textDocument : { uri } } ,
123+ documentManager
124+ )
125+
126+ assert . ok ( result , 'Expected semantic tokens result' )
127+
128+ const [ deltaLine , deltaStart , length , typeIndex ] = result ! . data
129+
130+ assert . strictEqual ( typeIndex , 0 , 'Expected function token type' )
131+ assert . strictEqual ( length , 3 , 'Expected token length for "foo"' )
132+ } )
133+
134+ it ( 'should mark x as a semantic variable' , async ( ) => {
135+ const uri = URI
136+
137+ const range = {
138+ start : { line : 0 , character : 0 } ,
139+ end : { line : 0 , character : 1 }
140+ }
141+
142+ fileInfoIndex . codeInfoCache . set (
143+ URI ,
144+ createCodeInfo ( {
145+ variables : [
146+ { name : 'x' , range : range }
147+ ]
148+ } )
149+ )
150+
151+ const result = await semanticTokensProvider . handleSemanticTokensRequest (
152+ { textDocument : { uri } } ,
153+ documentManager
154+ )
155+
156+ assert . ok ( result )
157+
158+ const [ deltaLine , deltaStart , length , typeIndex ] = result ! . data
159+
160+ assert . strictEqual ( typeIndex , 1 ) // variable
161+ assert . strictEqual ( length , 1 )
162+ } )
163+
164+ it ( 'should encode delta correctly for semantic tokens on same line' , async ( ) => {
165+ const uri = URI
166+
167+ const range1 = {
168+ start : { line : 0 , character : 1 } ,
169+ end : { line : 0 , character : 2 } // "x"
170+ } as unknown as Range
171+
172+ const range2 = {
173+ start : { line : 0 , character : 5 } ,
174+ end : { line : 0 , character : 6 } // "y"
175+ } as unknown as Range
176+
177+ fileInfoIndex . codeInfoCache . set (
178+ URI ,
179+ createCodeInfo ( {
180+ variables : [
181+ { name : 'x' , range : range1 } ,
182+ { name : 'y' , range : range2 }
183+ ]
184+ } )
185+ )
186+
187+ const result = await semanticTokensProvider . handleSemanticTokensRequest (
188+ { textDocument : { uri } } as any ,
189+ documentManager
190+ )
191+
192+ assert . ok ( result , 'Expected semantic tokens result' )
193+
194+ // Token 1
195+ const [ dl1 , ds1 , len1 , type1 ] = result ! . data . slice ( 0 , 4 )
196+
197+ // Token 2
198+ const [ dl2 , ds2 , len2 , type2 ] = result ! . data . slice ( 5 , 9 )
199+
200+ // First token (absolute)
201+ assert . strictEqual ( dl1 , 0 )
202+ assert . strictEqual ( ds1 , 1 )
203+
204+ // Second token:
205+ // same line → deltaLine = 0
206+ assert . strictEqual ( dl2 , 0 , 'Expected same line' )
207+
208+ // relative start: 5 - 1 = 4
209+ assert . strictEqual ( ds2 , 4 , 'Expected relative deltaStart' )
210+ } )
211+
212+ it ( 'should encode delta correctly for semantic tokens on different lines' , async ( ) => {
213+ const uri = URI
214+
215+ const range1 = {
216+ start : { line : 0 , character : 0 } ,
217+ end : { line : 0 , character : 1 } // "x"
218+ }
219+
220+ const range2 = {
221+ start : { line : 2 , character : 7 } ,
222+ end : { line : 2 , character : 8 } // "y"
223+ }
224+
225+ fileInfoIndex . codeInfoCache . set (
226+ URI ,
227+ createCodeInfo ( {
228+ variables : [ // Note: Variables are intentionally added in reverse order to test sorting of tokens
229+ { name : 'y' , range : range2 } ,
230+ { name : 'x' , range : range1 }
231+ ]
232+ } )
233+ )
234+
235+ const result = await semanticTokensProvider . handleSemanticTokensRequest (
236+ { textDocument : { uri } } as any ,
237+ documentManager
238+ )
239+
240+ assert . ok ( result , 'Expected semantic tokens result' )
241+
242+ // Token 1
243+ const [ dl1 , ds1 , len1 , type1 ] = result ! . data . slice ( 0 , 4 )
244+
245+ // Token 2
246+ const [ dl2 , ds2 , len2 , type2 ] = result ! . data . slice ( 5 , 9 )
247+
248+ // First token: absolute position
249+ assert . strictEqual ( dl1 , 0 , 'Expected first token to have dl1 = 0, sorting should ensure this is the case' )
250+ assert . strictEqual ( ds1 , 0 , 'Expected first token to have ds1 = 0, sorting should ensure this is the case' )
251+
252+ // Second token:
253+ // line 2 - line 0 = 2
254+ assert . strictEqual ( dl2 , 2 , 'Expected deltaLine = 2' )
255+
256+ // since new line → start should NOT be relative
257+ assert . strictEqual ( ds2 , 7 , 'Expected absolute start on new line' )
258+ } )
259+ } )
260+ } )
261+
262+ function createCodeInfo ( {
263+ variables = [ ] ,
264+ functions = [ ]
265+ } : {
266+ variables ?: Array < { name : string , range : Range } >
267+ functions ?: Array < { name : string , range : Range } >
268+ } ) {
269+ return {
270+ globalScopeInfo : {
271+ variables : new Map (
272+ variables . map ( v => [
273+ v . name ,
274+ {
275+ references : [
276+ { components : [ { range : v . range } ] }
277+ ]
278+ }
279+ ] )
280+ ) ,
281+
282+ functionOrUnboundReferences : new Map (
283+ functions . map ( f => [
284+ f . name ,
285+ {
286+ references : [
287+ { components : [ { range : f . range } ] }
288+ ]
289+ }
290+ ] )
291+ ) ,
292+
293+ functionScopes : new Map ( )
294+ }
295+ } as any
296+ }
0 commit comments