|
1 | 1 | // Copyright 2024 The MathWorks, Inc. |
2 | 2 |
|
3 | | -import { Location, Position, TextDocuments } from 'vscode-languageserver' |
| 3 | +import { Location, Position, TextDocuments, Range } from 'vscode-languageserver' |
4 | 4 | import { TextDocument } from 'vscode-languageserver-textdocument' |
5 | 5 | import FileInfoIndex, { FunctionVisibility, MatlabClassMemberInfo, MatlabCodeData, MatlabFunctionInfo } from './FileInfoIndex' |
6 | 6 | import { Actions, reportTelemetryAction } from '../logging/TelemetryUtils' |
7 | 7 | import Expression from '../utils/ExpressionUtils' |
8 | 8 | 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' |
9 | 14 |
|
10 | 15 | export enum RequestType { |
11 | 16 | Definition, |
@@ -190,6 +195,243 @@ class SymbolSearchService { |
190 | 195 |
|
191 | 196 | return codeData.classInfo.properties.get(propertyName) ?? null |
192 | 197 | } |
| 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 | + } |
193 | 435 | } |
194 | 436 |
|
195 | 437 | export default SymbolSearchService.getInstance() |
0 commit comments