Skip to content

Commit 2d285e3

Browse files
dklilleyGitHub Enterprise
authored andcommitted
Merge pull request mathworks#15 from development/dlilley.Feature_Navigation
Add indexing for non-open documents
2 parents 0b266ca + fad477b commit 2d285e3

File tree

13 files changed

+673
-135
lines changed

13 files changed

+673
-135
lines changed

matlab/+matlabls/+handlers/IndexingHandler.m

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,130 @@
33
% function, and class references and definitions.
44

55
properties (Access = private)
6-
RequestChannel = '/matlabls/indexDocument/request'
7-
ResponseChannel = '/matlabls/indexDocument/response'
6+
DocumentIndexingRequestChannel = '/matlabls/indexDocument/request'
7+
DocumentIndexingResponseChannel = '/matlabls/indexDocument/response/' % Needs to be appended with requestId
8+
9+
FolderIndexingRequestChannel = '/matlabls/indexFolders/request'
10+
FolderIndexingResponseChannel = '/matlabls/indexFolders/response/' % Needs to be appended with requestId
811
end
912

1013
methods
1114
function this = IndexingHandler (commManager)
1215
this = [email protected](commManager);
13-
this.RequestSubscriptions = this.CommManager.subscribe(this.RequestChannel, @this.handleIndexRequest);
16+
this.RequestSubscriptions(end + 1) = this.CommManager.subscribe(this.DocumentIndexingRequestChannel, @this.handleDocumentIndexRequest);
17+
this.RequestSubscriptions(end + 1) = this.CommManager.subscribe(this.FolderIndexingRequestChannel, @this.handleFolderIndexRequest);
1418
end
1519
end
1620

1721
methods (Access = private)
18-
function handleIndexRequest (this, msg)
22+
function handleDocumentIndexRequest (this, msg)
1923
% Indexes an individual document and provides the raw data.
2024

2125
code = msg.code;
2226
filePath = msg.filePath;
27+
requestId = num2str(msg.requestId);
2328

2429
codeData = matlabls.internal.computeCodeData(code, filePath);
25-
this.CommManager.publish(this.ResponseChannel, codeData)
30+
31+
responseChannel = strcat(this.DocumentIndexingResponseChannel, requestId);
32+
this.CommManager.publish(responseChannel, codeData)
33+
end
34+
35+
function handleFolderIndexRequest (this, msg)
36+
% Indexes M-files the provided folders
37+
38+
folders = msg.folders;
39+
requestId = num2str(msg.requestId);
40+
41+
files = this.getAllMFilesToIndex(folders);
42+
this.parseFiles(requestId, files)
43+
end
44+
45+
function filesToIndex = getAllMFilesToIndex (~, folders)
46+
% Gathers a list of all M files within the given folders.
47+
48+
filesToIndex = [];
49+
50+
for n = 1:numel(folders)
51+
fileListing = dir([folders{n} '/**/*.m']);
52+
fileNames = strings(numel(fileListing), 1);
53+
for m = 1:numel(fileListing)
54+
fileNames(m) = fullfile(fileListing(m).folder, fileListing(m).name);
55+
end
56+
filesToIndex = [filesToIndex; fileNames]; %#ok<AGROW>
57+
end
58+
end
59+
60+
function parseFiles (this, requestId, files)
61+
% Processes the given list of files and sends the data back to the language server.
62+
63+
if isMATLABReleaseOlderThan('R2021b')
64+
% If backgroundPool doesn't exist, leverage a timer to avoid blocking thread
65+
this.doParseFilesWithTimer(this, requestId, files);
66+
else
67+
parfeval(backgroundPool, @this.doParseFiles, 0, requestId, files);
68+
end
69+
end
70+
71+
function doParseFilesWithTimer (this, requestId, files, index)
72+
% This leverages a timer to achieve an "asynchronous" looping effect, allowing
73+
% other operations to take place between parsing each file. This prevents the MATLAB
74+
% thread from becomming blocked for an extended period of time.
75+
76+
if nargin == 3
77+
index = 1;
78+
end
79+
80+
filePath = files(index);
81+
isLastFile = index == numel(files);
82+
83+
this.parseFile(requestId, filePath, isLastFile);
84+
85+
if ~isLastFile
86+
% More files - queue next file to parse
87+
t = timer(TimerFcn = @timerCallback, StartDelay = 0.001);
88+
t.start();
89+
end
90+
91+
function timerCallback (t, ~)
92+
% Destroy existing timer
93+
t.stop();
94+
t.delete();
95+
96+
% Parse next file
97+
this.parseFiles(requestId, files, index + 1);
98+
end
99+
end
100+
101+
function doParseFiles (this, requestId, files)
102+
% This can be executed in a separate thread (e.g. parfeval) to avoid blocking the
103+
% MATLAB thread.
104+
105+
for n = 1:numel(files)
106+
filePath = files{n};
107+
isLastFile = n == numel(files);
108+
this.parseFile(requestId, filePath, isLastFile);
109+
end
110+
end
111+
112+
function parseFile (this, requestId, filePath, isLastFile)
113+
% Parses the given file and sends its data back to the language server
114+
115+
code = fileread(filePath);
116+
codeData = matlabls.internal.computeCodeData(code, filePath);
117+
118+
% Send data for this file
119+
msg.filePath = filePath;
120+
msg.codeData = codeData;
121+
122+
if isLastFile
123+
msg.isDone = true;
124+
else
125+
msg.isDone = false;
126+
end
127+
128+
responseChannel = strcat(this.FolderIndexingResponseChannel, requestId);
129+
this.CommManager.publish(responseChannel, msg);
26130
end
27131
end
28132
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
classdef (Hidden) NavigationSupportHandler < matlabls.handlers.FeatureHandler
2+
% NAVIGATIONHANDLER The feature handler to support navigation workflows.
3+
4+
properties (Access = private)
5+
ResolvePathRequestChannel = '/matlabls/navigation/resolvePath/request'
6+
ResolvePathResponseChannel = '/matlabls/navigation/resolvePath/response'
7+
end
8+
9+
methods
10+
function this = NavigationSupportHandler (commManager)
11+
this = [email protected](commManager);
12+
this.RequestSubscriptions = this.CommManager.subscribe(this.ResolvePathRequestChannel, @this.handleResolvePathRequest);
13+
end
14+
end
15+
16+
methods (Access = private)
17+
function handleResolvePathRequest (this, msg)
18+
% Handles requests to resolve file paths from code identifiers
19+
20+
names = msg.names;
21+
contextFile = msg.contextFile;
22+
23+
response.data = cell(1, numel(names));
24+
for n = 1:numel(names)
25+
name = names{n};
26+
path = matlabls.internal.resolvePath(name, contextFile);
27+
response.data{n} = struct(name = name, path = path);
28+
end
29+
30+
this.CommManager.publish(this.ResolvePathResponseChannel, response);
31+
end
32+
end
33+
end
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
function path = resolvePath (name, contextFile)
2+
% RESOLVEPATH Attempt to determine the file which most likely contains
3+
% the provided identifier from the perspective of the context file.
4+
%
5+
% For example, if file "foo.m" refers to "ClassName", this returns the
6+
% file most likely containing the definition of "ClassName", or an
7+
% empty string if that file could not be determined.
8+
9+
elementName = name;
10+
11+
% The given identifier may be a reference within a class (e.g. 'obj.Prop')
12+
removeDot = count(name, '.') == 1;
13+
if removeDot
14+
nameResolver = matlab.internal.language.introspective.resolveName(name);
15+
if isempty(nameResolver.classInfo)
16+
elementName = extractAfter(name, '.');
17+
end
18+
end
19+
20+
% Check for targets within the context file
21+
if isvarname(elementName) || iskeyword(elementName)
22+
nameResolver = matlab.internal.language.introspective.resolveName(contextFile);
23+
if ~isempty(nameResolver.classInfo) && (nameResolver.classInfo.isClass || nameResolver.classInfo.isMethod)
24+
classInfo = nameResolver.classInfo;
25+
className = matlab.internal.language.introspective.makePackagedName(classInfo.packageName, classInfo.className);
26+
targetName = append(className, '.', elementName);
27+
nameResolver = matlab.internal.language.introspective.resolveName(targetName);
28+
if nameResolver.isResolved
29+
if nameResolver.isCaseSensitive || isempty(matlab.internal.language.introspective.safeWhich(name, true))
30+
% Target not found within file. Broaden search (e.g. look in base classes)
31+
path = doEditResolve(targetName);
32+
33+
return;
34+
end
35+
end
36+
end
37+
end
38+
39+
% Check for targets elsewhere
40+
path = doEditResolve(name);
41+
end
42+
43+
function path = doEditResolve (name)
44+
% Attempt to resolve the name in the same way that edit.m does
45+
46+
[name, hasLocalFunction, result, ~, path] = matlab.internal.language.introspective.fixLocalFunctionCase(name);
47+
48+
if hasLocalFunction
49+
% Handle a situation like `myFunc>localFunc`
50+
if result && path(end) == 'p'
51+
% See if a corresponding M file exists
52+
path(end) = 'm';
53+
if ~isfile(path)
54+
path = '';
55+
return;
56+
end
57+
end
58+
59+
if ~result
60+
path = '';
61+
return;
62+
end
63+
else
64+
classResolver = matlab.internal.language.introspective.NameResolver(name, '', false);
65+
if isprop(classResolver, 'findBuiltins')
66+
classResolver.findBuiltins = false;
67+
end
68+
classResolver.executeResolve();
69+
70+
if isprop(classResolver, 'resolvedSymbol')
71+
resolvedSymbol = classResolver.resolvedSymbol;
72+
classInfo = resolvedSymbol.classInfo;
73+
whichTopic = resolvedSymbol.nameLocation;
74+
else
75+
classInfo = classResolver.classInfo;
76+
whichTopic = classResolver.nameLocation;
77+
end
78+
79+
if isempty(whichTopic)
80+
[~, path] = resolveWithFileSystemAndExts(name);
81+
else
82+
% whichTopic is the full path to the resolved output either by class
83+
% inference or by which
84+
85+
switch exist(whichTopic, 'file')
86+
case 0 % Name resolver found something which is not a file
87+
whichTopic = classInfo.definition;
88+
case 3 % MEX File
89+
path = '';
90+
return
91+
case {4, 6} % P File or Simulink Model
92+
% See if a corresponding M file exists
93+
mTopic = regexprep(whichTopic, '\.\w+$', '.m');
94+
if isfile(mTopic)
95+
whichTopic = mTopic;
96+
else
97+
path = '';
98+
return
99+
end
100+
end
101+
102+
if matlab.desktop.editor.EditorUtils.isAbsolute(whichTopic)
103+
path = whichTopic;
104+
else
105+
path = which(whichTopic);
106+
end
107+
end
108+
end
109+
end
110+
111+
function result = hasExtension(s)
112+
% Helper method that determines if filename specified has an extension.
113+
% Returns 1 if filename does have an extension, 0 otherwise
114+
[~,~,ext] = fileparts(s);
115+
result = ~isempty(ext);
116+
end
117+
118+
function [result, absPathname] = resolveWithFileSystemAndExts(argName)
119+
% Helper method that checks the filesystem for files by adding m or mlx
120+
result = 0;
121+
122+
if ~hasExtension(argName)
123+
argMlx = [argName '.mlx'];
124+
[result, absPathname] = resolveWithDir(argMlx);
125+
126+
if ~result
127+
argM = [argName '.m'];
128+
[result, absPathname] = resolveWithDir(argM);
129+
end
130+
end
131+
132+
if ~result
133+
absPathname = '';
134+
end
135+
end
136+
137+
function [result, absPathname] = resolveWithDir(argName)
138+
% Helper method that checks the filesystem for files
139+
result = 0;
140+
absPathname = '';
141+
142+
dir_result = dir(argName);
143+
144+
if isempty(dir_result) && isSimpleFile(argName)
145+
dir_result = dir(fullfile('private', argName));
146+
end
147+
148+
if ~isempty(dir_result)
149+
if (numel(dir_result) == 1) && ~dir_result.isdir
150+
result = 1; % File exists
151+
absPathname = fullfile(dir_result.folder, dir_result.name);
152+
end
153+
end
154+
end
155+
156+
function result = isSimpleFile(file)
157+
% Helper method that checks for directory seps.
158+
result = false;
159+
if isunix
160+
if ~contains(file, '/')
161+
result = true;
162+
end
163+
else % on windows be more restrictive
164+
if ~contains(file, ["\", "/", ":"]) % need to keep : for c: case
165+
result = true;
166+
end
167+
end
168+
end

matlab/+matlabls/MatlabLanguageServerHelper.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ function initializeFeatureHandlers (this)
2929
this.FeatureHandlers(end + 1) = matlabls.handlers.FormatSupportHandler(this.CommManager);
3030
this.FeatureHandlers(end + 1) = matlabls.handlers.IndexingHandler(this.CommManager);
3131
this.FeatureHandlers(end + 1) = matlabls.handlers.LintingSupportHandler(this.CommManager);
32+
this.FeatureHandlers(end + 1) = matlabls.handlers.NavigationSupportHandler(this.CommManager);
3233
end
3334
end
3435
end

0 commit comments

Comments
 (0)