Skip to content

Commit 496a949

Browse files
author
benjamih
committed
added comments + moved definition lookup to SymbolSearchService
1 parent b9d6f88 commit 496a949

File tree

3 files changed

+254
-246
lines changed

3 files changed

+254
-246
lines changed

src/indexing/SymbolSearchService.ts

Lines changed: 243 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
// Copyright 2024 The MathWorks, Inc.
22

3-
import { Location, Position, TextDocuments } from 'vscode-languageserver'
3+
import { Location, Position, TextDocuments, Range } from 'vscode-languageserver'
44
import { TextDocument } from 'vscode-languageserver-textdocument'
55
import FileInfoIndex, { FunctionVisibility, MatlabClassMemberInfo, MatlabCodeData, MatlabFunctionInfo } from './FileInfoIndex'
66
import { Actions, reportTelemetryAction } from '../logging/TelemetryUtils'
77
import Expression from '../utils/ExpressionUtils'
88
import { getTextOnLine } from '../utils/TextDocumentUtils'
9+
import { MatlabConnection } from '../lifecycle/MatlabCommunicationManager'
10+
import PathResolver from '../providers/navigation/PathResolver'
11+
import * as fs from 'fs/promises'
12+
import { URI } from 'vscode-uri'
13+
import Indexer from './Indexer'
914

1015
export enum RequestType {
1116
Definition,
@@ -190,6 +195,243 @@ class SymbolSearchService {
190195

191196
return codeData.classInfo.properties.get(propertyName) ?? null
192197
}
198+
199+
/**
200+
* Finds the definition(s) of an expression.
201+
*
202+
* @param uri The URI of the document containing the expression
203+
* @param position The position of the expression
204+
* @param expression The expression for which we are looking for the definition
205+
* @param matlabConnection The connection to MATLAB®
206+
* @param pathResolver The path resolver
207+
* @param indexer The workspace indexer
208+
* @returns The definition location(s)
209+
*/
210+
async findDefinition (uri: string, position: Position, expression: Expression, matlabConnection: MatlabConnection, pathResolver: PathResolver, indexer: Indexer): Promise<Location[]> {
211+
// Get code data for current file
212+
const codeData = FileInfoIndex.codeDataCache.get(uri)
213+
214+
if (codeData == null) {
215+
// File not indexed - unable to look for definition
216+
reportTelemetry(RequestType.Definition, 'File not indexed')
217+
return []
218+
}
219+
220+
// First check within the current file's code data
221+
const definitionInCodeData = this.findDefinitionInCodeData(uri, position, expression, codeData)
222+
223+
if (definitionInCodeData != null) {
224+
reportTelemetry(RequestType.Definition)
225+
return definitionInCodeData
226+
}
227+
228+
// Check the MATLAB path
229+
const definitionOnPath = await this.findDefinitionOnPath(uri, position, expression, matlabConnection, pathResolver, indexer)
230+
231+
if (definitionOnPath != null) {
232+
reportTelemetry(RequestType.Definition)
233+
return definitionOnPath
234+
}
235+
236+
// If not on path, may be in user's workspace
237+
reportTelemetry(RequestType.Definition)
238+
return this.findDefinitionInWorkspace(uri, expression)
239+
}
240+
241+
/**
242+
* Searches the given code data for the definition(s) of the given expression
243+
*
244+
* @param uri The URI corresponding to the provided code data
245+
* @param position The position of the expression
246+
* @param expression The expression for which we are looking for the definition
247+
* @param codeData The code data which is being searched
248+
* @returns The definition location(s), or null if no definition was found
249+
*/
250+
private findDefinitionInCodeData (uri: string, position: Position, expression: Expression, codeData: MatlabCodeData): Location[] | null {
251+
// If first part of expression targeted - look for a local variable
252+
if (expression.selectedComponent === 0) {
253+
const containingFunction = codeData.findContainingFunction(position)
254+
if (containingFunction != null) {
255+
const varDefs = this.getVariableDefsOrRefs(containingFunction, expression.unqualifiedTarget, uri, RequestType.Definition)
256+
if (varDefs != null) {
257+
return varDefs
258+
}
259+
}
260+
}
261+
262+
// Check for functions in file
263+
let functionDeclaration = this.getFunctionDeclaration(codeData, expression.fullExpression)
264+
if (functionDeclaration != null) {
265+
return [this.getLocationForFunctionDeclaration(functionDeclaration)]
266+
}
267+
268+
// Check for definitions within classes
269+
if (codeData.isClassDef && codeData.classInfo != null) {
270+
// Look for methods/properties within class definitions (e.g. obj.foo)
271+
functionDeclaration = this.getFunctionDeclaration(codeData, expression.last)
272+
if (functionDeclaration != null) {
273+
return [this.getLocationForFunctionDeclaration(functionDeclaration)]
274+
}
275+
276+
// Look for possible properties
277+
if (expression.selectedComponent === 1) {
278+
const propertyDeclaration = this.getPropertyDeclaration(codeData, expression.last)
279+
if (propertyDeclaration != null) {
280+
const propertyRange = Range.create(propertyDeclaration.range.start, propertyDeclaration.range.end)
281+
const uri = codeData.classInfo.uri
282+
if (uri != null) {
283+
return [Location.create(uri, propertyRange)]
284+
}
285+
}
286+
}
287+
}
288+
289+
return null
290+
}
291+
292+
/**
293+
* Gets the location of the given function's declaration. If the function does not have
294+
* a definite declaration, provides a location at the beginning of the file. For example,
295+
* this may be the case for built-in functions like 'plot'.
296+
*
297+
* @param functionInfo Info about the function
298+
* @returns The location of the function declaration
299+
*/
300+
private getLocationForFunctionDeclaration (functionInfo: MatlabFunctionInfo): Location {
301+
const range = functionInfo.declaration ?? Range.create(0, 0, 0, 0)
302+
return Location.create(functionInfo.uri, range)
303+
}
304+
305+
/**
306+
* Searches the MATLAB path for the definition of the given expression
307+
*
308+
* @param uri The URI of the file containing the expression
309+
* @param position The position of the expression
310+
* @param expression The expression for which we are looking for the definition
311+
* @param matlabConnection The connection to MATLAB
312+
* @returns The definition location(s), or null if no definition was found
313+
*/
314+
private async findDefinitionOnPath (uri: string, position: Position, expression: Expression, matlabConnection: MatlabConnection, pathResolver: PathResolver, indexer: Indexer): Promise<Location[] | null> {
315+
const resolvedPath = await pathResolver.resolvePaths([expression.targetExpression], uri, matlabConnection)
316+
const resolvedUri = resolvedPath[0].uri
317+
318+
if (resolvedUri === '') {
319+
// Not found
320+
return null
321+
}
322+
323+
// Ensure URI is not a directory. This can occur with some packages.
324+
const fileStats = await fs.stat(URI.parse(resolvedUri).fsPath)
325+
if (fileStats.isDirectory()) {
326+
return null
327+
}
328+
329+
if (!FileInfoIndex.codeDataCache.has(resolvedUri)) {
330+
// Index target file, if necessary
331+
await indexer.indexFile(resolvedUri)
332+
}
333+
334+
const codeData = FileInfoIndex.codeDataCache.get(resolvedUri)
335+
336+
// Find definition location within determined file
337+
if (codeData != null) {
338+
const definition = this.findDefinitionInCodeData(resolvedUri, position, expression, codeData)
339+
340+
if (definition != null) {
341+
return definition
342+
}
343+
}
344+
345+
// If a definition location cannot be identified, default to the beginning of the file.
346+
// This could be the case for builtin functions which don't actually have a definition in a .m file (e.g. plot).
347+
return [Location.create(resolvedUri, Range.create(0, 0, 0, 0))]
348+
}
349+
350+
/**
351+
* Searches the (indexed) workspace for the definition of the given expression. These files may not be on the MATLAB path.
352+
*
353+
* @param uri The URI of the file containing the expression
354+
* @param expression The expression for which we are looking for the definition
355+
* @returns The definition location(s). Returns an empty array if no definitions found.
356+
*/
357+
private findDefinitionInWorkspace (uri: string, expression: Expression): Location[] {
358+
const expressionToMatch = expression.fullExpression
359+
360+
for (const [fileUri, fileCodeData] of FileInfoIndex.codeDataCache) {
361+
if (uri === fileUri) continue // Already looked in the current file
362+
363+
let match = fileCodeData.packageName === '' ? '' : fileCodeData.packageName + '.'
364+
365+
if (fileCodeData.classInfo != null) {
366+
const classUri = fileCodeData.classInfo.uri
367+
if (classUri == null) continue
368+
369+
// Check class name
370+
match += fileCodeData.classInfo.name
371+
if (expressionToMatch === match) {
372+
const range = fileCodeData.classInfo.declaration ?? Range.create(0, 0, 0, 0)
373+
return [Location.create(classUri, range)]
374+
}
375+
376+
// Check properties
377+
const matchedProperty = this.findMatchingClassMember(expressionToMatch, match, classUri, fileCodeData.classInfo.properties)
378+
if (matchedProperty != null) {
379+
return matchedProperty
380+
}
381+
382+
// Check enums
383+
const matchedEnum = this.findMatchingClassMember(expressionToMatch, match, classUri, fileCodeData.classInfo.enumerations)
384+
if (matchedEnum != null) {
385+
return matchedEnum
386+
}
387+
}
388+
389+
// Check functions
390+
for (const [funcName, funcData] of fileCodeData.functions) {
391+
const funcMatch = (match === '') ? funcName : match + '.' + funcName
392+
393+
// Need to ensure that a function with a matching name should also be visible from the current file.
394+
if (expressionToMatch === funcMatch && this.isFunctionVisibleFromUri(uri, funcData)) {
395+
const range = funcData.declaration ?? Range.create(0, 0, 0, 0)
396+
return [Location.create(funcData.uri, range)]
397+
}
398+
}
399+
}
400+
401+
return []
402+
}
403+
404+
/**
405+
* Finds the class member (property or enumeration) in the given map which matches to given expression.
406+
*
407+
* @param expressionToMatch The expression being compared against
408+
* @param matchPrefix The prefix which should be attached to the class members before comparison
409+
* @param classUri The URI for the current class
410+
* @param classMemberMap The map of class members
411+
* @returns An array containing the location of the matched class member, or null if one was not found
412+
*/
413+
private findMatchingClassMember (expressionToMatch: string, matchPrefix: string, classUri: string, classMemberMap: Map<string, MatlabClassMemberInfo>): Location[] | null {
414+
for (const [memberName, memberData] of classMemberMap) {
415+
const match = matchPrefix + '.' + memberName
416+
if (expressionToMatch === match) {
417+
return [Location.create(classUri, memberData.range)]
418+
}
419+
}
420+
421+
return null
422+
}
423+
424+
/**
425+
* Determines whether the given function should be visible from the given file URI.
426+
* The function is visible if it is contained within the same file, or is public.
427+
*
428+
* @param uri The file's URI
429+
* @param funcData The function data
430+
* @returns true if the function should be visible from the given URI; false otherwise
431+
*/
432+
private isFunctionVisibleFromUri (uri: string, funcData: MatlabFunctionInfo): boolean {
433+
return uri === funcData.uri || funcData.visibility === FunctionVisibility.Public
434+
}
193435
}
194436

195437
export default SymbolSearchService.getInstance()

0 commit comments

Comments
 (0)