Skip to content

MATLAB language server - v1.3.3 #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.

**Useful Information**
- MATLAB Version:
- OS Version:
- Language Server Client: [e.g. MATLAB extension for Visual Studio Code]
- Client Version:
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ MATLAB language server supports these editors by installing the corresponding ex

### Unreleased

### 1.3.3
Release date: 2025-05-15

Added:
* Support for debugging P-coded files when the corresponding source file is available

Fixed:
* Resolves potential crashes when using code completion in files without a .m file extension

### 1.3.2
Release date: 2025-03-06

Expand Down
5 changes: 5 additions & 0 deletions matlab/+matlabls/+handlers/+completions/getCompletions.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
% GETCOMPLETIONS Retrieves the data for the possible completions at the cursor position in the given code.

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

completionResultsStr = matlabls.internal.getCompletionsData(code, fileName, cursorPosition);
completionsData = filterCompletionResults(completionResultsStr);
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "matlab-language-server",
"version": "1.3.2",
"version": "1.3.3",
"description": "Language Server for MATLAB code",
"main": "./src/index.ts",
"bin": "./out/index.js",
Expand Down
57 changes: 48 additions & 9 deletions src/debug/MatlabDebugAdaptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DebugProtocol } from '@vscode/debugprotocol';
import { DebugServices, BreakpointInfo } from './DebugServices'
import { ResolvablePromise, createResolvablePromise } from '../utils/PromiseUtils'
import { IMVM, MVMError, MatlabState } from '../mvm/impl/MVM';
import fs from 'node:fs';

enum BreakpointChangeType {
ADD,
Expand Down Expand Up @@ -233,6 +234,8 @@ export default class MatlabDebugAdaptor {

private _setupListeners (): void {
this._debugServices.on(DebugServices.Events.BreakpointAdded, async (breakpoint: BreakpointInfo) => {
breakpoint.filePath = this._mapToMFile(breakpoint.filePath, false);

this._matlabBreakpoints.push(breakpoint);

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

this._debugServices.on(DebugServices.Events.BreakpointRemoved, async (breakpoint: BreakpointInfo) => {
breakpoint.filePath = this._mapToMFile(breakpoint.filePath, false);

this._matlabBreakpoints = this._matlabBreakpoints.filter((existingBreakpoint) => {
return !existingBreakpoint.equals(breakpoint, true);
});
Expand Down Expand Up @@ -422,6 +427,7 @@ export default class MatlabDebugAdaptor {
}

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

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

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

let matlabBreakpointInfos: BreakpointInfo[] = [];
const listener = this._registerBreakpointChangeListener((changeType, bpInfo) => {
if (changeType === BreakpointChangeType.ADD && bpInfo.filePath === canonicalizedPath) {
if (changeType === BreakpointChangeType.ADD && bpInfo.filePath === pathToSetOrClear) {
matlabBreakpointInfos.push(bpInfo);
}
});

await this._mvm.setBreakpoint(canonicalizedPath, newBreakpoint.info.lineNumber, newBreakpoint.info.condition);
await this._mvm.setBreakpoint(pathToSetOrClear, newBreakpoint.info.lineNumber, newBreakpoint.info.condition);

listener.remove();

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

_mapToPFile (filePath: string, checkIfExists: boolean): string {
// If this is an m-file then convert to p-file and check existence
if (filePath.endsWith('.m')) {
const pFile = filePath.substring(0, filePath.length - 1) + 'p';
if (!checkIfExists || fs.existsSync(pFile)) {
return pFile;
} else {
return filePath;
}
}

// Not an m file so p-code not supported
return filePath;
}

_mapToMFile (filePath: string, checkIfExists: boolean): string {
// If this is an p-file then convert to m-file and check existence
if (filePath.endsWith('.p')) {
const mFile = filePath.substring(0, filePath.length - 1) + 'm';
if (!checkIfExists || fs.existsSync(mFile)) {
return mFile;
} else {
return filePath;
}
}

// Not an m file so p-code not supported
return filePath;
}

async continueRequest (response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments, request?: DebugProtocol.Request): Promise<void> {
try {
await this._mvm.eval("if system_dependent('IsDebugMode')==1, dbcont; end");
Expand Down Expand Up @@ -558,15 +594,18 @@ export default class MatlabDebugAdaptor {
if (stack[0]?.mwtype !== undefined) {
stack = stack[0]
const size = stack.mwsize[0];
const newStack = [];
const transformedStack = [];
for (let i = 0; i < size; i++) {
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))
transformedStack.push({ name: stack.mwdata.name[i], file: stack.mwdata.file[i], line: stack.mwdata.line[i] });
}
return newStack;
} else {
const numberOfStackFrames: number = stack.length;
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));
stack = transformedStack;
}

const numberOfStackFrames: number = stack.length;
return stack.map((stackFrame: MatlabData, i: number) => {
const fileName: string = this._mapToMFile(stackFrame.file, true);
return new debug.StackFrame(numberOfStackFrames - i + 1, stackFrame.name, new debug.Source(stackFrame.name as string, fileName), Math.abs(stackFrame.line), 1)
});
};

const stack = transformStack(stackResponse.result);
Expand Down
6 changes: 4 additions & 2 deletions src/indexing/Indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ConfigurationManager from '../lifecycle/ConfigurationManager'
import MVM from '../mvm/impl/MVM'
import Logger from '../logging/Logger'
import parse from '../mvm/MdaParser'
import * as FileNameUtils from '../utils/FileNameUtils'

interface WorkspaceFileIndexedResponse {
isDone: boolean
Expand Down Expand Up @@ -115,7 +116,8 @@ export default class Indexer {
return
}

const fileContentBuffer = await fs.readFile(URI.parse(uri).fsPath)
const filePath = FileNameUtils.getFilePathFromUri(uri)
const fileContentBuffer = await fs.readFile(filePath)
const code = fileContentBuffer.toString()
const rawCodeData = await this.getCodeData(code, uri)

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

try {
Expand Down
4 changes: 2 additions & 2 deletions src/indexing/SymbolSearchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import Expression from '../utils/ExpressionUtils'
import { getTextOnLine } from '../utils/TextDocumentUtils'
import PathResolver from '../providers/navigation/PathResolver'
import * as fs from 'fs/promises'
import { URI } from 'vscode-uri'
import Indexer from './Indexer'
import * as FileNameUtils from '../utils/FileNameUtils'

export enum RequestType {
Definition,
Expand Down Expand Up @@ -321,7 +321,7 @@ class SymbolSearchService {
}

// Ensure URI is not a directory. This can occur with some packages.
const fileStats = await fs.stat(URI.parse(resolvedUri).fsPath)
const fileStats = await fs.stat(FileNameUtils.getFilePathFromUri(resolvedUri))
if (fileStats.isDirectory()) {
return null
}
Expand Down
2 changes: 1 addition & 1 deletion src/lifecycle/MatlabCommunicationManager.js

Large diffs are not rendered by default.

6 changes: 2 additions & 4 deletions src/lifecycle/PathSynchronizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import Logger from '../logging/Logger'
import MatlabLifecycleManager from './MatlabLifecycleManager'
import * as os from 'os'
import path from 'path'
import { URI } from 'vscode-uri'
import MVM, { IMVM, MatlabState } from '../mvm/impl/MVM'
import parse from '../mvm/MdaParser'
import * as FileNameUtils from '../utils/FileNameUtils'

export default class PathSynchronizer {
constructor (private readonly matlabLifecycleManager: MatlabLifecycleManager, private readonly mvm: MVM) {}
Expand Down Expand Up @@ -161,9 +161,7 @@ export default class PathSynchronizer {

private convertWorkspaceFoldersToFilePaths (workspaceFolders: WorkspaceFolder[]): string[] {
return workspaceFolders.map(folder => {
const uri = URI.parse(folder.uri)

return path.normalize(uri.fsPath)
return path.normalize(FileNameUtils.getFilePathFromUri(folder.uri))
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/mvm/impl/MVM.js

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/notifications/NotificationService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 - 2024 The MathWorks, Inc.
// Copyright 2022 - 2025 The MathWorks, Inc.

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

// Execution
MatlabRequestInstance = 'matlab/request',
TerminalCompletionRequest = 'TerminalCompletionRequest',
TerminalCompletionResponse = 'TerminalCompletionResponse',

MVMEvalRequest = 'evalRequest',
MVMEvalComplete = 'evalResponse',
Expand Down
42 changes: 32 additions & 10 deletions src/providers/completion/CompletionSupportProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import { CompletionItem, CompletionItemKind, CompletionList, CompletionParams, ParameterInformation, Position, SignatureHelp, SignatureHelpParams, SignatureInformation, TextDocuments, InsertTextFormat } from 'vscode-languageserver'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { URI } from 'vscode-uri'
import MatlabLifecycleManager from '../../lifecycle/MatlabLifecycleManager'
import ConfigurationManager, { Argument } from '../../lifecycle/ConfigurationManager'
import MVM from '../../mvm/impl/MVM'
import Logger from '../../logging/Logger'
import parse from '../../mvm/MdaParser'
import * as FileNameUtils from '../../utils/FileNameUtils'

interface MCompletionData {
widgetData?: MWidgetData
Expand Down Expand Up @@ -100,11 +100,21 @@ class CompletionSupportProvider {
return CompletionList.create()
}

const completionData = await this.retrieveCompletionData(doc, params.position)
const completionData = await this.retrieveCompletionDataForDocument(doc, params.position)

return this.parseCompletionItems(completionData)
}

/**
* Returns completions for a give string
* @returns An array of possible completions
*/
async getCompletions (code: string, cursorOffset: number): Promise<CompletionList> {
const completionData = await this.retrieveCompletionData(code, '', cursorOffset);

return this.parseCompletionItems(completionData);
}

/**
* Handles a request for function signature help.
*
Expand All @@ -119,7 +129,7 @@ class CompletionSupportProvider {
return null
}

const completionData = await this.retrieveCompletionData(doc, params.position)
const completionData = await this.retrieveCompletionDataForDocument(doc, params.position)

return this.parseSignatureHelp(completionData)
}
Expand All @@ -131,18 +141,30 @@ class CompletionSupportProvider {
* @param position The cursor position in the document
* @returns The raw completion data
*/
private async retrieveCompletionData (doc: TextDocument, position: Position): Promise<MCompletionData> {
if (!this.mvm.isReady()) {
// MVM not yet ready
return {}
}

private async retrieveCompletionDataForDocument (doc: TextDocument, position: Position): Promise<MCompletionData> {
const docUri = doc.uri

const code = doc.getText()
const fileName = URI.parse(docUri).fsPath
const fileName = FileNameUtils.getFilePathFromUri(docUri, true)
const cursorPosition = doc.offsetAt(position)

return await this.retrieveCompletionData(code, fileName, cursorPosition);
}

/**
* Retrieves raw completion data from MATLAB.
*
* @param code The code to be completed
* @param fileName The name of the file with the completion, or empty string if there is no file
* @param cursorPosition The cursor position in the code
* @returns The raw completion data
*/
private async retrieveCompletionData (code: string, fileName: string, cursorPosition: number): Promise<MCompletionData> {
if (!this.mvm.isReady()) {
// MVM not yet ready
return {}
}

try {
const response = await this.mvm.feval(
'matlabls.handlers.completions.getCompletions',
Expand Down
17 changes: 3 additions & 14 deletions src/providers/linting/LintingSupportProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { execFile, ExecFileException } from 'child_process'
import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic, DiagnosticSeverity, Position, Range, TextDocumentEdit, TextEdit, VersionedTextDocumentIdentifier, WorkspaceEdit } from 'vscode-languageserver'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { URI } from 'vscode-uri'
import ConfigurationManager from '../../lifecycle/ConfigurationManager'
import MatlabLifecycleManager from '../../lifecycle/MatlabLifecycleManager'
import Logger from '../../logging/Logger'
Expand All @@ -14,6 +13,7 @@ import { MatlabLSCommands } from '../lspCommands/ExecuteCommandProvider'
import ClientConnection from '../../ClientConnection'
import MVM from '../../mvm/impl/MVM'
import parse from '../../mvm/MdaParser'
import * as FileNameUtils from '../../utils/FileNameUtils'

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

Expand Down Expand Up @@ -79,8 +79,7 @@ class LintingSupportProvider {
const matlabConnection = await this.matlabLifecycleManager.getMatlabConnection()
const isMatlabAvailable = matlabConnection != null

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

let lintData: string[] = []
const code = textDocument.getText()
Expand All @@ -94,7 +93,7 @@ class LintingSupportProvider {
if (isMatlabAvailable) {
// Use MATLAB-based linting for better results and fixes
lintData = await this.getLintResultsFromMatlab(code, fileName)
} else if (isMFile) {
} else if (FileNameUtils.isMFile(uri)) {
// Try to use mlint executable for basic linting
lintData = await this.getLintResultsFromExecutable(fileName)
}
Expand Down Expand Up @@ -537,16 +536,6 @@ class LintingSupportProvider {
a.severity === b.severity &&
a.source === b.source
}

/**
* Checks if the given URI corresponds to a MATLAB M-file.
*
* @param uri - The URI of the file to check.
* @returns True if the file is a MATLAB M-file (.m), false otherwise.
*/
private isMFile (uri: string): boolean {
return URI.parse(uri).fsPath.endsWith('.m')
}
}

export default LintingSupportProvider
Loading