Skip to content

Commit 4886ff0

Browse files
authored
Merge pull request #61 from mathworks/dklilley/release/1.3.3
MATLAB language server - v1.3.3
2 parents 4765aa2 + afcef80 commit 4886ff0

File tree

20 files changed

+287
-62
lines changed

20 files changed

+287
-62
lines changed

.github/ISSUE_TEMPLATE/bug_report.md

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ A clear and concise description of what you expected to happen.
2424
If applicable, add screenshots to help explain your problem.
2525

2626
**Useful Information**
27+
- MATLAB Version:
2728
- OS Version:
2829
- Language Server Client: [e.g. MATLAB extension for Visual Studio Code]
2930
- Client Version:

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ MATLAB language server supports these editors by installing the corresponding ex
2626

2727
### Unreleased
2828

29+
### 1.3.3
30+
Release date: 2025-05-15
31+
32+
Added:
33+
* Support for debugging P-coded files when the corresponding source file is available
34+
35+
Fixed:
36+
* Resolves potential crashes when using code completion in files without a .m file extension
37+
2938
### 1.3.2
3039
Release date: 2025-03-06
3140

matlab/+matlabls/+handlers/+completions/getCompletions.m

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
% GETCOMPLETIONS Retrieves the data for the possible completions at the cursor position in the given code.
33

44
% Copyright 2025 The MathWorks, Inc.
5+
[~, ~, ext] = fileparts(fileName);
6+
if ~isempty(fileName) && ~strcmpi(ext, '.m')
7+
% Expected .m file extension
8+
error('MATLAB:vscode:invalidFileExtension', 'The provided file must have a .m extension to process completions.');
9+
end
510

611
completionResultsStr = matlabls.internal.getCompletionsData(code, fileName, cursorPosition);
712
completionsData = filterCompletionResults(completionResultsStr);

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "matlab-language-server",
3-
"version": "1.3.2",
3+
"version": "1.3.3",
44
"description": "Language Server for MATLAB code",
55
"main": "./src/index.ts",
66
"bin": "./out/index.js",

src/debug/MatlabDebugAdaptor.ts

+48-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DebugProtocol } from '@vscode/debugprotocol';
55
import { DebugServices, BreakpointInfo } from './DebugServices'
66
import { ResolvablePromise, createResolvablePromise } from '../utils/PromiseUtils'
77
import { IMVM, MVMError, MatlabState } from '../mvm/impl/MVM';
8+
import fs from 'node:fs';
89

910
enum BreakpointChangeType {
1011
ADD,
@@ -233,6 +234,8 @@ export default class MatlabDebugAdaptor {
233234

234235
private _setupListeners (): void {
235236
this._debugServices.on(DebugServices.Events.BreakpointAdded, async (breakpoint: BreakpointInfo) => {
237+
breakpoint.filePath = this._mapToMFile(breakpoint.filePath, false);
238+
236239
this._matlabBreakpoints.push(breakpoint);
237240

238241
this._breakpointChangeListeners.forEach((listener) => {
@@ -241,6 +244,8 @@ export default class MatlabDebugAdaptor {
241244
});
242245

243246
this._debugServices.on(DebugServices.Events.BreakpointRemoved, async (breakpoint: BreakpointInfo) => {
247+
breakpoint.filePath = this._mapToMFile(breakpoint.filePath, false);
248+
244249
this._matlabBreakpoints = this._matlabBreakpoints.filter((existingBreakpoint) => {
245250
return !existingBreakpoint.equals(breakpoint, true);
246251
});
@@ -422,6 +427,7 @@ export default class MatlabDebugAdaptor {
422427
}
423428

424429
const canonicalizedPath = await this._getCanonicalPath(source.path);
430+
const pathToSetOrClear = this._mapToPFile(canonicalizedPath, true);
425431

426432
const newBreakpoints: BreakpointInfo[] = (args.breakpoints != null)
427433
? args.breakpoints.map((breakpoint) => {
@@ -458,7 +464,7 @@ export default class MatlabDebugAdaptor {
458464
// Remove all breakpoints that are now gone.
459465
const breakpointsRemovalPromises: Array<Promise<void>> = [];
460466
breakpointsToRemove.forEach((breakpoint: BreakpointInfo) => {
461-
breakpointsRemovalPromises.push(this._mvm.clearBreakpoint(breakpoint.filePath, breakpoint.lineNumber));
467+
breakpointsRemovalPromises.push(this._mvm.clearBreakpoint(pathToSetOrClear, breakpoint.lineNumber));
462468
})
463469
await Promise.all(breakpointsRemovalPromises);
464470

@@ -476,12 +482,12 @@ export default class MatlabDebugAdaptor {
476482

477483
let matlabBreakpointInfos: BreakpointInfo[] = [];
478484
const listener = this._registerBreakpointChangeListener((changeType, bpInfo) => {
479-
if (changeType === BreakpointChangeType.ADD && bpInfo.filePath === canonicalizedPath) {
485+
if (changeType === BreakpointChangeType.ADD && bpInfo.filePath === pathToSetOrClear) {
480486
matlabBreakpointInfos.push(bpInfo);
481487
}
482488
});
483489

484-
await this._mvm.setBreakpoint(canonicalizedPath, newBreakpoint.info.lineNumber, newBreakpoint.info.condition);
490+
await this._mvm.setBreakpoint(pathToSetOrClear, newBreakpoint.info.lineNumber, newBreakpoint.info.condition);
485491

486492
listener.remove();
487493

@@ -501,6 +507,36 @@ export default class MatlabDebugAdaptor {
501507
this._clearPendingBreakpointsRequest();
502508
}
503509

510+
_mapToPFile (filePath: string, checkIfExists: boolean): string {
511+
// If this is an m-file then convert to p-file and check existence
512+
if (filePath.endsWith('.m')) {
513+
const pFile = filePath.substring(0, filePath.length - 1) + 'p';
514+
if (!checkIfExists || fs.existsSync(pFile)) {
515+
return pFile;
516+
} else {
517+
return filePath;
518+
}
519+
}
520+
521+
// Not an m file so p-code not supported
522+
return filePath;
523+
}
524+
525+
_mapToMFile (filePath: string, checkIfExists: boolean): string {
526+
// If this is an p-file then convert to m-file and check existence
527+
if (filePath.endsWith('.p')) {
528+
const mFile = filePath.substring(0, filePath.length - 1) + 'm';
529+
if (!checkIfExists || fs.existsSync(mFile)) {
530+
return mFile;
531+
} else {
532+
return filePath;
533+
}
534+
}
535+
536+
// Not an m file so p-code not supported
537+
return filePath;
538+
}
539+
504540
async continueRequest (response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments, request?: DebugProtocol.Request): Promise<void> {
505541
try {
506542
await this._mvm.eval("if system_dependent('IsDebugMode')==1, dbcont; end");
@@ -558,15 +594,18 @@ export default class MatlabDebugAdaptor {
558594
if (stack[0]?.mwtype !== undefined) {
559595
stack = stack[0]
560596
const size = stack.mwsize[0];
561-
const newStack = [];
597+
const transformedStack = [];
562598
for (let i = 0; i < size; i++) {
563-
newStack.push(new debug.StackFrame(size - i + 1, stack.mwdata.name[i], new debug.Source(stack.mwdata.name[i], stack.mwdata.file[i]), Math.abs(stack.mwdata.line[i]), 1))
599+
transformedStack.push({ name: stack.mwdata.name[i], file: stack.mwdata.file[i], line: stack.mwdata.line[i] });
564600
}
565-
return newStack;
566-
} else {
567-
const numberOfStackFrames: number = stack.length;
568-
return stack.map((stackFrame: MatlabData, i: number) => new debug.StackFrame(numberOfStackFrames - i + 1, stackFrame.name, new debug.Source(stackFrame.name as string, stackFrame.file as string), Math.abs(stackFrame.line), 1));
601+
stack = transformedStack;
569602
}
603+
604+
const numberOfStackFrames: number = stack.length;
605+
return stack.map((stackFrame: MatlabData, i: number) => {
606+
const fileName: string = this._mapToMFile(stackFrame.file, true);
607+
return new debug.StackFrame(numberOfStackFrames - i + 1, stackFrame.name, new debug.Source(stackFrame.name as string, fileName), Math.abs(stackFrame.line), 1)
608+
});
570609
};
571610

572611
const stack = transformStack(stackResponse.result);

src/indexing/Indexer.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import ConfigurationManager from '../lifecycle/ConfigurationManager'
1010
import MVM from '../mvm/impl/MVM'
1111
import Logger from '../logging/Logger'
1212
import parse from '../mvm/MdaParser'
13+
import * as FileNameUtils from '../utils/FileNameUtils'
1314

1415
interface WorkspaceFileIndexedResponse {
1516
isDone: boolean
@@ -115,7 +116,8 @@ export default class Indexer {
115116
return
116117
}
117118

118-
const fileContentBuffer = await fs.readFile(URI.parse(uri).fsPath)
119+
const filePath = FileNameUtils.getFilePathFromUri(uri)
120+
const fileContentBuffer = await fs.readFile(filePath)
119121
const code = fileContentBuffer.toString()
120122
const rawCodeData = await this.getCodeData(code, uri)
121123

@@ -136,7 +138,7 @@ export default class Indexer {
136138
* @returns The raw data extracted from the document
137139
*/
138140
private async getCodeData (code: string, uri: string): Promise<RawCodeData | null> {
139-
const filePath = URI.parse(uri).fsPath
141+
const filePath = FileNameUtils.getFilePathFromUri(uri)
140142
const analysisLimit = (await ConfigurationManager.getConfiguration()).maxFileSizeForAnalysis
141143

142144
try {

src/indexing/SymbolSearchService.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import Expression from '../utils/ExpressionUtils'
88
import { getTextOnLine } from '../utils/TextDocumentUtils'
99
import PathResolver from '../providers/navigation/PathResolver'
1010
import * as fs from 'fs/promises'
11-
import { URI } from 'vscode-uri'
1211
import Indexer from './Indexer'
12+
import * as FileNameUtils from '../utils/FileNameUtils'
1313

1414
export enum RequestType {
1515
Definition,
@@ -321,7 +321,7 @@ class SymbolSearchService {
321321
}
322322

323323
// Ensure URI is not a directory. This can occur with some packages.
324-
const fileStats = await fs.stat(URI.parse(resolvedUri).fsPath)
324+
const fileStats = await fs.stat(FileNameUtils.getFilePathFromUri(resolvedUri))
325325
if (fileStats.isDirectory()) {
326326
return null
327327
}

src/lifecycle/MatlabCommunicationManager.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lifecycle/PathSynchronizer.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import Logger from '../logging/Logger'
66
import MatlabLifecycleManager from './MatlabLifecycleManager'
77
import * as os from 'os'
88
import path from 'path'
9-
import { URI } from 'vscode-uri'
109
import MVM, { IMVM, MatlabState } from '../mvm/impl/MVM'
1110
import parse from '../mvm/MdaParser'
11+
import * as FileNameUtils from '../utils/FileNameUtils'
1212

1313
export default class PathSynchronizer {
1414
constructor (private readonly matlabLifecycleManager: MatlabLifecycleManager, private readonly mvm: MVM) {}
@@ -161,9 +161,7 @@ export default class PathSynchronizer {
161161

162162
private convertWorkspaceFoldersToFilePaths (workspaceFolders: WorkspaceFolder[]): string[] {
163163
return workspaceFolders.map(folder => {
164-
const uri = URI.parse(folder.uri)
165-
166-
return path.normalize(uri.fsPath)
164+
return path.normalize(FileNameUtils.getFilePathFromUri(folder.uri))
167165
});
168166
}
169167

src/mvm/impl/MVM.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/notifications/NotificationService.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2022 - 2024 The MathWorks, Inc.
1+
// Copyright 2022 - 2025 The MathWorks, Inc.
22

33
import { GenericNotificationHandler, Disposable } from 'vscode-languageserver/node'
44
import ClientConnection from '../ClientConnection'
@@ -18,6 +18,8 @@ export enum Notification {
1818

1919
// Execution
2020
MatlabRequestInstance = 'matlab/request',
21+
TerminalCompletionRequest = 'TerminalCompletionRequest',
22+
TerminalCompletionResponse = 'TerminalCompletionResponse',
2123

2224
MVMEvalRequest = 'evalRequest',
2325
MVMEvalComplete = 'evalResponse',

src/providers/completion/CompletionSupportProvider.ts

+32-10
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
import { CompletionItem, CompletionItemKind, CompletionList, CompletionParams, ParameterInformation, Position, SignatureHelp, SignatureHelpParams, SignatureInformation, TextDocuments, InsertTextFormat } from 'vscode-languageserver'
44
import { TextDocument } from 'vscode-languageserver-textdocument'
5-
import { URI } from 'vscode-uri'
65
import MatlabLifecycleManager from '../../lifecycle/MatlabLifecycleManager'
76
import ConfigurationManager, { Argument } from '../../lifecycle/ConfigurationManager'
87
import MVM from '../../mvm/impl/MVM'
98
import Logger from '../../logging/Logger'
109
import parse from '../../mvm/MdaParser'
10+
import * as FileNameUtils from '../../utils/FileNameUtils'
1111

1212
interface MCompletionData {
1313
widgetData?: MWidgetData
@@ -100,11 +100,21 @@ class CompletionSupportProvider {
100100
return CompletionList.create()
101101
}
102102

103-
const completionData = await this.retrieveCompletionData(doc, params.position)
103+
const completionData = await this.retrieveCompletionDataForDocument(doc, params.position)
104104

105105
return this.parseCompletionItems(completionData)
106106
}
107107

108+
/**
109+
* Returns completions for a give string
110+
* @returns An array of possible completions
111+
*/
112+
async getCompletions (code: string, cursorOffset: number): Promise<CompletionList> {
113+
const completionData = await this.retrieveCompletionData(code, '', cursorOffset);
114+
115+
return this.parseCompletionItems(completionData);
116+
}
117+
108118
/**
109119
* Handles a request for function signature help.
110120
*
@@ -119,7 +129,7 @@ class CompletionSupportProvider {
119129
return null
120130
}
121131

122-
const completionData = await this.retrieveCompletionData(doc, params.position)
132+
const completionData = await this.retrieveCompletionDataForDocument(doc, params.position)
123133

124134
return this.parseSignatureHelp(completionData)
125135
}
@@ -131,18 +141,30 @@ class CompletionSupportProvider {
131141
* @param position The cursor position in the document
132142
* @returns The raw completion data
133143
*/
134-
private async retrieveCompletionData (doc: TextDocument, position: Position): Promise<MCompletionData> {
135-
if (!this.mvm.isReady()) {
136-
// MVM not yet ready
137-
return {}
138-
}
139-
144+
private async retrieveCompletionDataForDocument (doc: TextDocument, position: Position): Promise<MCompletionData> {
140145
const docUri = doc.uri
141146

142147
const code = doc.getText()
143-
const fileName = URI.parse(docUri).fsPath
148+
const fileName = FileNameUtils.getFilePathFromUri(docUri, true)
144149
const cursorPosition = doc.offsetAt(position)
145150

151+
return await this.retrieveCompletionData(code, fileName, cursorPosition);
152+
}
153+
154+
/**
155+
* Retrieves raw completion data from MATLAB.
156+
*
157+
* @param code The code to be completed
158+
* @param fileName The name of the file with the completion, or empty string if there is no file
159+
* @param cursorPosition The cursor position in the code
160+
* @returns The raw completion data
161+
*/
162+
private async retrieveCompletionData (code: string, fileName: string, cursorPosition: number): Promise<MCompletionData> {
163+
if (!this.mvm.isReady()) {
164+
// MVM not yet ready
165+
return {}
166+
}
167+
146168
try {
147169
const response = await this.mvm.feval(
148170
'matlabls.handlers.completions.getCompletions',

src/providers/linting/LintingSupportProvider.ts

+3-14
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { execFile, ExecFileException } from 'child_process'
44
import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic, DiagnosticSeverity, Position, Range, TextDocumentEdit, TextEdit, VersionedTextDocumentIdentifier, WorkspaceEdit } from 'vscode-languageserver'
55
import { TextDocument } from 'vscode-languageserver-textdocument'
6-
import { URI } from 'vscode-uri'
76
import ConfigurationManager from '../../lifecycle/ConfigurationManager'
87
import MatlabLifecycleManager from '../../lifecycle/MatlabLifecycleManager'
98
import Logger from '../../logging/Logger'
@@ -14,6 +13,7 @@ import { MatlabLSCommands } from '../lspCommands/ExecuteCommandProvider'
1413
import ClientConnection from '../../ClientConnection'
1514
import MVM from '../../mvm/impl/MVM'
1615
import parse from '../../mvm/MdaParser'
16+
import * as FileNameUtils from '../../utils/FileNameUtils'
1717

1818
type mlintSeverity = '0' | '1' | '2' | '3' | '4'
1919

@@ -79,8 +79,7 @@ class LintingSupportProvider {
7979
const matlabConnection = await this.matlabLifecycleManager.getMatlabConnection()
8080
const isMatlabAvailable = matlabConnection != null
8181

82-
const isMFile = this.isMFile(uri)
83-
const fileName = isMFile ? URI.parse(uri).fsPath : 'untitled.m'
82+
const fileName = FileNameUtils.getFilePathFromUri(uri, true)
8483

8584
let lintData: string[] = []
8685
const code = textDocument.getText()
@@ -94,7 +93,7 @@ class LintingSupportProvider {
9493
if (isMatlabAvailable) {
9594
// Use MATLAB-based linting for better results and fixes
9695
lintData = await this.getLintResultsFromMatlab(code, fileName)
97-
} else if (isMFile) {
96+
} else if (FileNameUtils.isMFile(uri)) {
9897
// Try to use mlint executable for basic linting
9998
lintData = await this.getLintResultsFromExecutable(fileName)
10099
}
@@ -537,16 +536,6 @@ class LintingSupportProvider {
537536
a.severity === b.severity &&
538537
a.source === b.source
539538
}
540-
541-
/**
542-
* Checks if the given URI corresponds to a MATLAB M-file.
543-
*
544-
* @param uri - The URI of the file to check.
545-
* @returns True if the file is a MATLAB M-file (.m), false otherwise.
546-
*/
547-
private isMFile (uri: string): boolean {
548-
return URI.parse(uri).fsPath.endsWith('.m')
549-
}
550539
}
551540

552541
export default LintingSupportProvider

0 commit comments

Comments
 (0)