Skip to content

Commit 25e8eb1

Browse files
dklilleyGitHub Enterprise
authored andcommitted
Merge pull request mathworks#55 from development/dlilley/feature/pathsynchronizer
Synchronize MATLAB path with client workspace folders
2 parents 13f40c3 + 98b1af4 commit 25e8eb1

File tree

4 files changed

+240
-0
lines changed

4 files changed

+240
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
classdef (Hidden) PathSynchronizerHandler < matlabls.handlers.FeatureHandler
2+
% PATHSYNCHRONIZERHANDLER The feature handler to support synchronizing the MATLAB path
3+
% with the client's workspace. This provides access points to add and remove from the path,
4+
% as well as get/set MATLAB's current working directory.
5+
6+
% Copyright 2024 The MathWorks, Inc.
7+
8+
properties (Access = private)
9+
CdRequestChannel = "/matlabls/pathSynchronizer/cd/request"
10+
11+
PwdRequestChannel = "/matlabls/pathSynchronizer/pwd/request"
12+
PwdResponseChannel = "/matlabls/pathSynchronizer/pwd/response"
13+
14+
AddPathRequestChannel = "/matlabls/pathSynchronizer/addpath/request"
15+
RmPathRequestChannel = "/matlabls/pathSynchronizer/rmpath/request"
16+
end
17+
18+
methods
19+
function this = PathSynchronizerHandler ()
20+
this.RequestSubscriptions(1) = matlabls.internal.CommunicationManager.subscribe(this.CdRequestChannel, @this.handleCdRequest);
21+
this.RequestSubscriptions(2) = matlabls.internal.CommunicationManager.subscribe(this.PwdRequestChannel, @this.handlePwdRequest);
22+
this.RequestSubscriptions(3) = matlabls.internal.CommunicationManager.subscribe(this.AddPathRequestChannel, @this.handleAddPathRequest);
23+
this.RequestSubscriptions(4) = matlabls.internal.CommunicationManager.subscribe(this.RmPathRequestChannel, @this.handleRmPathRequest);
24+
end
25+
end
26+
27+
methods (Access = private)
28+
function handleCdRequest (~, msg)
29+
path = msg.path;
30+
31+
try
32+
cd(path)
33+
catch e
34+
disp('Error during `cd` operation:')
35+
disp(e.message)
36+
end
37+
end
38+
39+
function handlePwdRequest (this, msg)
40+
try
41+
currentPath = pwd();
42+
43+
responseChannel = strcat(this.PwdResponseChannel, '/', msg.channelId);
44+
matlabls.internal.CommunicationManager.publish(responseChannel, currentPath);
45+
catch e
46+
disp('Error during `pwd` operation:')
47+
disp(e.message)
48+
end
49+
end
50+
51+
function handleAddPathRequest (~, msg)
52+
paths = msg.paths;
53+
paths = strjoin(paths, pathsep);
54+
55+
try
56+
addpath(paths)
57+
catch e
58+
disp('Error during `addpath` operation:')
59+
disp(e.message)
60+
end
61+
end
62+
63+
function handleRmPathRequest (~, msg)
64+
paths = msg.paths;
65+
paths = strjoin(paths, pathsep);
66+
67+
try
68+
rmpath(paths)
69+
catch e
70+
disp('Error during `rmpath` operation:')
71+
disp(e.message)
72+
end
73+
end
74+
end
75+
end

matlab/+matlabls/MatlabLanguageServerHelper.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function initializeFeatureHandlers (this)
3232
this.FeatureHandlers(end + 1) = matlabls.handlers.LintingSupportHandler();
3333
this.FeatureHandlers(end + 1) = matlabls.handlers.NavigationSupportHandler();
3434
this.FeatureHandlers(end + 1) = matlabls.handlers.FoldingSupportHandler();
35+
this.FeatureHandlers(end + 1) = matlabls.handlers.PathSynchronizerHandler();
3536
end
3637
end
3738
end

src/lifecycle/PathSynchronizer.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright 2024 The MathWorks, Inc.
2+
3+
import { WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode-languageserver'
4+
import ClientConnection, { Connection } from '../ClientConnection'
5+
import Logger from '../logging/Logger'
6+
import MatlabLifecycleManager from './MatlabLifecycleManager'
7+
import { MatlabConnection } from './MatlabCommunicationManager'
8+
import * as os from 'os'
9+
import path from 'path'
10+
11+
export default class PathSynchronizer {
12+
readonly CD_REQUEST_CHANNEL = '/matlabls/pathSynchronizer/cd/request'
13+
14+
readonly PWD_REQUEST_CHANNEL = '/matlabls/pathSynchronizer/pwd/request'
15+
readonly PWD_RESPONSE_CHANNEL = '/matlabls/pathSynchronizer/pwd/response'
16+
17+
readonly ADDPATH_REQUEST_CHANNEL = '/matlabls/pathSynchronizer/addpath/request'
18+
readonly RMPATH_REQUEST_CHANNEL = '/matlabls/pathSynchronizer/rmpath/request'
19+
20+
constructor (private readonly matlabLifecycleManager: MatlabLifecycleManager) {}
21+
22+
/**
23+
* Initializes the PathSynchronizer by setting up event listeners.
24+
*
25+
* Upon MATLAB connection, all workspace folders are added to the MATLAB search path.
26+
* Additionally, MATLAB's CWD is set to the first workspace folder to avoid potential
27+
* function shadowing issues.
28+
*
29+
* As workspace folders are added or removed, the MATLAB path is updated accordingly.
30+
*/
31+
initialize () {
32+
const clientConnection = ClientConnection.getConnection()
33+
34+
this.matlabLifecycleManager.eventEmitter.on('connected', () => this.handleMatlabConnected(clientConnection))
35+
36+
clientConnection.workspace.onDidChangeWorkspaceFolders(event => this.handleWorkspaceFoldersChanged(event))
37+
}
38+
39+
/**
40+
* Handles synchronizing the MATLAB path with the current workspace folders when MATLAB
41+
* has been connected.
42+
*
43+
* @param clientConnection The current client connection
44+
*/
45+
private async handleMatlabConnected (clientConnection: Connection): Promise<void> {
46+
const matlabConnection = await this.matlabLifecycleManager.getMatlabConnection()
47+
if (matlabConnection == null) {
48+
// As the connection was just established, this should not happen
49+
Logger.warn('MATLAB connection is unavailable after connection established')
50+
return
51+
}
52+
53+
const workspaceFolders = await clientConnection.workspace.getWorkspaceFolders()
54+
if (workspaceFolders == null || workspaceFolders.length === 0) {
55+
// No workspace folders - no action needs to be taken
56+
return
57+
}
58+
59+
const folderPaths = this.convertWorkspaceFoldersToFilePaths(workspaceFolders)
60+
61+
// cd to first workspace folder
62+
this.setWorkingDirectory(folderPaths[0], matlabConnection)
63+
64+
// add all workspace folders to path
65+
this.addToPath(folderPaths, matlabConnection)
66+
}
67+
68+
/**
69+
* Handles synchronizing the MATLAB path with newly added/removed workspace folders.
70+
*
71+
* @param event The workspace folders change event
72+
*/
73+
private async handleWorkspaceFoldersChanged (event: WorkspaceFoldersChangeEvent): Promise<void> {
74+
const matlabConnection = await this.matlabLifecycleManager.getMatlabConnection()
75+
if (matlabConnection == null) {
76+
return
77+
}
78+
79+
const cwd = await this.getCurrentWorkingDirectory(matlabConnection)
80+
81+
// addpath for all added folders
82+
const addedFolderPaths = this.convertWorkspaceFoldersToFilePaths(event.added)
83+
this.addToPath(addedFolderPaths, matlabConnection)
84+
85+
// rmpath for all removed folders
86+
const removedFolderPaths = this.convertWorkspaceFoldersToFilePaths(event.removed)
87+
this.removeFromPath(removedFolderPaths, matlabConnection)
88+
89+
// log warning if primary workspace folder was removed
90+
if (this.isCwdInPaths(removedFolderPaths, cwd)) {
91+
Logger.warn('The current working directory was removed from the workspace.')
92+
}
93+
}
94+
95+
private setWorkingDirectory (path: string, matlabConnection: MatlabConnection): void {
96+
Logger.log(`CWD set to: ${path}`)
97+
98+
matlabConnection.publish(this.CD_REQUEST_CHANNEL, {
99+
path
100+
})
101+
}
102+
103+
private getCurrentWorkingDirectory (matlabConnection: MatlabConnection): Promise<string> {
104+
const channelId = matlabConnection.getChannelId()
105+
const channel = `${this.PWD_RESPONSE_CHANNEL}/${channelId}`
106+
107+
return new Promise<string>(resolve => {
108+
const responseSub = matlabConnection.subscribe(channel, message => {
109+
const cwd = message as string
110+
matlabConnection.unsubscribe(responseSub)
111+
resolve(path.normalize(cwd))
112+
})
113+
114+
matlabConnection.publish(this.PWD_REQUEST_CHANNEL, {
115+
channelId
116+
})
117+
})
118+
}
119+
120+
private addToPath (paths: string[], matlabConnection: MatlabConnection): void {
121+
if (paths.length === 0) return
122+
123+
Logger.log(`Adding workspace folder(s) to the MATLAB Path: \n\t${paths.join('\n\t')}`)
124+
matlabConnection.publish(this.ADDPATH_REQUEST_CHANNEL, {
125+
paths
126+
})
127+
}
128+
129+
private removeFromPath (paths: string[], matlabConnection: MatlabConnection): void {
130+
if (paths.length === 0) return
131+
132+
Logger.log(`Removing workspace folder(s) from the MATLAB Path: \n\t${paths.join('\n\t')}`)
133+
matlabConnection.publish(this.RMPATH_REQUEST_CHANNEL, {
134+
paths
135+
})
136+
}
137+
138+
private convertWorkspaceFoldersToFilePaths (workspaceFolders: WorkspaceFolder[]): string[] {
139+
return workspaceFolders.map(folder => {
140+
let uri = decodeURIComponent(folder.uri)
141+
uri = uri.replace('file:///', '')
142+
return path.normalize(uri)
143+
});
144+
}
145+
146+
private isCwdInPaths (folderPaths: string[], cwd: string): boolean {
147+
if (os.platform() === 'win32') {
148+
// On Windows, paths are case-insensitive
149+
return folderPaths.some(folderPath => folderPath.toLowerCase() === cwd.toLowerCase());
150+
} else {
151+
// On Unix-like systems, paths are case-sensitive
152+
return folderPaths.includes(cwd);
153+
}
154+
}
155+
}

src/server.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import MVMServer from './mvm/MVMServer'
3030
import { stopLicensingServer } from './licensing/server'
3131
import { setInstallPath } from './licensing/config'
3232
import { handleInstallPathSettingChanged, handleSignInChanged, setupLicensingNotificationListenersAndUpdateClient } from './utils/LicensingUtils'
33+
import PathSynchronizer from './lifecycle/PathSynchronizer'
3334

3435
export async function startServer (): Promise<void> {
3536
cacheAndClearProxyEnvironmentVariables()
@@ -56,6 +57,8 @@ export async function startServer (): Promise<void> {
5657
const navigationSupportProvider = new NavigationSupportProvider(matlabLifecycleManager, indexer, documentIndexer, pathResolver)
5758
const renameSymbolProvider = new RenameSymbolProvider(matlabLifecycleManager, documentIndexer)
5859

60+
let pathSynchronizer: PathSynchronizer | null
61+
5962
// Create basic text document manager
6063
const documentManager: TextDocuments<TextDocument> = new TextDocuments(TextDocument)
6164

@@ -146,6 +149,12 @@ export async function startServer (): Promise<void> {
146149

147150
workspaceIndexer.setupCallbacks(capabilities)
148151

152+
if (capabilities.workspace?.workspaceFolders != null) {
153+
// If workspace folders are supported, try to synchronize the MATLAB path with the user's workspace.
154+
pathSynchronizer = new PathSynchronizer(matlabLifecycleManager)
155+
pathSynchronizer.initialize()
156+
}
157+
149158
mvm = new MVM(matlabLifecycleManager);
150159
mvmServer = new MVMServer(mvm, NotificationService);
151160
matlabDebugAdaptor = new MatlabDebugAdaptorServer(mvm, new DebugServices(mvm));

0 commit comments

Comments
 (0)